Пост написан по мотивам доклада Антона Губарева @antgubarev, бывшего тимлида команды Teachers — он готов ответить на ваши вопросы в комментариях.

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

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

Наши продукты

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

Исторически сложилось, что для ведения сделок используется внешний «Мегаплан», который интегрирован с нашей TRM (Teachers Relations Management, внутри ее еще ласково зовут Tramway) — системой, в которой хранятся данные по конкретным преподавателям и которую мы дополнительно используем при поиске учителя по узкому запросу.

Множество процессов и работа сотен людей зависят от этой связки.

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

Так онбординг видят кандидаты в преподаватели. Асессоры выставляют свои слоты (время, когда они доступны для проведения уроков), а преподаватели бронируют уроки на время из слотов.

Осенью 2019-го компания активно взялась за развитие этих систем, и за 2,5 месяца нам предстояло: 

  • реализовать новые фичи.  Школа росла: если раньше требовалось 100 преподавателей в месяц, то теперь — 1000, и от нас во многом зависело, «захлебнется» или же выстоит подбор при масштабировании (кстати, те наработки здорово помогли в период карантина, когда многие педагоги решили попробовать онлайн-преподавание);

  • не утонуть в старом коде — а значит, отрефакторить хотя бы те части, где рефакторинг назрел давно;

  • отделить свою работу от другой команды.

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

Какие были варианты

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

+

Точно успеем с фичами

Ничего толком не отрефакторим

Продолжим мешать друг другу с другой командой

Второй вариант, который мы рассматривали, — это микросервисы. Идея постепенно выносить нужный нам функционал выглядела как способ решить все проблемы, но… тут мы вспомнили про наши ограниченные ресурсы. А для микросервисов потребуется намного больше трудочасов не только на написание кода, но и на то, чтобы потом не потерять управление над всем этим.

+

Организуем фичи

Разберемся с легаси

Перестанем толкаться со второй командой

С высокой вероятностью не успеем по срокам

Нужно увеличить ресурсы на инфраструктуру: не факт, что их дадут

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

Мы взяли последнюю на тот момент LTS версию Symfony и принялись за работу.

Детали реализации

Структура папок. Все независимые сервисы-продукты мы стали складывать в папку modules. У нас получились выделить такие сервисы:

  • интеграция с «Мегапланом»;

  • бэкенд календаря тренировочных уроков «преподаватель — асессор»;

  • бэкенд онбординга;

  • и несколько прикладных проектов: работа с очередями, декораторы, http-клиенты, мониторинг, метрики, логирование.

В папке src практически ничего не было — только Kernel.php и некоторые совсем общие инфраструктурные вещи.

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

Во всех сервисах независимая конфигурация и полное отсутствие межмодульных use.

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

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

Конфигурация всех модулей лежит в самих модулях.

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

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

Например, преподаватель записался на тренировочный урок и нам нужно записать это в карточку (она же сделка) в CRM. Модуль онбординга, используя пакет megaplan-client, положит в очередь сообщение, что нужно изменить конкретное поле. Модуль «Мегаплана» содержит в себе консьюмеры, которые разгребают эту очередь. Работает и в обратную сторону. «Мегаплан» пошлет нам веб-хук, когда что-то изменилось, — мы положим его в сыром виде в очередь, а онбординг эту очередь вычитает и что-то поменяет. Да, тут есть задержка, но она не критична, а данные точно сохранны.

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

Мы замониторили все очереди: смотрим количество консьюмеров, количество сообщений в очередях. Плюс добавили алерты при каких-то аномалиях — каждую неделю кто-то из команды назначается дежурным и решает такие проблемы, если они возникают (тьфу-тьфу, случается нечасто).

Также мониторим все http-запросы: пишем в Elasticsearch, смотрим в Kibana.

У нас в компании есть общий request-id бандл, разработанный специально для таких целей. Его довольно просто использовать в контексте Symfony: можно либо handler вручную добавить, либо через контейнер сконфигурировать. 

Как работаем со старым кодом. Старый сервис жив и будет жить дальше. Нам нужно как-то взаимодействовать, ведь отделить некоторые вещи от него можно уже только «с мясом». Поэтому мы завели в нем отдельный бандл, занесли туда нужную логику и сделали API-методы для работы с ней. Этот бандл имеет перекрестные ссылки с другими бандлами и напрямую дергает классы и методы в легаси-монолите.

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

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

Что получилось в итоге

Уже больше года мы живем на продакшене. При этом вышли за сроки буквально на пару недель, но смогли реализовать все фичи (так что у бизнеса претензий не было) и вынесли 2 из 4 сервисов. 

У нас свой код. Весной 2019-го у нас набралось меньше 10 коммитов в старый код: туда мы ходим в крайних случаях. При этому мы успели отрефакторить самые проблемные участки, а еще где-то половину доделывали в спокойном темпе. У нас нет фича-фриза — если приходит задача, первым делом думаем, что еще можем вытащить из старого кода, и делаем в ее рамках текущего проекта. 

У нас свои процессы. Мы не зависим от команды, с которой работали вначале. Ввели свой контроль архитектуры: если человек, который  получил задачу, видит,  что где-то надо что-то менять, он собирает всю команду. Мы исторически работаем распределенно, поэтому записываем встречу в Google Meet и тут же на ней фиксируем решения. А потом прикладываем все записи к задаче — можно в любой момент пересмотреть и понять, почему было так. 

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

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

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