Прямой ответ
Граница сервиса должна совпадать с границей бизнес-возможности, а не с техническим слоем. Разделение по слоям — «сервис контроллеров», «сервис бизнес-логики», «сервис доступа к данным» — даёт систему, где любое изменение затрагивает все сервисы сразу.
Два рабочих критерия декомпозиции:
| Критерий | Вопрос | Пример |
|---|---|---|
| Бизнес-возможность | Какую функцию компании реализует сервис? | Оформление заказа, выставление счёта, доставка |
| Поддомен | К какой части предметной области относится? | Каталог, корзина, оплата |
Практическая проверка границы — частота совместных изменений. Если два сервиса почти всегда меняются в одном коммите или в одной задаче, граница проведена неверно: вместо двух сервисов получился один, разнесённый по сети.
Второй критерий — объём обмена. Сервисы, которые обмениваются десятками вызовов на один пользовательский сценарий, связаны сильнее, чем нужно. Такой обмен добавляет задержку на каждом шаге и создаёт распределённую транзакцию там, где её можно было избежать.
Признаки неверной границы:
- Задача требует изменения трёх сервисов. Граница режет сценарий пополам.
- Сервисы деплоятся вместе. Фактически это одно приложение с сетевыми вызовами.
- Общая база данных. Сервисы не могут менять схему независимо — см. статью про общую базу.
- Синхронная цепочка из пяти вызовов. Задержка складывается, а сбой любого звена ломает сценарий.
- Один сервис знает схему другого. Прямой доступ к чужим данным вместо обращения к владельцу.
Результат такого разделения называют распределённым монолитом: недостатки микросервисов присутствуют, а преимущества отсутствуют. О паттернах и антипаттернах — в статье про микросервисы.
Практика: как проводить границы
1. Начинайте крупно. Выделить сервис из модуля с чистой границей проще, чем объединить обратно два неудачно разделённых. О границах модулей — в статье про модульный монолит.
2. Используйте владение данными как критерий. Сервис владеет данными, которые создаёт и изменяет. Если два сервиса пишут в одну таблицу, граница неверна; подробнее — в статье про владение данными.
3. Учитывайте структуру команд. Закон Конвея работает в обе стороны: система повторяет структуру организации, и наоборот — границы сервисов определяют, как команды взаимодействуют. Сервис, который обслуживают две команды, будет источником конфликтов.
4. Проектируйте асинхронный обмен. Там, где допустима задержка, событие предпочтительнее синхронного вызова: оно снижает связанность и переживает временную недоступность потребителя.
5. Проверяйте границы на реальных задачах. Возьмите пять типовых изменений и посмотрите, сколько сервисов затрагивает каждое. Если регулярно больше одного — границы нужно пересмотреть.
6. Разделяйте ядро и вспомогательное. Поддомены делятся на ключевые (дают конкурентное преимущество), поддерживающие и типовые. Типовые задачи чаще покупают или используют готовые решения.
7. Фиксируйте причину разделения. Решение о границе должно быть записано с обоснованием — см. статью про архитектурные решения.
Рекомендации:
- Не разделяйте по технологии. «Сервис на Go» и «сервис на Python» — не границы; это способ получить два сервиса там, где нужен один.
- Измеряйте связанность до разделения. Число вызовов между модулями и совместные коммиты дают объективную картину.
- Оставьте возможность обратного хода. Слияние двух сервисов дороже разделения, поэтому сомнительные границы лучше не создавать.
- Проверяйте независимость деплоя. Если сервис нельзя выкатить без согласования с другим, независимость только на словах.
- Согласуйте контракт. Граница сервиса — это и граница API; о версионировании — в статье про версии API.
Практический сценарий: разделение по слоям дало трёхкратный рост задержки
Контекст: монолит разделили на три сервиса по слоям: приём запросов, бизнес-логика и доступ к данным. Целью было независимое масштабирование.
- Результат через месяц. Задержка p95 выросла с 180 до 540 миллисекунд. Один пользовательский запрос проходил через три сетевых перехода, каждый добавлял около 40 миллисекунд, плюс сериализация.
- Вторая проблема. Любое изменение бизнес-правила требовало правки двух сервисов: слой логики и слой данных менялись вместе, поскольку разделяли одну модель.
- Третья проблема. Независимое масштабирование не сработало: слой данных всегда был узким местом, и его масштабирование упиралось в базу, которая оставалась общей.
- Действия. (а) Вернули слои в одно приложение, сохранив разделение кода по модулям; (б) заново провели границы по бизнес-возможностям: заказы, оплата, доставка; (в) каждому сервису выделили собственную схему в базе; (г) синхронные вызовы между сервисами заменили событиями там, где допустима eventual consistency; (д) ввели проверку: задача не должна затрагивать больше одного сервиса.
- Результат. Задержка вернулась к 210 миллисекундам; доля задач, затрагивающих один сервис, выросла с 35 до 88 процентов.
Вывод: разделение по техническим слоям не создаёт независимости, потому что слои связаны по своей природе. Граница проходит там, где заканчивается бизнес-возможность и начинается другая.
FAQ
Сколько сервисов должно быть в системе? Столько, сколько нужно для независимого масштабирования и независимых релизов. Число не является целью; ориентир — размер команды и частота изменений.
Можно ли исправить неверную границу без остановки? Да: сначала объединяют обмен в явные контракты, затем переносят данные к владельцу, и только потом меняют развёртывание. Миграция legacy описана в статье про Strangler Fig.
Что делать, если бизнес-возможности пересекаются? Искать границу по владению данными и по инварианту. Если два сценария меняют одни данные в одной транзакции, они относятся к одному сервису.
Нужен ли отдельный сервис для интеграции с внешней системой? Часто да: у него свой цикл отказа, свой таймаут и своя модель данных. Такой сервис выступает адаптером и защищает основную модель от изменений извне.
Источники: microservices.io (decomposition patterns), Martin Fowler (ConwaysLaw) — ссылки в шапке.