Мы посидели, подумали и поняли, что вы правы. Мы пробуем объяснить всё на пальцах, но выходит рваными кусками и нигде нет полноценного описания системы. Так начался долгий путь сбора информации, поиска авторов и написания серии статей про Dodo IS. Погнали!
Благодарности: спасибо, что делитесь своим фидбэком с нами. Благодаря ему мы наконец описали систему, составили технорадар и скоро выкатим большое описание наших процессов. Без вас так бы и сидели ещё 5 лет.
Серия статей «Что такое Dodo IS?» расскажет про:
- Ранний монолит в Dodo IS (2011-2015 годы). (In progress...)
- Путь бэкофиса: раздельные базы и шина. (You are here)
- Путь клиентской части: фасад над базой (2016-2017 годы). (In progress...)
- История настоящих микросервисов. (2018-2019 годы). (In progress...)
- Законченный распил монолита и стабилизация архитектуры. (In progress...)
Если интересно узнать что-то ещё — пишите в комментариях.
Традиционно мы смотрим на систему, как на набор компонентов (технических или более высокоуровневых), бизнесовых модулей, взаимодействующих между собой ради достижения какой-либо цели. И если для проектирования такой взгляд оправдан, то для описания и понимания не совсем подходит. Причин тут несколько:
- Реальность отличается от того, что на бумаге. Не всё из задуманного получается. А нам интересно, как на самом деле всё оказалось и работает.
- Последовательное изложение информации. По сути можно пройтись хронологически от начала до текущего состояния.
- От простого к сложному. Не универсально, но в нашем случае именно так. От более простых подходов архитектура переходила к более сложными. Часто через усложнение решались проблемы скорости реализации и стабильности, а также десятки других свойств из списка нефункциональных требований (вот здесь хорошо рассказано про противопоставление сложности остальным требованиям).
В 2011 году архитектура Dodo IS выглядела так:
К 2020 году она немного усложнилась и стала такой:
Как произошла эта эволюция? Зачем нужны разные части системы? Какие архитектурные решения и почему были приняты? Разберёмся в этой серии статей.
Первые проблемы 2016 года: зачем сервисам выходить из монолита
Первые статьи из цикла будут про сервисы, которые первыми отделились от монолита. Чтобы ввести вас в контекст, расскажу, какие проблемы были у нас в системе к началу 2016 года, что нам пришлось заниматься разделением сервисов.
- Единая база MySql, в которую писали свои записи все приложения, существовавшие на тот момент в Dodo IS. Следствия были такие:
- Большая нагрузка (при этом 85% запросов приходилось на чтение).
- База разрасталась. Из-за этого её стоимость и поддержка становились проблемой.
- Единая точка отказа. Если одно приложение, пишущее в базу, внезапно начинало делать это активнее, то другие приложения чувствовали это на себе.
- Неэффективность в хранении и запросах. Часто данные хранились в некоторой структуре, которая была удобна для одних сценариев, но не подходила для других. Индексы ускоряли одни операции, но могли замедлять другие.
- Часть проблем сняли сделанные наспех кэши и read-реплики на базы (об этом будет отдельная статья), но они лишь позволили выиграть время и принципиально проблему не решали.
- Проблемой было наличие самого монолита. Следствия были такие:
- Единые и редкие релизы.
- Сложность в совместной разработке большого числа людей.
- Невозможность привносить новые технологии, новые фреймворки и библиотеки.
Проблемы с базой и монолитом много раз описывались, например, в контексте падений в начале 2018 года (Будь как Мунк, или пару слов о техническом долге, День, когда Dodo IS остановилась. Асинхронный сценарий и История о птице Додо из рода Фениксов. Великое падение Dodo IS), так что особо останавливаться не буду. Скажу только, что нам хотелось дать большую гибкость при разработке сервисов. В первую очередь это касалось тех, которые были самыми нагруженными и корневыми во всей системе — Auth и Трекер.
Путь бэкофиса: раздельные базы и шина
Навигация по главе
Схема монолита 2016 года
Перед вами основные блоки монолита Dodo IS 2016 года, а чуть ниже расшифровка их основных задач.
Касса Доставки. Учёт курьеров, выдача заказов курьерам.
Контакт Центр. Приём заказов через оператора.
Site. Наши сайты (dodopizza.ru, dodopizza.co.uk, dodopizza.by и т.д.).
Auth. Сервис авторизации и аутентификации для бэкофиса.
Трекер. Трекер заказов на кухне. Сервис отметки статусов готовности при приготовлении заказа.
Касса Ресторана. Приём заказов в ресторане, интерфейсы кассира.
Export. Выгрузка отчётов в 1C для бухгалтерии.
Оповещения и накладные. Голосовые команды на кухне (например, «Поступила новая пицца») + печать накладных для курьеров.
Менеджер Смены. Интерфейсы для работы менеджера смены: список заказов, графики производительности, вывод на смену сотрудников.
Менеджер Офиса. Интерфейсы для работы франчайзи и управляющего: приём сотрудников, отчёты по работе пиццерии.
Табло Ресторана. Отображение меню на телевизорах в пиццериях.
Админка. Настройки в конкретной пиццерии: меню, цены, учёт, промокоды, акции, баннеры для сайта и т.д.
Личный Кабинет Сотрудника. Графики работы сотрудников, информация о сотрудниках.
Табло Мотивации Кухни. Отдельный экран, который висит на кухне и отображает скорость работы пиццамейкеров.
Communication. Отправка sms и email.
FileStorage. Собственный сервис для приёма и выдачи статических файлов.
Первые попытки решить проблемы помогли нам, но стали лишь временной передышкой. Они не стали системными решениями, поэтому было ясно, что с базами надо что-то сделать. Например, разделить общую базу на несколько более специализированных.
Начинаем разгружать монолит: отделение Auth и Трекера
Основные сервисы, которые тогда больше других записывали и считывали из базы:
- Auth. Сервис авторизации и аутентификации для бэкофиса.
- Трекер. Трекер заказов на кухне. Сервис отметки статусов готовности при приготовлении заказа.
Чем занимается Auth
Auth — это сервис, через который пользователи логинятся в бэкофис (на клиентской части отдельный независимый вход). Также к нему обращаются в запросе, чтобы удостовериться, что есть нужные права на доступ, и что эти права не изменились с последнего входа. Через него же происходит вход устройств в пиццерии.
Например, нам хочется открыть на телевизоре, висящем в зале, табло со статусами готовых заказов. Тогда мы открываем auth.dodopizza.ru, выбираем «Вход как устройство», появляется код, который можно внести в специальной странице на компьютере менеджера смены, указав тип устройства (девайса). Телевизор сам перейдёт на нужный интерфейс своей пиццерии и начнёт отображать там имена клиентов, заказы которых готовы.
Откуда нагрузки?
Каждый залогиненный пользователь бэкофиса на каждый запрос ходит в базу, в таблицу пользователей, через sql-запрос вытаскивает оттуда пользователя и проверяет, есть ли у него нужные доступы и права на эту страницу.
Каждое из устройств делает то же самое только с таблицей устройств, проверяя свою роль и свои доступы. Большое количество запросов в мастер-базу приводит к её загрузке и трате ресурсов общей базы на эти операции.
Разгружаем Auth
У Auth изолированный домен, то есть данные о пользователях, логинах или устройствах поступают в сервис (пока будущий) и там остаются. Если они кому-то понадобятся, то он пойдёт в этот сервис за данными.
БЫЛО. Схема работы изначально была такой:
Хочется немного пояснить, как это работало:
- Запрос извне приходит на бэкэнд (там Asp.Net MVC), приносит с собой куку сессии, которая используется для получения сессионных данных из Redis(1). В ней либо есть информация о доступах, и тогда доступ в контроллер открыт (3,4), либо нет.
- Если доступа нет, нужно пройти процедуру авторизации. Здесь для упрощения она показана как часть пути в том же атрибуте, хотя это переход на страницу логина. В случае позитивного сценария мы получим правильно заполненную сессию и перейдём в Backoffice Controller.
- Если данные есть, то нужно проверить их на актуальность в базе пользователя. Не изменилась ли его роль, не надо ли его не пускать теперь на страницу. В этом случае после получения сессии (1) надо напрямую сходить в базу и проверить доступы пользователя с помощью слоя логики аутентификации (2). Далее либо на логин-страницу, либо переход в контроллер. Такая вот простая система, но при этом не совсем стандартная.
- Если все процедуры пройдены, то пропускаем дальше в логике в контроллерах и методах.
Данные пользователей отделены от всех других данных, они хранятся в отдельной таблице membership, функции из слоя логики AuthService вполне могут стать api-методами. Границы домена определены вполне чётко: пользователи, их роли, данные о доступах, выдача и отзыв доступов. Всё выглядит так, что можно вынести в отдельный сервис.
СТАЛО. Так и сделали:
У такого подхода есть ряд проблем. Например, вызов метода внутри процесса — не то же самое, что вызов по http внешнего сервиса. Латенси, надёжность, поддерживаемость, прозрачность операции совершенно другие. Подробнее именно о таких проблемах рассказывал Андрей Моревский в своем докладе «50 оттенков микросервисов».
Сервис аутентификации и с ним сервис устройств используются для бэкофиса, то есть для сервисов и интерфейсов, используемых на производстве. Аутентификация для клиентских сервисов (вроде сайта или мобильного приложения) происходит отдельно без использования Auth. Отделение заняло около года, а сейчас мы опять занимаемся этой темой, переводя систему уже на новые сервисы аутентификации (со стандартными протоколами).
- Нам хотелось перевести данные о пользователях, устройствах и аутентификации из баз по стране в одну. Для этого пришлось переводить все таблицы и использование с идентификатора int на глобальный идентификатор UUId (недавно перерабатывали этот код Роман Букин «Uuid — большая история маленькой структуры» и open-source проект Primitives). Хранение данных по пользователям (так как это персональная информация) имеет свои ограничения и для некоторых стран надо хранить их отдельно. Но глобальный идентификатор пользователя должен быть.
- Много таблиц в базе имеет аудит информацию о том пользователе, который совершил операцию. Это потребовало дополнительного механизма, чтобы была консистентность.
- После создания api-сервисов был долгий и постепенный период перевода на другую систему. Переключения должны были происходить бесшовно для пользователей и требовали ручной работы.
Схема регистрации устройства в пиццерии:
Общая архитектура после выделения Auth и Devices-сервиса:
Чем занимается Трекер
Теперь про второй из нагруженных сервисов. Трекер выполняет двойственную роль:
- С одной стороны, его задача — показывать сотрудникам на кухне, какие заказы сейчас в работе, какие продукты сейчас нужно готовить.
- С другой стороны — оцифровывать все процессы на кухне.
Когда в заказе появляется новый продукт (например, пицца), он попадает на станцию трекера «Раскатка». На этой станции стоит пиццамейкер, который берёт плюшку нужного размера и раскатывает её, после чего отмечает на планшете трекера, что выполнил свою задачу и передаёт раскатанную основу теста на следующую станцию — «Начинение».
Там следующий пиццамейкер начиняет пиццу, затем отмечает на планшете, что выполнил свою задачу и ставит пиццу в печь (это тоже отдельная станция, которую нужно отметить на планшете). Такая система была с самого начала в Додо и самого начала существования Dodo IS. Она позволяет полностью отслеживать и оцифровывать все операции. Кроме того трекер подсказывает, как готовить тот или иной продукт, проводит каждый вид продукта по своим схемам изготовления, хранит оптимальное время приготовления продукта и трекает все операции над продуктом.
Так выглядит экран планшета на станции трекера «Раскатка»
Откуда нагрузки?
В каждой из пиццерий примерно по пять планшетов с трекером. В 2016 году у нас было больше 100 пиццерий (а сейчас более 600). Каждый из планшетов делает раз в 10 секунд запрос на бэкэнд и выгребает данные из таблицы заказа (связка с клиентом и адресом), состава заказа (связка с продуктом и указание количества), таблицы учёта мотивации (в ней трекается время нажатия). Когда пиццамейкер нажимает на продукт на трекере, происходит обновление записей во всех этих таблицах. Таблица заказа общая, в неё же одновременно идут вставки при принятии заказа, обновления от других частей системы и многочисленные считывания, например, на телевизоре, который висит в пиццерии и показывает готовые заказы клиентам.
В период борьбы с нагрузками, когда всё и вся кэшировалось и переводилось на асинхронную реплику базы, эти операции с трекером продолжили ходить в мастер-базу. Тут не должно быть никакого отставания, данные должны быть актуальными, рассинхрон недопустим.
Также отсутствие собственных таблиц и индексов на них не позволяло написать более специфичные запросы, заточенные под своё использование. Для примера, трекеру может быть эффективно иметь индекс на пиццерию на таблице заказов. Мы всегда выгребаем из базы трекера заказы по пиццерии. При этом для приёма заказа не так важно, в какую пиццерию он падает, важнее, какой клиент сделал этот заказ. А значит там нужен индекс по клиенту. Ещё для трекера в таблице заказа не обязательно хранить id напечатанного чека или связанные с заказом бонусные акции. Эта информация наш сервис трекера не интересует. В общей монолитной базе таблицы могли быть только компромиссным вариантом между всеми пользователями. Это было одной из изначальных проблем.
БЫЛО. Изначально архитектура была такая:
Даже после выделения в отдельные процессы большая часть кодовой базы оставалась общей для разных сервисов. Всё, что ниже контроллеров, было единым и жило в одном репозитории. Использовались общие методы сервисов, репозиториев, общая база, в которой лежали общие таблицы.
Разгружаем Трекер
Главная проблема с трекером в том, что данные должны синхронизироваться между различными базами. Это же и главное его отличие от разделения Auth-сервиса, заказ и его статус могут изменяться и должны отображаться в различных сервисах.
Мы принимаем заказ на Кассе Ресторана (это сервис), он сохраняется в базе в статусе «Принят». После этого он должен попасть на трекер, где ещё несколько раз изменит свой статус: от «Кухня» до «Упакован». При этом с заказом могут происходить какие-то внешние воздействия от Кассы или интерфейса Менеджера смены. Приведу в таблице статусы заказа с их описанием:
Схема изменения статусов заказа выглядит так:
Статусы меняются между разными системами. И здесь трекер не является конечной системой, в которой замыкаются данные. Мы видели несколько возможных подходов для разделения в таком случае:
- Концентрируем все действия заказа в одном сервисе. В нашем случае этот вариант требует слишком большого сервиса по работе с заказом. Если бы мы остановились на нём, то получился бы второй монолит. Проблемы бы мы не решили.
- Одна система делает вызов в другую. Второй вариант уже интереснее. Но при нём возможны цепочки вызовов (каскадные сбои), связность компонентов выше, управлять этим сложнее.
- Организуем события, и каждый сервис обменивается с другим через эти события. В итоге был выбран именно третий вариант, по которому все сервисы начинают обмениваться событиями друг с другом.
То, что мы выбрали третий вариант значило, что для трекера будет своя база, а на каждое изменение заказа он будет посылать событие об этом, на которое подписываются другие сервисы и которое в том числе попадает в мастер-базу. Для этого нам нужен был некоторый сервис, который обеспечит доставку сообщений между сервисами.
К тому времени у нас в стеке уже был RabbitMQ, отсюда и итоговое решение использовать его как брокер сообщений. На схеме показан переход заказа от Кассы Ресторана через Трекер, где он меняет свои статусы и отображение его на интерфейсе Заказы менеджера. СТАЛО:
- На Кассе полностью готов заказ, и его пора отправить на трекер. Бросается событие, на которое подписан трекер.
- Трекер, принимая себе заказ, сохраняет его в свою собственную базу, делая при этом событие «ЗаказПринятТрекером» и посылая его в RMQ.
- В шине событий на заказ уже подписаны несколько обработчиков. Для нас важен тот, который делает синхронизацию с монолитной базой.
- Обработчик принимает событие, выбирает из него значимые для него данные: в нашем случае это статус заказа «ПринятТрекером» и обновляет свою сущность заказа в основной базе.
Если кому-то нужен заказ именно из монолитной таблицы orders, то можно считать его и оттуда. Например, такое нужно интерфейсу Заказы в Менеджере Смены:
Все другие сервисы тоже могут подписаться на события заказа из трекера, чтобы использовать их для себя.
Если через некоторое время заказ берётся в работу, то его статус сначала изменяется в своей базе (базе Трекера), а потом сразу генерируется событие «ЗаказВРаботе». Оно также попадает в RMQ, откуда синхронизируется в монолитной базе и доставляется другим сервисам. На этом пути могут быть разные проблемы, подробнее о них можно посмотреть в докладе Жени Пешкова про детали реализации Eventual Consistency в Трекере.
Итоговая архитектура после изменений в Auth и Трекере
Подводя промежуточный итог: изначально у меня была мысль упаковать девятилетнюю историю системы Dodo IS в одну статью. Хотелось быстро и просто рассказать об этапах эволюции. Однако сев за материал, я понял, что всё гораздо сложнее и интереснее, чем кажется.
Размышляя над пользой (или её отсутствием) от такого материала, я пришёл к выводу, что непрерывное развитие невозможно без полноценных летописей событий, развёрнутых ретроспектив и анализа своих прошлых решений.
Надеюсь, что вам было полезно и интересно узнать про наш путь. Сейчас я стою перед выбором, какую часть системы Dodo IS описать в следующей статье: пишите в комментариях или голосуйте.
Awake
По-моему это объяснение необходимости микросервисов какое-то странное. В плане трекера вы получили бы абсолютно то же быстродействие просто используя кэш в оперативке. 5 тысяч планшетов делают обновление раз в 10 секунд, то есть 500 запросов в секунду. Это же ни о чём. Увеличив количество планшетов в 10 раз получите 5000 запросов в секунду всего-то (ну то есть с огромным запасом). Просто банальная выборка из ОЧЕНЬ маленькой базы Редиса или чего-то ещё. Где тут какие-то нагрузки вы нашли? По-моему вы или велосипед ради велосипеда написали, либо причины всё-таки другие.
Yuriy_krd
Тоже очень удивился такой мотивации. И переход на микросервисы кажется надуманным. Типа, «чтобы было, потому что сейчас так модно». Еще всегда задавался вопросом: «Зачем сети пиццерий команда в почти полторы сотни разрабов?!».
VIkrom
В подавляющем большинстве случаев истинная причина перехода на микросервисы — потому что модно.
edelika
Но трекер нельзя кэшировать, ему нужны самые оперативные данные
Редис подходит только для кэша, который вы не боитесь потерять — он не гарантирует сохранность всех данных, которые содержит. Пиццерии нельзя терять заказы и продукты в заказах. Да и в сам Редис данные должны сначала попасть :-)
Awake
«Но трекер нельзя кэшировать, ему нужны самые оперативные данные» конечно можно и нужно. Никто не предлагает делать кэш на минуту.
«Редис подходит только для кэша, который вы не боитесь потерять — он не гарантирует сохранность всех данных, которые содержит. Пиццерии нельзя терять заказы и продукты в заказах. Да и в сам Редис данные должны сначала попасть :-)»
и в чём проблема? Мастер-данные храните где-угодно, по мере изменения данных делаете кэши протухшими и обновляете данные.
edelika
Кэш, даже, допустим, в 5 сек. + обновление раз в 10 сек может привести к тому, что продукт не выйдет на нужный трекер в течение 20 сек. Умножаем на кол-во планшетов в пиццерии и получаем 100 сек промедления только на стороне Dodo IS. На кухне за это время можно приготовить парочку пицц. А для пользователя это будет выглядеть как "пицца не протыкивается" — не переходит с одного планшета на другой. Кроме того, если заказ отменяют, кухне надо знать об этом максимально быстро, ччтобы лишнего не наготовить.
В комментарии ниже автор статьи говорит о том, что инвалидация кэша была нетривиальной задачей. Без разматывания клубка тут как-то сложновато.
Кэш в памяти в распределенной системе без инвалидации — отсутствие консистентности. На одной машине кэш обновился, на другой еще нет, в итоге получаем, что пицца на планшете скачет — то есть, то нет. Это очень критичная проблема для кухни.
Использование рэдиса в качестве распределенного кэша приводит к проблемам производительности. Если рэдис заэвиктит еще актуальную запись (а он это делает регулярно), снова начинаются скачки, но теперь уже лейтенси.
Но рэдис тут больше по надежности не подходит.
MatveyGrigorev
Саша, привет!
Проблемы, конечно, не в одном трекере — с него начали т.к. он один из самых критичных для бизнеса. Будучи выделенным в полноценный микросервис он прекрасно держит нагрузки.
Основные проблемы описаны в начале статьи отдельным списком: единая база, очень сложный процесс релиза, сильная связность кода разных сервисов внутри монолита и как следствие большое количество критичных багов, регулярно ломающих всю систему.
Например, сложности возникают в тот момент когда два десятка сервисов (в статье упомянуты только самые крупные) ходят в одну и ту же неоптимальную для каждого из них базу и она становится единой точкой отказа. При этом в ней появляются огромные таблицы типа списка заказов с кучей полей, бОльшая часть из которых нужна лишь какому-то небольшому количеству клиентов.
И кеши тут уже не сильно помогут, особенно когда критична скорость обновления и консистентность данных: в идеале они должны прилететь на соответствующий планшет до того как пиццамейкер физически передаст раскатанное тесто со станции раскатки на станцию начинения.
Отдельная беда — когда какой-нибудь из сервисов неудачным запросом лочит лишние строки в этой гигантской таблице с заказами: вся система начинает складываться как карточный домик.
Кстати, от Редиса, который у нас используется повсеместно (на картинках в статье далеко не все уместилось), мы в последнее время отказываемся, т.к. с ним внезапно всплывает много проблем: SLA в Ажуре всего 99.9, высокая стоимость, плюс куча способов выстрелить себе в ногу, чем регулярно пользуются разработчики (например случайно засовывая туда большие объекты в перемешку с мелкими).
Ну и не забывай что это все создавалось не в 2020м году крупной международной компанией, а в 2011м в режиме стартапа, когда было важно не держать 5к RPS, а быстро реализовать критичные для выживания бизнеса фичи)
Awake
О, привет! Я ж не против микросервисов, я против такого объяснения их необходимости) в целом-то да, монолит зло) Я более чем за похожую событийку, просто кмк именно решение сходу — закэшировать всё с минимальным временем протухания и параллельно прекратить наращивать большие таблицы, а потом уже устраивать такую ревизию.
MatveyGrigorev
Так примерно и произошло — сначала обмазали все кешами и выделили read-реплики базы, оптимизировали что можно, а потом пошли пилить монолит)
pritchin Автор
Наверное не стоило начинать не с начала, а где-то с середины. Отсюда тема проблем не совсем раскрыта, согласен. Я постараюсь в следующих статьях эту тему объяснить более подробно.
Пока вот примерно так:
DoubleW
Вы сомневаетесь что современные веб разработчики могут медленными сделать всего 500 rps? :-D