Сайт и интернет-магазин «Эльдорадо» — это около 40 тысяч покупок ежедневно. Объяснять, что это значит для бизнеса компании, наверное, не надо.
Исторически магазин работает на движке Bitrix с огромным количеством кастомного кода и дополнений. В качестве хранилища выступает кластер MySQL с четырьмя мастер-серверами.
Монолитные приложения есть у значительного числа компаний, и работать с ними приходится очень многим. Способов борьбы с монолитом предостаточно, вот только про удачные, к сожалению, пишут мало. Надеюсь, что рассказ о том, как мы подпираем наш монолит (пока его не распилили) будет вам интересен.
Мы прекрасно понимаем, что массивная архитектура может приносить много проблем. Но разрушить ее легко, простым волевым решением, невозможно: продажи идут, сайт должен работать, изменения для пользователей не должны быть радикальными. Поэтому переход от монолита к набору микросервисов занимает время, которое нам нужно продержаться: обеспечить работоспособность системы и ее устойчивость к нагрузкам.
В чем состояла проблема
Кластер БД на сайте eldorado.ru очень долгое время был построен вот по такой схеме:
Все мастера в этой схеме работают одновременно и все в активном режиме, последовательно проигрывая репликационный поток ...M1->M2->M3->M4->M1->M2->M3->M4->M1->M2…
В нашем сетапе такая конфигурация давала единственный плюс — она позволяла системе работать и держать его нагрузку. Дело в том, что балансировщик запросов приложения после любого апдейта для обеспечения консистентности переключает весь read поток на этот мастер, а одного мастера когда-то не хватало, чтобы держать весь поток чтения.
Но такая схема не могла обеспечить ни надежности, ни быстродействия работы. Хотя она выглядела просто, но у нее был целый ряд недостатков. Она очень медленно обновляла данные в кластере: в худшем случае было до пяти плечей репликации (в зависимости от того, на каком мастере изначально инициируются изменения). Из-за этих задержек множество проблем возникало и в работе сайта, и при оформлении заказов в интернет-магазине.
Минусы такой схемы:
- Слейвы самой отдаленной от активного мастера зоны получают обновление данных в худшем случае только через 4х время выполнения транзакции, иногда возникали бешеные задержки репликации;
- Любая авария на любом из мастеров приводит к неконсистентности данных на всем кластере до ее устранения;
- В некоторых случаях аппаратных аварий восстановление кольца без потери данных близко к невозможному (снятие-разворачивание из бэкапа — многие часы);
- Даже плановое отключение любого мастера автоматически требует либо снятия с нагрузки слейвов либо их переключения на другой мастер;
- Нет инструментов, которые могут нормально работать в такой топологии (и хорошо, что нет, не используйте ее);
- Любой тяжелый UPDATE/DELETE и даже SELECT при определенных обстоятельствах блокирует репликацию во всем кольце на время своего выполнения;
- Слейв до завершения выполнения транзакции еще не знает, что он отстает в slave_status по seconds_behind_master.
Вы можете в своих тестовых окружениях эмулировать такое поведение включив искусственную задержку репликации на 1-2 секунды на слейвах (что мы и делали), это отличный способ проверить ваше приложение на готовность к таким распределенным архитектурам через опцию MASTER_DELAY = N.
И, наконец, переход на другую БД в нашем случае — не вариант, потому что система слишком масштабна и многое в ней завязано на использование особенностей MySQL и даже на нюансы работы его внутреннего оптимизатора запросов.
Как мы ее решали
Мы не хотели изменять схему самостоятельно (это рискованная операция) и начали с поиска консалтинговой компании, которая могла бы предложить и развернуть новую архитектуру, причем сделать это так, чтобы сайт оставался доступен, а переключение не было бы заметным. Среди этих компаний были и самые крупные интеграторы, и разработчики ПО.
Часть компаний нам просто не ответили (и это нормально), а другие написали, что взяться за такую задачу они не готовы. При этом вопрос о возможной стоимости проекта даже не вставал.
Если большие интеграторы не хотят связываться с задачей, то самим решить ее стало вдвойне интересно. В нашем распоряжении было хорошее оборудование, проблемы со стабильностью и отказоустойчивостью стояли во втором приоритете, и в самом начале мы хотели хоть как-то обеспечить ускорение передачи данных. Для этого хорошо подходили несколько опций, которые появились в MySQL версий 5.6 и 5.7.
Правда, документация прозрачно намекала на то, что просто включить их не получиться, т.к. в кольце обязательно будет слейв с меньшей версией, а тут вот так:
The 5.7 master is able to read the old binary logs written prior to the upgrade and to send them to the 5.7 slaves. The slaves recognize the old format and handle it properly.
Binary logs created by the master subsequent to the upgrade are in 5.7 format. These too are recognized by the 5.7 slaves.
In other words, when upgrading to MySQL 5.7, the slaves must be MySQL 5.7 before you can upgrade the master to 5.7.
Для тестов нам было достаточно поднять тестовое кольцо, например через mysqld_multi, и погонять на нем типовые запросы (можно даже на одном хосте, 4 инстанса на разных портах с разными оффсетами), примерно так:
mysql -h127.0.0.1 -P 3302 -e "RESET SLAVE; RESET MASTER; SHOW MASTER STATUS\G;"
mysql -h127.0.0.1 -P 3302 -e "CHANGE MASTER TO MASTER_HOST='127.0.0.1', MASTER_PORT=3301, MASTER_USER='root', MASTER_PASSWORD='', MASTER_LOG_FILE='master1-binlog.000001', MASTER_LOG_POS = 107;START SLAVE;"
mysql -h127.0.0.1 -P 3303 -e "RESET SLAVE; RESET MASTER; SHOW MASTER STATUS\G;"
mysql -h127.0.0.1 -P 3303 -e "CHANGE MASTER TO MASTER_HOST='127.0.0.1', MASTER_PORT=3302, MASTER_USER='root', MASTER_PASSWORD='', MASTER_LOG_FILE='master2-binlog.000001', MASTER_LOG_POS = 107;START SLAVE;"
mysql -h127.0.0.1 -P 3304 -e "RESET SLAVE; RESET MASTER; SHOW MASTER STATUS\G;"
mysql -h127.0.0.1 -P 3304 -e "CHANGE MASTER TO MASTER_HOST='127.0.0.1', MASTER_PORT=3303, MASTER_USER='root', MASTER_PASSWORD='', MASTER_LOG_FILE='master3-binlog.000001', MASTER_LOG_POS = 107;START SLAVE;"
mysql -h127.0.0.1 -P 3301 -e "RESET SLAVE; RESET MASTER; SHOW MASTER STATUS\G;"
mysql -h127.0.0.1 -P 3301 -e "CHANGE MASTER TO MASTER_HOST='127.0.0.1', MASTER_PORT=3304, MASTER_USER='root', MASTER_PASSWORD='', MASTER_LOG_FILE='master4-binlog.000001', MASTER_LOG_POS = 107;START SLAVE;"
После этого можно менять версию любого инстанса запуская конфиг с нужным портом другим бинарником, который положили рядом, и производя mysql_upgrade для данных/системных таблиц.
На проде мы бы могли в течение ограниченного времени пустить весь трафик только на один мастер, обновить в это время другой, затем переключиться на него и дообновить все остальные. Но для этого нужно было обеспечить совместимость формата бинлогов между версиями, чтобы абсолютно все транзакции были проиграны успешно.
Еще немного полезной для нашего случая документации:
To avoid incompatibilities, set the following variables on the MySQL 5.6 master:
binlog_checksum=NONE
binlog_row_image=FULL
binlog_rows_query_log_events=OFF
log_bin_use_v1_row_events=1 (NDB Cluster only)
Этот флаг оказался для нас обязательным, хотя никакого NDB не используем, без него репликация заканчивалась между серверами 5.6 — 5.5, а mysqlbinlog читает лог без этой опции с ошибкой ERROR: Error in Log_event::read_log_event(): 'Sanity check failed', data_len: 8203, event_type: 30, если включить, то все даже заводится и работает.
GTID мы не стали включать, т.к. помимо требования к совместимости со всеми старыми инструментами, объективно не видим достаточного количества плюсов для перехода.
gtid_mode=OFF
Самый простой тест, чтобы проверить корректность работы репликации — залить дамп по очереди и на сервер с 5.5, и на сервер с 5.6 и посмотреть, все ли будет ок.
К сожалению, хотя вполне ожидаемо, тесты были неудачными.
Last_Error: Column 18 of table 'eldorado2.b_adv_banner' cannot be converted from type '<unknown type>' to type 'datetime'
datetime в 5.6 особенный, в него добавлены микросекунды, поэтому в 5.6 у него новый datetime, неизвестный в 5.5
версия 5.6 — может работать в кластере в кольце параллельно с 5.5, если при этом ни в одной из таблиц, которые бегают по репликации не создается полей с новыми типами полей. (datetime 5.6 != datetime 5.5, аналогично time, timestamp, всего в нашей бд таких полей больше 240).
Полностью гарантировать отсутствие DDL с этими полями мы не могли, а ставить под угрозу работоспособность всего кластера не хотелось. Но у нас был более безопасный «План Б».
Он подразумевал наличие дополнительного железа для маневров и поднятию полной копии кластера рядом, к нашему счастью, такое железо у нас было. А раз уж есть такая возможность, то надо было сделать сразу «нормальный» кластер.
Но, при этом надо обеспечить сохранение работоспособности всех текущих инструментов мониторинга, отладки, анализа бинлогов и максимально устранить все имеющиеся недостатки текущей архитектуры.
Многоканальная репликация
Нужна серебряная пуля, чтобы и сайты были целы, и админы сыты. Это – многоканальная репликация. Мы априори не доверяли новым возможностям и не были уверены в технологии, нигде такой информации или кейсов мы найти не смогли, публичного опыта в крупных production мало.
Поэтому все продумали сами, план был такой:
- От одного из слейвов мы начинаем разворачивать еще одно плечо: с нуля разворачиваем новый кластер сразу на версии 5.7, выстраиваем всю новую топологию уже с новыми опциями;
- Проводим все необходимые нам нагрузочные тесты и проверяем стабильность;
- Тюним конфигурацию, и если все хорошо — заново разворачиваем весь кластер, но уже не модифицируя никак данные, чтобы он был готов для последующего переключения продуктивной нагрузки.
— Каждый ублажает впереди стоящего, в то время, как стоящий позади, ублажает его самого. Вопрос заключается в том, кто из них счастлив в большей степени, нежели остальные? Я ошеломленно уставился на диковинную тварь с лицом усталой женщины. Так вот она, знаменитая «загадка Сфинкса»! На мой вкус, все это было как-то чересчур!
(Макс Фрай «Мой рагнарек»)
В целевой схеме 4 мастера — это 4 независимых друг от друга потока записи на каждый слейв, которые обрабатываются независимо.
На всех мастерах теперь можно было выключить log_slave_updates – им просто не надо теперь ничего никуда не нужно ретранслировать, они отдают все изменения в основном потоке (=>еще ниже нагрузка на мастера).
А заодно можно еще по пути включить минимальный формат бинлогов и параллельную обработку транзакций (достаточно условно, надо правильно ее понимать):
slave_parallel_workers=5
slave_parallel_type=LOGICAL_CLOCK
binlog_row_image=minimal
При таком сетапе мы в любой момент могли переключить нагрузку на новый кластер, но этот маршрут односторонний и отката не предусматривает.
На время жизни подключений к старому кластеру опция log_slave_updates на одном мастере-точке входа в новый кластер еще есть и поэтому все изменения из коннектов к «старому» кластеру отлично доезжают в новый, а сразу же после их отмирания эту опцию отключили, приложение же в этот момент смотрело на 3 других мастера и потоки данных никак не пересекались.
В итоге мы получили такой набор достоинств:
- Если долгий запрос и блокирует что-то, то это влияет только на один поток репликации из 4х и никак не влияет на другие;
- Новый формат бинлогов который раньше был невозможен просто из-за версии MySQL теперь занимает в разы меньше места и соответственно трафика, за счет этого намного больше изменений может проходить по всему кластеру;
- Теперь можно любой мастер выключить абсолютно безболезненно не влияя на все остальное;
- Аварии мастеров теперь не так страшны, теперь можно в любой непонятной ситуации за минуту склонировать любой сервер, перегенерить server_id, создать креды для доступа слейвов и — новый мастер готов.
Есть и «минус»:
- У каждого мастера стало намного больше слейвов и можно легче упереться в канал (на самом деле это не увеличение трафика, а перераспределение во времени и пространстве).
Что дала новая схема
Переезд на новую схему оказался успешным, мы провели его за один день, 28 августа 2020 года. Опыт использования новой архитектуры показал, что в три-четыре раза сократилось число проблем с репликацией (совсем избавиться от них невозможно). Повысилась стабильность системы. А главным результатом стало повышение максимальной пропускной способности системы. Если раньше разработчики любые непонятные проблемы могли списывать на репликацию, то теперь у них это не прокатывает.
Число клиентских проблем, порожденных задержками репликации, сократилось в несколько раз, а это значит, что клиенты стали испытывать хотя бы немного меньше боли. Теперь мы можем выключать когда угодно любой мастер-сервер, чтобы провести на нем работы. Это не оказывает влияния на весь кластер и не останавливает процесс репликации.
Кластер обслуживает основной сайт «Эльдорадо» – пока еще в большой части старое монолитное приложение с карточками товаров, личным кабинетом, корзиной, обработкой заказов, колл-центром и т.п. На момент написания статьи общая нагрузка на кластер на чтение (только на слейвы) составляет 40k rps, примерно по 5к rps на каждый сервер БД без учета технической нагрузки на отдельные технические слейвы, которая в пиковые моменты существенно выше. Может показаться, что это не очень много, но надо учитывать характер и сложность этих запросов.
Очень надеемся, что кому-нибудь пригодится наш опыт. Кроме многоканальной репликации мы также используем много интересных вещей, таких как blackhole и federated таблицы, они также позволяют снять очень много головной боли (и немного добавить тем, кто не понимает зачем они нужны), если кому-то интересны нюансы и любые другие вопросы по нашему MySQL – велкам в комментарии.
За пол года промышленной эксплуатации мы пока не столкнулись с какими-либо проблемами, связанными с многоканальностью и однозначно можем рекомендовать такой сетап, как достаточно отказоустойчивый и надежный.
Сам же монолит сейчас находится в процессе распила на некоторое количество отдельных и независимых сервисов, часть из которых мы будем шардировать и рассказывать вам о полученном опыте — следите за обновлениями.
Отдельное спасибо моей отличной команде, без нее мы бы это не сделали!
P.S. Кстати, нам по-прежнему очень нужны талантливые программисты. Если вы такой, приходите, будет интересно.
Cheburator2033
Спасибо, бро! Наконец-то нормальный хардкорный текст.