Русский
Русский
English
Статистика
Реклама

Команда яндекс.маркета

Как и зачем мы создаём собственную курьерскую платформу. Три истории Яндекс.Маркета

28.04.2021 14:06:12 | Автор: admin
Всем привет, меня зовут Алексей Остриков, я руковожу разработкой в Яндекс.Маркете. Когда-то я много-много писал код, затем полтора года руководил группой бэкенда одного из сервисов Маркета, а сейчас отвечаю за разработку курьерской платформы Маркета.

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


На фото команда курьерской платформы десять месяцев назад. В те времена она помещалась в одной комнате. Сейчас нас стало в 5 раз больше.

Зачем мы всё это делали


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

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

Второе прозрачность. Когда что-то идёт не так (происходят переносы, срывы сроков), то мы не знаем, почему они произошли. Мы не можем пойти и подсказать: Ребят, давайте делать вот так. Мы и сами не видим, и клиенту не можем показать какие-то дополнительные вещи. Например, что заказ приедет не к восьми, а в интервале 15 минут. А всё потому, что в процессе нет такого уровня прозрачности.

Третье деньги. Когда мы работаем с подрядчиком, есть договор, в котором прописаны суммы. И мы можем менять эти циферки в рамках договора. А когда мы отвечаем за весь процесс от а до я, то можно видеть, какие части системы спроектированы экономически невыгодно. И можно, например, поменять провайдера SMS или формат документооборота. Или можно заметить, что у курьеров слишком большие пробеги. И если строить маршруты кучнее, то в итоге получится развозить больше заказов. Благодаря этому можно ещё и сэкономить деньги доставка станет более эффективной.

Это и были те три цели, которые мы ставили во главу всего.

Как выглядит платформа


Давайте посмотрим, что у нас получилось.



На изображении показана схема процесса. У нас есть большие склады, на которых хранятся сотни тысяч заказов. С каждого склада вечером уезжают фуры, доверху набитые заказами. Там может быть 56 тысяч заказов. Эти фуры едут в здания поменьше, которые называются сортировочные центры. В них за несколько часов большая куча заказов превращается в маленькие кучки для курьеров. И когда с утра приезжают курьеры на машинах, то каждый курьер знает, что ему нужно забрать кучку вот с этим QR-кодом, загрузить себе в машину и поехать развозить.

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


Как это видит курьер

У курьеров есть приложение для Android, написанное на React Native. И в этом приложении они видят весь свой день. Они чётко понимают последовательность: на какой адрес ехать сначала, на какой потом. Когда позвонить клиенту, когда отвезти возвраты в сортировочный центр, как начать день, как закончить. Они всё видят в приложении и практически не задаются лишними вопросами. Мы им очень помогаем. По сути, они просто выполняют задания.



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

Кстати, про бэкенд. Мы в Маркете очень любим Java, в основном версию 11. И все бэкенд-сервисы, про которые пойдёт речь, написаны на Java.

Архитектура




В архитектуре нашей платформы есть три главных узла. Первый отвечает за коммуникацию с внешним миром. Курьерское приложение стучится на балансер, который выведен вовне, общаясь с ним по стандартному JSON HTTP API. По сути, этот узел отвечает за всю логику текущего дня, когда курьеры что-то переносят, отменяют, выдают заказы, получают новые задания.

Второй узел это сервис, который коммуницирует с внутренними сервисами Яндекса. Все сервисы это классические RESTful-сервисы со стандартной коммуникацией. Когда вы сделаете заказ на Маркете, через какое-то время к вам прилетит документ в JSON-формате, где будет всё написано: когда доставляем, кому доставляем, в какой интервал. И у нас это состояние сохранится в базу данных. Всё просто.

Помимо этого, второй узел также коммуницирует с другими внутренними сервисами, уже не Маркета, а Яндекса. Например, за уточнением геокоординат мы уходим в геосервис. Чтобы отправить push-уведомление, идём в сервис, который рассылает push и SMS. Для авторизации используем другой сервис. Для расчёта маршрутизации на завтра ещё один сервис. Таким образом осуществляется вся коммуникация с внутренними службами.

Этот узел также является входной точкой, у него есть API, в которую стучится наша админка. У неё есть свой endpoint, который называется, скажем, /partner. И наша админка, всё состояние системы, конфигурируется через коммуникацию с этим сервисом.

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

И в центре всего находится база данных, в которой, собственно, и хранится всё состояние. Все сервисы входят в одну базу данных.



Отказоустойчивость


У Яндекса есть несколько дата-центров, и наш сервис регионально распределен по трём дата-центрам. Как это выглядит.

База данных это три хоста, каждый в своём дата-центре. Один хост мастер, две другие реплики. В мастер мы пишем, с реплик читаем. Все другие Java-сервисы это также Java-процессы, которые крутятся в нескольких дата-центрах.

Один из узлов это наш API. Он крутится во всех трёх дата-центрах, потому что поток сообщений больше, чем из внутренних сервисов. Более того, такая схема позволяет вам довольно легко горизонтально масштабироваться.

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

Балансеру придётся просто делить трафик на большее количество точек, которые выполняют запросы. И у нас сейчас, например, не один узел, а по два узла в каждом дата-центре.

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

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

Все сервисы в Яндексе устроены схожим образом, мы все умеем переживать отказ одного из дата-центров. Как это реализовано, что такое graceful degradation и как сервисы в Яндексе переносят отключение одного из дата-центров, мы уже рассказывали.

Итак, это была архитектура. А теперь начинаются истории.

История первая про Яндекс.Ровер


Недавно у нас была Yet another Conference, там Роверу уделили много внимания. Я продолжу тему.

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

Яндекс.Ровер это маленький робот размером со среднюю собачку. Туда можно положить еду, пару коробок заказов, он поедет по городу и привезёт заказы без помощи человека. Там есть лидар, робот понимает своё положение в пространстве. Он умеет доставлять малогабаритные заказы.

И мы подумали: а почему бы и нет? Уточнили детали эксперимента: на тот момент нужно было проверить гипотезу, что людям это понравится. И мы решили довезти 50 заказов за полторы недели в очень лайтовом режиме.



Мы придумали максимально простой флоу, когда человеку приходит SMS с предложением нестандартного способа доставки не курьер привезёт, а Ровер. Весь эксперимент проходил во внутреннем дворе Яндекса. Человек выбирал подъезд, к которому Ровер подъедет. Когда робот приезжал открывалась крышка, клиент брал заказ, закрывал крышку, и Ровер уезжал за новым заказом. Всё просто.

Затем мы пришли к команде Ровера, чтобы договориться про API.

В API Ровера есть простые методы: открыть крышку, закрыть крышку, поехать в такую-то точку, получить состояние. Классика. Тоже JSON. Очень просто.

Что ещё очень важно: и такие маленькие истории, и любые большие истории лучше делать через featureflags. Фактически у вас есть рубильник, по которому вы можете в production включить эту историю. Когда она вам больше не нужна, эксперимент завершен успешно или не успешно, либо заметили какие-то баги, вы просто вырубаете её. И вам не нужно делать ещё один деплой новой версии кода на прод. Эта штука здорово облегчает жизнь.

Казалось бы, всё просто и всё должно работать. Там даже две недели нечего разрабатывать, можно за несколько дней сделать. Но посмотрите, где собака зарыта.

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

И в этот момент мы отправим SMS человеку, например, что Ровер ждёт на месте. Это невозможно сделать синхронно, и нужно как-то решить эту проблему.

Есть много разных подходов. Мы сделали максимально простой вариант.

Мы решили, что можно запустить самый обычный фоновый Java-тред либо задачу в Executer. Этот фоновый тред тут же запускается отслеживать процесс. И как только процесс выполнен, мы отправляем уведомление.



Выглядит это, например, так. Это практически копия кода с production, за исключением вырезанных комментариев. Но есть подвох. Нельзя делать таким образом серьёзные системы. Допустим, мы выкатываем новую версию на бэкенд. Хост перезагружается, состояние теряется, и всё, Ровер уезжает в бесконечность, его больше никто не видит.

Но почему? Если мы знаем, что наша цель доставить 50 заказов за полторы недели, мы сами выбираем время, когда следим за бэкендом. Если что-то пойдёт не так, можно вручную что-то изменить. Для такой задачи это решение более чем достаточное. И в этом кроется мораль первой истории.

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

История вторая про базы данных


Но сначала несколько слов о том, как устроены основные сущности. Есть сервис Яндекс.Маршрутизация, который в конце дня строит маршруты курьерам.

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

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

Теперь давайте посмотрим, как эти сущности можно показать в базе данных. Это делается, например, вот так.



Это очень аккуратная модель данных. Мы не придумали ни одной новой сущности, и сильно не схлопывали уже существующие.

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

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

Затем добавили трекинг и Ровер. Всего лишь по две таблички. В трекинге курьер отправляет свои координаты, мы их фиксируем в отдельной табличке. И есть трекинг заказа со своей моделью состояний, есть дополнительные штуки, типа SMS ушла / не ушла. Не стоит это добавлять прямо в задание. Лучше вынести в отдельную табличку, ведь этот трекинг нужен не для всех типов заданий.



В Ровере его координаты и доставка. У нас доставка Ровером это трекинг как бы для Ровера. Можно его добавить в трекинг заказа, но зачем? Ведь когда мы избавимся от этого эксперимента, когда он будет выключен, эти опции навсегда останутся в сущности трекинга. Там будут null-поля.

Может возникнуть вопрос: а зачем делать табличку с координатами? Один Ровер доставляет пять заказов в день. Не нужно хранить координаты в базе данных, можно просто ходить в API Ровера и получать их на runtime.

Суть в том, что так и было сделано изначально. Этой таблички не было, мы сразу ходили на сервис и всё это брали. Но во время тестов мы увидели, что много людей открывают карту с катящимся Ровером, и нагрузка на этот сервис кратно возрастает. Допустим, семь человек открыли. А там на страничке каждые две секунды Java Script запрашивает координаты. И коллеги нам писали в чат: Откуда такая нагрузка? У вас же там один человек должен кататься.

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

Эту историю можно было сделать с помощью 20 таблиц. Можно было использовать две таблицы: курьер и заказ. Но в первом случае это был бы over-engineering, а во втором случае это было бы слишком сложно поддерживать. Сложная логика, сложно тестировать.

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

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

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

  1. дойти до всех клиентов, обеспечить обратную совместимость;
  2. выкатить новый API, всех клиентов переключить на новый API;
  3. выпилить старый код о клиентах, выпилить старый код на бэкенде.


Это очень затратно.

Ошибки в коде по сравнению с этим вообще ерунда. Код вы просто переписали, прогнали тесты. Тесты зелёные вы запушили в мастер. А вот API базы данных уделяйте особое внимание.

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



Есть один совет. Когда вы очень быстро разрабатываете что-то, в базе данных возникают неточности, иногда не хватает внешнего ключа или появляется дубль поля. Поэтому иногда, раз в два-три месяца, просто смотрите только на базу. Та же Intellij IDEA умеет генерировать классные схемы. И там это всё видно.

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

История третья про качество


Есть вещи, которые лучше с самого начала делать хорошо. Важно действовать по принципу нормально делай нормально будет.

Например, у нас есть один процесс, который крайне важен для платформы. Весь день мы набираем заказы на завтрашний день, но вечером срабатывает пометка, что после 22:00 мы не набираем заказы, а до 01:00 готовимся к завтрашнему дню. Потом начинается распределение заказов по сортировочным центрам. Мы идём в Яндекс.Маршрутизацию, она строит маршруты.

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

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

Мы понимали, что если процесс не завершится с десяти вечера до часу ночи, то сортировочные центры даже не будут знать, как им сортировать заказы, на какие кучки. Там будет всё простаивать. Курьеры выйдут позже, и у нас будут провалы по качеству.

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

Расскажу про один из вариантов, как можно настроить такой процесс.

Процесс этот многосоставный. Расчёт маршрутов и их публикацию можно разбить на части и, например, создать очереди. Тогда у нас есть несколько очередей, которые отвечают за законченные участки работы. И есть consumer на этих очередях, которые сидят и ждут сообщения.



Например, день закончился, мы хотим рассчитать маршруты на завтра. Мы отправляем запрос в первую очередь: сформируй задание и запусти расчёт. Consumer забирает первое сообщение, идёт в сервис маршрутизации. Это асинхронный API. Consumer получает ответ, что задача взята в работу.

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

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

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

Мы кладём то, что рассчиталось, в третью очередь. Сообщение исчезает из второй очереди. И просыпается третий consumer, берёт этот контекст с сервиса Яндекс.Маршрутизация и на его основе создаёт state завтрашнего дня. Он создаёт задания курьеров, он создаёт заказы, создаёт смены. Это тоже немалая работа. Он тратит на это какое-то время. И когда всё создано, эта транзакция завершается и задание из очереди удаляется.

Если на любом участке этого процесса что-то идёт не так, сервер перезагружается. При последующем восстановлении мы просто увидим тот момент, на котором мы закончили. Допустим, прошла первая фаза и вторая. И перейдём к третьей.

С такой архитектурой последние месяцы у нас всё идёт довольно гладко, проблем нет. Но раньше был сплошной try-catch. Непонятно, где процесс сбоился, какие статусы поменяли в базе данных и так далее.

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

Что мы сделали в офлайне


Я описал большую часть того, что происходит на нашей платформе. Но кое-что осталось за кадром.

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

Это вторая часть платформы. Мы сами написали весь софт. У нас теперь есть терминалы, с помощью которых кладовщики считывают код с коробочек и кладут их в соответствующие ячейки. Там довольно сложная логика. Этот бэкенд не сильно проще, чем тот, про который я уже рассказывал.

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

Когда у нас есть первая и вторая часть пазла, мы просто можем арендовать какое-то здание, нанять курьеров на машинах. Сказать им, как нажимать, что нажимать, как пикать, какую коробку куда класть, и всё. Благодаря этому мы уже запустились в семи городах, у нас больше десяти сортировочных центров.

И открытие нашей платформы в новом городе по времени занимает совсем немного. Более того, мы научились не только развозить заказы по конкретным людям. Мы умеем с помощью курьеров привозить заказы в пункты выдачи. Мы также написали софт для них. И на этих точках тоже выдаём заказы людям.

Итоги


В начале я говорил, зачем мы начинали создавать собственную курьерскую платформу. Теперь расскажу, чего мы достигли. Это невероятно, но при использовании нашей платформы мы смогли приблизиться почти к 100% попадания в интервал. Например, за последнюю неделю качество доставляемости в Москве было порядка 9598%. Это значит, что в 9598% случаев мы не опаздываем. Мы укладываемся в интервал, который выбрал клиент. И мы даже не могли мечтать о такой точности, когда полагались исключительно на внешние службы доставки. Поэтому сейчас мы постепенно распространяем нашу платформу на все регионы. И будем улучшать доставляемость.

Мы получили нереальную прозрачность. Эта прозрачность нужна и нам. У нас всё логируется: все действия, весь процесс выдачи заказа. У нас есть возможность вернуться в историю на пять месяцев и сравнить какую-то метрику с текущей.

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

Вдобавок получилось оптимизировать издержки, потому что у нас есть доступ ко всем элементам цепочки. В итоге сейчас довезти один заказ стоит на четверть дешевле, чем это было раньше, когда мы работали с внешними службами. Да, траты на доставку заказа уменьшились на 25%.

И если обобщать все идеи, о которых шла речь, можно выделить следующее.

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

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

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

В таком режиме, мне кажется, вы не имеете права делать что-то, инвестируя в long-term и не получая ничего на короткой дистанции. Столь сильно закладываться на будущее в таком формате работы нельзя, потому что будущее может просто не наступить.

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

Если говорить коротко, постарайтесь делать максимально просто, но оставляя возможности для расширения.

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

И последнее. Я рассказывал про Ровер, что хорошо подобные процессы делать с помощью featureflags (фиче-флагов). Советую послушать доклад Марии Кузнецовой с митапа по Java. Она рассказывала, как устроены фиче-флаги в нашей системе и мониторинге.
Подробнее..

Как не держать лишнее железо и справляться с ростом нагрузки внедрение graceful degradation в Яндекс.Маркете

14.01.2021 12:05:51 | Автор: admin

Привет, меня зовут Евгений. Я разрабатываю инфраструктуру поиска Яндекс.Маркета. Хочу рассказать, как graceful degradation помогает нам обрабатывать больше запросов, чем физически могут выдержать наши сервера, и что происходит с поиском в Маркете, если один из дата-центров отключается.

Проблема

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

Обычное состояниеОбычное состояние

Но когда все ДЦ работают, каждый из них оказывается задействован лишь на 60%. Ещё 10% резервируются для экспериментов и непредвиденного роста, а оставшиеся 30% используются только в случае, если один из ДЦ отключается, и нужно перераспределить запросы.

ДЦ 2 отключёнДЦ 2 отключён

Если сервис работает на сотнях серверов, 30% это огромное количество железа. А поскольку отключаются ДЦ очень редко, резервные мощности почти всегда простаивают.

Нам хотелось бы меньше греть воздух, ведь если сократить количество железа в каждом ДЦ хотя бы на 10%, речь уже шла бы о внушительных суммах. При этом, если просто вытащить часть железа, а затем при пиковой нагрузке какой-то из ДЦ отключится, оставшиеся мощности могут не справиться. Резко вырастет время ответа пользователю, а некоторые запросы будут вообще оставаться без ответа.

ДЦ 1 и ДЦ 3 не справляются с нагрузкойДЦ 1 и ДЦ 3 не справляются с нагрузкой

Применяем graceful degradation

Graceful degradation или изящная деградация это подход к построению систем, при котором система продолжает выполнять свою основную функцию, даже если отдельные её элементы вышли из строя.

Мой коллега из Яндекс.Такси уже рассказывал про graceful degradation в случае, когда ломается часть сервиса. Мы же применили этот подход на бэкендах для ситуации, когда у нас отключается значительное количество серверов.

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

Чтобы запустить graceful degradation, нам надо было решить две задачи:

  1. Разработать механизм уменьшения нагрузки.

  2. Сделать автоматизацию включения механизма.

Механизм уменьшения нагрузки

Уменьшить нагрузку можно по-разному. Один из вариантов отвечать ошибкой на некоторые запросы. Но тогда надо понять, на что можно так отвечать, а на что нет. То есть нужен какой-то параметр важность, которого мы не знаем. Если спросить у наших клиентов, что важно, они ответят, что важны все запросы, и ничего нельзя отбрасывать. Поэтому этот путь нам не подошёл.

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

Таким образом, мы выбрали третий вариант снижать качество. Основное преимущество этого подхода возможность уменьшать нагрузку на 5%, 10% и так далее.

Допустим, вы ищете себе новый телефон и заходите в категорию Смартфоны на Яндекс.Маркете. Я уже подробно описывал, как работает наш поиск: запрос отправляется во фронтенд, надо показать пользователю 48 лучших предложений из категории с телефонами для конкретного региона. Фронтенд делает свой запрос в поисковый бэкенд.

Бэкенд получает запрос и раcпределяет его на 8 серверов с шардами. В шардах хранятся предложения от магазинов.

Общая схема обработки поискового запросаОбщая схема обработки поискового запроса

На каждом шарде поиск проходит несколько стадий. На стадии фильтрации ищется примерно 50000 предложений, это число зависит от категории. На этапе ранжирования для каждого предложения вычисляется релевантность, учитывается цена, рейтинг товара, рейтинг магазина и ещё более 2000 факторов. ML по факторам вычисляет вес каждого предложения. Затем берётся только 48 лучших. Meta Search получает эти 48 предложений с каждого шарда, то есть всего 48*8=384 предложения. Предложения снова ранжируются, опять берётся 48 лучших. Последние 48 уже показываются пользователю. То есть чтобы показать нашим пользователям 48 телефонов, мы обрабатываем 400 000 предложений.

Количество обрабатываемых документов без graceful degradationКоличество обрабатываемых документов без graceful degradation

В случае с graceful degradation, когда надо уменьшить нагрузку, мы можем скомандовать: теперь обрабатывай 95% документов, а теперь 90% или 80%. Если обрабатывать 95%, то есть 400000*0.95=380 000 документов, то из них всё равно выбираются 48 лучших предложений для выдачи. И в среднем только 2 предложения будут отличаться от изначальной выдачи без снижения качества. При таком маленьком изменении большинство пользователей даже не заметят разницы.

Количество обрабатываемых документов при включении механизма уменьшения нагрузкиКоличество обрабатываемых документов при включении механизма уменьшения нагрузки

Автоматизация включения механизма

Автоматизация работает за счёт постоянного мониторинга загрузки CPU. Если нагрузка становится выше 90%, автоматика начинает снижать качество. На 90% снижение небольшое, но если нагрузка продолжает расти, процент деградации повышается линейно и доходит до максимума при 100% загрузки CPU. Такой подход позволяет снижать качество минимально.

Общий алгоритм выглядит так:

При выключении ДЦ: балансеры перераспределяют запросы в оставшиеся ДЦ => нагрузка на CPU повышается => при превышении порогового значения происходит снижение качества по заданной формуле.

При включении ДЦ: балансеры перераспределяют запросы на все ДЦ => нагрузка на CPU снижается => понижение качества прекращается.

Повышение нагрузки при выключении ДЦ. Линии на верхнем графике показывают загрузку CPU в отдельных ДЦ. Нагрузка выросла с 82% до 98%. Нижний график показывает процент срезанных документов. Повышение нагрузки при выключении ДЦ. Линии на верхнем графике показывают загрузку CPU в отдельных ДЦ. Нагрузка выросла с 82% до 98%. Нижний график показывает процент срезанных документов.

Внедрение

Для внедрения мы провели несколько экспериментов и понижали качество на небольшом проценте пользователей. В течение недели мы постоянно отслеживали уменьшение нагрузки и одновременно количество переходов на магазины-партнёры. Имея на руках цифры, мы договорились с бизнесом на максимальное снижение кликов в 1% при снижении качества. Исходя из этого мы посчитали, сколько серверов можем отдать другим проектам или выключить для резерва. У нас получилось 15% всех мощностей поиска.

В конце, когда всё было готово, мы провели эксперимент с отключением дата-центра, чтобы убедиться, что всё работает как надо.

Выводы

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

Подробнее..

Как катать релизы несколько раз в день и спать спокойно. Доклад Яндекса

26.02.2021 12:14:08 | Автор: admin
Высокие темпы разработки сопряжены с рисками, влияющими на отказоустойчивость и стабильность особенно если хочется экспериментировать и пробовать разное. Разработчик Маркета Мария Кузнецова рассказала о релизном цикле своей команды от и до, а также о мониторингах и других вещах, позволяющих обновлять сервис со скоростью три релиза в день.



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

(...) Сейчас у нас в среднем выкатывается по три релиза в день.



Стек технологий, которые мы используем типичен для Java-приложений Маркета. Мы используем 11-ю Java, Spring, PostgreSQL для хранения данных, Liquibase для накатывания миграций и Quartz для регулярных Cron-задач. Конечно, у нас реализовано много интеграций с внутренними сервисами.

Начать я хочу с того, как у нас устроен процесс релиза.

1. Релизы


С самого начала проекта мы живем в парадигме trunk-based development. Чтобы код попал в продакшен, нужно поставить пул-реквест и пройти код-ревью. Причем в пул-реквесте запускаются также и прикоммитные проверки, в первую очередь это юнит-тесты и функциональные тесты.

Среды у нас сейчас только две продакшен и тестинг. Когда код-ревью пройдено и код попал в trunk, запускается вот такой релизный пайплайн.



Дальше я покажу все шаги подробнее.



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



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

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

Конечно, у нас есть ограничения при выкатке релиза. Парадигма trunk-based development диктует то, что не должно быть долго живущих feature branches, и получается так, что в trunk может оказаться незаконченная функциональность.

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

При чем здесь кот Шредингера? Часто новая функциональность в продакшене должна и работать, и не работать одновременно: для каких-то пользователей должна быть включена, а для других все должно работать по-старому. Можно было бы, конечно, поднять отдельный инстанс, бэкенд с новой функциональностью, пустить туда какую-то часть пользователей, а остальных оставить на старых.

Но такой подход звучит дорого.

2. Feature toggles


Поэтому мы пришли к тому, что стали использовать feature toggles. Toggles с английского переводится как переключатель, и это в точности описывает его предназначение.

if (configurationService.isBooleanEnabled(NEW_FEATURE_ENABLED)) {    //new feature here} else {        //old logic}

Можно выкатить код и пока не использовать его в продашкене, например, ждать поддержки фронтенда или же дальше его реализовывать.

Нам очень важно уметь включать-выключать функциональность по отмашке от коллег. Поэтому свой toggles мы сложили в базу.

public class User {    private Map<String, UserProperty> properties = new HashMap<>();    String getPropertyValue(String key) {        UserPropertyEntity userProperty = properties.get(key);        return userProperty == null ? null : userProperty.getValue();    }}

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

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



Конечно, у такого подхода есть минусы. Toggles скапливаются, и здесь ничего не остается, кроме как их убирать. Также увеличивается сложность тестов, потому что проверять свой код нужно во всех режимах работы toggles. Кроме того, toggles лежат в базе, поэтому получается лишний поход в БД. Здесь ничего не остается, кроме как поставить кэш. Какие плюсы мы за это получаем?



Мы получаем возможность спокойно жить в парадигме trunk based development. Также можем проводить точечные эксперименты на пользователях.

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

Мобильное приложение обычно тоже раскатывается не сразу на всех пользователей, а вначале на какой-то процент из них. И мы можем получить такую ситуацию, что в зависимости от пользователя бэкенд должен иметь разную логику. Это не только изменения в API, но и именно изменения в бизнес-логике. Toggles нам тоже помогают решить такую задачу.

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

А вот если, например, у нас возникли проблемы с фото посылки, можно отключить эту функциональность, разобраться с проблемой, устранить ее и включить обратно.

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

3. Метрики и мониторинги


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

Какую информацию мы собираем? На слайд я выписала формальное определение метрик и мониторинга.



Но предлагаю рассмотреть, что это такое, на примере.



Метрика обычно представляется как график, он описывает какой-то параметр нашей системы. А мониторинг это событие, превышение заданного ранее порога и какое-то действие, которое необходимо предпринять при превышении порога.

Какие могут быть действия? Конечно, самое важное уведомить ответственных: написать в мессенджер или позвонить. Реже запустить процесс откатки последнего релиза. У себя в проекте мы такой вариант не используем. Еще есть вариант мониторинга, который мы называем загорается лампочка. Произошло интересующее нас событие, и оно отобразилось в интерфейсе мониторинга. Дальше тоже происходит действие уведомить ответственных или еще что-то.



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

На слайде пример, как нам в базе перестало хватать CPU.

Окей, у нас Java-приложение, и, конечно, стоит собирать информацию о состоянии JVM.



Мы используем Spring, поэтому для решения такой задачи хорошо подходит библиотека Spring Boot Actuator. Она добавляет endpoint в ваше приложение, и к этому endpoint можно обратиться по http и получить необходимую информацию о памяти или что-то еще. Окей, приложение запущено. Дальше оно вообще работает или нет? Что с ним происходит? Можно отправлять запросы в это приложение или нет?

@RestController@RequiredArgsConstructorpublic class MonitoringController {    private final ComplexMonitoring generalMonitoring;    private final ComplexMonitoring pingMonitoring;    @RequestMapping(value = "/ping")    public String ping() {        return pingMonitoring.getResult();    }    @RequestMapping(value = "/monitoring")    public String monitoring() {        return generalMonitoring.getResult();    }}

Такие вещи нужно понимать не только нам, но и балансеру. Для этого мы добавляем в приложение контроллер с двумя методами Ping и Monitoring. Рассмотрим вначале Ping. Он отвечает на вопросы, живо ли приложение, можно ли отправлять на него запросы. И отвечает он это в первую очередь не нам, но балансеру. Но мы же используем этот метод для мониторинга того, живо приложение или нет.

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

public enum MonitoringStatus { OK, WARNING, CRITICAL;}@RequiredArgsConstructorpublic class MonitoringEvent { private final String name; private volatile long validTill; private volatile MonitoringStatus status;}

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

public interface ComplexMonitoring { void addTemporary(String name, MonitoringStatus status, long validTillMilis); Result getResult(); //тут можно делать проверку для статусов в MonitoringEvent} 

Получается такой простейший интерфейс мониторинга. Добавить или обновить событие и вернуть текущее состояние мониторинга в оговоренном формате.

У нас есть клиент мобильное приложение, нужно понимать, как качественно мы отвечаем клиенту и какая вообще нагрузка на нас идет. Поэтому стоит собирать RPS, тайминги и пятисотки.



Как мерить RPS, понятно из названия. Стоит отметить, что хорошо смотреть как общий RPS по приложению, так и по отдельным методам API. А вот с двумя остальными с таймингами и пятисотками все не так понятно. В каждый момент времени может быть много запросов и, соответственно, много времен ответов. Всю эту информацию нужно агрегировать. Для этого хорошо подходят перцентили. Этот термин пришел к нам из статистики.

Чтобы рассказать, что это такое, на слайде есть нижний график, на нем выделена точка в 400 мс. Это график 99 перцентиля какого-то метода из нашего API. Что значат эти 400 мс? Что в этот момент 99% запросов отрабатывали не хуже 400 мс.

Остались пятисотки. Не стоит мерить просто количество пятисоток, потому что не будет понятен масштаб. Стоит мерить долю или процент. Но собирать можно не только пятисотые коды ответа, но и любые другие коды, которые вам могут быть интересны.

Как мы собираем RPS, тайминги и пятисотки? Когда запрос оказался у нас в инфраструктуру, он попадает на L7 balancer. А дальше он не сразу попадает в приложение, перед этим есть nginx.



А вот уже из nginx он попадает в приложение. У nginx есть access.log, в который можно собирать всю необходимую информацию. Это коды ответа, время ответа и сам запрос.



Часто, когда возникают проблемы с временем ответа или пятисотки, стоит посмотреть, что в это время было с хранилищем.

У нас PostgreSQL, поэтому мы именно его мониторим и смотрим. Мы строим все эти графики не сами они нам предоставляются облаком, в котором развернута наша база. Что у нас есть? Количество запросов, транзакций, лаг репликации, среднее время транзакции и так далее. На самом деле на этих графиках представлен факап, который у нас произошел.



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

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

public class MetricQueryRealJobExecutor { private static final RowMapper<Metric> METRIC_ROW_MAPPER = BeanPropertyRowMapper.newInstance(Metric.class); private final JdbcTemplate jdbcTemplate; public void doJob(MetricQuery task) { List<Metric> metrics = jdbcTemplate.query(task.getQuery(), METRIC_ROW_MAPPER); metrics.forEach(metric -> KEY_VALUE_LOG.info(KeyValueLogFormat.format(metric)) ); } @Data private static class Metric { private String key; private double value; }} 

Но далеко не всегда на такие мониторинги должна реагировать разработка. Очень часто на них вначале нужна реакция бизнеса. Вообще задача получения статистики по хранилищу выходит однотипной. Нужно сходить в базу, рассчитать статистику, а результат записать в key-value-лог. Поэтому мы сделали обертку для таких задач.

Сложили в базу такую тройку: уникальный ключ для метрики, сам запрос, в котором можно эту метрику посчитать, и Cron-выражение, когда считать. То есть в базе получилась тройка: ключ запрос cron expression. А над всей этой тройкой сделали Cron-задачу, которая достает ее из хранилища и добавляет к общему пулу Cron-задач. Дальше эта задача выполняется.

Итого, что мы получаем?

  • Плохой код не должен попадать в продакшен, для этого мы ставим всевозможные преграды: тесты, стрельбы, мониторинги. Задача без тестов не считается сделанной. Лучше заниматься оптимизацией работы тестов, чем получить проблему в продакшене.
  • Feature toggles помогают нам управлять логикой работы бэкенда, а мы, в свою очередь, можем легко управлять toggles, и их плюсы все-таки перевешивают минусы.
  • Мы должны уметь быстро обнаруживать проблему, в идеале раньше, чем ее заметят наши пользователи. Но ее мало обнаружить, нужно еще ее интерпретировать. Поэтому собирайте много метрик о состоянии системы. Это помогает в поиске проблемы.

И, конечно, давайте будем писать код без багов, тогда у нас вообще все будет хорошо. На этом у меня все.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru