
Привет! Меня зовут Олег Кретинин, и я разработчик в команде общих компонентов в Яндекс Еде. Сегодня я расскажу о том, как мы смогли успешно снять нагрузку с нашей базы данных, а также уменьшить её размер.
Помимо сервисов, написанных на C++, Go и Python, у нас есть монолит, он же «кора», на PHP, который всё ещё представляет огромную кодовую базу, хранит кучу логики и предоставляет данные по API для 120 сервисов.
После обновления фреймворка и версии PHP мы принялись за решение другой проблемы, которая всё чаще и чаще давала о себе знать. В тот период у нас возросло количество инцидентов, связанных с базой данных, и нам нужно было что‑то придумать, чтобы стабилизировать проект максимально быстро. Случалось, что всё сыпалось во время праздничных дней, когда количество заказов увеличивалось на 30–40%, или во время разовых массовых операций, например когда однажды в большую сеть ресторанов добавлялся бесплатный соус к каждой позиции меню.
Исходные данные
Монолит на PHP 8.1, Symfony 6, 930 эндпоинтов (~400 из них — внешние ручки API), 216 консюмеров для обработки очередей и 209 прочих фоновых задач.
Основная БД — MySQL 5.7 в виде MariaDB Galera Cluster размером 4 ТБ+ и 400+ таблиц. Общий RPS ~100K. Базируется в трёх дата‑центрах, в каждом по связке master‑slave + несколько слейвов для аналитики.
HAProxy — отслеживает работоспособность мастера в ДЦ и при выходе из строя переключает на рабочий.
ProxySQL — по описанным правилам в виде регулярок распределяет, какие запросы пойдут в master, какие — в slave.
Задачи, которые нам предстояло решить
Повышенный CPU и ОЗУ на MySQL. В монолите полно легаси; добавленные ранее запросы не были рассчитаны на текущую высокую нагрузку и объём данных. К тому же никто не занимался анализом запросов в общей картине.
Из‑за того, что база содержит 400+ таблиц, сложно контролировать вносимые изменения в структуру при работе большого количества команд.
-
Flow Control (FC) — фича Galera Cluster, которая включается тогда, когда кластер понимает, что данных на изменение приходит очень много и они не успевают переливаться в другие реплики (упёрлись в диск, CPU, закончились воркеры и т. д.). Чтобы все реплики смогли догнать мастер и применить накопившиеся транзакции, flow control останавливает запись в мастере вовсе, что порождает следующие проблемы:
Ожидание записи в кластер, что ведёт к полному исчерпанию php‑fpm‑воркеров и, следовательно, к росту таймингов.
Из‑за отсутствия в реплике ожидаемых данных падают воркеры очередей и срабатывают ретраи, вследствие чего дополнительно повышается нагрузка на БД, а сама очередь начинает забиваться.
В коре есть использование Galera‑специфичной функциональности — wsrep‑sync‑wait. Она позволяет ходить в слейв с гарантией синхронизации. То есть если вы делаете запрос, а у слейва из‑за FC статус
!= "Synced"
, то мы будем ждать, пока он не синхронизируется. Из‑за этого мы получаем последствия в виде тех же растущих таймингов, забитых php‑fpm‑воркеров и очередей.
Как мы решали проблемы
Сначала мы распределили ответственность за таблицы по командам. Таблицы, которые не подходили ни под какой стрим, перешли к нам как к общим компонентам. В рамках активностей по закрытию техдолга мы стали еженедельно собирать slowqueries‑логи с помощью утилиты pt‑query‑digest и распределять между ответственными.
Для этого мы написали свой парсер логов, который позволял нам легко анализировать выгрузки и быстро находить самые плохие запросы. Основные проблемы мы поправили довольно быстро, и общий график стал выглядеть вот так.


Основная проблема была в модифицирующих запросах: любой резкий рост приводил к растущему Flow Control, а следовательно, и к остановке записи в мастер. Нам необходимо было определить самые проблемные зоны и начать с них. Для этого мы написали в приложении код, который парсил каждый SQL‑запрос, собирал информацию об участвующих в запросе таблицах, их типе, используемых полях и отправлял это в метрику. Так у нас появилось понимание самых используемых таблиц, их связей и типов нагрузок на них.

Очевидно, что функциональность с этими таблицами нужно выносить в отдельный сервис, но это занимает немало времени в виде прохождения ревью, выделения квот в инфраструктуре, назначения ответственных и т. д. А для нашей цели по ослаблению нагрузки на базу требуется более быстрый результат с использованием ограниченных ресурсов. Мы подумали: а почему бы просто не унести некоторые таблицы в отдельные базы, подменяя коннекшен в приложении? Так мы написали свой инструмент и назвали его TableSwitcher (TS).
TableSwitcher
TableSwitcher — обёртка над методами Doctrine, которая содержит в себе стратегии, позволяющие плавно переключить запись и чтение отдельных таблиц на новую БД. Стратегии переключаются последовательно, где финальная будет означать, что мы пишем и читаем данные только из табличек новой базы.
Принцип основан на оперировании первичным ключом (PK) конкретной записи. Из параметров эксперимента мы узнаём, какая база на данный момент является ведущей. Затем мы сохраняем строку в ведущую базу, получаем её PK (если он инкрементальный) и делаем запись во вторую базу с этим же PK. Когда данные в таблицах у нас идентичны, мы переключаем стратегию и делаем ведущей уже нашу новую БД, которая будет генерировать PK, а последовательная запись будет происходить в старую базу.

Полный флоу выглядит следующим образом:
Создание новой БД и добавление её коннекшена в проект.
Разрыв в коде всех джоинов с таблицами старой БД в виде замены отдельными запросами.
Замена всех обращений в БД в коде на TableSwitcher.
Переключение стратегии, где начинаем писать данные параллельно в новую базу.
Переливка отсутствующих данных из старой БД в новую.
Включение стратегии, где генерация ID и первоначальное сохранение происходит в новой базе, а затем в старой.
Если проблем не наблюдается, переходим на последнюю стратегию, где перестаём писать в старую БД.
Выпиливание TS и развилок с фича‑флагами.
Таблица возможных стратегий TS:
Стратегия |
Чтение |
Запись |
Генерация ID |
1 |
#1 |
#1 |
#1 |
2 |
#1 |
#1 и #2 |
#1 |
3 |
#2 |
#1 и #2 |
#1 |
4 |
#2 |
#2 и #1 |
#2 |
5 |
#2 |
#2 |
#2 |
Чтобы использовать такой подход, в выносимых таблицах не должно быть джоинов с таблицами основной БД и зависимостей от транзакций. Следовательно, не все нагруженные таблицы могут нам подойти из‑за большого количества связей и использований.
Поиск данных для переноса
Чтобы найти таблицы, которые можно легко вынести, мы написали скрипт. Он собрал все связи между сущностям Doctrine. Затем соединили это с метриками реальных запросов и загрузили данные в OrientDB. У нас получился граф, который помог визуально определить, какие логические группы и в какие новые БД мы можем унести.

Далее мы провели анализ получившихся групп и определили целесообразность переноса. Рассматривались следующие критерии:
Нагрузка. Таблицы должны быть в топе по нагруженности.
Использование в коде. Например, в нашем случае трудозатраты на вынесение таблицы orders, которая связана почти с каждым сервисом в коде, были бы колоссальные, поэтому данная и подобные ей таблицы не рассматривались.
Связанность с другими таблицами. Это таблицы явно выделенные в логические группы: например, группа таблиц логистики, логи, таблицы по информации о компенсациях или же одиночные таблицы без связей.
Актуальность работ. Важно быть на связи с другими командами: возможно, кто‑то уже в процессе реализации отдельного сервиса на основе этих таблиц и перенос для них неактуален.
Специфика запросов. Актуально, когда переезд происходит с одного типа БД на другой (в нашем случае с MySQL на PostgreSQL). Например, в raw‑запросах нужно поискать вещи типа
ON DUPLICATE KEY UPDATE
,GROUP_CONCAT()
,DATE_ADD
,SHOW VARIABLES LIKE wsrep_sync_wait
, функции по работе с JSON и т. д. То есть найти всё то, что не сможет выполняться одинаково на старой и новой базе, провести ресёрч и понять, возможно ли будет малой кровью повторить идентичное поведение.
Базой для переноса мы выбрали PostgreSQL, потому что это основная база для всех наших сервисов. В будущем это без проблем позволит прикрепить указанную базу на новый сервис. На основе наших критериев мы отобрали 13 таблиц, в которые преимущественно записывали данные и которые подходили под одну логическую группу, а конкретно — группу логов.
Старт переноса и решение возникших проблем
Начать мы решили с таблицы history. Она содержит историю изменений заказов, ресторанов, смены логистических параметров и пр. Эти данные отображаются в админке, используются в основном службой поддержки для разбора пользовательских обращений и не участвуют в важной логике. Средний RPS на запись в эту таблицу — 2к+, а весит она около 200 ГБ.
Первым делом мы удалили немногочисленные джоины и переписали на отдельные подзапросы. Потом реализовали классы TableSwitcher и написали скрипт по переносу данных. Но почти сразу столкнулись с проблемами.
Сразу же, на этапе сборки, из‑за отсутствия в окружении новой базы у нас посыпались функциональные тесты. Пришлось актуализировать окружение и добавлять PostgreSQL, миграции, фичафлаги, параметры, править envs.
Переключив TS на вторую стратегию, где мы начали писать параллельно в новую базу, мы сразу увидели растущие тайминги и забитые php‑fpm‑воркеры, из‑за чего пришлось откатить наш эксперимент. Получилось, что, выбирая параметры сервера для нашей БД, мы опирались только на ожидаемую нагрузку, но не учли одну вещь. Оказывается, у каждого доступного к выбору инстанса в Yandex Cloud своё количество коннектов к БД, пропорциональное мощности выбранного сервера. Эта проблема была решена простым увеличением флейвора БД. А чтобы не попасть в подобную ситуацию ещё раз, мы добавили в нашу функциональность эксперимента попроцентную раскатку.
Потом мы снова начали параллельно писать в обе базы, перенесли данные нашим скриптом и переключились на стратегию, где уже читали из новой БД и где PostgreSQL генерировал ID. В таком режиме мы работали несколько дней до тех пор, пока не произошло страшное и наш master не переключился на другой дата‑центр...
Так как в Yandex Cloud по умолчанию используется схема «один master + по одному slave в каждом ДЦ», то возникает потребность выяснить, в каком ДЦ сейчас находится мастер и какой хост использовать. Когда мы добавляли параметры подключения БД в сервис, то указали специальный единый FQDN‑хост, который умеет определять активного мастера. И всё работало хорошо, пока не выяснилось, что использование этого единого хоста не рекомендуется для использования в продакшене, потому что обновление CNAME‑записи при смене ДЦ мастера может составлять от пары секунд до 10 минут.
Как итог: из‑за долгой смены CNAME мы несколько минут пытались писать в slave, вследствие чего возросли тайминги и забились fpm‑воркеры. Естественно, последовал очередной откат эксперимента, и нужно было сделать всё заново.
Для решения данной проблемы мы решили использовать связку HAProxy + PgBouncer. Мы уже давно определяли активного мастера в MySQL с помощью HAProxy, и проблем с ним на нашем трафике никогда не возникало. Для определения мастера в PostgreSQL, помимо новых правил HAProxy, мы добавили свой кастомный.sh‑чекер. PgBouncer же нужен, чтобы сократить время на установку коннектов к нашим новым БД.
Конфиг
Файл haproxy.cfg
#объявляем точку входа, указываем порт к которому будет подключаться pgbouncer
frontend history_master_rw
mode tcp
bind :::6433,127.0.0.1:6433
default_backend history_master_rw
backend history_master_rw
mode tcp
#нативная проверка доступности хоста
option pgsql-check
option external-check
#кастомный чекер, который определяет мастер среди хостов
external-check command /path/check_pg_master.sh
#тут сами хосты и политики реконнектов
server history1 hostname1.db:5432 check inter 1s rise 2 fall 3 on-marked-down shutdown-sessions
server history2 hostname2.db:5432 check inter 1s rise 2 fall 3 on-marked-down shutdown-sessions
server history3 hostname3.db:5432 check inter 1s rise 2 fall 3 on-marked-down shutdown-sessions
Файл check_pg_master.sh
#!/bin/bash
PG_PSQL=/usr/bin/psql
# переменные которые прокидывает сюда haproxy
VIP=$1
VPT=$2
RIP=$3
if [ "$4" == "" ]; then
RPT=$VPT
else
RPT=$4
fi
STATUS=$(PGPASSWORD="password" $PG_PSQL "host=$RIP port=$RPT sslmode=prefer dbname=db_name user=username" -qtAX -c "select pg_is_in_recovery()" )
if [ "$STATUS" == "f" ]; then
# Master
exit 0
else
exit 1
fi
Файл pgbouncer.conf
[databases]
#перечисляем все базы, с портами в haproxy
history=host=localhost port=6433 dbname=dbname_history user=user password=password pool_mode=transaction
orders=host=localhost port=6434 dbname=dbname_orders user=user password=password
[pgbouncer]
#порт к которому будет подключаться наше приложение
listen_port=6455
listen_addr=127.0.0.1
pool_mode=session
log_connections=0
log_disconnections=0
log_stats=0
log_pooler_errors=1
syslog=1
ignore_startup_parameters = extra_float_digits
Таким образом мы решили проблему отказа мастера при переезде в другой ДЦ.
Далее нужно было восстановиться после отката эксперимента. Раз данные в MySQL снова ушли вперёд PostgreSQL, нужно было не только перенести отсутствующие записи, но и обновить уже существующие. Наш скрипт по переносу данных в первой версии умел делать только вставку отсутствующих строк, но не мог обновлять уже существующие записи, поэтому пришлось потратить ещё какое‑то время на доработку.
Стоило помнить, что при откатах с третьей стратегии, когда уже PostgreSQL генерирует ID, нужно обновлять sequence отдельным запросом. Потому что делая запись вместе с существующим ID, PostgreSQL, естественно, не делает nextval(), и инкремент остаётся в том же значении, на котором вы остановились.
С третьей попытки у нас всё‑таки получилось переключиться на последнюю стратегию и полностью перейти на PostgreSQL для данной таблицы.
Переключение остальных таблиц происходило тоже далеко не гладко. Вот пара интересных проблем, с которыми мы столкнулись.
При отвязке джоинов основное внимание мы акцентировали на запросах, сделанных через query builder, и в целом проблем не возникало. Но в огромной кодовой базе мы несколько раз пропускали разрыв связей в raw‑запросах вида:
"SELECT * FROM place_log pl JOIN places p ON p.id = pl.place_id"
. Получалось, что после полного переключения коннекта мы пытались притянуть таблицу places, которой в новой базе не существует.Были случаи, когда в основной БД произошел rollback, который, конечно же, никак не мог повлиять на rollback в новой базе. Следовательно, приходилось дописывать логику, вынося изменение записей новой БД в пост‑ивенты или оборачивая транзакцией из другого коннекшена.
Воркеры очередей у нас представлены демонами, в которых есть специальная стратегия восстановления подключения для MySQL, если переключился дата‑центр или база на какое‑то время моргнула. Для новых postgres‑подключений такой стратегии не было добавлено. Так что в один прекрасный день у нас посыпалась обработка задач в очередях, потому что подключение с базой было разорвано, а скрипт‑демон продолжал обрабатывать приходящие задачи. Быстро поправить это получилось только ручным перезапуском воркеров, но были потери в заказах.
Несколько выводов о подходе
Чтобы использовать TableSwitcher‑подход, требуется много подготовки. На слабосвязанных таблицах он показал хороший результат. Однако работа по написанию кода и правке тестов для вынесения таблиц в отдельный кластер равна работе по переходу на обращение в сервис по API.
Под несложную логику лучше сразу закладывать интерфейс, который в будущем позволит заменить обращения в новую БД походом в сервис.
TableSwitcher‑подход позволяет быстрее разгрузить базу. Но для полноценного распила монолита всё равно придётся написать код перехода на эндпоинты сервиса и поправить тесты, поэтому см. п. 2.
Система с бо́льшим количеством узлов менее устойчива, и есть определённые риски при отказе какой‑то из БД.
Всегда нужно ставить задачу на удаление развилок с экспериментами. Нередки случаи, когда из‑за дефолтного состояния фича‑флага тесты запускаются на старой функциональности.
При переключении стратегии, когда пишем в обе базы, нужно учитывать небольшое увеличение таймингов.
Когда у вас больше одной базы данных, вы рискуете словить проблемы с транзакциями. Разработчики могут не понять, что внутри метода используется два разных коннекта, и обернуть в транзакцию в надежде, что всё откатится в случае необходимости.
Первым делом внедряйте разные метрики, чтобы правильно понять ситуацию и прикладывать усилия в нужном направлении. Наши метрики по запросам и граф связей таблиц нам очень помогли.
Обязательно нужно предусмотреть постепенную раскатку функциональности и держать под рукой достаточно графиков, так как проблемы в одной базе могут затронуть весь сервис (как в нашем случае при переполнении пула коннектов).
Дополнительные работы
Наша база всё ещё живёт на железных выделенных серверах, а не в облаке. У неё нет общих унифицированных инструментов диагностики, мониторинга, восстановления и обновления, как в Yandex Cloud, поэтому возникает сложность в поддержке со стороны отдела эксплуатации. Также у нас нет возможности масштабировать эти железки, поэтому изначально, с запасом, было заложено очень много ресурсов. В совокупности это 6 ТБ ОЗУ и 1536 ядер, что очень нерационально и дорого. Поэтому следующее вложение в стабильность — перенос базы монолита в YC.
В отделе эксплуатации подсчитали: чтобы перенести базу с наименьшими рисками, она должна весить не более 750 ГБ против 4 ТБ на тот момент. То есть не распиливая монолит полностью, нам нужно было сократить количество данных в таблицах более чем в пять раз.
Частично наша задача начала продвигаться уже на этапе вынесения таблиц, но уменьшение базы до нужного размера таким подходом требует огромных трудозатрат. Да и невозможно отлинковать все таблицы, не распилив монолит полностью.
Мы стали пользоваться более простыми подходами. Сначала при помощи запроса…
select * from (select table_name, round(((data_length + index_length) / 1024 / 1024 / 1024), 2) `GB_size`, update_time from information_schema.tables where table_schema = "eda") sub order by sub.GB_size desc
…опираясь на поле updated_time
, мы отсеяли забытые таблицы со старыми данными, которые, например, забыли почистить после переноса логики на отдельный сервис. Тем самым сократили больше 150 ГБ.
Просмотрев записи самых тяжелых таблиц, мы увидели, что есть данные, датированные ещё 2015 годом. Для аналитиков у нас используются другие хранилища, которые ежечасно обновляются, а значит, такое долгое хранение в основной БД нам не требуется. Эти таблицы мы добавили в ежедневные чистки по расписанию и освободили больше 300 ГБ.
Затем мы организовали встречу с ответственными командами и решили изучить детальнее контент таблиц и их потребителей. Мы смогли избавиться от таблиц, в которые давно ничего не пишем. Но при этом задумались: что, если есть таблицы, в которые мы продолжаем до сих пор что‑то писать, но эти данные никому не нужны?
Встал вопрос, как определить такие таблицы. На тот момент монолит содержал 900+ эндпоинтов. Мы знали, что далеко не все ручки актуальны, поэтому собрали по access log nginx все обращения в монолит и отсеяли неиспользуемые. Далее выгребли из кода все репозитории из этих эндпоинтов и получили названия таблиц, по которым ещё раз прошлись в коде и убедились, что больше нигде на чтение они не используются.
Казалось бы, сомнительный подход, но он позволил нам полностью отказаться от трёх таблиц общим весом 150 ГБ.
Некоторые индексы на таблицы добавлялись разными разработчиками и под разные задачи. Возможно, некоторые индексы уже не используются. Это мы проверяем посредством такого запроса:
SELECT DISTINCT stat.table_name,
indx.index_name,
cntRows,
round((stat_value * @@innodb_page_size) / 1024 / 1024 / 1024, 2) `SizeinGB`
FROM sys.schema_unused_indexes indx
INNER JOIN mysql.innodb_index_stats stat on stat.index_name = indx.index_name AND stat.stat_name = 'size'
LEFT JOIN (
SELECT TABLE_NAME, SUM(TABLE_ROWS) as cntRows
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'eda'
GROUP by TABLE_NAME
) cntTable ON cntTable.TABLE_NAME = stat.table_name
В нашем случае такие индексы нашлись, но занимали совсем немного места, поэтому данный подход для нас был не сильно продуктивным. Важно помнить, что данный запрос нужно смотреть на всех хостах, потому что есть запросы, которые ходят только на мастер, а значит, на слейве данной статистики по индексу не будет, хотя по факту он используется.
Такими нехитрыми способами мы смогли срезать ~600 ГБ данных. Но до требуемого размера в 750 ГБ ещё далеко, и надо решать, что делать с остальным данными.
Исторически сложилось, что информация о заказе в Яндекс Еде хранилась несколько месяцев. Некоторые данные, например таблицы с составом заказа, мета‑информацией, историей логистики, составляют львиную долю от всего хранимого в базе. Связанность у сущностей очень крепкая, отделить и унести пул таблиц не представляется возможным. Поэтому мы стали разбираться: а для каких процессов вообще требуется долгое хранение заказа?
Обойдя все группы, мы выяснили, что старые данные о заказе используются только командой поддержки клиентов, конкретно — для выдачи компенсаций, а также для выборочных сверок, которые могут происходить спустя месяц после завершения заказа. Мы собрали информацию об используемых данных и смогли составить список сервисов, в которых эта информация уже есть. В течение нескольких недель мы перевели походы в БД «на ручки» этих сервисов, а следовательно, уже было не нужно хранить данные о заказах в монолите. Мы добавили ежедневные чистки и оставили хранение информации по заказу только на месяц. Именно этот шаг помог нам дойти до заветного размера базы в 750 ГБ.
Итоги
С помощью нашего инструмента TableSwitcher у нас получилось унести 27 из 400+ таблиц в пять разных postgres‑кластеров. Общий вес вынесенных таблиц составил ~600 ГБ, а RPS сократился на 7K (в основном пишущего трафика). Проблема с Flow Control на Galera Cluster нас перестала беспокоить, а проблема с ресурсами теперь не стоит так остро из‑за тенденции к снижению нагрузки.
Ещё пять таблиц логистики у нас так и не вышло перенести, потому что использовалась довольно сложная логика с блокировками, транзакциями и ретраями. После нескольких неудачных попыток переключения стратегий мы просто переключились на перенос новой группы таблиц.
Наш ресёрч и незамысловатые подходы к оптимизации и хранению данных помогли уменьшить размер базы в несколько раз, тем самым разблокировав проект по переносу нашей БД в облако. Это не менее увлекательная, но уже совсем другая история.
Главная идея этой статьи — не предоставление пошагового гайда, а подсветка проблем, с которыми столкнулись мы и с которыми может столкнуться читатель при решении такой типовой задачи, как уменьшение размера и нагрузки БД. Само собой, используемые подходы зависят как от специфики проекта, так и от технических требований, поэтому может быть придумано огромное количество реализаций. Было бы интересно узнать, а какими подходами пользовались вы? Делитесь своим опытом в комментариях.
Комментарии (29)
QA-smoothie
03.02.2025 07:33Добрый день, есть несколько вопросов!
Почему вы переносили данные в postgres, а не YDB? есть же своя собственная БД с которой больше экспертизы.
В каких целях вы успользуете galera? из статьи ясно что у вас есть проблема с FC, но зачем galera не раскрыто.
Зачем вы использовали TableSwitcher + "собирали данные из проложения" кроме того, что описано в статьи ? данные операции покрывает proxysql.
Зачем хранить в реляционной СУБД архивные данные? Кажется Clickhouse от Яндекса как для этого.
Почему не использовали kafka, CDC коннекторы при миграции?
Какие еще варианты решения проблемы рассматривали и почему отвергли их?
Статья больше показывает, что вы использутете инструменты которые не умеете готовить и ловите проблемы (мое личное мнение), 4ТБ, всего 400 таблиц и всего 100rps - но это нормальная нагрузка для многихSir_Duk
03.02.2025 07:33Автор писал про 100К RPS. Это невероятно много, по сравнению с реальным количеством обращений.
QA-smoothie
03.02.2025 07:33100k rps - не очень много для современных СУБД
Плюс там не указано сколько нод, но указана галера, а это 3 ноды + слейвы + аналитика и так 3 раза (3 ДЦ)
итого на инстанс меньше 10к rps (при условии, что нормально настроена маршрутизация запросов ) - смешной показатель
tigroid3 Автор
03.02.2025 07:33postgres, а не YDB?
Привет! Конкретно в нашем стриме, у нас экспертизы все таки больше с pg, все наши сервисы по дефолту работают с pg.
В каких целях вы используете galera?
У нас все таки легаси монолит) Яндекс.Еда как проект, изначально принадлежал и разрабатывался другой компанией (до того как его купил Я.), решение в виде использования galera cluster принималось видимо по каким то своим соображениям, актуальным на тот момент. Могу предположить, что использовали из-за multi master схемы, которая одновременно позволяет и разгрузить бд, и особо не прилагать усилий для поддержки логики по получению неконсистентных данных из слейвов
Почему не использовали kafka
kafka в нашем проекте у нас нет вообще, а затащить ее только ради этого очень дорого
Зачем хранить в реляционной СУБД архивные данные? Кажется Clickhouse от Яндекса как для этого., CDC коннекторы при миграции?
Как уже сказал, хранятся они там по причине "исторически так сложилось, никто не переделывал". Использовать clickhouse актуально только для таблиц логов, а как я писал выше, переносили мы таблички не только с такими данными. Нужен был какой то универсальный инструмент, который могли бы использовать другие команды для своих таблиц.
Так же, все решения, которые нельзя реализовать в рамках приложения, требуют привлечения ребят из инфраструктуры, а это дополнительные ресурсы и время. В нашем решении пришлось привлечь ребят из инфры только чтобы получить квоты на создание баз. Остальное уже чисто руками разработчиков
Какие еще варианты решения проблемы рассматривали и почему отвергли их?
Был вариант например, с вынесением таблиц при помощи DataTransfer, но нам он не подошел потому что там нет плавной раскатки. К тому же, при переключении приложения на новый коннект потребуется какой то небольшой downtime.
P.S. Хотя с помощью него мы уже успешно мигрировали базу в облако)
100k rps - но это нормальная нагрузка для многих
100к рпс да, это нормально, но у нас и не было проблемы с читающим трафиком, проблема именно с пишущим, когда прилетала большая пачка модифицирующих запросов и срабатывал flow control.
QA-smoothie
03.02.2025 07:33Спасибо за ваши ответы! Всё стало намного прозрачнее – вы просто избавляетесь от легаси в пользу того, где больше экспертизы. Желаем вам удачи!
P.S. У нас Percona XtraDB Cluster (3 ноды, одна из них мастер), перед ними ProxySQL. В среднем нагрузка — 180K QPS, из них модификации — 12K QPS. Редко wsrep-очереди стреляют 500+.
Это к тому, что Galera без проблем держит большие нагрузки. Но, как вы верно отметили, в первую очередь важно бороться с медленными запросами.
acsent1
03.02.2025 07:33Те получается вы начали пилить велосипед, потому что у вас нет нормального взаимодействия между отделами?
tigroid3 Автор
03.02.2025 07:33не совсем понял откуда у вас такой вывод получился? вроде про проблемы взаимодействия с другими отделами я ничего не писал
anaxita
03.02.2025 07:33Такое мнение сложилось после нескольких упоминаний про то что самим быстрее чем девопсов привлекать, сколько времени заняла эта фича кстати в днях?
tigroid3 Автор
03.02.2025 07:33Изначально мы стали говорить про clickhouse и kafka, эти решения тяжелые и не универсальные для нашего случая, поэтому я в дополнение к тому, что они нам не подходят, добавил что это требует дополнительных сил в виде инфраструктуры) Проблем в коммуникации нет, просто у каждого свой бэклог/цели и нельзя просто принести огромный проект который сразу же все начнут делать
сколько времени заняла эта фича кстати в днях?
Первый десяток табличек где то за 3 месяца в 1 руки. Это со сборами всяких аппрувов под квоты на базы, написание кода и переключение. Когда попробовали и написали доку, тогда уже остальные команды стали разносить свои таблички, но опять же, занималась не фулл тайм. Итого где то до полу года ушло на все от старта, до решения основных проблем.
rukhi7
03.02.2025 07:33такой вывод напрашивается хотя бы при взгляде на такую картинку:
обычно такая картинка получается когда третья рука справа не знает что делает 5-я рука слева, и они часто делают одно и то же и еще и жалуются что пальцев на все не хватает. Но с другой стороны если бы не было такого богатства вам бы нечего было делать и эта статья не появилась бы.
Вам успехов в борьбе с космической запутанностью.
К сожалению всегда есть опасность что система заставит вас переродиться и вы начнете добавлять запутанность вместо того чтобы уничтожать ее. Не поддавайтесь!
xEpozZ
03.02.2025 07:33Отличная статья.
Изменились ли показатели на первых графиках с временем выполнения запросов в бд? Или срез пика в прошлом году это и есть итог ваших работ?
Не кажется ли средний показатель в 300 мс неприемлемо большим?
Как несколько коннектов повлияло на rps монолита?
Стал ли haproxy чаще сбоить? Замечал сбои при подобных распилах
tigroid3 Автор
03.02.2025 07:33Изменились ли показатели на первых графиках с временем выполнения запросов в бд? Или срез пика в прошлом году это и есть итог ваших работ?
В целом, не изменилось. Так и есть, пик показывает, что основные проблемы были исправлены в конце 23, когда собрали самые проблемные запросы, всеми навалились и исправили. На остальном таймлайне просто хотелось показать, что мы продолжали мониторить запросы и в случае появления новых проблем их быстро правили. Сейчас, по выгрузкам мы видим, что "стреляющие" случаи - единичные, поэтому вкладываться в их оптимизацию нет имеет смысла.
Не кажется ли средний показатель в 300 мс неприемлемо большим?
А это же только в рамках выборки slow queries, а не всех запросов базы. Т.е. в эту выборку может попасть например 10 запросов за неделю, а для такого незначительного кол-ва думаю что приемлемо. К сожалению график собирал с агрегированных данных уже, поэтому кол-во этих запросов на тот момент посмотреть не получится
Как несколько коннектов повлияло на rps монолита?
RPS в монолите не изменился никак, вроде и не должен был) Может имелось ввиду тайминги? Если да, то в моменте, когда писали в обе базы, на некоторых эндпоинтах выросли на ~100мс, после полного перехода тайминги пришли в норму
Стал ли haproxy чаще сбоить? Замечал сбои при подобных распилах?
Нет, проблем не наблюдали, все как часики
NeoNN
03.02.2025 07:33Как редко компании рассказывают про свое легаси, на котором все держится, одни микросервисы да микросервисы на конференциях, а работать начнёшь- там пхп, сям хранимки, а ещё местами C++ код 20-летней давности...
marliotto
03.02.2025 07:33Как решалась проблема, если удавалось сделать запись только в одну БД, а вторая была недоступна или же происходил откат транзакции?
tigroid3 Автор
03.02.2025 07:33Хороший вопрос)
или же происходил откат транзакции
Т.к. TS у нас последовательный, то в обертке это выглядело по типу conn1->rollback();
conn2->rollback(); тут вроде проблем не словилиесли удавалось сделать запись только в одну БД, а вторая была недоступна
На эту ситуацию у нас был алерт и мы запускали вручную скрипт чтобы синхронизировать данные за какой то интервал времени. Но за весь период переноса, такая ситуация возникала буквально пару раз и то, потому что мы долго задерживались на стратегии когда писали в обе базы.
rukhi7
03.02.2025 07:33действительно хорошая статья. В яндексе появились разработчики, которые разгребают мусорные завалы, а не насыпают в них. А главное появилась понимание необходимости разгрести эти завалы. Похоже можно подумать о покупке акций кампании.
onets
03.02.2025 07:33Не очень понятно с кластером - в начале статьи 3 ДЦ и в каждом мастер и слейв, а потом - один переходящий мастер и три слева.
Но в любом случае вопрос - а можно было сделать какой-нибудь статичный шардинг ключ? По регионам например? Тогда одну базу можно разбить на 89 меньшего размера. Хотя Москва и Питер все равно останутся самыми нагруженным.
tigroid3 Автор
03.02.2025 07:33Может где то некорректно выразился но да, 3 ДЦ, в каждом master+slave. Galera cluster же это заворачивает в multi master схему, что позволяет писать в любой мастер из ДЦ. Слейвы для чтения, либо для замены вышедшего из строя мастера рамках ДЦ
Тогда одну базу можно разбить на 89 меньшего размера
А не было необходимости вроде. Проблема же не в том что данных очень много и выборки медленно работают, а в том что из за большого количества вставок в базу в моменте, данные не успевали доезжать до реплики из за чего мастер останавливался. Т.е. решать надо было что то с модифицирующими запросами. Вот тут про шардирование и репликацию ответил)
Pusk1
03.02.2025 07:33Может невнимательно читал, но обычно начинают с партиционирования нагруженных транзакционных таблиц. Если там лежат исторические данные, то просто по дате. Тогда большинство запросов будут работать только с одной партицией за последний месяц или нделю, а данные попадать в кэш. То есть, памяти и дисковых операций потребуется на порядок меньше, как и CPU. Требует ревью нагруженных запросов.
При заявленном RPS и размере БД такого подхода обычно хватает.
Ещё можно разделять на партиции по ID клиента, номеру партнёра или ещё как-то. Это обычно более заморочено и не всеми БД поддерживается разделение на партиции сразу по нескольким полям. Но это больше про классическое шардирование, когда сервера физически можно разделить, а справочники реплицировать между ними.
Ежедневные или еженедельные чистки по расписанию тоже отлично, если бизнес позволяет. Обычно приходится сливать куда-то данные для аналитики и отчетности при таком подходе. Тут вы большие молодцы:)tigroid3 Автор
03.02.2025 07:33Наша проблема же заключалась в том, что у нас срабатывал flow control при большом RPS на insert, данные не успевали засинкаться с репликами (а galera cluster - это multi master схема). Поэтому не думаю, что партиции бы нам помогли в данном случае, ибо объем данных не меняется
Классическое шардирование возможно бы помогло, но тут несколько моментов:
Неизвестно, как себя поведет тот же галеровский wait sync с бОльшим количеством нод. Т.е. если один шард притупит, есть вероятность словить еще больше проблем (это просто предположение)
В конечном итоге, после таких оптимизаций, данные то никуда не денутся и в ближайшем будущем их все придется разносить по сервисам) А тут мы вроде и подготовились к реализации нового сервиса, перенеся данные в отдельную базу), и тут же нагрузку сняли с базы монолита
Pusk1
03.02.2025 07:33Если вы не в скорость вставки, а в скорость сихронизации упирались, то полность согласен, что кластеризация бы не помогла. Ну и всё равно пились потихоньку на микросервисы большую и распухшую базу. Так что отличный первый шаг по разделению получился:) Успехов!
Dhwtj
03.02.2025 07:33В совокупности это 6 ТБ ОЗУ и 1536 ядер
как монолит на этом работал? чуть подробнее про это, плиз
tigroid3 Автор
03.02.2025 07:33Это ресурсы не монолита, а дедиков, на которых базировался mysql. У нас 3 ДЦ, в каждом по master+slave, итого 6 ног. Ресурсы одной ноды - 1ТБ ОЗУ и 256 ядер.
На вопрос почему так много, нет точного ответа, но предполагаю, что эти сервера были собраны еще до того, как Еда, как проект, стала принадлежать Яндексу. Просто собрали сервера сильно с запасом, выкрутили настроечки в виде threads и buffer_pool_size в максимальные значения и в таком виде продалось Я. Ну а нам в целом, этого хватало с лихвой.
yurchenko_oleg
03.02.2025 07:33Похоже, что проблемы с производительностью наступили по причине большого объёма данных. Не только большое количество изменений данных мешает синхронизации реплик, но и нагрузка самих реплик запросами. Достаточно дать на реплику слишком большую нагрузку запросами, и синхронизация сломается.
Для решения проблем достаточно было почистить базу от ненужных старых данных. Это уменьшает нагрузку на реплики запросами чтения данных. Запросы по маленьким таблицам требуют меньше ресурсов, чем по большим таблицам. Вот и освобождаются ресурсы для синхронизации.
Не надо было делать недостаточно продуманные и протестированные изменения на production. Привезли некоторые проблемы с согласованностью данных и доставили неприятности бизнесу и клиентам («но были потери в заказах»).
tigroid3 Автор
03.02.2025 07:33У вас есть какие то подробности или может материалы по поводу того, что селективная нагрузка или объем данных (который не переливается в данный момент) влияет на синк реплик?
Binlog же не хранит никакой информации о старых данных, а тем более информации о селектах, поэтому нет никакой причины тормозить синк. Единственное место которое я вижу, где может повлиять размер таблиц - это неоптимизированные запросы, в которых используется какой нибудь file sort.
bxu
Наконец-то Яндекс признал, что насамом деле все там держится на PHP
onets
И mysql 5.7
Dhwtj
пытаюсь поддерживать легаси проект на PHP после C# и это ужасно
Dadadam999
Советую рефакторить и перелезать на 8+ версию. Вкатывался тоже после C#, но повезло, во всех проектах активно использовали ООП и поэтому чувствовал себя хорошо.
А так легаси на php это боль. Желаю поскорее его привести к нормальному виду)