Пока школьники выбирают тетрадки, а студенты капают слезинками на барные столики в честь приближающихся сессий, я решил вспомнить, как мы делали достаточно крупную и крутую систему онлайн обучения. Но не просто делали, а буквально летели на скоростных к тому самому сентябрю, который горит так же, как и всеобщие дедлайны вокруг.
Возможно в этой статье я не открою вам ничего нового ни о микросервисах, ни в целом об архитектуре приложений. Зато опишу, через что моя команда прошла на пути к запуску продукта за 5 месяцев разработки, как мы переосмыслили наши подходы, как применили лучшие практики, что удалось поставить на рельсы сразу, а что проектировали заново. В общем (и целом) делюсь, какие преимущества микросервисной архитектуры мы для себя еще раз подчеркнули и с какими проблемами столкнулись. Погнали.
Что было изначально
Сначала расскажу о партнере: это Восточно-Европейский институт психоанализа (ВЕИП). Институт работал с готовой платформой Moodle: она была супер медленная, тормозящая и не отвечала задачам партнера. Для нее дорого обходилась аренда инфраструктуры + эта платформа казалась очень неудобной пользователям, а ее кастомизация была бы долгой и дорогой. Плюсов, как видите, не особо.
"Нам нужна новая система, которая избавит пользователей от проблем в работе. И которую, в перспективе, получится масштабировать", — Алексей Тяпин, начальник управления по развитию и цифровизации института
Так нам достался идеальный продукт-оунер, а еще (огромным бонусом) крутые сотрудники со стороны заказчика: они понимали в общих чертах, какую систему хотят получить и уверенно озвучивали бизнес-требования, а также отвечали на вопросы в любое время и сразу, без долгой бюрократии и согласований (!). Это очень помогло нам уложиться в довольно ограниченные сроки (да-да, всего 5 месяцев).
Поскольку нам так повезло с коммуникациями, мы быстро сделали предпроектную аналитику, чтобы как можно скорее начать проектировать архитектуру будущего приложения. Попутно мы продолжали собирать детальную информацию.
Имея в наличии предпроектную аналитику, мы сразу поняли, что предстоит делать систему на микросервисах. Исходили из того, что в будущих релизах будут новые killer фичи, новые сервисы – все это должно вырасти в целую экосистему. Вся команда работала с большим энтузиазмом – тут можно было опробовать новые для нас технологии и отточить раннее приобретенные скиллы.
Начинаем. Подбор стека
Frontend сделан на базе библиотек ReactJS и Redux Toolkit для упрощения работы с состоянием приложения. Реализовано разбиение кода на сегменты, каждый сегмент вызывается тогда, когда он нужен пользователю. Мы используем React Query, чтобы проще организовывать цепочки вызовов API сервисов. С этим никаких сложностей не возникло – привычный для нас стек.
Для backend мы с командой обсуждали два варианта взаимодействия сервисов – очереди и gRPC, а основной наш стек – это node. js, PHP, Golang. Писать сами сервисы мы планировали на разных языках в зависимости от задачи.
Для очередей мы обычно используем RabbitMQ и довольны им. Но в этом случае нам почти всегда нужно собирать данные от других сервисов и возвращать их по HTTP в браузер, то есть ожидать ответ сервисов и использовать паттерн RPC.
Выбор пал на gRPC. Не буду перечислять его преимущества, об этом и так много где написано, например, здесь и здесь. Нас подкупили его быстрота (передача данных в бинарном виде по HTTP/2) и, конечно, декларативность – мы описываем интерфейс, модели данных в. proto файле и можем сгенерировать код клиентов и сервера под любой язык из нашего стека, остается только реализовать методы.
Следующий вопрос, который встал перед нами – как frontend будет общаться с backend. У нас было на обсуждении 2 варианта – прямо по gRPC или по REST API. Конечно, вариантов на самом деле много – можно сделать и на веб-сокетах и на GraphQL, но мы были ограничены сжатыми сроками до релиза, поэтому их и более экзотические варианты даже не рассматривали.
Сначала нам показалось, что если фронтенд будет обращаться на сервер по gRPC будет круто – получится единый механизм взаимодействия. Но, оказалось, не все так гладко. Нам пришлось:
реализовать ролевую модель и разграничение прав на каждом из сервисов,
защитить методы для внутрисервисного взаимодействия,
а еще мы не понимали, как спрятать TLS ключи на фронтенде, и нужны ли они тогда вообще,
ну и понять, как проверять токен пользователя (неужели из каждого сервиса ходить в сервис авторизации?! ).
Тут мы начали понимать, что нам необходим какой-то grpc-gateway, и, погуглив, в первой же строке нашли нечто даже лучшее – grpc-gateway и еще ряд фичей, до обсуждения которых мы еще не добрались (чуть ниже про них расскажу). Почему нам понравилось это решение:
код гетвея генерируется из прото файлов,
можно подключать middleware,
можно ограничивать rpm,
это http grpc-gateway, то есть сам сервер принимает http запросы и «конвертирует» их в grpc,
генерация документации в openAPI (swagger) происходит из тех же прото-файлов,
есть возможность проверять токены,
есть возможность тут же проверять права пользователя.
Мы продумали и нарисовали HLD диаграмму и начали реализовывать сервисы.
Весь функционал мы разбили на следующие сервисы:
пользователи (go)
группы (go)
зачетка (go)
дисциплины и учебные материалы (go)
тестирования (go)
лекции (go)
мероприятия (go)
уведомления (go)
сервис отправки писем (php)
сервис файлового хранилища (php)
чат (go)
сервис справочников (php)
парсер файлов квизов для тестирования (node. js)
Подготовка инфраструктуры
Обычно мы использовали просто docker контейнеры или Docker Swarm. Здесь мы понимали, что приложение должно быть отказоустойчивым, то есть крутиться в Kubernetes. Но на правильную настройку кластера и настройку CI/CD каждого сервиса требуется много времени.
Поэтому мы все же подняли одно окружение для разработчиков и для QA в Docker Swarm – это быстро и пока не ушло в продакшн вполне годится. А в Kubernetes мы переехали ближе к релизу, подняли там четыре окружения – dev, test, stage, prod.
Разный стек: и хорошо, и плохо
Писать каждый сервис на более подходящем стеке – это, конечно, здорово. К примеру, сервис с парсером файлов квизов для тестирований мы вообще почти не писали. Мы нашли полностью готовую и рабочую библиотеку на node. js, завернули ее в grpc сервер и, вуаля, работает!
Но мы столкнулись со следующей проблемой – поддержкой протофайлов в каждом сервисе в актуальном состоянии. Проблема была в том, что у нас довольно активная связь сервисов между собой и могут возникать ошибки, потому что мы где-то что-то забыли обновить.
Да, конечно, мы реализовали на go grpc reflection и теперь каждый сервис предоставляет информацию о методах, которые он поддерживает. Но это хорошо работает для разработки и тестирования, в продакшн такое не пойдет – у нас в контейнере всего один скомпилированный бинарник. Получается, что «на лету» обновить протофайлы, сгенерировать код и скомпилировать не удастся, а делать это при билде нам показалось чересчур костыльным. В общем, сейчас мы движемся к тому, чтобы создать отдельный репозиторий для Go сервисов и поместить туда общий код. И затем подтягивать его в каждый сервис именно оттуда.
А как же быть с сервисами на php и node. js? Ничего сложного – это простые сервисы, которые не обновляются и не имеют исходящих запросов в другие сервисы. Им обновлять протофайлы нет необходимости.
gPRC сервер на PHP
Вот еще одна из проблем, с которой мы столкнулись: как оказалось, на php можно без проблем выполнять grpc запросы к другим сервисам, а вот поднять сервер нельзя. Об этом говорится в официальной документации по grpc.
Поискав еще немного информации по этой теме, мы все же нашли один вариант – фреймворк Spiral на основе RoadRunner. Решили попробовать реализовать сервер gRPC на нем.
Под капотом у него все работает на Go, который запускает воркеры. Эти воркеры, в свою очередь, можно писать на php. В целом инструмент очень мощный, но нам показалось, что он плохо документирован. В документации были примеры будто бы от старой версии, хотя выбрана последняя версия документации. И в примерах кода на гитхабе эти самые примеры не совпадают с теми, что в документации. В общем, каша.
Возможно, мы решили попробовать этот фреймворк в неподходящее время – он переезжал на новую версию RoadRunner и на PHP8. Видимо, тогда разработчики фреймворка еще не успели актуализировать документацию и примеры кода на github.
День Х
Представьте: сегодня 31 августа, завтра начало нового учебного года и новые студенты должны обучаться уже в новой системе. А это значит, что они должны уже завтра быть добавлены в систему и получать первые учебные материалы.
И мы успели: первый релиз прошел довольно гладко и практически без проблем, а мелкие недоработки пофиксили на следующий день.
Для сбора фидбека мы установили чат на сайт, так студенты могли писать о проблемах и сбоях напрямую разработчикам. Проблем, кстати, было немного, но иногда возникали довольно экзотические:
некоторые пользователи использовали браузер, который не обновлялся несколько лет,
или еще: некоторые расширения, установленные у пользователей системы, добавляли заголовки в HTTP запрос, а из-за этого были ошибки в CORS.
Сейчас системой пользуется уже более 2 000 студентов института, а мы продолжаем делать новый функционал и иногда сталкиваемся с проблемами, которые необходимо решить. Расскажу про них подробнее.
Проблема: связи серверов
В некоторых местах мы получили сильную связанность сервисов друг с другом. Например, при получении студентом списка его дисциплин приходится пройти такой путь:
сервис пользователей – узнать информацию о пользователе
сервис групп – узнать группу пользователя
сервис дисциплин – получить список дисциплин, затем для каждой сервис лекций – узнать прогресс прохождения лекций, сервис зачетки – узнать текущий балл.
Больше всего это мешало QA, так как разработчики при поиске проблемы все же могут посмотреть в коде, что и куда прокидывается, у QA такой возможности нет. После обсуждения мы попробовали начать документировать это в виде диаграмм последовательностей.
И, как оказалось, это довольно долго. Мы, конечно, ребята усидчивые, но всё же решили поискать еще (и правильно сделали!). Цель поисков: помочь QA и разработчикам быстро локализовать ошибки в сервисах, чтобы они знали где все «заглохло» и в логи какого сервиса смотреть.
К счастью, поиски долго не продолжались: openTracing, а именно jaeger, пришел к нам на помощь. Сейчас занимаемся его внедрением и в будущем напишем, как это повлияло на наши процессы.