Статьи

Подходит ли синхронная репликация для вашего приложения?

Этот пост принадлежит Джей Янссену из MySQL Performance Blog.

Я общаюсь со многими людьми, которые действительно заинтересованы в Percona XtraDB Cluster (PXC), и в основном они заинтересованы в PXC как решении высокой доступности. Но то, о чем они обычно не задумываются, так это то, подходит ли им переход от асинхронной к синхронной репликации или нет.

Факты о Galera репликации

Здесь есть много разных фактов о Galera, и не всегда очевидно, как они повлияют на нагрузку на вашу базу данных. Например:

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

Закон Каллагана

Но что это все на самом деле означает? Ну, на конференции Percona Live несколько недель назад я услышал замечательный принцип, который действительно помогает инкапсулировать большую часть этой информации и соотнести ее с рабочей нагрузкой вашего приложения:

[В кластере Galera] данная строка не может быть изменена более одного раза за RTT

Это было приписано Марку Каллагану из Facebook Алексеем Юрченко из Codership во время выступления на конференции . Отныне это будет известно как «закон Каллагана» в кругах Галеры навсегда, хотя Марк не сразу вспомнил, что сказал это.

Применяется к автономному экземпляру Innodb

Давайте разберемся с этим немного. Наша единица блокировки в Innodb — одна строка (ну, индексная запись PRIMARY KEY для этой строки). Это означает, что обычно на одном узле Innodb мы можем иметь различные модификации, если они не касаются одной и той же строки. Блокировки строк сохраняются для изменений до тех пор, пока транзакция не завершится, и это по умолчанию переносит fsync в журнал повторов, поэтому, применяя закон Каллагана к Innodb с одним сервером, мы получим:

[На одном узле сервера Innodb] данная строка не может быть изменена дольше, чем время для fsync

Очевидно, вы можете ослабить это, просто не синхронизируя каждую транзакцию (innodb_flush_log_at_trx_commit! = 1), или обойти ее с помощью синхронизации с памятью (кэш-память с батарейным или емкостным доступом) и т. Д., Но принцип в основном тот же. Если мы хотим, чтобы эта транзакция сохранялась после сбоя, она должна попасть на диск.

Это не влияет на стандартную репликацию MySQL из этого экземпляра, поскольку репликация MySQL является асинхронной.

Как насчет полусинхронной репликации MySQL?

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

[На мастере полусинхронной репликации] вы не можете фиксировать (вообще) более одного раза за RTT. 

Применяется к кластеру Galera

В кластере мы также защищаем данные, но не за счет того, что они попадают на диск (хотя вы можете сделать это). Мы защищаем данные, обеспечивая их доступ к каждому узлу в кластере.

Но почему каждый узел, а не просто кворум? Ну, оказывается, порядок транзакций действительно, действительно имеет значение (действительно!). Применив репликацию ко всем узлам, мы можем (одновременно) установить глобальное упорядочение для транзакции, поэтому к тому времени, когда исходный узел получит подтверждение транзакции от всех других узлов, GTID также (по замыслу) будет установлен. В результате мы никогда не получим недетерминированное упорядочение транзакций.

Так что это возвращает нас к закону Каллагана для Галеры. У нас должна быть групповая связь для репликации и установления глобального порядка для каждой транзакции, и затраты на это для Galera составляют примерно один RTT между двумя узлами в кластере, которые находятся дальше друг от друга (независимо от того, откуда происходит фиксация!). Наименьшее количество данных, которое мы можем изменить в Innodb за один раз, — это одна строка, поэтому максимальная любая отдельная строка может быть изменена в пределах кластера только один раз за RTT.

А как насчет WAN кластеров?

Закон Каллагана применим и к кластерам WAN. Локальные сети обычно имеют RTT менее миллисекунды. WAN обычно имеют значение от нескольких мс до нескольких сотен. Это действительно откроет большое окно, где строки не смогут обновляться в лучшем случае больше, чем несколько раз в секунду.

Некоторые вещи правило не означает на Галере

  • Это НЕ означает, что вы не можете изменять разные строки одновременно. Ты сможешь.
  • Это НЕ означает, что вы не можете изменять данные на нескольких узлах кластера одновременно. Ты сможешь.
  • Он НЕ устанавливает нижнюю границу производительности, только верхнюю границу. Наилучшая производительность, которую вы можете ожидать — это модифицировать заданную строку один раз за RTT, она может стать медленнее, если время применения начнет отставать.

Так что насчет моего заявления?

Подумайте о своей рабочей нагрузке. Как часто вы обновляете какую-либо строку? Мы называем строки, которые сильно обновляются, « горячими точками ».

Примеры горячих точек

Пример 1.  Ваше приложение представляет собой онлайн-игру, и вы отслеживаете глобальную статистику достижений в одной таблице со строкой для каждой статистики; всего несколько сотен рядов. Когда игрок делает достижение, ваше приложение обновляет эту таблицу следующим заявлением:

UPDATE achievements SET count = count + 1 where achievement = 'killed_troll';

Сколько игроков может выполнить это достижение одновременно?

Пример 2.  У вас есть пользователи и группы в вашем приложении. Они хранятся в отдельных таблицах, а также существует таблица users_groups для определения отношений между ними. Когда кто-то присоединяется к группе, вы запускаете транзакцию, которая добавляет строку отношения в users_groups, но также обновляет группы некоторыми метаданными:

BEGIN;
INSERT INTO users_groups (user_id, group_id) VALUES (100, 1);
UPDATE groups SET last_joined=NOW(), last_user_id=100 WHERE id=1;
COMMIT;

Как часто несколько пользователей могут присоединиться к одной группе?

Полученные результаты

В обоих приведенных выше примерах вы можете представить множество одновременно работающих клиентов, пытающихся изменить одну и ту же запись одновременно. Но что на самом деле произойдет с клиентами, которые пытаются обновить одну и ту же строку в том же RTT? Это зависит от того, с какого узла в кластере поступают записи:

С того же узла: он будет вести себя так же, как стандартный Innodb. Первая транзакция получит необходимые блокировки строк во время фиксации (что займет 1 RTT). Другие транзакции будут блокировать ожидание, пока блокировка (и), в которой они нуждаются, будут доступны. Приложение просто ждет в тех случаях.

Из других узлов:  сначала совершить победы. Другие, которые пытаются зафиксировать ПОСЛЕ первого и пока первый находится в локальной очереди на своих узлах, получат ошибку взаимоблокировки.

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

обходные

Если ваши горячие точки были действительно плохими в автономном Innodb, вы можете подумать об уменьшении fsync: установите innodb_flush_log_at_trx_commit во что-то, кроме 1, и внезапно вы сможете обновиться намного быстрее. Я вижу эту настройку очень часто по причинам «производительности», когда надежность данных не так важна. Это нормально, если вы тщательно взвешиваете оба варианта.

Но в Галере нельзя расслаблять синхронную репликацию. Вы не можете изменить закон, вы можете только приспосабливаться к нему, но как вы можете это сделать?

Написать в один узел

Если ваша проблема действительно заключается в ошибках взаимоблокировки, а не в ожидании, вы можете просто отправить все записи на один узел. Это должно предотвратить ошибки взаимоблокировки, но не изменит блокировку в ожидании, которое ваше приложение должно будет сделать для горячих точек.

wsrep_retry_autocommit

Если все ваши горячие точки — это обновления с автокоммитами, вы можете положиться на wsrep_retry_autocommit, чтобы автоматически повторить транзакции для вас. Однако каждый автокоммит повторяется только количество раз, указанное этой переменной (по умолчанию 1 повтор). Это означает больше ожидания, и после превышения лимита вы все равно получите ошибку взаимоблокировки.

Это не реализовано для полных транзакций BEGIN… COMMIT, состоящих из нескольких операторов, поскольку нельзя предполагать, что они не применяют логику приложения между операторами, которые небезопасно повторять после изменений состояния базы данных.

повторить тупики

Теперь мы начинаем проникать на (* gasp *) территорию, где ваше приложение должно быть изменено. Как правило, если вы используете Innodb, вы должны иметь возможность обрабатывать ошибки взаимоблокировки в вашем приложении. Поднимите руки, если ваше приложение имеет такую ​​логику (я обычно получаю менее 5 человек из 100).

Но что делать? Автоматическая повторная попытка или предоставление конечному пользователю возможности повторить попытку вручную — типичные ответы. Тем не менее, это означает большую задержку в ожидании записи и, возможно, плохое взаимодействие с пользователем.

пакетная запись

Вместо того, чтобы обновлять глобальные счетчики по одному (из Примера 1, выше), как насчет поддержания счетчика в memcache или redis и только периодического сброса в базу данных?

if( $last_count % 100 == 0 ) {
  $db->do( "UPDATE achievements SET count = $last_count where achievement = 'killed_troll'";
}

измени свою схему

В приведенном выше примере 2, как выше показано перемещение столбца ‘join’ в таблицу users_groups, чтобы нам не нужно было так часто обновлять строку родительской группы?

INSERT INTO users_groups (user_id, group_id, joined) VALUES (100, 1, NOW());

Вывод

Выбор системы для репликации ваших данных в распределенную систему требует компромиссов. Большинство из нас привыкли к компромиссам, которые мы берем при развертывании обычного автономного MySQL Innodb с асинхронными ведомыми. Мы не можем думать о компромиссах, но мы их делаем (кто-нибудь одержимо проверяет положение раба, чтобы убедиться, что он догнал хозяина?).

Синхронная репликация с PXC и Galera ничем не отличается в том, что есть компромиссы, они просто не то, что мы обычно ожидаем.

Если закон Каллагана вызовет у вас проблемы, и вы не готовы адаптироваться к работе с ним, синхронная репликация PXC / Galera, вероятно, вам не подходит .