Как мы быстро распилили монолит, когда командам стало тесно
Пост написан по мотивам доклада Антона Губарева @antgubarev, бывшего тимлида команды Teachers — он готов ответить на ваши вопросы в комментариях.
Преподаватели Skyeng не сразу попадают «на передовую» — для начала они проходят отбор и обучение. Направление найма и онбординга преподавателей появилось в 2015 году — тогда же был сделан первый коммит в наш (уже бывший) легаси-монолит. Прежняя команда активно его поддерживала и старалась развивать, но в один момент ей стало объективно сложно справляться со всеми проектами. Так появилась моя команда.
Мы стали отвечать за набор фичей внутри монолита. Первые месяц или два пытались работать с тем, что было, но быстро поняли, что у двух команд свои процессы и взгляды на работу. Чтобы не утонуть в согласованиях, решили внедрять сервисную архитектуру.
Наши продукты
Для работы с кандидатами используется отдельная CRM. Нам нужно завести профиль каждому и провести вводное обучение, включая тренировочные уроки с «ненастоящими» учениками. Весь этот процесс называется онбординг, и это то, что видят перед собой кандидаты в преподаватели все время, пока они проходят отбор.
Исторически сложилось, что для ведения сделок используется внешний «Мегаплан», который интегрирован с нашей TRM (Teachers Relations Management, внутри ее еще ласково зовут Tramway) — системой, в которой хранятся данные по конкретным преподавателям и которую мы дополнительно используем при поиске учителя по узкому запросу.
Также у нас есть другой крупный продукт — это календарь тренировочных уроков. Такой урок выглядит как и обычный, но вместо ученика на другом конце сидит специально обученный асессор: он оценивает, как преподаватель ведет занятие, как выглядит, как общается, определяет его уровень и так далее.
Осенью 2019-го компания активно взялась за развитие этих систем, и за 2,5 месяца нам предстояло:
реализовать новые фичи. Школа росла: если раньше требовалось 100 преподавателей в месяц, то теперь — 1000, и от нас во многом зависело, «захлебнется» или же выстоит подбор при масштабировании (кстати, те наработки здорово помогли в период карантина, когда многие педагоги решили попробовать онлайн-преподавание);
не утонуть в старом коде — а значит, отрефакторить хотя бы те части, где рефакторинг назрел давно;
отделить свою работу от другой команды.
А еще нам нужно было не сойти с ума: поначалу было всего двое разработчиков — и это накладывало свои ограничения на выбор пути. В первую очередь от нас требовали фичи, причем в строго оговоренные сроки.
Какие были варианты
Отделяться бандлами внутри монолита. Теоретически, мы все могли пойти по пути наименьшего сопротивления и остаться жить в монолите на еще какое-то время. Так, наверное, поступили бы многие, потому что это решение проблемы бизнеса. Но минусы этого решения перевесили бы плюсы.
+ | − |
Точно успеем с фичами | Ничего толком не отрефакторим Продолжим мешать друг другу с другой командой |
Второй вариант, который мы рассматривали, — это микросервисы. Идея постепенно выносить нужный нам функционал выглядела как способ решить все проблемы, но… тут мы вспомнили про наши ограниченные ресурсы. А для микросервисов потребуется намного больше трудочасов не только на написание кода, но и на то, чтобы потом не потерять управление над всем этим.
+ | − |
Организуем фичи Разберемся с легаси Перестанем толкаться со второй командой | С высокой вероятностью не успеем по срокам Нужно увеличить ресурсы на инфраструктуру: не факт, что их дадут |
Постепенно выносить нужную нам функциональность. Так мы и пришли к альтернативному решению — родилась идея выделить единое приложение, один общий сервис, и выносить туда продукты и сервисы по отдельности. То, что сейчас называют модульным монолитом, только в нашем случае модули были ближе к полноценным микросервисам, и дальше вы поймете почему.
Мы взяли последнюю на тот момент LTS версию Symfony и принялись за работу.
Детали реализации
Структура папок. Все независимые сервисы-продукты мы стали складывать в папку modules. У нас получились выделить такие сервисы:
интеграция с «Мегапланом»;
бэкенд календаря тренировочных уроков «преподаватель — асессор»;
бэкенд онбординга;
и несколько прикладных проектов: работа с очередями, декораторы, http-клиенты, мониторинг, метрики, логирование.
Общие пакеты положили в папку packages: в основном это клиенты для различных API, внешних и внутрикомпанейских сервисов, а также общая структура данных и DTO, которая используется для межмодульного взаимодействия.
Так как модули друг друга напрямую не используют, мы можем быстро отделить любой из них в отдельный проект, не переписывая при этом половину исходного проекта.
Конфигурация. В конфигурации приложения также были только инфраструктурные вещи, которые никак не влияют на бизнес-логику: настройки коннектов к базам данных, настройки монолога и пр.
Межмодульное взаимодействие. Помимо REST API, мы используем шину очередей. Для нас крайне важна целостность данных, а менеджеры сопровождения часто меняют статусы у сделок, и если мы потеряем их из-за того, что не получилось отправить запрос по http, это может поломать весь дальнейший процесс онбординга.
Например, преподаватель записался на тренировочный урок и нам нужно записать это в карточку (она же сделка) в CRM. Модуль онбординга, используя пакет megaplan-client, положит в очередь сообщение, что нужно изменить конкретное поле. Модуль «Мегаплана» содержит в себе консьюмеры, которые разгребают эту очередь. Работает и в обратную сторону. «Мегаплан» пошлет нам веб-хук, когда что-то изменилось, — мы положим его в сыром виде в очередь, а онбординг эту очередь вычитает и что-то поменяет. Да, тут есть задержка, но она не критична, а данные точно сохранны.
Мониторинг данных между сервисами. Да, мы отказались от микросервисов, но проблема «как отследить все эти межмодульные взаимодействия» нас настигла.
Мы замониторили все очереди: смотрим количество консьюмеров, количество сообщений в очередях. Плюс добавили алерты при каких-то аномалиях — каждую неделю кто-то из команды назначается дежурным и решает такие проблемы, если они возникают (тьфу-тьфу, случается нечасто).
У нас в компании есть общий request-id бандл, разработанный специально для таких целей. Его довольно просто использовать в контексте Symfony: можно либо handler вручную добавить, либо через контейнер сконфигурировать.
Как работаем со старым кодом. Старый сервис жив и будет жить дальше. Нам нужно как-то взаимодействовать, ведь отделить некоторые вещи от него можно уже только «с мясом». Поэтому мы завели в нем отдельный бандл, занесли туда нужную логику и сделали API-методы для работы с ней. Этот бандл имеет перекрестные ссылки с другими бандлами и напрямую дергает классы и методы в легаси-монолите.
И если это не описано, то новый разработчик, придя в команду, будет очень долго вникать. Его онбординг будет занимать там, наверное, месяцы. Вот поэтому мы особое внимание уделяем документации, у нас там все время что-то пополняется.
Мы серьезно отнеслись к вопросу документирования. На этапе проведения технического ревью отдельным пунктом вносилось что и где мы будем менять в документации. А на код ревью это проверялось как пункт чек-листа ревьювера. Эти меры позволили держать нам информацию о проекте в достаточно полном виде, чтобы самим быстрее разбираться с нашей довольно сложной бизнес-логикой.
Что получилось в итоге
Уже больше года мы живем на продакшене. При этом вышли за сроки буквально на пару недель, но смогли реализовать все фичи (так что у бизнеса претензий не было) и вынесли 2 из 4 сервисов.
У нас свой код. Весной 2019-го у нас набралось меньше 10 коммитов в старый код: туда мы ходим в крайних случаях. При этому мы успели отрефакторить самые проблемные участки, а еще где-то половину доделывали в спокойном темпе. У нас нет фича-фриза — если приходит задача, первым делом думаем, что еще можем вытащить из старого кода, и делаем в ее рамках текущего проекта.
У нас свои процессы. Мы не зависим от команды, с которой работали вначале. Ввели свой контроль архитектуры: если человек, который получил задачу, видит, что где-то надо что-то менять, он собирает всю команду. Мы исторически работаем распределенно, поэтому записываем встречу в Google Meet и тут же на ней фиксируем решения. А потом прикладываем все записи к задаче — можно в любой момент пересмотреть и понять, почему было так.
Также завели свою документацию, и на код-ревью отдельно смотрим, чтобы изменения были описаны. Стараемся все основательно документировать, потому что в команду приходят новички.
Выросли до 5 разработчиков. Новые люди, приходя в проект, не испытывают сложностей со входом: не надо разгребать код, понимать, как он работает и пр.
P. S. На старте казалось, что будет проще, что возьмем больше и кинем дальше. Но ни я, ни кто-то еще из команды не жалеет, что мы пошли таким путем. Самое главное, что мы смогли отделить свою работу от другой команды, потому что это прям было очень болезненно. И по этому критерию все у нас получилось.