company_banner

Memcached plugin: NoSQL в MySQL



    Здравствуйте! Меня зовут Максим Матюхин, я PHP-программист Badoo. В своей работе мы активно используем MySQL. Но иногда нам не хватает её производительности, поэтому мы постоянно ищем возможности ускорить её работу.

    В 2010 году Yoshinori Matsunobu представил NoSQL-плагин для MySQL под названием HandlerSocket. Заявлялось, что этот плагин позволяет выполнять более 750 000 запросов в секунду. Нам стало любопытно, и мы почти сразу же стали использовать это решение. Результат нам настолько понравился, что мы начали выступать с докладами и писать статьи, рекламируя HandlerSocket.

    По-видимому, мы были одними из немногих пользователей этого плагина — начиная с версии MySQL 5.7 он перестал работать. Зато в этой версии появился другой плагин от Oracle — InnoDB memcached plugin, который обещал похожий функционал.

    Несмотря на то, что memcached-плагин появился ещё в MySQL 5.6 в 2013 году, статей о нём не так много и в большинстве своём они повторяют документацию: создаётся простая табличка, и к ней делаются запросы через memcached-клиент.

    Мы имеем большой опыт работы с Memcached и привыкли к простоте взаимодействия с ним. От InnoDB memcached plugin мы ожидали такой же простоты. Но на деле оказалось, что если паттерны использования плагина хотя бы немного отличаются от описанных в документации и статьях, то всплывает масса нюансов и ограничений, которые определённо стоит учитывать, если вы собираетесь пользоваться плагином.

    MySQL HandlerSocket


    В этой статье мы так или иначе будем сравнивать новый memcached-плагин со старым HandlerSocket. Поэтому напомню, что представлял собой последний.

    После установки плагина HandlerSocket, MySQL начинала слушать два дополнительных порта:

    1. Первый порт принимал клиентские запросы для чтения данных.
    2. Второй порт принимал клиентские запросы для записи данных.

    Клиент должен был установить обычное TCP-соединение на один из этих портов (никакой аутентификации не поддерживалось), и после этого нужно было отправить команду «open index» (специальная команда, с помощью которой клиент сообщал, из какой таблицы какого индекса какие поля мы собираемся читать (или писать)).

    Если команда «open index» срабатывала успешно, то потом можно было отправлять GET-ы или INSERT/UPDATE/DELETE-команды в зависимости от порта, на который было установлено соединение.

    HandlerSocket позволял выполнять не только GET-ы по первичному ключу, но и простые выборки из неуникального индекса, выборки по диапазону, поддерживал multiget-ы и  LIMIT. При этом с таблицей можно было работать как из обычного SQL, так и через плагин. Это, например, позволяло делать какие-то изменения в транзакциях через SQL, а потом читать эти данные через HandlerSocket.

    Важно, что HandlerSocket обрабатывал все коннекты ограниченным пулом потоков через epoll, поэтому легко можно было поддерживать десятки тысяч соединений, в то время как в самой MySQL на каждое соединение создаётся поток (thread) и их количество сильно ограничено.

    В то же время это всё ещё обычный MySQL сервер — знакомая нам технология. Мы знаем, как его реплицировать и мониторить. Мониторить HandlerSocket сложно, так как он не предоставляет каких-либо специфических метрик; тем не менее некоторые стандартные метрики MySQL и InnoDB оказываются полезными.

    Были, конечно, и неудобства, в частности данный плагин не поддерживал работу с типом timestamp. Ну и HandlerSocket протокол сложнее читать и потому сложнее отлаживать.

    Подробнее о HandlerSocket можно почитать здесь. Также вы можете посмотреть одну из наших презентаций.

    InnoDB memcached plugin


    Что же нам предлагает новый memcached plugin?

    Как следует из названия, его идея в том, чтобы использовать memcached-клиент для работы с MySQL и через memcached-команды получать и сохранять данные.

    Об основных преимуществах плагина можно прочитать здесь.

    Нас больше всего заинтересовали следующие:

    1. Низкое потребление CPU.
    2. Данные хранятся в InnoDB, что даёт определённые гарантии.
    3. С данными можно работать как через Memcached, так и через SQL; их можно реплицировать встроенными в MySQL средствами.

    К этому списку можно добавить ещё такие плюсы, как:

    1. Быстрый и дешёвый коннект. Обычное MySQL-соединение обрабатывается одним thread-ом, и количество thread-ов ограничено, а в memcached-плагине один thread обрабатывает все соединения в event loop-е.
    2. Возможность одним GET-запросом запросить сразу несколько ключей.
    3. Если сравнивать с MySQL HandlerSocket, то в memcached-плагине не надо использовать команду  «Open Table» и все операции чтения и записи происходят на одном порте.


    Больше деталей о работе плагина можно найти в официальной документации. Для нас самыми полезными показались страницы:

    1. InnoDB memcached Architecture.
    2. InnoDB memcached Plugin Internals.

    После установки плагина MySQL начинает принимать соединения на порте 11211 (стандартный memcached-порт). Также появляется специальная база данных (схема) innodb_memcache, в которой вы будете конфигурировать доступ к своим таблицам.

    Простой пример


    Допустим, у вас уже есть таблица, с которой вы хотите работать через memcached-протокол:

    CREATE TABLE `auth` (
      `email` varchar(96) NOT NULL,
      `password` varchar(64) NOT NULL,
      `type` varchar(32) NOT NULL DEFAULT '',
      PRIMARY KEY (`email`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
    

    и вы хотите получать и изменять данные по первичному ключу.

    Вам нужно сначала описать соответствие между memcached-ключом и SQL-таблицей в  таблице innodb_memcache.containers. Эта таблица выглядит примерно так (я убрал описание кодировок, чтобы было проще читать):

    CREATE TABLE `containers` (
      `name` varchar(50) NOT NULL,
      `db_schema` varchar(250) NOT NULL,
      `db_table` varchar(250) NOT NULL,
      `key_columns` varchar(250) NOT NULL,
      `value_columns` varchar(250) DEFAULT NULL,
      `flags` varchar(250) NOT NULL DEFAULT '0',
      `cas_column` varchar(250) DEFAULT NULL,
      `expire_time_column` varchar(250) DEFAULT NULL,
      `unique_idx_name_on_key` varchar(250) NOT NULL,
      PRIMARY KEY (`name`)
    ) ENGINE=InnoDB DEFAULT

    Самые важные поля:

    • name —  префикс вашего Memcached-ключа;
    • db_schema — название базы (схемы);
    • db_table — ваша таблица;
    • key_columns — название поля в таблице, по которому будем искать (обычно это ваш primary key);
    • value_columns — список полей из таблицы, которые будут доступны memcached-плагину;
    • unique_idx_name_on_key — индекс, по которому нужно искать (несмотря на то, что вы уже указали key_columns, они могут входить в разные индексы и нужно явно указать индекс).

    Остальные поля для начала не очень важны.

    Добавим описание нашей таблицы в innodb_memcache.containers:

    INSERT INTO innodb_memcache.containers SET 
        name='auth', 
        db_schema='test', 
        db_table='auth', 
        key_columns='email', 
        value_columns='password|type', 
        flags='0', 
        cas_column='0',
        expire_time_column='0',
        unique_idx_name_on_key='PRIMARY';

    В этом примере name=’auth’ — это префикс нашего memcached-ключа. В документации он зачастую называется table_id, и далее в статье я буду использовать этот термин.

    Теперь TELNET-ом подключимся к memcached-плагину и попробуем сохранить и получить данные:

    [21:26:22] maxm@localhost: ~> telnet memchached-mysql.dev 11211
    Trying 127.0.0.1...
    Connected to memchached-mysql.dev.
    Escape character is '^]'.
    get @@auth.max@example.com
    END
    
    set @@auth.max@example.com 0 0 10
    1234567|89
    STORED
    
    get @@auth.max@example.com
    VALUE @@auth.max@example.com 0 10
    1234567|89
    END

    Сначала мы отправили GET-запрос, он нам ничего не вернул. Потом мы сохранили данные SET-запросом, после чего получили их обратно GET-ом.

    GET вернул такую строку: 1234567|89. Это значения полей «password» и «type», разделённые символом "|". Поля возвращаются в том порядке, в каком они были описаны в innodb_memcache.containers.value_columns.

    Возможно, вы сейчас задались вопросом: «А что будет, если в «password» встретится символ "|"?» Об этом я расскажу ниже.

    Через SQL эти данные тоже доступны:

    MySQL [(none)]> select * from auth where email='max@example.com';
    +-----------------+----------+------+
    | email       	
    | password | type |
    +-----------------+----------+------+
    | max@example.com | 1234567  | 89   |
    +-----------------+----------+------+
    1 row in set (0.00 sec)

    Дефолтный table_id


    Есть ещё такой режим работы:

    get @@auth
    VALUE @@auth 0 21
    test/auth
    END
    
    get max@example.com
    VALUE max@example.com 0 10
    1234567|99
    END
    
    set ivan@example.com 0 0 10
    qwerty|xxx
    STORED
    
    get ivan@example.com
    VALUE ivan@example.com 0 10
    qwerty|xxx
    END

    В этом примере запросом get @@auth мы делаем table_id auth префиксом по умолчанию для данного соединения. После этого все последующие запросы можно делать без указания table_id.

    Пока всё просто и логично. Но если начать разбираться, то обнаруживается много нюансов. Расскажу о том, что нашли мы.

    Нюансы


    Кеширование таблицы innodb_memcache.containers


    Memcached-плагин читает таблицу innodb_memcache.containers один раз при старте. Далее, если по Memcached-протоколу приходит неизвестный table_id, плагин ищет его в таблице. Поэтому вы легко можете добавлять новые ключи (table_id), но если захотите изменить настройки существующего table_id, придётся перезапускать memcached-плагин:

    mysql> UNINSTALL PLUGIN daemon_memcached;
    mysql> INSTALL PLUGIN daemon_memcached soname "libmemcached.so";

    В промежутке между этими двумя запросами Memcached-интерфейс работать не будет. Из-за этого зачастую проще создать новый table_id, чем менять существующий и перезапускать плагин.

    Для нас было неожиданностью, что такой важный нюанс работы плагина описан на странице Adapting a memcached Application for the InnoDB memcached Plugin, —  не очень логичное место для такой информации.

    Flags, cas_column, expire_time_column


    Данные поля нужны для имитации некоторых особенностей Memcached. Документация по ним противоречива. Большинство примеров в ней иллюстрируют работу с таблицами, в которых эти поля есть. Может возникнуть опасение, что вам понадобится добавлять их в ваши таблицы (а это как минимум три INT-поля). Но нет. Если у вас в таблицах нет таких полей и вы не собираетесь использовать такой функционал Memcached, как CAS, expiration или флаги, то вам не надо добавлять эти поля в таблицы.

    При конфигурации таблицы в innodb_memcache.containers нужно в эти поля вписать ‘0’, сделать именно строку с нулём:

    INSERT INTO innodb_memcache.containers SET 
        name='auth', 
        db_schema='test', 
        db_table='auth', 
        key_columns='email', 
        value_columns='password|type', 
        flags='0', 
        cas_column='0',
        expire_time_column='0',
        unique_idx_name_on_key='PRIMARY';

    Досадно, что у cas_column и expire_time_column значение по умолчанию — NULL, и, если вы выполните INSERT INTO innodb_memcache.containers, не указав значение ‘0’ для этих полей, в них сохранится NULL и этот memcache-префикс попросту не будет работать.

    Типы данных


    Из документации не очень понятно, какие типы данных можно использовать при работе с плагином. В нескольких местах сказано, что плагин может работать только с текстовыми полями (CHAR, VARCHAR, BLOB). Вот здесь: Adapting an Existing MySQL Schema for the InnoDB memcached Plugin предлагают числа хранить в строковых полях, и если вам потом из SQL надо работать с этими числовыми полями, то создавать VIEW, в котором  VARCHAR-поля с числами будут конвертироваться в INTEGER-поля:

    CREATE VIEW numbers AS SELECT c1 KEY, CAST(c2 AS UNSIGNED INTEGER) val
      FROM demo_test WHERE c2 BETWEEN '0' and '9999999999';

    Однако кое-где в документации всё же написано, что можно работать с числами. У нас пока имеется только реальный продакшен-опыт с текстовыми полями, но результаты экспериментов показывают, что с числами плагин тоже работает:

    CREATE TABLE `numbers` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `counter` int(10) unsigned NOT NULL DEFAULT '0',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB
    
    INSERT INTO innodb_memcache.containers SET name='numbers', db_schema='test', db_table='numbers', key_columns='id', value_columns='counter', flags='0', cas_column='0',expire_time_column='0',unique_idx_name_on_key='PRIMARY';

    После этого через Memcached-протокол:

    get @@numbers.1
    END
    set @@numbers.1 0 0 2
    12
    STORED
    get @@numbers.1
    VALUE @@numbers.1 0 2
    12
    END

    Мы видим, что memcached-плагин может вернуть любые типы данных. Но он их возвращает в том виде, в котором они лежат в InnoDB, поэтому, например, в случае с timestamp/datetime/float/decimal/JSON возвращается бинарная строка. Но целые числа возвращаются такими, какими мы видим их через SQL.

    Multiget


    Memcached-протокол позволяет запрашивать несколько ключей одним запросом:

    get @@numbers.2 @@numbers.1
    VALUE @@numbers.2 0 2
    12
    VALUE @@numbers.1 0 2
    13
    END

    То, что multiget работает, — уже хорошо. Но он работает в рамках одного table_id:

    get @@auth.ivan@example.com @@numbers.2
    VALUE @@auth.ivan@example.com 0 10
    qwerty|xxx
    END

    В документации этот момент описан здесь: https://dev.mysql.com/doc/refman/8.0/en/innodb-memcached-multiple-get-range-query.html. Оказывается, в multiget-е можно указать table_id только для первого ключа, если все остальные ключи берутся из дефолтного table_id (пример из документации):

    get @@aaa.AA BB
    VALUE @@aaa.AA 8 12
    HELLO, HELLO
    VALUE BB 10 16
    GOODBYE, GOODBYE
    END

    В этом примере второй ключ берётся из дефолтного table_id. Мы могли бы указать намного больше ключей из дефолтного table_id, а для первого ключа мы указали отдельный table_id, и такая возможность есть только в случае с первым ключом.

    Можно сказать, что multiget работает в рамках одной таблицы, потому что полагаться на такую логику в продакшен-коде совсем не хочется: она неочевидна, об этом легко забыть, ошибиться.

    Если сравнивать с HandlerSocket, то там тоже multiget работал в рамках одной таблицы. Но это ограничение выглядело естественным: клиент открывает индекс в таблице и запрашивает из него одно или несколько значений. А вот при работе c memcached-плагином multiget по нескольким ключам с разными префиксами — нормальная практика. И от MySQL memcached-плагина ожидаешь того же. Но нет :(

    INCR, DEL


    Я уже приводил примеры GET-/SET-запросов. У INCR- и DEL-запросов есть особенность. Она заключается в том, что они работают только при использовании дефолтного table_id:

    DELETE @@numbers.1
    ERROR
    
    get @@numbers
    VALUE @@numbers 0 24
    test/numbers
    END
    
    delete 1
    DELETED

    Ограничения memcached-протокола


    Memcached имеет текстовый протокол, что накладывает некоторые ограничения. Например, memcached-ключи не должны содержать пробельные символы (пробел, перевод строки). Если посмотреть ещё раз на описание таблицы из нашего примера:

    CREATE TABLE `auth` (
      `email` varchar(96) NOT NULL,
      `password` varchar(64) NOT NULL,
      `type` varchar(32) NOT NULL DEFAULT '',
      PRIMARY KEY (`email`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

    то это значит, что в поле «email» не должно быть таких символов.

    Также длина memcached-ключей должна быть менее 250 байт (байт, а не символов). Если отправить больше, получите ошибку:

    "CLIENT_ERROR bad command line format"

    Помимо этого, надо учитывать тот факт, что memcached-плагин добавляет в memcached-протокол свой синтаксис. Например, он использует символ "|" в качестве разделителя полей в ответе. Вам нужно следить за тем, чтобы в вашей таблице не использовался этот символ. Разделитель можно настроить, но параметры настройки будут распространяться на все таблицы на всём MySQL-сервере.

    Разделитель полей value_columns


    Если через memcached-протокол необходимо вернуть несколько колонок, как в нашем первом примере:

    get @@auth.max@example.com
    VALUE @@auth.max@example.com 0 10
    1234567|89
    END

    то значения колонок разделяются стандартным разделителем "|". Возникает вопрос: «А что будет, если, например, в первом поле в строке будет символ "|"»? Memcached-плагин в этом случае вернёт строку как есть, примерно так: 1234|567|89. В общем случае нельзя понять, где здесь какое поле.

    Поэтому важно сразу выбирать правильный разделитель. А поскольку он будет использоваться для всех ключей всех таблиц, это должен быть универсальный символ, который не будет встречаться ни в одном поле, с которым которым вы будете работать через memcached-протокол.

    Резюме


    Нельзя сказать, что memcached-плагин плох. Но складывается впечатление, что он был написан для определённой схемы работы: MySQL-сервер с одной таблицей, к которой есть доступ по memcached-протоколу, и этот table_id сделан дефолтным. Клиенты устанавливают длительное (persistent) соединение с Memcached-плагином и делают запросы к дефолтному table_id. Наверное, в такой схеме всё будет работать без нареканий. Если же отойти от неё, натыкаешься на различные неудобства.

    Возможно, вы ожидали увидеть какие-нибудь отчёты о производительности плагина. Но мы пока не решились использовать его в высоконагруженных местах. Мы использовали его только в нескольких не очень нагруженных системах и там он работает примерно с такой же скоростью, как HandlerSocket, но честных бенчмарков мы не делали. Но всё же плагин предоставляет такой интерфейс, с которым программист может легко допустить ошибку, — нужно много нюансов держать в голове. Поэтому массово использовать этот плагин мы пока не готовы.

    Мы завели несколько feature requests в баг-трекере MySQL:

    https://bugs.mysql.com/bug.php?id=95091
    https://bugs.mysql.com/bug.php?id=95092
    https://bugs.mysql.com/bug.php?id=95093
    https://bugs.mysql.com/bug.php?id=95094

    Будем надеяться, команда разработчиков memcached-плагина будет совершенствовать свой продукт.
    • +48
    • 5.6k
    • 6
    Badoo
    414.02
    Big Dating
    Share post

    Comments 6

      +1

      Что с авторизацией доступа?

      +1
      Поскольку вы практически используете плагин Memcached, могли бы вы привести данные по его производительности? Например, удельная производительность для записи/чтения в расчете на нод. Особенно интересует запись бинарных данных большой длины.
        0
        Я думаю что этот плагин плохо подходит для работы с бинарными данными из-за наличия разделителей. То есть в качестве разделителя надо выбрать такой символ, которого не может быть в ваших данных. Тут проще и надёжнее делать base64 или что-то похожее, но это увеличит размер данных

        Про производительность я в последнем разделе написал, что у нас там нет большой нагрузки и скорее всего в ближайшее время не появится. Поэтому на наших цифрах судить о производительности бессмысленно. У нас сейчас всё работает на дефолтных настройках на серверах, на которых есть и другая нагрузка, и нам пока даже нет смысла тратить время на исследования и оптимизации.
        Цифры будут зависеть от многих факторов
        • есть ли другая нагрзука на сервер,
        • реплицируются ли данные,
        • включено ли кеширование (здесь про cache_policies )
        • размеры батчей (см здесь)

        Если мы всё же решимся использовать этот плагин в каком-нибудь высоконагруженном месте, то скорее напишем отдельную статью с цифрами
        +1
        > Memcached имеет текстовый протокол, что накладывает некоторые ограничения.

        Поправьте меня если ошибаюсь, но вроде по udp там был бинарный протокол, нет?
          0
          Да, вы правы.
          Похоже я тут был не совсем точен. Судя по доке ограничение на 250 символов — это ограничение именно memcached, а не memcached-протокола
          Также судя по доке, innodb_memcached поддерживает бинарный протокол. Но мы пока использовали только текстовый

        Only users with full accounts can post comments. Log in, please.