Привет, Хабр! Последние пару месяцев мы прожили в очень интересной ситуации, и я хотел бы поделиться нашей историей скейлинга инфраструктуры. За это время СберМаркет вырос в заказах в 4 раза и запустил сервис в 17 новых городах. Взрывной рост спроса на доставку продуктов потребовал от нас масштабирования инфраструктуры. О самых интересных и полезных выводах читайте под катом.



Меня зовут Дима Бобылев, я — технический директор СберМаркета.  Поскольку это первый пост в нашем блоге, скажу несколько слов о себе и о компании. Прошлой осенью я участвовал в конкурсе молодых лидеров Рунета. Для контеста я написал небольшую историю о том, как мы в СберМаркете видим внутреннюю культуру и подход к развитию сервиса. И хотя выиграть в конкурсе не получилось, зато я сформулировал для себя основные принципы развития ИТ-экосистемы.

При управлении командой важно понимать и находить баланс между тем, что нужно бизнесу, и потребностями каждого конкретного разработчика. Сейчас СберМаркет растёт в 13 раз год к году, и это влияет на продукт, требуя постоянно увеличивать объемы и темпы разработки. Несмотря на это, мы выделяем достаточно времени разработчикам для предварительного анализа и качественного написания кода. Сформированный подход помогает не только в создании работающего продукта, но также в его дальнейшем масштабировании и развитии. В результате такого роста СберМаркет уже стал лидером среди сервисов доставки продуктов: мы ежедневно доставляем около 18 тысяч заказов в день, хотя еще в начале февраля их было порядка 3500.


Однажды клиент попросил курьера СберМаркета доставить продукты ему бесконтактно — прямо на балкон

Но перейдем к конкретике. Последние несколько месяцев мы активно занимались масштабированием инфраструктуры нашей компании. Такая потребность объяснялась внешними и внутренними факторами. Одновременно с расширением клиентской базы количество подключенных магазинов выросло с 90 в начале года до более чем 200 к середине мая. Мы, конечно, подготовились, зарезервировали основную инфраструктуру плюс рассчитывали на возможность вертикального и горизонтального масштабирования всех виртуальных машин, размещенных в облаке Яндекса. Однако практика показала: «Всё, что может пойти не так, пойдет не так». И сегодня я хочу поделиться самыми любопытными ситуациями, которые случились за эти недели. Надеюсь, наш опыт окажется полезным для вас.

Slave в полной боевой готовности


Еще перед началом пандемии мы столкнулись с ростом количества запросов на наши backend сервера. Тенденция заказывать продукты с доставкой на дом стала набирать обороты, а с введением первых мер самоизоляции в связи с COVID-19,  нагрузка  драматически росла на глазах в течение всего дня. Возникла потребность оперативно разгрузить master-серверы основной БД и перенести часть запросов на чтение на серверы-реплики (slave).

Мы заранее готовились к этому шагу, и для подобного маневра уже было запущено 2 slave-сервера. На них в основном работали batch-задачи генерации информационных фидов для обмена данными с партнерами. Эти процессы создавали лишнюю нагрузку и совершенно справедливо были вынесены «за скобки» парой месяцев ранее. 

Поскольку на Slave происходила репликация, мы придерживались концепции, что приложения могут работать с ними только в режиме read only.  Disaster Recovery Plan предполагал, что в случае катастрофы мы сможем просто монтировать Slave на место Master и переключить все запросы на запись и чтение на Slave. Однако, мы также хотели использовать реплики для нужд отдела аналитики, поэтому сервера не были полностью переведены в статус read only, а на каждом хосте был свой набор пользователей, и некоторые имели права записи для сохранения промежуточных результатов расчетов.

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

Производительности «мастера» перестало хватать, поэтому мы начали выносить часть самых тяжелых запросов на чтение на реплику. Для прозрачного направления запросов на запись в мастер, а чтение — на слейв, мы использовали ruby gem «Octopus». Мы создали специального пользователя с постфиксом _readonly без прав на запись. Но из-за ошибки в конфигурации одного из хостов часть запросов на запись ушла на slave-сервер от имени пользователя, у которого были соответствующие права.

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

Когда мы разобрались с причинами «расползания» основного slave, аналитический уже вышел из строя по той же причине. Несмотря на наличие двух дополнительных серверов, на которые мы планировали перевести нагрузку в случае краха мастера, из-за досадной ошибки оказалось, что в критический момент нет ни одного.

Но так как мы делали не только dump БД (рестор на тот момент составлял около 5 часов), но и snapshot master-сервера, запустить реплику удалось в течение 2 часов. Правда после этого нас ожидало накатывание лога репликации в течение продолжительного времени (потому что процесс идет в однопоточном режиме, но это уже совсем другая история).

Вывод: После такого инцидента стало ясно, что надо отказаться от практики ограничения записи для пользователей и объявить readonly весь сервер. При подобном подходе можно не сомневаться, что реплики будут доступны в критически важный момент.

Оптимизация даже одного тяжелого запроса может «вернуть к жизни» БД


Хотя мы постоянно обновляем каталог на сайте, запросы, которые  мы выносили на Slave-серверы, допускали небольшое отставание от Master. Время, за которое мы обнаружили  и устранили проблему «внезапно сошедших с дистанции» слейвов было больше «психологического барьера» (за это время могло произойти обновление цен, а клиенты видели бы устаревшие данные), и  мы были вынуждены переключить все запросы на основной сервер БД. В результате сайт работал медленно… но хотя бы работал. И пока Slave-восстанавливался, нам ничего не оставалось кроме оптимизации. 

Пока Slave-сервера восстанавливались, минуты медленно тянулись, Master оставался перегруженным, а мы бросили все силы на оптимизацию активных задач согласно «Правилу Парето»: выбрали ТОП-запросов, дающих большую часть нагрузки и начали тюнинг. Делалось это прямо «на лету».

Интересный эффект заключался в том, что загруженный под завязку MySQL отзывается на даже незначительное улучшение процессов. Оптимизация пары запросов, которые давали  всего 5% общей нагрузки уже показали ощутимую разгрузку CPU. В результате нам удалось обеспечить приемлемый запас ресурсов для работы Master с базой данных и получить необходимое время для восстановления реплик. 

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

Организуйте мониторинг работоспособности сервисов-партнеров


Мы занимаемся обработкой заказов от клиентов, и поэтому наши сервисы постоянно взаимодействуют со сторонними API — это шлюзы для отправки SMS, платежные платформы, системы роутинга, геокодер, служба ФНС и множество других систем. И когда нагрузка стала расти стремительно, мы начали упираться в ограничения API наших сервисов-партнеров, о которых раньше даже не задумывались.

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

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

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

Вывод: Нужно обязательно мониторить условия работы всех партнерских сервисов и иметь их в виду. Даже если сегодня кажется, что они вам «с большим запасом», это не значит, что завтра они не станут препятствием для роста. И, конечно, лучше договориться о финансовых  условиях возросших запросов к сервису заранее. 

Иногда оказывается, что «нужно больше золота» (с) не помогает


Мы привыкли к «затыкам» в основной БД или на серверах приложений, но при масштабировании неприятности могут проявиться там, где их и не ждали  Для полнотекстового поиска на сайте мы используем движок Apache Solr. С ростом нагрузки мы отметили снижения времени отклика, а загрузка процессора сервера достигала уже 100%. Что может быть проще — выдадим контейнеру с Solr больше ресурсов.

Вместо ожидаемого прироста производительности сервер просто «умер». Он сразу загружался на 100% и отвечал еще медленнее. Изначально у нас было 2 ядра и 2 Гб ОЗУ. Мы решили сделать то, что обычно помогает — дали серверу 8 ядер и 32 Гб. Все стало намного хуже (как именно и почему — расскажем в отдельном посте). 

За несколько дней мы разобрались в тонкостях этого вопроса, и добились оптимальной производительности при 8 ядрах и 32 Гбайт. Эта конфигурация позволяет и сегодня продолжать наращивать нагрузку, что очень важно, потому что рост идет не только по клиентам, но и по количеству подключенных магазинов — за 2 месяца их численность увеличилась в 2 раза. 

Вывод: Стандартные методы типа «добавить больше железа» работают не всегда. Так что при масштабировании любого сервиса нужно хорошо понимать, как он использует ресурсы и заранее тестировать, его работу в новых условиях. 

Stateless — ключ к простому горизонтальному масштабированию


В целом наша команда придерживается известного подхода: сервисы не должны иметь внутреннего состояния (stateless) и должны быть независимы от среды выполнения. Это позволяло нам переживать рост нагрузки простым горизонтальным масштабированием. Но у нас был один сервис-исключение – обработчик долгих фоновых задач. Он занимался отправкой email и sms, обработкой ивентов, генерацией фидов, импортом цен и стоков, обработкой изображений. Так сложилось, что он зависел от локального файлового хранилища и находился в единственном экземпляре. 

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

Вывод: Правило Stateless нужно соблюдать для всех компонентов без исключения, даже если кажется «что тут точно не упремся». Лучше потратить немного времени на правильную организацию работы всех систем, чем потом в спешке переписывать код и чинить сервис, испытывающий перегрузку.

7 принципов для интенсивного роста


Несмотря на доступность дополнительных мощностей, в процессе роста мы наступили на несколько граблей. За это время количество заказов увеличилось более чем в 4 раза. Сейчас мы уже доставляем более 17 000 заказов в день в 62 городах и планируем еще сильнее расширить географию — в первом полугодии 2020 ожидается запуск сервиса по всей России. Для того, чтобы справиться с растущей нагрузкой, учитывая уже набитые шишки, мы вывели для себя 7 основных принципов работы в условиях постоянного роста:

  1. Инцидент-менеджмент. Мы создали доску в Jira, где каждый инцидент  отражается в виде тикета. Это поможет фактически приоритезировать и выполнять связанные с инцидентом задачи. Ведь в сущности не страшно ошибаться — страшно ошибаться дважды по одному и тому же поводу. Для тех случаев, когда инциденты повторяются раньше, чем удается исправить причину, должна быть готова инструкция к действию, потому что во время большой нагрузки важно реагировать молниеносно.
  2. Мониторинг требуется для всех элементов инфраструктуры без исключения. Именно благодаря ему мы могли прогнозировать рост нагрузки и правильно выбирать «бутылочные горлышки» для приоритезации устранения. Скорее всего, при высокой нагрузке сломается или начнет тормозить все, о чем вы и не думали. Поэтому новые алерты лучше всего создавать сразу после наступления первых инцидентов, чтобы мониторить и предвосхищать их.
  3. Правильные алерты просто необходимы при резком росте нагрузки. Во-первых, они должны сообщать о том что точно сломалось. Во-вторых, алертов не должно быть много, потому что обилие некритических алертов приводит к игнорированию всех оповещений вообще.
  4. Приложения должны быть stateless. Мы убедились в том, что для этого правила не должно быть исключений. Нужна полная независимость от среды выполнения. Для этого вы можете хранить разделяемые данные в БД или, например, прямо в S3. А еще лучше следовать правилам https://12factor.net. Во время резкого роста времени оптимизировать код просто нет, и справляться с нагрузкой придется методом прямого увеличения вычислительных ресурсов и горизонтального масштабирования.
  5. Квоты и производительность внешних сервисов. При быстром росте проблема может возникнуть не только в вашей инфраструктуре, но и во внешнем сервисе. Самое обидное, когда это происходит не по причине сбоя, а из-за достижения квот или лимитов. Так что внешние сервисы должны скейлится так же хорошо, как и вы сами. 
  6. Разделяйте процессы и очереди. Это очень помогает, когда на одном из шлюзов происходит затык. Мы не столкнулись бы с задержками в передаче данных, если бы заполненные очереди отправки SMS не мешали обмену уведомлениями между информационными системами. Да и количество воркеров было бы проще увеличить, если бы они работали отдельно.
  7. Финансовые реалии. Когда идёт взрывной рост потоков данных, времени задумываться о тарифах и подписках нет. Но о них надо помнить, особенно, если вы небольшая компания. Большой счет может выставить владелец любого API, а также ваш хостинг-провайдер. Так что читать договоры нужно внимательно.

Заключение


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

В следующих постах мы поделимся своим опытом расследования проседания производительности в Apache Solr, а также расскажем об оптимизации запросов и о том, как взаимодействие с ФНС помогает компании экономить деньги. Подписывайтесь на наш блог, чтобы ничего не пропустить, и рассказывайте в комментариях, случались ли у вас подобные неприятности во время роста трафика.