golang

Микросервисная трансформация в Купере. Часть II: Как выносить функциональность

  • пятница, 6 марта 2026 г. в 00:00:08
https://habr.com/ru/companies/kuper/articles/1005852/

Всем привет! С вами Федор Засечкин, и это моя вторая статья из цикла о том, как мы в Купере распиливали монолит. Если хотите максимально погрузиться в контекст, перед чтением этого текста загляните по ссылке:

Микросервисная трансформация в Купере — как это было. Часть I: Начинаем распил монолитов

А в продолжение темы я хочу поговорить о том, с чего начинается вынос функциональности в сервисы. Будут и вопросы чисто технического характера, и организационные — потому что в нашем случае вынос состоял в основном из проектов с кросс-командным взаимодействием и иннерсорсом. Поехали!

Контекст функциональности

Если вы читали прошлую статью, то знаете, что все бизнес-процессы домена «Операции» у нас были плотно завязаны на монолит.

Рассмотрим, какая логика жила в монолите к моменту завершения работ по повышению стабильности систем.

Направление состояло из пяти отделов: Доставка, Сборка, Supply, Финтех, Платформа.
Каждый отдел был тесно связан с монолитом хотя бы на уровне интеграции данных, а некоторые процессы были полностью в нем реализованы. При этом основной стек направления — Golang. Все команды, кроме Платформы и нескольких команд Доставки, не имели компетенции в Ruby, а монолит был написан на Ruby on Rails. Некоторые микросервисы уже выделялись и писались на Go, но в них реализовывались в основном новые фичи. Для части этих сервисов требовалась тесная интеграция с монолитом, в котором находилась core-логика.

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

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

Как поступили бы вы?

Мы начали с инженерной культуры

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

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

Наши основные шаги:

  1. Разделить SLA сервисов на технические и бизнесовые.

  2. Добавить в технические индикаторы метрики процессов, выполняемых в сторонних для команды сервисах. (Например, для команды Сборки добавили индикаторы доступности соответствующих HTTP-ручек монолита.)

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

Поначалу было сложно. Оказалось, что на бизнесовые SLI влияет множество факторов, порой лежащих вне зоны прямого контроля команды. Эти зависимости приходилось искать и учиться считать. Часто это происходило реактивно — мы узнавали о слепых зонах уже по факту инцидентов. То же касалось и процесса эскалации. Понимание того, как работает вся система целиком, выстраивалось постепенно. Но со временем команды «притерлись», и споры о том, где именно произошел сбой и кто должен его чинить, сошли на нет.

Процесс был долгим и трудным, но мы все-таки смогли очертить границы ответственности для основного функционала домена.

Приоритизируем функционал

Итак, мы понимаем, что куда положить + кто за что ответственен. С чего же начать вынос?
Для нас сработала матрица Эйзенхауэра с переформулированными шкалами:

Важно → Критично для процесса (Business critical)

Срочно → Частота обращения к функционалу (RPS)

На самый старт мы выносим функции, без которых бизнес-процесс не может быть завершен и которые имеют высокий RPS.

Если брать процесс сборки заказа, более высокий приоритет достанется выносу эндпоинта просмотра информации о заказе, так как: 1) без информации о заказе собрать товары невозможно; 2) эти данные часто нужно отображать пользователю. Добавление промо-продукции оставим напоследок: заказов с промо мало, и это лишь дополнение к заказу.

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

Кейс №1. Как мы выносили сборку заказа

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

  1. Необходимо поддерживать бесперебойность процесса сборки заказа.

  2. Важно не потерять скорость поставки новых фич для проверки гипотез.

Решение — новые фичи делать в новом сервисе, а монолит оставить базовой платформой.

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

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

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

  1. Перенос реализации API (HTTP/gRPC) и последующее переключение клиентов.

  2. Перенос логики расчетов и хранения данных, которые необходимы сервису (раньше он получал их по API из монолита).

Общим было то, что для повышения надежности и контроля над доставкой мы пользовались фича-флагами (feature flags). Они позволяют в случае чего быстро вернуться к стабильному функционалу и проверить работоспособность кода на малой доле пользователей перед глобальным переключением.

Бывало, что результаты выполнения новой логики не всегда совпадали с существующей. Чтобы уменьшить влияние на пользователей, а также увеличить прозрачность раскатки, можно использовать паттерн Strangler Fig. О том, как это работает, хорошо написал мой коллега Дима Салахутдинов, читайте: Декомпозиция монолитной системы с использованием Strangler-паттерна

Главное достоинство первого подхода — итеративное переключение функционала.

Недостатки:

− временное увеличение связности систем;

− довольно заметное увеличение объема работ — из-за временного использования данных монолита при выполнении логики в сервисе.

Кейс №2. Как мы выносили плановую доставку

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

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

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

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

Достоинства второго подхода:

+ низкая связность систем, не нужно поддерживать две системы одновременно и корпеть над интеграцией;

+ раскатку фичи нужно проводить только для клиентов.

Недостатки:

− для реализации нужно больше людей;

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

TL;DR

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

  1. Правильно определите границы бизнес-процессов.

  2. Договоритесь, кто за что отвечает внутри этих процессов.

  3. Составьте план раскатки так, чтобы пользователи ее не заметили.

  4. Используйте Strangler-паттерн, чтобы принять решение о включении функционала.

Будете ждать третью часть цикла?