Хабрахабр

Memcached plugin от MySQL: инструкция по осторожному применению

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Что же нам предлагает новый 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-ом.

Это значения полей «password» и «type», разделённые символом "|". GET вернул такую строку: 1234567|89. Поля возвращаются в том порядке, в каком они были описаны в 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 работал в рамках одной таблицы. И от MySQL memcached-плагина ожидаешь того же. А вот при работе c memcached-плагином multiget по нескольким ключам с разными префиксами — нормальная практика. Но нет 🙁

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-плагина будет совершенствовать свой продукт.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть