Параллельная репликация — долгожданная функция MySQL, доступная в MariaDB 10.0 и MySQL 5.7. Мы уже представили результаты тестов с MariaDB 10.0 в предыдущем посте серии; в этом посте мы рассмотрим новый тип репликации, представленный в MariaDB 10.1: оптимистическая параллельная репликация.

К этому посту есть приложение: Под капотом. Бенчмаркинг — это сложное искусство, а точное представление результатов еще сложнее. Если бы все детали были помещены в одну статью, то получился бы очень длинный пост. Ссылки на приложение должны удовлетворить читателей, жаждущих более подробной информации.

Оптимистичная параллельная репликация построена поверх предыдущей реализации параллельной репликации. Чтобы полностью понять оптимистическую параллельную репликацию в MariaDB 10.1, мы должны углубиться в детали реализации параллельной репликации в MariaDB 10.0.

Ключевое дизайнерское решение: фиксация по порядку

В Части 1 серии мы объяснили, как параллельная репликация работает в MariaDB 10.0 и в ранней версии MySQL 5.7. Короче говоря, и MySQL 5.7, и MariaDB 10.0 определяют параллелизм на ведущем устройстве и отправляют эту информацию подчиненным через двоичные журналы [1]. Однако MariaDB и MySQL различаются способом фиксации транзакций на ведомых устройствах.

В MariaDB транзакции, выполняемые параллельно на подчиненных устройствах, фиксируются в том же порядке, в котором они появляются в двоичных журналах мастера [2]. Таким образом, если ведомое устройство выполняет T1 и T2 параллельно, и эти транзакции появляются в этом порядке в двоичных журналах ведущего, поток, выполняющий T2, будет ждать завершения T1 перед фиксацией T2 (даже если T2 готов к фиксации до T1).

По умолчанию ведомое устройство MySQL, выполняющее транзакции параллельно, просто фиксирует транзакции по мере их завершения, не применяя какой-либо порядок. В приведенном выше примере T2 зафиксирует перед T1. Это может (и будет в большинстве случаев) генерировать различный порядок транзакций в бинарных журналах ведомых устройств [3] [4].

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

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

Это уничтожение и повтор транзакций происходит не очень часто в MariaDB 10.0, но его реализация необходима, чтобы избежать блокировки репликации. Возникновение этих повторных попыток можно отслеживать с помощью глобального состояния slave_retried_transactions. Ниже приведен график из такого мониторинга, на котором видно, что для четырехминутного интервала потребовалось три повторных попытки. Это особенно мало, учитывая, что на этом подчиненном устройстве выполнялось ~2,5 тыс. транзакций в секунду (три попытки для ~600 000 транзакций).

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

Выполняйте все транзакции параллельно, фиксируя по порядку, чтобы убедиться, что данные непротиворечивы, и обнаруживая тупиковые ситуации, чтобы избежать репликации для блокировки.

Это называется оптимистичной параллельной репликацией и реализовано в MariaDB 10.1. Этот тип параллельной репликации не зависит от определения параллелизма мастером. Подчиненный пытается запустить как можно больше транзакций параллельно в пределах параметра slave_parallel_threads, при этом принудительно фиксируя по порядку. Если конфликта не происходит, достигается максимальная скорость. Если обнаружен конфликт (взаимная блокировка), оптимистично выполненная транзакция будет отменена, чтобы разблокировать фиксацию в порядке (эта транзакция будет повторена позже).

Пока конфликтов не слишком много или пока стоимость откатов не перевешивает выгоду от параллельного запуска большего количества транзакций, оптимистичная параллельная репликация должна давать хорошие результаты. Что еще неизвестно, так это то, как эти результаты будут сравниваться с консервативной параллельной репликацией (консервативная — это название в MariaDB 10.1 для параллельной репликации в MariaDB 10.0). Чтобы ответить на этот вопрос, необходимо провести тесты. Результаты представлены ниже.

Конечно, полные детали реализации более сложны (подробнее об откате и повторных попытках, DML vs DDL и нетранзакционных механизмах хранения и различных оптимистичных режимах параллельной репликации можно прочитать в приложении). Однако этого введения достаточно, чтобы понять остальную часть этого поста, поэтому давайте перейдем непосредственно к бенчмаркингу.

Тест

Тест такой же, как и в предыдущем посте: наверстать 24 часа транзакций. Четыре тестовых окружения также очень похожи, их описание можно найти в приложении. На приведенных ниже графиках мы сравниваем ускорение для неоптимистичных исполнений (slave_parallel_mode=conservative) с ускорениями для оптимистичных исполнений (slave_parallel_mode=aggressive). Пришлось выбрать aggressive, а не optimistic, так как последний не даст хороших результатов при репликации через промежуточный мастер (подробнее в приложении).

На графиках ось Y показывает ускорение, где единица соответствует эталонному времени без использования параллельной репликации (slave_parallel_mode=none). Ось X показывает количество используемых потоков: обратите внимание на логарифмическую шкалу. Кривая, останавливающаяся на отметке 40 по оси X, показывает неоптимистичные результаты. Данные, использованные для построения этих графиков, можно найти в приложении.

Некоторые напоминания из предыдущего поста:

  • Идентификация параллелизма (подчиненная группа) была выполнена с максимальным размером группы 35, поэтому увеличение количества потоков после 40 для неоптимистичных тестов не представляет интереса.
  • Конфигурация бинарного логирования оказала заметное влияние на производительность. Время наверстывания обычно больше при включении log-slave-updates, а отключение двоичного логирования не такая уж большая победа. Приведенные ниже результаты получены с включенным ведением двоичного журнала, но с отключенным log-slave-updates, называемым SB в Части 3.
  • Рабочая нагрузка четырех сред различна: E2 — это рабочая нагрузка, связанная с ЦП, E1 также в основном связана с ЦП, но с некоторыми промахами кэша, E3 — это смешанная рабочая нагрузка ЦП и ввода-вывода, а E4 — это рабочая нагрузка, связанная с вводом-выводом.
  • Поскольку E4 представляет собой рабочую нагрузку, связанную с вводом-выводом (в основном промахи кеша в пуле буферов InnoDB), снижение долговечности, называемое ND в Части 3, не получает заметного улучшения (аналогичное поведение было наблюдается для E3). По этой причине представленные ниже результаты включают высокую износостойкость только для E3 и E4 (обозначаемых как HD).

Обсуждение

Первый сюрприз возникает при наблюдении за ускорением при очень большом количестве потоков. Несмотря на то, что серверы, используемые для тестов, имеют только 12 ядер с поддержкой Hyper-Threading, что в сумме дает 24 потока, ускорение все равно увеличивается за пределы 80 потоков для всех конфигураций и до 2560 потоков для E3. Очевидно, что мы не можем использовать больше вычислительной мощности при повышении slave_parallel_threads с 40 до 80, поэтому такое возрастание ускорения нельзя объяснить просто использованием большего количества ЦП.

Мы считаем, что эти ускорения вызваны предварительной выборкой репликации. При увеличении количества потоков транзакции, которым нужны данные, отсутствующие в кеше, вызовут более раннее чтение с диска (предварительная выборка). Запуск этого чтения раньше является большой победой и не потребляет много ресурсов ЦП, поскольку поток перейдет в состояние IOWait. Даже если эта транзакция вызовет конфликт и будет отменена, данные будут находиться в кэше для повторной попытки. Также в этом случае стоимость отката незначительна по сравнению с выгодой от наличия данных в кеше для повторной попытки, поэтому дополнительные конфликты не являются проблемой. Эта концепция не нова; он известен как предварительная выборка репликации и уже обсуждался ранее Baron Schwartz, Domas Mituzas и Yoshinori Matsunobu.

Во всех средах агрессивная параллельная репликация может обеспечить гораздо большее ускорение, чем консервативная параллельная репликация. Также очень приятно видеть более значительные ускорения при низкой долговечности на E1 и E2 (консервативная параллельная репликация не дает там хороших ускорений). Совершенно очевидно, что агрессивная параллельная репликация — это большое улучшение по сравнению с консервативной параллельной репликацией.

Я подозреваю (и надеюсь), что есть еще узкие места, которые нужно устранить. Оптимистичная параллельная репликация — это значительное улучшение по сравнению с консервативной параллельной репликацией, но мы все еще далеки от ускорения с 6 до 10, которое мы ищем (возможно, эти ожидания нереалистичны…).

Что-то, что может замедлить наши тесты, — это DDL (язык определения данных: [CREATE | ALTER | TRUNCATE | DROP | ...] TABLE, ...), поскольку инструкции DDL блокируют конвейер репликации. Как подробно описано в приложении, прежде чем можно будет запустить DDL, все ранее запущенные транзакции должны быть зафиксированы. Более того, прежде чем начинать какие-либо транзакции после DDL, DDL должен завершиться. Ниже приведены некоторые выдержки из SHOW GLOBAL STATUS во время теста:

  • E1: 197 499 Com_create_table
  • E1: 1148 Com_truncate
  • E1: 489 Com_drop_table
  • E1: 484 Com_rename_table

Таким образом, в самом быстром прогоне E1 (7060 секунд) мы делали в среднем 27 CREATE TABLE в секунду [5]. Это явно не годится для параллельной репликации. Чтобы облегчить выявление таких проблем, я открыл MDEV-10664 — Добавить статусы об оптимистичных остановках параллельной репликации.

Вывод

Как сказано в Части 3 (но все же стоит повторить): можно протестировать параллельную репликацию MariaDB, даже если мастером является старая версия MySQL/MariaDB. В нашем случае наши мастера использовали MySQL 5.6, но то же самое можно было применить и к другим версиям.

В целом, оптимистичная параллельная репликация показывает очень многообещающие результаты: почти все ускорения лучше, чем консервативная параллельная репликация.

Самым большим сюрпризом стало то, что ускорение превысило 80 потоков. Мы могли бы подумать, что количество потоков, превышающее количество процессорных единиц, замедлит работу, но это не так. Вероятно, это вызвано тем, что потоки большую часть времени находятся в состоянии ожидания: либо в ожидании фиксации предыдущей транзакции, либо в ожидании ввода-вывода. Для чистой рабочей нагрузки ЦП мы можем ожидать конкуренцию, и это, вероятно, объясняет пробуксовку для E2.

Еще одним сюрпризом является то, что наилучшее ускорение (3,78) достигается для рабочей нагрузки, связанной с вводом-выводом (E4). В этом случае наибольший выигрыш от параллельной репликации, по-видимому, заключается в увеличении количества операций ввода-вывода при чтении из дисковой подсистемы за счет параллельного планирования множества операций ввода-вывода. В этой среде (и, возможно, в других) оптимистичная параллельная репликация с большим количеством потоков действует как предварительная выборка репликации.

До сих пор все наши тесты проводились с использованием магнитных дисков. Неясно, как консервативная и оптимистичная параллельная репликация будет вести себя с твердотельными дисками (SSD). Необходимы дополнительные тесты, чтобы понять, как параллельная репликация будет вести себя с твердотельными накопителями.

Если вам интересна эта тема и вы хотите узнать больше, я буду выступать с докладом о Параллельной репликации MySQL и MariaDB на Percona Live Amsterdam в октябре. Все переговоры от Booking.com это:

Мы также проведем Ужин Percona Live Community Dinner 4 октября. Вы также можете встретиться с нами там, если хотите узнать больше о интересных вещах, которые мы делаем на Booking.com.

[1] MySQL 5.7 и MariaDB 10.0 имеют немного другую реализацию идентификации параллелизма на мастере. MariaDB 10.0 использует оптимизацию групповой фиксации бинарного журнала, точно описанную в Части 1 (идентификатор групповой фиксации отображается в выводе MariaDB mysqlbinlog как cid). Начиная с версии 5.7.6, MySQL помечает каждую транзакцию двумя логическими временными метками (last_committed и sequence_number в выводе MySQL mysqlbinlog).

[2] MariaDB также имеет функцию параллельной репликации не по порядку, основанную на реализации GTID. Этот тип параллельной репликации может не фиксировать транзакции в том порядке, в котором они отображаются в двоичных журналах мастера. Чтобы воспользоваться преимуществами параллельной репликации вне порядка, приложение должно давать подсказки, чтобы объявить, что может выполняться параллельно. Это не тот тип параллельной репликации, который мы обсуждаем в этом посте (мы фокусируемся на упорядоченном типе).

[3] Разрешая фиксацию транзакций не по порядку на ведомых устройствах, MySQL может облегчить проблему, ранее обсуждавшуюся в Части 3, когда слишком маленькое значение для slave_parallel_threads может привести к неоптимальному ускорению.

[4] Опция slave-preserve-commit-order позволяет включить фиксацию по порядку в MySQL, но для этой опции требуется log-slave-updates. Я открыл ошибку № 75396, чтобы снять это ограничение, поскольку необходимость log-slave-updates для включения slave-preserve-commit-order выглядит как шаг назад:

  • Цель параллельной репликации — ускорить репликацию.
  • Согласно большинству наших тестов, log-slave-updates замедляет репликацию.

Более того, хранить копию мастер-транзакций на слейвах (log-slave-updates) не нужно, когда Бинлог-серверы уже хранят точную копию бинарных логов мастера. Таким образом, необходимость log-slave-updates для включения slave-preserve-commit-order является ограничением, без которого мы могли бы обойтись.

[5] Это число CREATE TABLE очень велико. Похоже, шаблон CREATE TABLE IF NOT EXISTS очень часто встречается у E1. Проверка существования таблицы и избегание CREATE TABLE, когда она уже существует, может быть большой победой для параллельной репликации в E1. А для тех, кто не верит, возможно 27 CREATE TABLE в секунду (мне тоже пришлось самому проверить):

$ for i in $(seq 4978 5003); do
  mysqlbinlog $(printf "binlog.%06d" $i) |
    grep -i "CREATE TABLE IF NOT EXISTS"; done | wc -l
211753

Хотели бы вы стать инженером в Booking.com? "Работать с нами"!