Спасаем базу от разрушительной нагрузки

Спасаем базу от разрушительной нагрузки

Кирилл Егоров

Мой канал: https://t.me/HeadOfWeb

Доклад для: https://t.me/php_nn


Интересный кейс по оптимизации нагрузки на БД в огромном enterprise проекте.


Вводные данные

Достался огромный проект, которому более 10 лет. Он представлял из себя: гигабайт исходников, порядка 300 модулей в админ панели, несколько крупных сервисов, сайт, клиентские кабинеты, рабочие места сотрудников.

Около 30000 QPS на одну из основных баз данных MySQL. Позже начала доходить до 40000 QPS в пике.

Репликация Master-Master на 2 ДЦ + по несколько реплик в каждом ДЦ.

Проект и компания стремительно растут, нагрузка за год удваивается.

Необходимо обеспечить бесперебойную работу 24/7 с жестким SLA.


Жалобы

  • периодически сервер MySQL уходит в себя. Приходится переключаться на резервный;
  • огромное количество дедлоков;
  • из-за зависания бд в полку уходят PHP сервера;
  • предложения пользователям показываются медленно, время ответа может занимать до 30с;
  • все возможные оптимизации настроек сервера БД сделаны;
  • трюки и вмешательство DevOps и разработчиков иногда помогают исправить ситуацию;
  • все упирается в базу;
  • запас ресурсов сервера на 1-2 месяца, с учетом роста нагрузки.

На рефакторинг времени нет, встанут продажи.

Топ менеджмент компании в очередной раз принимает решение закупить более мощный сервер в стойку (64x2 CPU в HT, 1TB RAM) (железо свое), очень дорого и больно, но выхода нет.

Новые сервера закупаются, ситуация из уровня критической переходит в среднее напряженное состояние. Запас мощности около 50%.

Компания делает очередной успешный виток в своем развитии, нагрузка начинает расти быстрее. Очевидно, что ресурсов железа хватит на 3-4 месяца, максимум пол года. Сервера в 2 раза мощнее текущих стоят уже просто космических денег, к тому же их нужно несколько.

CIO и акционеры дают добро на выделение команды под рефакторинг. Забегая вперед, скажу что рефакторинг занял около года, проходил поэтапно. Разработка новых сервисов не останавливалась, команда фактически тратила на рефакторинг только половину своего времени.

Для начала определили источники основных болей БД

  • система ценообразования;
  • сервис формирования прайс-листов по расписанию;
  • система обновления цен и остатков (конфликтует с системой ценообразования);
  • бизнес логи, уведомления, версии документов;
  • нагрузка на веб сервисы (API для клиентов);
  • качество кода основного сайта, лишние запросы, отсутствие кэширования.

Что уже было в процессе на тот момент:

  • часть сервисов выносили в отдельную БД (они хоть и не давали большой нагрузки на CPU, влияли на объем реплицируемых данных);
  • новые таблицы в базе не создавались.

Система ценообразования

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

Суть системы в онлайн расчете цены для клиента на большие пачки товаров. Предложение может достигать 150 000 товаров. Для каждого товара существует обилие правил отображения, скидочных политик, политик выставления цены. Усугубляется все тем, что предложения персональные и формируются динамически, зависят от нескольких сотен условий, т.е кэшированию и перерасчету на поддаются.

Просчеты в архитектуре

Под запрос клиента создается временная таблица в памяти MySQL, которая содержит результаты. Сами результаты получаются из подсистемы поиска, вставляются во временную табличку где поэтапно (больше 20 этапов подготовки) идет обработка этих результатов, делаются JOIN условий, делаются Insert Select из других таблиц. Есть даже этапы выгрузки временной таблицы в файл и обратной загрузки во временную таблицу. Так же есть пара внешних сервисов дополнительных расчетов.

MySQL работает калькулятором цен, при этом такая архитектура не масштабируется. Программисты написали код (сложные и хитрые запросы), который основную работу перекладывает на БД (удобно же). Проблема в том, что мы не можем добавить калькуляторов.

Болевые точки:

  • большая временная таблица под каждый запрос;
  • сложные запросы до 30 JOIN (работали медленно);
  • Insert Select’ы (вызывают shared next-key locks), как только менеджеры заливают правила (а заливают часто);
  • экспорт в файл импорт из файла;
  • неэффективный кусок кода на nodeJs (архитектурные проблемы).

Детали того как и на что переписывали - тема для отдельной статьи. Постараюсь коротко.

Чтобы не получить факап с запуском переписанного с нуля проекта, решили не трогать структуру хранения данных. Все таблички остались прежними, все форматы запросов и ответов API так же остались прежними, переписывали только код.

Единственное что было изменено в структуре - отказались от временных таблиц, результаты хранили в памяти приложения, переписали все SQL запросы. Полностью отказались от insert select и перегонки через файл.

Новая версия приложения держала большое количество кэша в runtime, не сбрасывая его между запросами (RoadRunner).

Переписали алгоритмически неэффективный кусок кода с NodeJs на PHP 7, сэкономили на очередях (nodeJs общался с приложением через RabbitMQ). Интересная часть, так же заслуживающая отдельного рассказа. Если в 2ух словах, изначальный алгоритм проверял до 80 000 комбинаций правил, нашли как уменьшить количество комбинаций за счет предсказания, какие правила не будут применены, или их результаты будут переопределены. Снизили до 5000 - 7000 комбинаций.

Добавили несколько более оптимальных индексов в БД.

Запуск

Новую версию запускали в 4 крупных этапа (приложение поделили на 4 части). При этом одновременно работало и поддерживалось старое и новое приложение. Полный feature freeze объявить не удалось, были моменты когда приходилось добавлять новые возможности в оба приложения, но большие хотелки отложили в бэклог как менее значимые для бизнеса.

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

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

Решение оказалось значительно быстрее:

  • предложения по наличию в 3-5 раз;
  • предложения под заказ в 4-6 раз;
  • формирование прайс-листов в 10 раз.

Подсистема API не давала особой нагрузки, не сравнивали.

Естественно и сам код написали более эффективно.

Результаты:

  • снизилась нагрузка на CPU;
  • освободилась RAM;
  • значительно увеличилась скорость ответа (особенно порадовало пользователей);
  • пропали взаимные блокировки с системной обновления цен и остатков;
  • новый сервис значительно проще поддерживать, добавлять функционал.

Сервис формирования прайс-листов по расписанию

Сервис управлялся менеджерами, они указывали время начала сборки прайс-листа, планировщик честно запускал сборку.

В момент, когда базе становилось плохо, сервис мог запустить 30-40 процессов по формированию прайс-листов одновременно, что значительно ухудшало положение с блокировками.

Сам механизм формирования прайс-листов запускал расчеты по системе ценообразования, не на одну позицию, а на все наличие указанных в настройке складов.

Система ценообразования была переписана и оптимизирована. Под этот сервис была выделена отдельная логика, которая оптимально работала с большими наборами данных.

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


Система обновления цен и остатков

Решили не трогать, система очень объемная и сложная, более того, взаимные блокировки исчезли. Могли потратить на нее несколько месяцев, но КПД оптимизации был бы низким.


Бизнес логи, уведомления, старые версии документов

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


Нагрузка на веб сервисы (API для клиентов)

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

Оказалось, что клиенты очень часто запрашивают одну и ту же информацию о статусе заказа до нескольких RPS на 1 заказ. У каких-то клиентов бывают сбои и они заваливают нас запросами.

Ответы по статусам состояний закэшировали на короткое время.

Изменили порядок аутентификации таким образом, что пользователи с ограниченными доступом получали отказ быстрее, без подгрузки тех. информации.

Основные работы - закэшировать все что можно.


Качество кода основного сайта, лишние запросы, отсутствие кэширования

Собрали и агрегировали логи запросов к БД, нашли самые частые запросы, закэшировали данные.

В рамках этих доработок произошел интересный случай, на который стоит обратить внимание.

Разработчик, по неопытности, при добавлении кэширования написал приблизительно следующий код. Создал свойство класса виде массива, где ключем был идентификатор пользователя, а значением массив данных клиента, после чего сохранял этот массив в memcached.

Псевдокод:

/**
 * @var array $dataCache
 */
private $dataCache = [] ;
…
$this->dataCache = $this->cache->load(‘client_info’);
…
if(empty($this->dataCache[$clientId])){
    $this->dataCache[$clientId] = $this->storage->loadInfo($clientId);
    $this->cache->save(‘client_info’, $this->dataCache, $time);
}

Вместо того чтобы закэшировать данные каждого клиента в отдельном ключе, он закэшировал массив данных всех клиентов.

Во время кодревью на этот момент не обратили внимания и пропустили в прод.

Это привело к большому количеству проблем:

  • параллельные запросы начали перетирать кэш друг друга;
  • все запросы начали пытаться писать в один ключ одновременно (под полсотни RPS выстроилось в очередь), обычно такие проблемы решают при помощи memcached CAS token, но в этом случае это бы не спасло;
  • пошли тормоза с чтением и записью в кэш;
  • наконец, сохраняемые данные превысили максимальный размер значения (по умолчанию 1Mb, у нас больше), данные перестали сохраняться;
  • от этого одна нода с php-fpm ушла в полку.

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

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


Итоги за год переработки

Нагрузка на сервер БД 4000 - 5000 QPS. Практически в 10 раз ниже.

На серверах БД больше оперативной памяти для кэширования индексов огромных таблиц.

Клиенты довольны быстрой скоростью ответа.

Переписан сервис который доставлял боль в поддержке.

Переход на новую версию прошел бесшовно и незаметно для большинства клиентов.











Report Page