2. Границы сервисов

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

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

В этой статье мы разберем, как определить оптимальные границы сервисов с помощью принципов предметно-ориентированного проектирования (Domain-Driven Design, DDD), почему физический «размер» сервиса в строках кода — это последний параметр, о котором должен думать инженер, и как правильная нарезка защищает процессорные мощности Go-рантайма.


Bounded Context: Фундамент границ

Главным концептуальным компасом при нарезке распределенной системы служит понятие Bounded Context (Ограниченный контекст), сформулированное Эриком Эвансом в книге по Domain-Driven Design.

В любой крупной компании одно и то же слово означает совершенно разные вещи в зависимости от бизнес-подразделения.

Возьмем простую сущность Product (Товар):

  • Для Отдела продаж и маркетинга товар — это привлекательное описание, фотографии высокого разрешения, SEO-теги, скидочные акции и отзывы покупателей.
  • Для Складской логистики (Warehouse) товар — это физические габариты в миллиметрах, вес брутто, класс опасности при транспортировке и номер складской паллеты.
  • Для Бухгалтерии и биллинга товар — это код налоговой классификации (НДС), таможенная пошлина, себестоимость и валюта расчетов.
flowchart TD
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    subgraph CatalogContext ["Каталог и Витрина (Catalog Bounded Context)"]
        P1["Сущность Product:<br/>- Title, Description<br/>- Images []URL<br/>- MarketingCategories"]
    end

    subgraph WarehouseContext ["Склад и Доставка (Warehouse Bounded Context)"]
        P2["Сущность Product:<br/>- Dimensions (HxWxD)<br/>- WeightGrams<br/>- StorageLocationID"]
    end

    subgraph BillingContext ["Биллинг (Billing Bounded Context)"]
        P3["Сущность Product:<br/>- TaxRateCode<br/>- CustomsTariffID<br/>- BasePriceCurrency"]:::process
    end
  

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

Золотое правило Bounded Context:
Граница микросервиса должна совпадать с границей ограниченного контекста, внутри которого действует единый, непротиворечивый язык общения (Ubiquitous Language), а модели данных сфокусированы на решении конкретной бизнес-задачи.


Признаки правильной границы

Как практикующему Go-инженеру понять, что архитектурная граница проведена верно? Обратите внимание на четыре ключевых индикатора:


1. Единая ответственность (SRP)

Сервис обязан отвечать за одну цельную бизнес-функцию.

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


2. Автономность данных

Сервис должен располагать всеми необходимыми данными для выполнения своей основной работы.

Если для обработки типового запроса сервису A требуется синхронно выполнить цепочку RPC-запросов к сервисам B, C и D, эти сервисы обладают высокой связанностью (High Coupling). При падении сервиса C сервис A становится полностью недееспособен.

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


3. Темп изменений

Разные компоненты бизнеса эволюционируют с принципиально разной скоростью:

  • Маркетинговые промо-механики и реферальные программы могут обновляться дважды в неделю;
  • Ядро финансового учета и биллинга меняется раз в полгода после аудита.

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


Типичные ошибки при нарезке

Большинство неудачных микросервисных архитектур терпят крах из-за двух классических заблуждений:

1. Технологические слои (Layered Services)

Грубейшая ошибка: Нарезка сервисов по горизонтальным техническим уровням: «Database Service», «Logging Service» или «UI Backend Service».

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

flowchart LR
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    subgraph Bad ["Антипаттерн: Горизонтальная нарезка"]
        direction TB
        B1["UI Service"]:::data --> B2["Business Logic Service"] --> B3["Database Service"]
    end

    subgraph Good ["Паттерн: Вертикальная бизнес-нарезка"]
        direction TB
        G1["Billing Service (API + DB)"]
        G2["Orders Service (API + DB)"]
        G3["Inventory Service (API + DB)"]
    end
  

2. Сущностные сервисы (Entity Services)

Ошибка наивного моделирования: Создание сервисов вокруг отдельных таблиц реляционной базы данных (например, UserService, OrderService, ItemService).

Это порождает так называемую анемичную модель и катастрофическую «болтливость» сети (Chatty Architecture): чтобы выполнить элементарную операцию оформления заказа, приложению приходится выполнить 15 последовательных сетевых вызовов между микро-сущностями.

Вместо сервиса вокруг таблицы создавайте сервис вокруг Бизнес-процесса (например, Checkout Service, управляющий процессом покупки).


Mechanical Sympathy: Границы и рантайм Go

Архитектурные границы — это не просто диаграммы в Miro, это прямое воздействие на ядро процессора и планировщик Go.


Стратегия декомпозиции

  1. Правило крупного калибра (Coarse-Grained First):
    Если границы между доменами еще размыты, держите код внутри единого модульного монолита. Вынести хорошо изолированный Go-пакет в отдельный микросервис занимает пару дней. Обратно склеить десять ошибочно нарезанных сервисов с рассогласованными базами данных требует месяцев мучительного рефакторинга.
  2. Асинхронные события как маркер границы:
    Если два модуля могут взаимодействовать через доменные события по схеме Fire-and-Forget (например: «Заказ оплачен» $\to$ «Отправить уведомление по почте») — между ними проходит идеальная граница для разделения на микросервисы. Если модулям требуется жесткая двухсторонняя транзакция — держите их вместе.
  3. Согласование с Законом Конвея:
    Если две команды сидят в разных часовых поясах, попытка заставить их коммитить в один сервис приведет к постоянным конфликтам веток и блокировкам. Нарежьте сервисы так, чтобы каждая команда единолично владела своими компонентами.

Подводные камни разделения данных


Итог

  1. Bounded Context — основа границ: Сервисы очерчиваются вокруг единого бизнес-смысла терминов, а не вокруг таблиц базы данных.
  2. Высокое зацепление (Cohesion) и слабая связанность (Coupling): Сервис должен быть самодостаточным и не требовать хоровода синхронных RPC-вызовов к соседям для выполнения базовых задач.
  3. Остерегайтесь нано-сервисов: Сетевой налог и сериализация в Go могут превратить высоконагруженную систему в генератор сетевых задержек.
  4. Организационная гармония: Границы сервисов должны усиливать автономность инженерных команд, уменьшая потребность в синхронизации между людьми.

Границы очерчены. Но как только мы разделили систему, мы столкнулись с самым бескомпромиссным правилом микросервисов: кому принадлежат данные и как запретить сервисам влезать в чужие базы?

Об этом мы поговорим в следующей статье: 3. Data ownership.