Привет, Хабр! Меня зовут Андрей Перепелкин. Я руководитель группы бэкенд-разработчиков, вошел в IT более 15 лет назад, 10 лет занимаюсь Java и около 4 плотно работаю с микросервисами. 

В этой статье я расскажу, как:

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

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

  • встраиваем микросервисы в инфраструктуру и собираем метрики и логи, не загружая этим разработчиков.

Я работаю в «Метр квадратный». Компания существует всего около двух лет, но у нас большая экосистема и 16 команд разработчиков — больше 150 человек. 

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

Сложности на старте

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

Интеграция продуктов

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

Команды независимые — подходы разные

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

Команды независимые — проблемы общие

Наши группы разработчиков не зависят друг от друга, но они будут сталкиваться со сходными задачами: настройка развертывания продуктов, хранение файлов, мониторинг. Если пустить все на самотек, вскоре мы получим целый парк велосипедов. Команды не должны раз за разом заново решать инфраструктурные проблемы.

Следовательно, мы должны были:

  • организовать процесс разработки так, чтобы он легко масштабировался; 

  • сформировать общее поле разработки. У команд должен быть единый стиль, подход к продуктам;

  • вынести инфраструктуру и управлять ей отдельно.

Не сразу, но мы решили эти задачи при помощи правильной комбинации инструментов.

Инструментарий

Наш основной язык — Java, но отдельные проекты написаны на Kotlin. Мы пользуемся набором модулей Spring Boot и Spring Cloud, а в качестве протокола межсервисного взаимодействия взяли gRPC. 

За обмен сообщениями отвечает Apache Kafka, а за управление проектами —Gradle. Сборка происходит с помощью Gitlab CI. Для управления контейнерами мы применяем Docker, Helm и Kubernetes, а хостимся в Yandex.Cloud.

Эталонный микросервис

Разработка началась с микросервиса аутентификации, который необходим практически во всех продуктах. 

Мы разделили его на модуль API и модуль сервиса и разложили их по разным репозиториям. В качестве протокола для API использовали gRPC. Там находится кодогенерированный пакет — описание контракта работы с сервисом. Из этого модуля мы получаем артефакты, которые можно подключать к клиентам. Кроме Java-артефактов, создаются и артефакты для JavaScript. В начале мы использовали подход BFF, и было удобно просто подключать npm пакеты с gRPC клиентом к NodeJS сервису. 

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

Конечно, с эталонным сервисом есть определенные проблемы: 

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

  • однажды команды начнут вносить в этот сервис изменения. Непонятно, перенести изменения в эталонном сервисе в проекты, созданные на его основе.

Варианты реализации

Первый выход, который приходит на ум, — создать мультимодульный проект Gradle, где все общее описывается в основном проекте, а различия — в sub-модулях. Для этого нужно либо использовать sub-модули в Git, либо создать монорепозиторий. Но так проект получается достаточно «жирным», и мы получаем все проблемы монорепозиториев, связанные со сложностью организации сборки, версионированием ну и, собственно, управлением репозиторием. 

Второй вариант — сделать как в Maven: создать родительский pom-файл с необходимыми и рекомендуемыми зависимостями. Это классический dependency management, то есть настройка плагинов и зависимостей, которые мы хотим включить в проект.

Однако, Maven проигрывает в лаконичности Gradle. А еще опыт подсказывает, что, как только появляются комбинации настроек, ветки становится сложно поддерживать. Все чаще встречается копипаст.

Поэтому мы вспомнили опыт команды Netflix и решили использовать плагины Gradle.

Плагины

Netflix использует маленькие утилитарные плагины Nebula для управления релизами, добавления в проект дополнительной информации из Git, для метрик по сборке.

Мы написали похожий утилитарный Quality plugin, чтобы доставлять правила контроля качества и стилистику кода до разработчиков. Пока калибровали правила, поняли, что с его помощью получается достаточно легко доставлять изменения правил. Достаточно поменять версию плагина, и у всех появляются новые правила. Получилось удобно.

Кстати, мы выбрали язык Kotlin в качестве DSL для Gradle. Часть наших проектов уже использует Kotlin, и мы решили, что будет удобнее описать проект тем же языком. Еще одним плюсом стала поддержка Intellij Idea.

Затем мы создали еще один плагин для конфигурации модуля API. Когда конфигурация модуля разрастается, там появляются новые зависимости, необходимые шаги сборки, кодогенерации, — все это делает проект все более громоздким. 

Конфигурация проекта занимает более ста строк. Это еще не все влезло на скриншот
Конфигурация проекта занимает более ста строк. Это еще не все влезло на скриншот

Поэтому мы взяли конфигурации и вынесли в практически неизменном виде в плагин для модуля API. Получился лаконичный фрагмент кода для продуктовых сервисов. Кстати, эту идею мы позаимствовали у Spring Boot.

Модуль API. Из него можно исключить ненужные в конкретном случае зависимости
Модуль API. Из него можно исключить ненужные в конкретном случае зависимости

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

Стартер аутентификации

Затем мы вернулись к сервису аутентификации. Там использован OAuth 2.0. Это протокол, который позволяет авторизовывать клиентов без логина и пароля, при помощи обмена JWT-токенами. Вот только у Spring из коробки нет конфигурации для работы с gRPС. Самый простой способ решения проблемы — сконфигурировать стартер Spring, в котором будут необходимые настройки.

Опыт показал, что на подобные стартеры можно возложить и некоторые инфраструктурные задачи, например, логирование. Логирование у нас реализовано при помощи библиотеки Logback. Мы используем Elasticsearch, Fluentd собирает логи, а смотрим их в Kibana. 

Много стартеров = путаница зависимостей

Когда было готово три стартера и пара плагинов, мы столкнулись с еще одной проблемой. 

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

В Maven эта проблема решается при помощи Dependency Management, но в Gradle такого инструмента нет.

Поэтому мы использовали плагин Java-platform. Он позволяет создать некую экосистему Java, рекомендовать версии и настройки. Так появился еще один артефакт для управления экосистемой, для объединения всех стартеров, плагинов и проектов.

Монорепозиторий

Поначалу мы хранили каждый плагин и каждый стартер в отдельном репозитории, но это создавало ряд неудобств. Каждое изменение тянуло за собой несколько больших исправлений, часть которых могла потеряться или забыться. Например, изменив стартер, приходилось менять Java-platform, а потом и плагин, который доносит Java-platform.

В конце концов, мы решили объединить модули в монорепозиторий и настроили сквозное версионирование.

Структура монорепозитория. Core — плагин, platform — экосистема java, starters - инфраструктурные стартеры. У нас нет путаницы с версиями Spring и других библиотек, потому что все они записаны в корневом build.gradle. 
Структура монорепозитория. Core — плагин, platform — экосистема java, starters - инфраструктурные стартеры. У нас нет путаницы с версиями Spring и других библиотек, потому что все они записаны в корневом build.gradle. 

Таким образом, внося изменение, мы получаем pull request, а плагин доносит изменение. 

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

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

Развитие инфраструктурных модулей

Мы построили работу вокруг монорепозитория и развиваем эту структуру до сих пор. Над его наполнением работает вся команда.

Вначале у нас работала только доставка одинаковых зависимостей до команды разработки. Следующей появилась трассировка. Затем мы подключили мониторинг Prometheus. Вся подготовка, все необходимые метрики (например, включение микрометра) тоже подготовлены стартером.

В числе последних усовершенствований Sentry система, которая регистрирует инциденты, например, исключения Java. К тому же, мы взяли и настроили библиотеки и реализовали еще один стартер для трассировки запросов в gRPС. 

Сборка проектов

Благодаря общему корпоративному плагину, нам удалось реализовать общий пайплайн сборки и деплоя.

Мы использовали GitLab CI и вынесли из проектов общий скрипт, описывающий пайплайн. Теперь достаточно его запустить, и проект встроится в систему сборки. Это делается одной директивой — всего три строчки, и сервис собирается и доставляется до сред разработки.

Для версионирования мы использовали SemVer, а за инкремент отвечает release plugin. GitLab hook автоматически запускает правильный пайплайн. 

Semver для сервисов
Semver для сервисов

Мы заставили команды разработки задуматься, какие изменения они делают. И у нас нет путаницы в версиях, все автоматически инкрементируется. Всегда можно посмотреть, какая версия собрана или используется.

Доставка на стенды

Доставкой на стенды занимается пакетный менеджер Helm. Здесь мы тоже постарались упростить себе жизнь, и вынесли в Helm-библиотеку все общее, что было в нашем проекте. Например, туда попало переменное окружение: метки, настройка ingress-хостов, правила именования. Все это включается в шаблоны, которые описывают деплой.

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

Гибкая организация разработки

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

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

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

Что мы получили

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

Мы доставляем до независимых команд общие тренды разработки с помощью Gradle-плагина. У разработчиков общие правила, одинаковые зависимости, и они сразу встроены во все инфраструктурные проекты.

У нас порядка 50 микросервисов и 80 репозиториев с учетом модулей API, и мы успешно управляем этим хозяйством. Чтобы масштабироваться и заводить новые проекты, нам не нужно раздумывать над вопросом «как?». 

Работая в банковской сфере, я встречался с тем, что для подготовки сервиса к инфраструктуре нужно было прочесть до пяти мануалов. Причем практически все настраивалось вручную.

У нас нет таких объемных инструкций, но у разработчиков не возникает вопросов, как создать и встроить в инфраструктуру новый сервис. Это происходит автоматически, а все метрики одинаковые. Мы даже реализовали общие дашборды, например, по Java-метрикам. Каждый сервис, созданный с помощью плагина, получает корректное логирование без дополнительной настройки.

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

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

Комментарии (3)


  1. Ermak
    02.12.2021 16:19

    Тема мониторинга не раскрыта совсем.


    1. andrew_p Автор
      06.12.2021 10:57
      +2

      Мониторинг как таковой заслуживает отдельной статьи, в данной статье рассматриванием мониторинг с точки зрения удобства подключения к проекту. Если в двух словах - то для измерений пользуемся micrometer который входит в нашу экосистему разработки и подключается автоматом в проекты, дополнительно используем micrometer-jvm-extras который предоставляет чуть больше информации по памяти jvm приложения. Метрики собирает prometheus, его же используем для рассылки алертов, а дашборды делаем в grafana.


  1. silentz
    03.12.2021 14:31
    +1

    Сейчас набегут люди и будут мне объяснять, что я не прав (и возможно я действительно не прав), но... Отвергать мавен из-за его не "лаконичности" - такой себе резон. Сколько не работал со сборками на мавене или гредле никогда не было проблем с пониманием зависимостей, тиражированием. Проблема с гредлом появляются тогда, когда начинают писать свои плагины - а рано или поздно так происходит - так как появляется рандомный принципал инженер, который эту практику притаскивает и увольняется. И еще: я может просто не понимаю про какие "комбинации настроек" вы говорите, что их версии мешают вам собирать проекты - лично я считаю, что система сборки не должна вообще ничего знать о версионировании и каких-то настройках - это не её забота. Если нужны определенные версии 3rd party зависимостей - делаешь parent pom, суёшь в него properties и переиспользуешь его в других репозиториях, при этом совсем не обязательно иметь один моно-репозиторий.