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

в 12:01, , рубрики: mysql, mysql memcached, Блог компании Badoo, высокая производительность, Программирование, Разработка веб-сайтов

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

Здравствуйте! Меня зовут Максим Матюхин, я 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-плагина будет совершенствовать свой продукт.

Автор: max_m

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js