Как распилить монолит и не получить монолит распределённый

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

Меня зовут Александр Лобцов, в Авито я распиливаю монолиты с 2020 года. В этой статье я разберу типичные грабли распила монолита в том порядке, в котором на них наступают. От целеполагания до эксплуатации и закона Конвея. Грабли разберём детально — почему они появляются, как разобраться с последствиями, если уже на них наступил, ну и главное — что не трогать вообще.

Без лишнего хайпа. Микросервисы не спасут мир. Иногда правильнее просто оставить монолит.

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

Содержание

  • Когда пилить НЕ надо

  • Пилить по доменам, а не по слоям

  • Общая база — всё ещё монолит

  • Не разрывай транзакцию, которую разрывать нельзя

  • Strangler fig, а не big bang

  • Эксплуатация дорожает нелинейно

  • Закон Конвея победит в любом случае

  • TLDR; вместо вывода

Когда пилить НЕ надо

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

На что нужно обратить внимание, чтобы понять, нужно ли резать?

Доставка

  • 10–20% релизов откатывается из-за чужих изменений в одном поезде.

  • Ревью не укладывается в SLA команды — заявлено 4 часа, а по факту уходит в 2–4 раза больше.

  • Большая часть времени релиза уходит на тестирование из-за чужих изменений.

  • CI заставляет копить изменения в поезд вместо того, чтобы делать мелкие выкатки.

Сам по себе CI не причина, а усилитель: медленный пайплайн превращает большое количество мелких выкладок в одну большую. Растёт поезд — растут откаты.

Независимость команд

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

  • Одна команда даёт 30–40% изменений И они локализованы в её файлах.

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

Отказы

  • Ошибка в куске кода выходит за пределы своего пользовательского сценария.

  • Продукт активно развивается — blast radius растёт вместе с частотой изменений.

Важно то, что сам по себе вынос изоляции не даёт. Синхронный сервис в критическом пути падает — сценарий всё равно ложится, добавляется новый класс отказов (timeout, retry). Изоляцию даёт то, что вокруг границы: timeouts, circuit breaker, fallback и асинхронность там, где бизнес терпит.

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

Ресурсы

  • Масштабируешь весь монолит ради одного горячего куска…

  • …а этот кусок упирается в другой тип ресурса (память/GPU VS CPU).

Абсолютная утилизация (те же 80%) не критерий. Монолит обычно дешевле отскейлить репликами. Критерий — неоднородность профиля. Платишь за копии всего кода ради одного горячего эндпойнта.

Швы

Домен уже выделен модулем или библиотекой с ясным владельцем.

Стоп-условия

  • Нет команды, которая займётся поддержкой вынесенного куска.

  • Кусок нельзя корректно деградировать.

  • Жёсткие транзакционные инварианты можно трогать в последнюю очередь.

  • Нет платформенной базы и нет плана её завести.

  • Монолит быстро деплоится, его ведёт одна команда, перечисленных выше болей нет.

Стоп-условия — вето. Любое из них перевешивает любое количество галочек за распил. Пилим только тогда, когда метрики говорят «пора», а стоп-лист — «можно».

Пилить по доменам, а не по слоям

Когда команда «выносит слой данных» или делает «отдельно сервис API» получается сильная связанность или болтливость по сети.

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

При связанности возникают такие ситуации:

  • «Сначала катим А, потом В, а откатываем в обратном порядке».

  • Изменение контракта требует изменения потребителя, а добавление поля ломает всех. Значит, контракт спроектирован как общая память, а не как API.

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

  • латентность складывается, если вызовы последовательные и зависимые;

  • каждый хоп — новая точка отказа, надёжность перемножается;

  • N+1 по сети — то, что было джойном, стало циклом запросов.

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

Поэтому важно помнить, что сервис — это то, что меняется вместе и владеет своими данными (bounded context), а не просто технический слой. Нужно искать швы:

  • Анализ зависимостей — основной инструмент, показывает, кто и зачем вызывает. Показывает, зачем кусок используется и что в нём — домен.

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

  • Определение частоты совместных изменений находит не шов, а склейку. Если два куска постоянно меняются вместе, значит — они один домен. Если меняется один и тот же код — выносить смысла нет, в микросервисе поселится та же проблема. Co-change — сигнал провести анализ, а не готовая граница.

  • Готовые границы — модули и библиотеки с явным владельцем. Иногда швы уже прорисованы, искать нечего.

Что не трогать

В покое стоит оставить сильно связанные компоненты. Главное — сначала определить природу этой связанности:

  • Связанность через данные и инварианты (одна сущность, общие правила согласованности) — порезать можно только с потерями. Оставляем.

  • Связанность через код (общие хелперы, сквозные утилиты, всё импортирует всё) — накопленный бардак, который лечить нужно внутри монолита без распила. Сначала наводим порядок, потом уже режем.

Ещё один критерий — частота изменений. Компоненты, которые меняются раз в месяц и никого не задевают, выносить не нужно, можно поставить в конец очереди. Это хорошо рифмуется с концепцией blast radius — чем реже меняется компонент, тем меньше он множит операционных издержек.

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

Когда слой — не антипаттерн

Горизонтальные слои вполне оправданы, если не хранят состояние и не владеют данными. К ним можно отнести API gateway, composition/BFF.

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

Как делаем в Авито? Composition — на домен, владеет им команда домена (не на клиента, как в классическом BFF). Composition в другой composition не ходит. Клиенты пишут свой composition. У вертикалей есть свои бизнес-домены, которые запрашивают данные у горизонтальных бизнес-доменов и компонуют.

Плюсы такой схемы в том, что владение composition совпадает с владением доменом. Узкого горлышка не возникает, команда правит свой слой сама.

Цена, правда, у этого подхода тоже есть. Горизонтальные домены в основании получают кратный трафик.

Кейс: библиотеки → сервисы

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

Нашли решение — вместо библиотек сделали микросервисы. Ядро монолита осталось, вертикали уехали.

В результате: вместо около 100 ревью в месяц ожидаем примерно 25–30. Прогноз основан на доле ревью, которые приходились на бампы библиотек.

Границы уже были нарисованы в go.mod — совпадали и с доменами, и с командами. Швы искать не пришлось, мы их уже видели. Отсюда совет: сначала проверь, не проведены ли уже границы по модулям, библиотекам или владениям.

Тут ещё больше контентаТут ещё больше контента

Общая база — всё ещё монолит

Допустим, вы вынесли сервис, но он по-прежнему тянется к общей базе. Связность никуда не делась, схему независимо не поменяешь, а через все таблицы тянется скрытый контракт. Это и есть распределённый монолит.

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

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

Дамп нужных таблиц с остановкой сервиса

Плюсы: 

  • просто, не требует написания кода;

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

  • откат при ошибке практически бесплатный.

Минусы:

  • нужно выделять окно простоя — и чем больше объём данных, тем длиннее оно будет;

  • оценить корректность результата можно только после переключения;

  • плавной раскатки не получится.

В чём легко ошибиться

Часто недооценивают время на восстановление. Сам дамп снять довольно быстро, а вот подъём базы с пересборкой индексов может занять в разы больше времени. Ещё легко упустить мелочи, например, sequences, права доступа, расширения.

Когда используем

Стратегия подходит для небольших объёмов данных и внутренних систем, при условии, что у вас есть подходящее окно простоя.

Outbox через очередь в своей базе

Реализуем паттерн pgq через очередь, которая живёт в нашей базе. Бизнес-данные и событие пишем в одной транзакции, а отдельный процесс потом отправляет их наружу. Для этого мы задействуем встроенный механизм pgq, который отправляет события в Kafka.

Плюсы:

  • не нужно останавливать сервис — работа идёт без простоя;

  • мы сами контролируем, какие события и с какими полями уходят наружу, включая события об удалениях;

  • можно вернуться к нужному оффсету и переиграть доставку — это даёт гибкость;

  • нужный объём данных появляется не случайно, а за счёт самой конструкции решения.

Минусы:

  • есть лаги доставки событий;

  • доставка по принципу at-least-once неизбежно ведёт к дублям — поэтому потребитель обязан быть идемпотентным.

Важный момент: нужно отличать подход от подписки на чужие события — они написаны как уведомления, у них обычно нет половины нужных полей, и удаления теряются.

Параллельная запись в две базы

Мы рассматривали вариант параллельной записи в две базы, но в итоге отказались — он оказался антипаттерном.

Проблема в том, что две записи не могут быть атомарными — если вторая упадёт, базы начнут разъезжаться, а вы узнаете об этом не сразу. По сути, это попытка вручную собрать распределённую транзакцию — решение, которое выглядит простым, но на деле лишь маскирует сложность.

Минусы (плюсов не нашли):

  • всё равно придётся внедрять отдельный процесс сверки данных и исправления расхождений — сложность не исчезает, а переезжает на другую часть системы;

  • скорость записи падает — каждая операция должна пройти через две базы;

  • если новая база начнёт сбоить, это ударит и по старому, уже стабильному рабочему потоку — и риски распространятся шире.

Что выбрали вместо этого

Outbox. Важно, что отказ от dual write и использование pgq — это не два независимых решения, а единый подход — они закрывают одну и ту же проблему, просто с разных сторон.

Репликация лога транзакций (CDC)

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

Плюсы:

  • приложение можно не трогать — вся магия происходит на уровне базы;

  • корректно передаются даже удаления — это важно, если нужна общая картина изменений;

  • лаг обычно небольшой, можно долго держать оба контура параллельно — удобно для плавного перехода.

Минусы:

  • растёт инфраструктурная сложность — настраивать и поддерживать CDC непросто;

  • DDL-операции становятся болезненными: любые изменения схемы могут ломать пайплайн репликации;

  • главное ограничение — вы получаете сырые строки из исходных таблиц, а не сразу готовую модель данных. Маппинг всё равно придётся писать вручную — CDC закрывает вопрос переноса, но не решает задачу смены модели.

Где всё может легко сломаться

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

Поверх любой стратегии

До отрезания сверяемся на реальном трафике. Читаем из обоих хранилищ, отвечаем из старого, расхождения записываем в метрику. Режем, когда расхождения стабильно на нуле, а не когда «вроде доехало».

Не разрывай транзакцию, которую разрывать нельзя

То, что в монолите было одной ACID-транзакцией, после распила превращается в два сетевых вызова с частичными сбоями. Там, где бизнес терпит, можно пробовать понятные механизмы — saga, transactional outbox, идемпотентность, eventual consistency. 

Где не терпит (деньги, остатки, лимиты) — лучше вообще не проводить границу сервиса через такой инвариант. В итоге именно согласованность — критерий того, где резать монолит.

Кейс: усиление вглубь

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

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

Виноватого найти сложно. Каждый уровень локально ведёт себя разумно, ретрай на таймаут — практика стандартная. Метрики каждого сервиса отдельно друг от друга выглядят объяснимо. Причина видна только при сквозном трейсинге.

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

В нашем случае мы поставили защиту на уровне над сервисами — rate limiter. Ограничиваем RPS как для сервиса целиком, так и для отдельных ручек. При срабатывании потребитель получает ответ 429.

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

Чтобы контролировать такие сценарии, в арсенале нужно иметь сразу несколько средств:

  • ретраи с backoff и jitter;

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

  • circuit breaker;

  • сквозной трейсинг — без него не будет диагностики.

Когда saga/outbox не вариант

Не все сценарии можно закрыть привычными подходами вроде saga и transactional outbox. И дело не в плохой реализации, а в фундаментальных ограничениях самих паттернов.

Saga не работает, когда нет компенсации. Например, когда письмо отправлено, пуш ушёл, деньги выведены на внешний счёт. Такую операцию делаем самой последней в цепочке и только после подтверждения всех 

Outbox не работает, когда у сервиса нет своей транзакционной базы — он опирается на возможность атомарно записать в одной транзакции и событие, и бизнес-данные. Состояние без транзакционной системы хранится во внешней. Трюк не сработает — просто не получится обеспечить outbox атомарность.

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

Кликни здесь и узнаешьКликни здесь и узнаешь

Strangler fig, а не big bang

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

Рабочий путь — шаблон strangler fig. Оборачиваем и откусываем по куску, каждый шаг в проде, каждый — можно откатить. Сам паттерн уже был описан много раз, поэтому расскажу только о том, чего в описаниях обычно нет: как понять, что новый путь работает, и когда удалять старый.

Порядок шагов

  1. оборачиваем кусок;

  2. включаем новый путь рядом со старым;

  3. сверяем компаратором до нуля расхождений;

  4. переключаем чтения;

  5. заводим задачу на удаление старого с датой;

  6. дожидаемся нуля вызовов старого пути;

  7. удаляем.

Вы восхитительны.

Как сверяем чтения

В Авито компаратор кастомный, мы сами под него пишем правила — в том числе парсинг URL и сравнение данных из query.

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

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

И здесь нужно заранее знать о ловушке. Ноль расхождений достигается двумя способами:

  • починкой расхождений;

  • дописанным правилом, прощающим расхождения.

Второй способ быстрый и незаметный. Но здесь важна дисциплина: каждое новое правило должно быть осознанным решением, принимающим, что разница допустима, а не способом добить метрику до зелёных показателей.

Как переключаем записи

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

Персистентность здесь принципиальна. Redis без неё — это оптимизация, а не гарантия: ключ теряется при failover, при вытеснении по памяти, при истечении TTL до того, как придёт запоздалый ретрай. В итоге вы получаете ровно то, от чего защищались.

Удаление старого кода

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

В Авито удаление — обязательный шаг, и держится он на двух механизмах, а не на силе воли инженеров:

  • Сразу заводим отдельную задачу на удаление, с конкретной датой.

  • На старом пути висит метрика — пока по нему идёт хоть один запрос, мы его не удаляем. Ноль запросов 1–2 дня* — удаляем.

Задача не даёт забыть о себе, метрика — не даёт удалить раньше необходимого.

* Тут стоит оговориться сразу: окно в 1-2 дня работает при равномерном трафике. Если ручка дёргается постоянно, двое суток тишины действительно означают, что никто не ходит по этому пути. Но для редких сценариев — месячные джобы, квартальные отчёты, интеграции, которые просыпаются по событию, — считать нужно не в днях, а в периодах, на которые завязан этот сценарий. У нас таких не было, поэтому хватало и окна в пару дней.

Если бизнес хочет заморозить монолит

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

Работает то, что переводится в деньги. Реально посчитать и показать можно три вещи:

  • Время ревью (в 2–4 раза больше SLA — это задержка на каждой задаче, умноженная на поток задач).

  • Бампы библиотек (те самые сто ревью в месяц).

  • Количество откатов (10-20% релизов — прямой рецепт выброшенной работы).

Заметьте закономерность: все три метрики — скорость команды. И это ровно те же метрики, что в чек-листе из раздела «Когда пилить НЕ надо». То есть аргументация для бизнеса — не отдельный жанр. Берёте цифры, по которым сами решали пилить, и показываете их же.

Обратное тоже верно и объясняет, почему решение о распиле часто приходит сверху, а не снизу. Боли, для которых у вас нет числа — изоляция отказов, blast radius, — плохо продаются. Не потому что они неважные, а потому что их не во что конвертировать. У нас, например, нет цифры стоимости часа простоя, поэтому даунтайм остаётся качественным аргументом.

И нельзя трогать работающий код, не имеющий никаких болей — не переписывайте его «раз уж полезли».

Жми сюда!Жми сюда!

Эксплуатация дорожает нелинейно

Был один процесс — стало N сервисов. Команда не вывозит: логи разрозненные, нет сквозного трейсинга, CI/CD и on-call на каждый сервис. Операционная сложность растёт быстрее, чем число сервисов.

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

Как мы вывезли без роста команды

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

В команде on-call дежурный по сервису, а не централизованный. Модель дежурства уже была построена по владению, и распил её не поменял.

Отсюда — уточнение правила

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

Предсказатель не в числе сервисов. Он в том, совпадает ли владение до распила с владением после:

  • Если код в монолите вела та же команда, что теперь ведёт сервис, — вынос операционно почти бесплатен.

  • Если инциденты по этому куску разбирал кто-то другой — платформенная команда, дежурный по монолиту, тот, кто первый увидел, — то после выноса нагрузка не переезжает, а появляется. Это новая работа, и её надо закладывать.

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

Закон Конвея победит в любом случае

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

Ничьи сервисы

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

У нас не было формального закрепления, всё держалось на договорённости. И тут стоит оговориться, иначе совет введёт вас в заблуждение: договорённости хватало из-за условий, которых в вашей команде могло и не быть. Распил был технопроектом, границы совпадали с вертикалями, владельцы существовали до выноса.

Если распил идёт снизу, границы не совпадают с оргструктурой, договорённость продержится до первой реорганизации.

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

Когда взаимодействие команд неизбежно

Кросс-доменный флоу — это нормально, убирать его не надо. Главная задача: сделать так, чтобы участие команды заканчивалось на её контракте.

Что поможет развязаться:

— События вместо синхронных вызовов. Следующий шаг подписывается, а не вызывается. Не нужен совместный релиз.

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

— Фича-тогглы. Код доезжает заранее, включается по готовности всего остального.

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

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

И честно про тогглы

Флаги у нас иногда живут дольше, чем нужно. Иногда мы действительно их не выпиливаем, иногда ставим на это задачу.

Мне кажется, это самый полезный контраст, который нужно вынести из этой статьи. Удаление старого пути в strangler fig в Авито происходит всегда — потому что под этим есть механизм: задача с датой и метрика вызовов. Выпиливание тогглов происходит через раз — потому что механизма нет, есть только понимание, что надо бы его сделать.

Разница здесь не в дисциплине команды, она-то одна. Разница лишь в том, что у одного шага есть процедура, а у другого нет.

То же самое я писал в разделе про эксплуатацию — владение держалось на договорённости и работало, потому что его подпирали условия, а не потому, что договорённость сильна сама по себе.

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

TLDR; вместо вывода

  • Режьте под измеримую боль, а не потому что «монолит — это плохо». Нет метрики, которую улучшаете, — нет распила.

  • Стоп-условия важнее сигналов «за». Нет владельца — не выносим, какие бы ни были цифры.

  • Сервис — это то, что меняется вместе и владеет своими данными. Слой законен, только если он ничем не владеет.

  • Частота совместных изменений показывает, где не резать.

  • Общая база означает, что вы не распилили, а разложили монолит по процессам.

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

  • Распил создаёт новый класс отказов, которого в монолите не было. Готовьтесь к нему заранее.

  • Удаление старого кода — часть работы, а не уборка после неё.

  • Считайте не сервисы, а совпадение владения до и после.

  • Что не подпёрто механизмом, то не выносим.

Приходите 17 ноября на Backend Roulette в «Svoy Яузу». Будем решать инженерные кейсы за покерными столами, а эксперты Avito Tech в роли крупье помогут разобрать решения. После игры — афтерпати с DJ и баром!

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев