Как ускорить разработку в пять раз: архитектура микросервиса

Kate

Administrator
Команда форума
Украинские события опять разделили нашу историю на периоды «До» и «После». IT все сегодняшние пертурбации коснулось нисколько не меньше, чем другие отрасли. И если в тучные годы компании могли себе позволить некоторые послабления, то сейчас проблемы оплаты, разрыв устоявшихся связей, снижение платежеспособности заказчиков и прочие последствия вынуждают их задуматься над оптимизацией расходов на разработку.

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

Мы в компании Датана с командой, включающей шесть разработчиков, за один 2021-й год реализовали пять проектов. Каждый из проектов хоть и не был гигантом типа Госуслуг, но все же имел целый ряд сложностей и, как правило, такие проекты реализуются порядка одного года каждый. Мы смогли реализовать за год пять таких проектов, т.е. наша скорость разработки была примерно в пять раз выше «среднего по больнице.

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

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

Нет смысла подробно описывать проекты, да и предлагаемые архитектурные подходы не оптимизировались специально для них. Эти подходы я оттачивал на протяжении многих лет на разных проектах, включая ETL-системы и Web-бэкенды. Также я уже 2 года преподаю их в компании Otus на курсе «Backend-разработка на Kotlin», внося улучшения на каждом потоке студентов. И, если боевые металлургические проекты обсуждать здесь мне не позволяет NDA, то учебные проекты доступны в открытом виде на github и мы вполне сможем изучить их в этой статье. Давайте откроем проект маркетплейса учебной группы мая 2021 года и далее будем обсуждать именно его: ссылка.

Но предварительно несколько слов об общих особенностях подхода в целом и проекта в частности. Первое. Проект написан на Kotlin и, я вам скажу, значительная доля наших темпов была обеспечена именно этим языком. Ни Java, ни JS/TS, ни Python, по моему опыту, таких темпов не обеспечивает. Но все те же подходы мы также вполне успешно применяли и при разработке на дугих языках, включая Python.

Второе. Откуда вообще возникают задержки при разработке? Вроде «ты ж программист» — взял и сделал. Да, все легко делается, пока проект мелкий и простой. Но по мере роста проекта количество сущностей в нем начинает расти бешеными темпами и они начинают конфликтовать между собой, вызывая:

  1. Баги. Невозможно заранее предусмотреть все варианты развития логики. Значительная часть логики вскрывается уже на продуктовой площадке и выливается в баг-репорты.
  2. Доделки. На этапе разработки и планирования практически невозможно предусмотреть все нюансы поставленной задачи. В итоге, приходится постоянно расширять логику и предусматривать ранее незапланированные кейсы.
  3. Переделки. Не всегда выявленные нюансы могут ограничиться только доделками. Нередко приходится переделывать часть программы.
И, чем крупнее ваш проект, тем больше возникает багов, недоделок и переделок. Я наблюдал проекты, которые полностью перестали развиваться, а занимались только поддержкой.

Очевидно, давно уже всем известно, как бороться с такими напастями:

  1. Более тщательной проработкой проекта. Порой тщательной настолько, что проект становится водопадом, в котором планирование занимает чуть ли не столько же времени, сколько сама разработка. А потом оказывается, что все равно что-то не предусмотрели, после чего ТЗ вновь долго согласовывается и так же долго внедряется. Не очень способствует оптимизации.
  2. Гибкими подходами. Мы признаемся в том, что не всемогущи и ограничиваем тщательность начальной проработки. При этом планируем рефакторинг в несколько итераций. Это отличный подход на бумаге, но на деле тут есть один нюанс: ваш проект должен предусматривать подобные рефакторинги, а для этого у него должна быть гибкая архитектура, чего в большинстве проектов, сделанных на Spring-MVC (да и на множестве других фреймворках) не наблюдается.
Итак, как же нам достичь гибкой архитектуры? Тут ничего нового нет. Все эти избитые вопросы с собеседований типа SOLID, GRASP, Банда четырех, чистая архитектура, DDD и прочее — это как раз про это.

Взгляните на проект маркетплейса. Что первое бросается в глаза — это большое количество модулей, т.е. модульная архитектура. Каждый модуль появляется для того, чтобы максимально изолировать какую-то функциональность. Когда она изолирована, мы всегда с легкостью можем локализовать источник проблем и всегда легко сможем корректировать (исправлять баги, доделывать, переделывать) именно эту функциональсть, не затрагивая другие части программы. На всех собеседованиях спрашивают про DI, все знают, что Spring на этом и построен, но почему-то в реальных проектах я не часто вижу модульную архитектуру.

Второе, что можно увидеть — это то, что вся программа состоит из дублирующих модулей. Например, несколько модулей с окончанием -app, т.е. приложений, т.е. фреймворков. Это сделано в учебных целях для демонстрации взаимозаменяемости каждого модуля. Да, оказывается приложения можно делать не только на Spring, но и на множестве других фреймворков, которые могут подходить больше конкретному приложению. Причем делать так, чтобы в любой момент можно было сменить фреймворк без ущерба для остальной логики приложения.

Аналогично, серия модулей repo обслуживает хранение. Сейчас Oracle объявила санкции российским потребителям и у многих компаний встает вопрос: как заменять базы данных этой компании на что-то другое. И ORM-библиотеки не всегда могут помочь в этом вопросе, потому как диалекты SQL могут вносить серьезные изменения в производительность и пр. нюансы, так и переход может выполняться не только на SQL-базы, но и на NoSQL или NewSQL. Когда у вас хранение выделено в отдельный модуль, то потребуется разработать только один новый модуль, подключить его в модуле фреймворка и все. Другие модули затронуты не будут.

Отдельно хочется упомянуть модули ok-marketplace-mp-transport-mp и ok-marketplace-be-service-openapi. В этих модулях находятся разные версии API для одного и того же микросервиса. Как видите, даже API мы выносим в отдельные модули. И это дает возможность нам не только легко менять спецификацию без существенных переделок, но и поддерживать одновременно несколько версий API в одном микросервисе. Например, для бесшовного апгрейда отдельных микросервисов системы.

Для того, чтобы изменение API не влияло на остальные компоненты, данные из транспортных моделей перекладываются во внутренние модели, размещенные в модуле ok-marketplace-be-common. Внутренние модели используются большинством остальных модулей микросервиса и собраны в контекст (о нем чуть далее). Они используются исключительно внутри этой программы, нигде не публикуются, а значит их изменение никак не отражается на внешних интеграциях или хранении. Именно поэтому они формируются так, как удобно нам. Например, там могут быть избыточные поля, они могут быть мутабельными, время нам удобно хранить не как ISO 8601 или Long-таймстэмпом, а как Instant.

Перекладка данных из транспортных моделей во внутренние производится с помощью модулей-маперов. Почему они тоже выделены в отдельные модули, а не объединены с транспортными моделями? Пример такой. У нас был реализован WebRTC-интерфейс, состоящий из трех компонентов: (1) сигнального сервера, (2) сервиса-продюсера видеопотока и (3) фронтенда-потребителя видеопотока. Все эти компоненты использовали те же самые транспортные модели, но собственные внутренние. Поэтому маперы не могут включаться внутрь модуля транспортных моделей и точно так же не могут включаться в фреймворк.

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

В проекте можно увидеть модуль ok-marketplace-be-logics. Он занимает особую роль. В нем происходит выполнение бизнес-логики. Бизнес-логика не может зависеть от конкретных реализаций хранения или форматов данных, она просто описывает то, что обычно рисуется на PBM-диаграмах, т.е. операции обработки полученных данных и вычисление результата. Именно поэтому этот модуль зависит только от ok-marketplace-be-common (плюс небольшие библиотеки-помощники типа валидаторов). В модуле бизнес-логики мы не интересуемся, в какую базу будут сохранены данные и как эта база устроена. В нем мы просто указываем, что такие-то данные необходимо сохранить.

Строится модуль бизнес-логики на базе классического шаблона Chain of Responsibilities (CoR, Цепочка обязанностей), который описан был еще в книге Банды четырех в далеком 1994 году, т.е. за год до появления Java и JavaScript. К сожалению, редко видел людей, которые им пользуются, хотя то, что он используется в Spring, знает больше людей :)

Шаблон представляет из себя классический конвейер Форда, в котором одинаково устроенные функции-обработчики последовательно выполняют работу над объектом класса-контекста (да, это его я упомянул выше). Работа с контекстом происходит следующим образом:

  1. Он создается при каждом вызове контроллера в фреймворке.
  2. Полученные в запросе данные десериализуются в транспортные модели, мапятся во внутренние модели, которые и складываются в контекст.
  3. Подготовленный контекст пропускается через цепочку всех обработчиков, вычисляя результат.
  4. Результат отправляется как ответ микросервиса.
Использование CoR дает нам главным образом неимоверную гибкость. Если завтра аналитики потребуют добавить еще одну валидацию или извлечь дополнительные данные из еще одного источника, мы просто добавим еще один или несколько обработчиков в бизнес-цепочку CoR. Изменение порядка обработки, изменение поведения, дополнительная функциональность — все это делается очень легко и управляемо. И, когда происходит что-то не то с бизнес-логикой, мы всегда знаем где искать проблемы.

Многие текущие изменения и бизнес-логике мы в команде выполняли в течении пары часов, максимум пары дней (в случае глобальных изменений). В нашей команде ни разу не прозвучала фраза, которую я встречал в других командах: «В ТЗ/архитектуру/постановку это требование не было заложено, поэтому на переделку требуется несколько месяцев».

Шаблон CoR в проекте реализован в проекте в виде библиотеки в модуле ok-marketplace-mp-common-cor. В боевых проектах мы используем его Open Source реализацию из git-проекта. Особенность библиотеки в том, что она оптимизирована для высокой читаемости логики человеком. Даже через месяц в проекте не просто становится разобраться, ведь все забывается. А благодаря библиотеке вся бизнес-логика приложения наглядно представлена в одном файле. И да, если в начале проекта число обработчиков редко превышает десяток, то в типичном боевом микросервисе их в итоге накапливается десятки и даже сотни.

В целом, это все про структуру проекта. Однако есть еще некоторые нюансы, которые влияют на количество багов и, значит, на скорость вывода в прод. В современной разработке хорошей практикой считается передавать как можно большую часть работы по валидации кода на компилятор, а не на тестировщиков. В частности поэтому предпочтительнее использовать класс вместо словаря (HashMap), где это возможно. Ведь элемент словаря компилятор не проверит на наличие или отсутствие, а элемент класса — легко.

Так вот, для связи микросервисов мы используем OpenAPI. Сейчас он стал уже довольно распространен, но все еще не так широко, как хотелось бы. OpenAPI спецификацию может написать даже аналитик, не без согласования с тимлидом, конечно. Далее из этой спецификации генерируются транспортные модели — те самые модули ok-marketplace-be-transport-openapi и ok-marketplace-mp-transport-mp. Зачем это делается? Наш CI настроен таким образом, что сразу после изменения транспортных моделей происходит пересборка всего проекта. Поскольку Котлин относится к языкам со строгой типизацией, микросервисы, изменение транспортных моделей в которых не учтены, при пересборке падают. Такие падения позволяют нам контролировать согласованность кода в проекте и это гораздо лучше, чем если о несогласованности мы узнаем уже только в проде.

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

Обо мне: Сергей Окатов,

руководитель отдела разработки, архитектор компании Datana;

автор и руководитель курса “Backend разработка на Kotlin” в компании Otus.


 
Сверху