Получить доступ

Монолит, микросервисы и границы сервисов

Когда микросервисы оправданы, а когда вредны; bounded context и DDD как способ резать систему; распределённый монолит и его признаки; база на сервис и организационная цена.

Микросервисы решают организационную задачу

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

Что микросервисы действительно покупают: независимый деплой (команда выкатывает свой кусок, не согласуя релиз со всеми), независимое масштабирование (сервису отчётов нужно много CPU, сервису аутентификации — нет), изоляцию отказов (при грамотных границах падение отчётов не роняет оплату), автономию команд и свободу выбора технологий.

Что они за это берут: сеть между вашими модулями, распределённые транзакции, которых больше нет (см. сагу), необходимость наблюдаемости и трассировки, версионирование контрактов, сложное тестирование, отдельный контур эксплуатации и дежурств. Это постоянный налог, а не разовая инвестиция.

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

Модульный монолит как разумная точка старта

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

Практика модульного монолита: пакет на модуль, публичный API модуля явно выделен, доступ во внутренности запрещён (в Java это можно проверять ArchUnit или модулями JPMS), у каждого модуля свой набор таблиц и никаких JOIN через границу. Такой монолит режется на сервисы, когда действительно понадобится, — по уже существующим швам.

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

Отдельно стоит отличать «монолит» от «легаси»: большой хорошо структурированный монолит с чёткими модулями лучше десятка сервисов, связанных синхронными вызовами и общей базой.

Границы: bounded context вместо сущностей

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

DDD предлагает резать по bounded context — области, внутри которой термины предметной языка означают одно и то же. Ключевое наблюдение: одно и то же слово в разных контекстах — это разные модели. «Заказ» для оформления — корзина с ценами и промокодами; «заказ» для склада — набор позиций с весом и адресом; «заказ» для бухгалтерии — документ с НДС. Попытка сделать один общий класс Order, устраивающий всех, порождает объект с сорока полями, который меняется по любой причине.

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

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

Распределённый монолит

Худший из миров: цена распределённой системы уплачена, а её преимущества не получены. Это самый частый реальный исход неудачного перехода, и уметь описать его признаки полезнее, чем знать определение микросервисов.

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

Про перемножение доступности стоит держать цифру в голове: цепочка из пяти сервисов с доступностью 99,9% каждый даёт примерно 99,5% — то есть в пять раз больше простоя, чем у одного. Синхронные цепочки — главный источник каскадных отказов, и лечатся они не только таймаутами и circuit breaker, но в первую очередь пересмотром границ и переходом на асинхронное взаимодействие там, где ответ не нужен немедленно.

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

Данные и связь между сервисами

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

Для чтения есть три подхода. Синхронный запрос к владельцу — просто, но добавляет связность и задержку и делает вас зависимым от его доступности. Локальная реплика нужных данных, наполняемая событиями (event-carried state transfer) — быстро и автономно, но данные слегка устаревшие и их надо поддерживать. Отдельная модель чтения, собранная из событий нескольких сервисов, — это уже CQRS, ему посвящён отдельный урок.

Для записи распределённых транзакций не существует: 2PC в микросервисах не применяют из-за блокирующего координатора и деградации доступности. Вместо этого — сага из локальных транзакций с компенсациями, а для надёжной публикации событий — transactional outbox. Механика подробно разобрана в уроке про сагу и CDC.

Общее правило проектирования, которое стоит произнести: явно решите, где у вас строгая согласованность (внутри агрегата и его транзакции), а где согласованность в конечном счёте (между сервисами), и отразите это в API и в UI. Промежуточные состояния вроде payment_pending — это часть контракта, а не временное неудобство.

Что придётся построить вокруг

Переход к микросервисам оплачивается инфраструктурой, и на lead-интервью проверяют, помните ли вы про эту часть счёта.

Обязательный минимум: автоматизированный деплой на каждый сервис отдельно; централизованные логи с correlation id; метрики и распределённая трассировка, иначе отладка сценария через пять сервисов невозможна; service discovery и конфигурация; шлюз на входе с аутентификацией и лимитами; политика ретраев, таймаутов и circuit breaker по умолчанию.

Тестирование меняется качественно: end-to-end тесты через все сервисы становятся медленными и хрупкими, поэтому опорой становятся контрактные тесты (consumer-driven contracts) — потребитель декларирует свои ожидания, а сборка поставщика проверяет их. Это единственный способ менять контракты без релизного поезда.

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

Как отвечать на собеседовании

Начинайте с честного trade-off, а не с плюсов: микросервисы покупают независимость деплоя, масштабирования и владения, а платят сетью, распределёнными данными и эксплуатацией. Это сразу задаёт уровень разговора.

На вопрос «как бы вы разрезали систему» отвечайте через bounded context и бизнес-возможности, а не через сущности и слои. Приведите пример с разным смыслом слова «заказ» в оформлении, на складе и в бухгалтерии — он убедительнее любого определения.

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

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

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем