Что следует знать об организации работы при переходе на микросервисную архитектуру

Привет! Меня зовут Руслан Колодяжный, я CTO R&D-центра финтех-компании Wirex. За 5 лет существования наша компания выросла со стартапа до международной платформы, которой пользуются более 3 миллионов людей по всему миру. Одним из важных этапов нашего развития был переход от монолитной архитектуры к микросервисной, подкрепленный новой организационной структурой. В этой статье я расскажу о том, как изменения организационного подхода повлияли на процесс разработки. Возможно, опыт нашей компании будет полезным тем, кто планирует переход на микросервисную архитектуру и хочет сделать этот процесс максимально эффективным.

Предыстория

Все началось с приложения, написанного на .NET Framework. В качестве облачной платформы мы выбрали Azure, как базу данных использовали Azure SQL, а системой контроля версий выступал Bitbucket. В качестве хостинга для API отдали предпочтение Azure App Services. Back-end хостился в виде Azure Cloud Service workers, которые были тесно связаны между собой и являлись монолитом. Разработка back-end велась в одном репозитории командой из 5 человек. Задачей команды было максимально быстро доставить изменения в production и получить обратную связь от пользователей.

Спустя полгода после начала работы количество пользователей выросло до 50 тысяч. Для его увеличения приняли решение расширить функциональность приложения. Число разработчиков увеличилось до 15, и они были разделены на несколько команд. Как методологию разработки мы использовали Feature driven development, фокус разработчиков был сосредоточен на функциональных элементах (features), полезных с точки зрения пользователя. Владение кодовой базой на данном этапе было коллективным (collective code ownership), ответственность команд заключалась в имплементации новой функциональности, а также доставке изменений в production.

Рис. 1. Коллективное владение единой кодовой базой

Коллективное владение кодовой базой, рост количества разработчиков и фокус на скорости релизов приводили к тому, что время на интеграцию кода (слияние изменений) и прохождение регрессионного тестирования увеличивалось.

Каждая из команд имела в своем владении dev environment, которая была максимально похожа на prod. Разработка новой функциональности (feature) для команды из 3–4 человек в среднем занимала 2–3 недели. Но за счет того, что количество QA-инженеров было ограничено, формировалась очередь на прохождение регрессионного тестирования на UAT, и в некоторых случаях это могла быть неделя или даже больше. Эти факторы вместе с ограничениями масштабируемости монолитного приложения в дни рассылки маркетинговых кампаний и постоянно растущая клиентская база дали толчок к переходу на микросервисную архитектуру.

На пути к изменениям: от монолита к микросервисам

Мы начали всю функциональность максимально переносить из монолитного приложения во вспомогательные или независимые микросервисы. В качестве инфраструктурной платформы был выбран Azure Service Fabric, так как она позволяет быстро настроить кластер и управлять масштабированием. Также Azure Service Fabric предоставляет богатую программную модель в виде stateful/stateless-сервисов, поддержку actor model и reliable state, которые мы использовали в своих микросервисах.

При разделении монолита мы применяли DDD, шаблон Decompose by Subdomain. Количество микросервисов увеличивалось с добавлением в продукт новой функциональности. Каждому микросервису мы выделили свою кодовую базу (репозиторий). При проектировании применялся подход отдельных баз данных (БД) под каждый микросервис. Для разделения ответственности в своем продукте мы использовали CQRS- и Event Sourcing-подходы, shared libraries поставлялись через NuGet.

Также был настроен процесс CI/CD с помощью TeamCity & Octopus. Микросервисы доставлялись в production в виде гранулярных rolling upgrade-деплоев.

Рис. 2. Распределение ответственности команд по микросервисам, коллективное владение распределенной кодовой базой

Распределенная кодовая база исключила все проблемы слияния. Команды больше фокусировались на процессе разработки и в то же время меньше беспокоились о продолжительном регрессионном тестировании. Затраты, выделяемые на масштабирование монолитного приложения, стали существенно ниже. Независимые микросервисы теперь масштабировались по необходимости, и мы могли выдержать нагрузку в 10 раз больше. Это позволило выдерживать пиковые нагрузки при запуске маркетинговых кампаний. Такие действия вместе с маркетинговой стратегией помогли увеличить клиентскую базу до 2 миллионов пользователей.

При разделении на микросервисы особое внимание уделяли безопасности.

Мы смогли реализовать ограничение по доступности своих сервисов на уровне подсетей (Network Security Groups, Demilitarized Zone). Некоторые компоненты, которые владеют информацией security sensitive, вынесены в изолированные кластеры. Вследствие кластеризации мы ежегодно проходим PCI DSS-сертификацию.

Введение strong code ownership и распределение ответственности

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

Решением стало введение строгого типа владения кодовой базой (strong code ownership) и разделение зон ответственности. Каждый сервис должен был принадлежать только одной команде, которая отвечает за внесение изменений, будущее развитие этого сервиса, его поддержку и доставку в production.

Воплощением strong code ownership в Wirex стали доменные команды, которые появились вместо feature teams. Команды были созданы вокруг доменов — областей знаний, логически объединенных бизнес-процессов и нашего приложения. Их сфера ответственности касалась нескольких микросервисов. Таких команд у нас пять, каждая из них управляет своими микросервисами.

Рис. 3. Распределение ответственности доменных команд по микросервисам, строгий тип владения кодовой базой

Сейчас у каждой доменной команды есть своя development environment, которая максимально приближена по инфраструктуре к production environment.
Появилась полноценная команда DevOps из 8 человек, которая полностью отвечает за инфраструктуру и безопасность. Скорость разработки и количество релизов увеличились примерно в 5 раз.

Как это изменило структуру разработки в компании в целом (не только Back-end)

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

Рис. 4. Процесс сбора требований и декомпозирования задач на доменную структуру

Доменные команды оценивают задачи и добавляют их в рабочий план (бэклог) в соответствии с приоритетами. Экспертные знания в своих сервисах позволяют им выполнять задачи качественнее и быстрее. В разработке появилась возможность не только накапливать, но и разбирать технический долг, ускорилась доставка сервисов в production. Качественное отличие проявилось в виде более частых гранулярных релизов. Доменные команды делают гранулярные технические релизы микросервисов, затем производят продуктовый релиз включением функциональности с админпанели.

Рис. 5. В бэклогах присутствуют задачи по сервисам, которыми владеет доменная команда

Выводы

Это только небольшая часть описания процесса перехода. Многое осталось за кадром, в том числе и особенности работы ключевого направления команды продуктовой разработки. Наш опыт показал, что для масштабируемости разработки и реализации высоконагруженных систем с помощью микросервисной архитектуры важны не только технические, но и корректные менеджерские решения. При имплементации микросервисной архитектуры нужно учитывать потребность изменений на уровне организационной структуры всей компании, в нашем случае это был переход от collective code ownership к strong code ownership. Чтобы улучшить взаимодействие команд, разработчикам и менеджменту необходимо объединять усилия и улучшать процессы жизненного цикла разработки программного обеспечения.

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось1
До обраногоВ обраному3
LinkedIn

Схожі статті




38 коментарів

Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.

Спасибо за статтью, буду рад прочесть продолжение.
Сравнивали ли стоимость разворачивания/содержание инфраструктуры на Amazon сервисах?

Детальное сравнение проводили еще когда начали вводить изменения. Верхнеуровнево разница есть, но все кроется в том, какие технологии, встроеные функциональности cloud поставщиков используются, и тд. Все можно и нужно обсуждать детально с провайдером, в большинстве случаем охотно выходят на связь. И тогда же, после такого ревью и общения, выбрали вектор platform agnostic подхода, чтобы была возможность активно рассматривать разных клауд провайдеров.
С другой стороны мы очень тесно сотрудничаем сейчас с Microsoft по многоим направлениям, где Azure Cloud — одно из основных. Сейчас наш продукт работает во многих регионах. Это в т.ч значит, что мы активно используем клауд ресурсы, что дает нам возможность развивать более активно наше партнерство с Microsoft, где мы обсуждаем не только цены, но и ценность,которую получает Wirex, кроме простого потребления продуктов/услуг. Например мы напрямую взаимодействуем с инженерами, архитекторами Microsoft, организованы knowledge-sharing сессии, организовываем сертификации и тд. Придерживаемся такого подхода со всеми партнерами, ну и общаемся с AWS и Google Cloud постоянно.

Спасибо за ответ, очень познавательно.

Скорость разработки и количество релизов увеличились примерно в 5 раз.

А как вы замеряли?

Достаточно взглянуть на релизный пайплайн год назад и сейчас.
Ранее технический релиз был событием к которому готовились заранее, сильно связанные компоненты обновлялись и релизились только вместе. Была борьба за QA, DevOps инженеров, ресурсы environment.
Strong code ownership и доменная структура позволяет командам имплементировать изменения максимально быстро и качественно за счет ограниченной зоны ответственности.
Каждая команда разработчиков имеет выделенного DevOps инженера, выделенных QA. Так что скорость технических релизов в каком-то смысле не является чем-то сверхестественным с таким подходом.

Спасибо за статью. Самое интересное(всякие детали, проблема и решения перехода) не освещено, напишите еще статью? )

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

С точки зрения литературы могу рекомендовать книгу «Monolith to Microservices» by Sam Newman (www.oreilly.com/...​roservices/9781492047834).

Если есть конкретные вопросы которые мы можем осветить, или страхи связанные с переходом от монолита к микросервисной архитектуре — озвучивайте )

Мы готовы делится опытом и можем подготовить техническую статью.

Спасибо за статью! Очень интересно будет почитать про техническую трансформации продукта в контексте бизнеса и развития, пишите еще.

Воплощением strong code ownership в Wirex стали доменные команды, которые появились вместо feature teams. Команды были созданы вокруг доменов — областей знаний, логически объединенных бизнес-процессов и нашего приложения. Их сфера ответственности касалась нескольких микросервисов. Таких команд у нас пять, каждая из них управляет своими микросервисами.

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

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

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

strong code ownership

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

разработка по роадмапу и долгосрочное планирование

Это как при waterflow?

Необязательно. Agile — это не всегда всегда внезапное открытие новой амбразуры и перебрасывание всех существующих ресурсов туда вне зависимости, что они делали до этого (что неизбежно влечёт за собой потерю качества и трудности с пониманием бизнес-логики). В идеале здесь каждый субдомен рассматривается почти как отдельный подпродукт, чтобы максимально выиграть от DDD, и strong code ownership позволяет командам перформить гораздо круче, чем без него (сужу по своему опыту). Но, опять же, в условиях нехватки ресурсов и/или частой смены приоритетов и/или существующей бизнес-логики на уровне целого продукта так вряд ли получится, конечно.

В идеале здесь каждый субдомен рассматривается почти как отдельный подпродукт, чтобы максимально выиграть от DDD, и strong code ownership позволяет командам перформить гораздо круче, чем без него (сужу по своему опыту).

проблема там в приставке «под-» в слове «подпродукт» невероятно частая ситуация когда такое или любое другое разбиение например в рамках крупной корпорации просто «теряет» сам «продукт» и получается так что существуют только «локальные команды» а общего видения ни стратегии нет

как частность также нет ещё и тактики что начинает выливаться в «тёрки» между командами как организационные так и чисто технические

вот как пример ситуация которую я наблюдаю на своих «плюсах» за последних несколько лет чтобы разрешить использовать новый стандарт си++ нужно принять решение уже уровня «продукта» а решить это некому просто потому что такого человека нет )) ну так получилось причины случаются разные

проблема там в приставке «под-» в слове «подпродукт» невероятно частая ситуация когда такое или любое другое разбиение например в рамках крупной корпорации просто «теряет» сам «продукт» и получается так что существуют только «локальные команды» а общего видения ни стратегии нет

В рамках крупной — наверное, да, но при определённом размере решения эта ситуация возникнет в любом случае при идентичном подходе к управлению.

и получается так что существуют только «локальные команды» а общего видения ни стратегии нет

как частность также нет ещё и тактики что начинает выливаться в «тёрки» между командами как организационные так и чисто технические

Ну, для такого существуют Product и Program Manager’ы.

но при определённом размере решения эта ситуация возникнет в любом случае при идентичном подходе к управлению.

да

Ну, для такого существуют Product и Program Manager’ы.

я выделил момент об котором шла речь ))

При разделении монолита мы применяли DDD, шаблон Decompose by Subdomain. Количество микросервисов увеличивалось с добавлением в продукт новой функциональности. Каждому микросервису мы выделили свою кодовую базу (репозиторий). При проектировании применялся подход отдельных баз данных (БД) под каждый микросервис. Для разделения ответственности в своем продукте мы использовали CQRS- и Event Sourcing-подходы, shared libraries поставлялись через NuGet.

По тексту заманчиво, хотелось бы уточнить пару моментов, которые интересны:
1. Данные в сервисах максимально дублируются и обновляются только по ивентам, или нередко требуется вычитка данных из соседних сервисов, чтобы выполнять запросы? Или, может, у Вас обработка стримов где-то есть?
2. С какими проблемами event sourcing’а столкнулись, как и какими инструментами решали конкретно в Вашем случае? Возможно, какие-то Ажуровские сервисы позволяют решать их из коробки — я больше по AWS.
3. Какой функционал написан на акторах и какие проблемы Вы решили этим?
Спасибо.

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

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

По остальным вопросам, они довольно объемные для комментария, можем встретиться на каком-то событии и детально обсудить. Например мы будем учавствовать на .NET fwdays’20, можем пообщаться.

1. Конечно избыточность в хранении присутствует, есть сервисы которые владеют максимально полной и специфической моделью данных, при добавлении / изменении сервис генерирует ивент и отправляет его в Azure Event Hub, далее сервисы подписчики при необходимости создают / обновляют свою проекцию данных. В некоторых случаях для составления собственной проекции необходима вычитка из соседних сервисов но мы стараемся избегать такого кода.
2. Проблем с EventSourcing у нас нет — модель событий не меняется, в качестве хранилища используем Event Store от Грега Янга. Важно отметить что EventSourcing у нас используется точечно а не повсеместно, чего не скажешь о CQRS.
3. Акторы у нас присутствуют во многих сервисах и реализуют параллельную обработку: карточных транзакций, банк трансферов, синхронизацию с блокчейн и т.д.

Интересно было бы узнать как организованы общее транзакции в отдельных БД, при такой микросерверной архитектуре. Как организован процесс управления релизами ведь изменения одних микросервисов (например АПИ интерфейсов) влияет на работу других. А также как организован процесс интеграционного тестирования системы. Насколько выросла рабочая нагрузка на девопсов в связи с создание и поддержкой таких интеграциионых тестовых сред и насколько часто тестировщики репортят баг-репорты связаные не с реальными проблемами в коде, а с проблемами тестовой среды (например с неподключеным микросервисом) и как разработчики работающие на отдельных микросервисах определяют в чьем сервисе произошла проблема

изменения одних микросервисов (например АПИ интерфейсов) влияет на работу других

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

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

...но в большинстве случаев «а-ля микросервисы» именно такие(.

Идеология это конечно хорошо, но даже в теории это не возможно, нет?)

Идеология это конечно хорошо, но даже в теории это не возможно, нет?)

Версионирование?.. Это даже микросервисов не касается напрямую, в принципе.

Версионирование ивентов?) А потом к нему ещё и версионировать event handler’ы?)

Вопрос был об API интерфейсов.

зменения одних микросервисов (например АПИ интерфейсов) влияет на работу других.

Подозреваю что речь шла об общении микросервисов между собой а так как у автора

использовали CQRS- и Event Sourcing-подходы
shared libraries

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

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

Если только так, то это меняет дело, конечно, но в комменте прямой вопрос всё-таки).

и все это конечно классно до того момента пока у вас не будет 8 команд и 30 микросервисов и релизов продукта раз в 3 спринта — с этого момента начинается хаос как это все собрать в кучу.

Мой коммент вилами по воде, конечно, т.к. ничего не знаю о конкретном случае, но т.к. нормально разбивают по сервисам редко — возникает вопрос, почему 30 сервисов так жёстко связаны, и не задизайнили ли их ещё до того, как бизнес-логика устаканилась.

Правильно собрать все релизы в кучу — это основная организационная задача в рамках перехода на микросервисы. Для этого мы создали особую матричную организационную структуру из независимых команд и точек синхронизаций релиз-pipeline в синергии с принципами реализации новой функциональности с обязательным включением feature toggles в каждый релиз. Послностью раскрыть детали такого подхода планирем в следующей статье.

С feature toggle знаком, но в целом про

особую матричную организационную структуру из независимых команд и точек синхронизаций релиз-pipeline

будет интересно почитать.

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

Это пока у вас только оперативное общение сервисов между собой, пока полностью не переходите на EventSourcing, где event log становится единственным источником правды для системы, когда по разным причинам может оказаться нужным вычитывать лог «с начала времён» и корректно обрабатывать старые события даже в запущенном с нуля сервисе.

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

По факту ведь это решается просто миграциями + хранением избыточных event log для разных версий.
Концептуально сложного все равно ничего нет, да конечно лишние телодвижения и все такое но такие издержки неизбежны при распределенной системе.

Концепутально, по-моему опыту, организовать zero downtime деплои при переходе продюсера с одной версии формата событий на другой сложнее чем сделать то же с миграцией схемы базы на «распределенном» монолите. И это не просто лишние телодвижения, а заметно большая вероятность влияния человеческого фактора разного рода.

Отличные вопросы :)
как говорится в знающих кругах:
Что такое микро-сервисы? — это перенос головной боли с разработчиков на головы операционистов (админов)

Very интересная article

Підписатись на коментарі