За 5 месяцев до дедлайна: как мы успели сделать систему онлайн-обучения к 1 сентября
В этой статье мы не откроем ничего нового ни о микросервисах, ни в целом об архитектуре приложений. Зато опишем, через что мы прошли на пути к запуску продукта за 5 месяцев разработки, как переосмыслили наши подходы, как применили лучшие практики, что удалось поставить на рельсы сразу, а что проектировали заново. Поделимся, какие преимущества микросервисной архитектуры мы для себя еще раз подчеркнули и с какими проблемами столкнулись. Итак, вот кейс.
Привет, мы компания разработки sheverev. Разрабатываем веб-сервисы и мобильные приложения c продуктовым подходом. Это первая наша статья на VC, но будут еще. Подписывайтесь на нас, мы будем говорить о хороших и не очень решениях и честно делиться опытом разработки.
Что было изначально
Наш партнер Восточно-Европейский институт психоанализа работал с готовой платформой 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 диаграмму и начали реализовывать сервисы.
(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, пришел к нам на помощь. Сейчас занимаемся его внедрением и в будущем напишем, как это повлияло на наши процессы.
P. S. У нас на сайте есть большой кейс про этот проект, в нем можно посмотреть скриншоты из системы и прочитать о том, как мы исследовали пользовательский опыт методом глубинных интервью. И в нашем Телеграм-канале есть видеообзор функций системы для роли студента.