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.
Стратегия декомпозиции
- Правило крупного калибра (Coarse-Grained First):
Если границы между доменами еще размыты, держите код внутри единого модульного монолита. Вынести хорошо изолированный Go-пакет в отдельный микросервис занимает пару дней. Обратно склеить десять ошибочно нарезанных сервисов с рассогласованными базами данных требует месяцев мучительного рефакторинга. - Асинхронные события как маркер границы:
Если два модуля могут взаимодействовать через доменные события по схеме Fire-and-Forget (например: «Заказ оплачен» $\to$ «Отправить уведомление по почте») — между ними проходит идеальная граница для разделения на микросервисы. Если модулям требуется жесткая двухсторонняя транзакция — держите их вместе. - Согласование с Законом Конвея:
Если две команды сидят в разных часовых поясах, попытка заставить их коммитить в один сервис приведет к постоянным конфликтам веток и блокировкам. Нарежьте сервисы так, чтобы каждая команда единолично владела своими компонентами.
Подводные камни разделения данных
Итог
- Bounded Context — основа границ: Сервисы очерчиваются вокруг единого бизнес-смысла терминов, а не вокруг таблиц базы данных.
- Высокое зацепление (Cohesion) и слабая связанность (Coupling): Сервис должен быть самодостаточным и не требовать хоровода синхронных RPC-вызовов к соседям для выполнения базовых задач.
- Остерегайтесь нано-сервисов: Сетевой налог и сериализация в Go могут превратить высоконагруженную систему в генератор сетевых задержек.
- Организационная гармония: Границы сервисов должны усиливать автономность инженерных команд, уменьшая потребность в синхронизации между людьми.
Границы очерчены. Но как только мы разделили систему, мы столкнулись с самым бескомпромиссным правилом микросервисов: кому принадлежат данные и как запретить сервисам влезать в чужие базы?
Об этом мы поговорим в следующей статье: 3. Data ownership.