Перейти к основному содержанию
Обсудить проект

Инженерная практика

Мультисервисная архитектура

APLIKE / DIGITAL STUDIODESIGN · ENGINEERING · GROWTHSYSTEMS · AUTOMATION · RELIABILITY
Мультисервисная архитектура программной системы

Мультисервисная архитектура (Multi-Service Architecture, MSA) - подход к проектированию программных систем, при котором приложение разделяется на несколько сервисов с определенными зонами ответственности.

Сервисы взаимодействуют через сетевые интерфейсы или обмен сообщениями и могут независимо развиваться, развертываться и масштабироваться.

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

Упрощенная схема:

Client
  ↓
API Gateway
  ↓
Services
├── Users
├── Catalog
├── Orders
├── Payments
└── Notifications
       ↓
Databases / Cache / Message Broker

Когда разделение на сервисы может быть оправдано

  • Продукт состоит из нескольких достаточно независимых бизнес-областей.
  • Разные части системы развиваются с разной скоростью.
  • Несколько команд должны независимо выпускать изменения.
  • Отдельные компоненты имеют существенно разный профиль нагрузки.
  • Требуется изолировать отказ или нагрузку одной части системы от остальных.
  • Монолит становится сложным для разработки, тестирования и выпуска.

Когда мультисервисная архитектура может быть избыточна

  • Приложение небольшое и имеет простую предметную область.
  • Систему разрабатывает одна небольшая команда.
  • Нет потребности в независимом масштабировании компонентов.
  • Независимые релизы сервисов не дают практического преимущества.
  • Не сформированы процессы CI/CD, мониторинга и эксплуатации.
  • Дополнительная распределенная инфраструктура будет сложнее самого приложения.

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

Декомпозиция по бизнес-областям

Основная задача при разделении системы - определить корректные границы сервисов.

Разбиение только по техническим слоям обычно создает сильную связанность.

Например, структура:

User Service
Order Service
Payment Service
Delivery Service

обычно понятнее с точки зрения бизнес-функций, чем разделение:

Controller Service
Business Logic Service
Database Service

При определении границ учитываются:

  • Бизнес-возможности.
  • Модель данных.
  • Жизненный цикл сущностей.
  • Частота изменений.
  • Зависимости между функциями.
  • Организационная структура команд.

Bounded Context

В системах со сложной предметной областью для определения границ могут использоваться идеи Domain-Driven Design и Bounded Context.

Один и тот же термин в разных частях бизнеса может иметь различный смысл.

Например:

Catalog Context
Product = описание, характеристики, изображения

Order Context
Product = идентификатор, цена, количество

Warehouse Context
Product = SKU, остаток, склад

Каждый контекст имеет собственную модель и отвечает за определенную часть бизнес-логики.

Независимость сервисов

Одно из основных преимуществ сервисного разделения - возможность изменять отдельную часть системы с минимальным влиянием на остальные.

Для этого желательно обеспечить:

  • Понятные границы ответственности.
  • Стабильные контракты взаимодействия.
  • Минимум общей внутренней логики между сервисами.
  • Независимый процесс сборки и развертывания там, где это необходимо.
  • Возможность отдельно масштабировать наиболее нагруженные компоненты.

Владение данными

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

Например:

User Service    → Users
Order Service   → Orders
Payment Service → Payments

Другие сервисы не должны произвольно изменять внутренние таблицы соседнего сервиса.

Взаимодействие происходит через публичный контракт:

Service A
   ↓
API / Event
   ↓
Service B

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

Важнее логическое владение данными и отсутствие неконтролируемого доступа к внутренней структуре другого компонента.

Database per Service

Один из распространенных вариантов - отдельная база или схема данных для каждого сервиса.

Такая модель усиливает автономность:

  • Сервис самостоятельно изменяет свою схему.
  • Другие компоненты не зависят напрямую от его таблиц.
  • Хранилище можно масштабировать отдельно.
  • Можно использовать разные технологии хранения для разных задач.

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

Синхронное взаимодействие

Сервисы могут взаимодействовать посредством непосредственных запросов.

Например:

Order Service
      ↓
HTTP / gRPC
      ↓
Payment Service

Для синхронного взаимодействия часто используются:

  • REST.
  • gRPC.
  • Другие RPC-механизмы.

Преимущество такого подхода - простой и понятный request-response сценарий.

Недостаток - вызывающий сервис зависит от доступности и времени ответа вызываемого компонента.

Асинхронное взаимодействие

Для части операций непосредственный ответ другого сервиса не требуется.

Тогда взаимодействие может происходить через брокер сообщений:

Order Service
      ↓
OrderCreated
      ↓
Message Broker
   ├── Payment Service
   ├── Notification Service
   └── Analytics Service

Асинхронная модель позволяет уменьшать временную связанность компонентов.

Для обмена сообщениями могут использоваться:

  • Kafka.
  • RabbitMQ.
  • Другие брокеры сообщений.

Event-Driven Architecture

В event-driven модели сервис публикует событие о произошедшем изменении, а заинтересованные компоненты самостоятельно реагируют на него.

Например:

PaymentCompleted
      ↓
Message Broker
   ├── Order Service
   ├── Delivery Service
   ├── Notification Service
   └── Analytics Service

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

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

Контракты взаимодействия

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

Контракт определяет:

  • Структуру запроса.
  • Структуру ответа.
  • Типы данных.
  • Ошибки.
  • Семантику операции.
  • Правила совместимости.

Для документирования могут использоваться:

  • OpenAPI для HTTP API.
  • Protocol Buffers для gRPC.
  • AsyncAPI для событийных интерфейсов.

Обратная совместимость

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

Поэтому изменения контрактов желательно выполнять совместимо.

Например:

Добавить новое поле
↓
Обновить клиентов
↓
Перестать использовать старое поле
↓
Удалить старое поле

Такая последовательность безопаснее одновременного изменения producer и всех consumers.

API Gateway

API Gateway может использоваться как единая точка входа для внешних клиентов.

Схема:

Web / Mobile
      ↓
API Gateway
   ├── User Service
   ├── Catalog Service
   ├── Order Service
   └── Payment Service

Gateway может выполнять:

  • Маршрутизацию.
  • Аутентификацию.
  • Rate Limiting.
  • Терминацию TLS.
  • Логирование.
  • Агрегацию отдельных запросов.

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

Service Discovery

В динамической инфраструктуре адреса экземпляров сервисов могут изменяться.

Service Discovery позволяет другим компонентам находить доступные экземпляры.

Например:

Order Service
      ↓
Service Discovery
      ↓
payment-service
   ├── Instance 1
   ├── Instance 2
   └── Instance 3

В Kubernetes часть этой задачи решается встроенными Service и DNS-механизмами, поэтому отдельный реестр сервисов может не требоваться.

Балансировка нагрузки

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

Requests
   ↓
Load Balancer
├── Service #1
├── Service #2
└── Service #3

Это позволяет горизонтально масштабировать отдельные части системы.

Частичные отказы

В распределенной системе один сервис может продолжать работать, когда другой временно недоступен.

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

Возможны:

  • Timeout.
  • Потеря соединения.
  • Медленный ответ.
  • Частичный ответ.
  • Повторная доставка сообщения.
  • Недоступность внешнего сервиса.

Timeouts

Сетевые операции не должны ожидать ответ бесконечно.

Для внешних вызовов устанавливаются ограничения времени ожидания.

При выборе timeout учитываются:

  • Ожидаемое время ответа.
  • SLA вызываемого сервиса.
  • Общий latency budget пользовательского запроса.
  • Количество последовательных сетевых вызовов.

Retry

Некоторые временные ошибки можно обрабатывать повторной попыткой.

Однако автоматический retry подходит не для любой операции.

Особенно важно учитывать:

  • Идемпотентность операции.
  • Максимальное количество повторов.
  • Backoff между попытками.
  • Общий timeout операции.

Неконтролируемые повторные запросы во время аварии могут дополнительно увеличить нагрузку и ухудшить состояние системы.

Circuit Breaker

Circuit Breaker временно прекращает обращения к зависимому сервису, если большое количество запросов завершается ошибкой.

Упрощенно:

Closed
  ↓
Много ошибок
  ↓
Open
  ↓
Ожидание
  ↓
Half-Open
  ↓
Проверка восстановления

Такой механизм позволяет уменьшить каскадное влияние отказа одного компонента на остальные.

Bulkhead

Принцип Bulkhead предполагает ограничение ресурсов, которые одна зависимость или операция может занять внутри системы.

Например, можно отдельно ограничить:

  • Количество соединений.
  • Число параллельных запросов.
  • Размер thread pool.
  • Очередь выполнения.

Это уменьшает риск ситуации, когда отказ одного внешнего сервиса блокирует весь компонент.

Идемпотентность

В распределенной системе запрос или сообщение может быть обработано повторно.

Поэтому для критических операций желательно обеспечивать идемпотентность.

Например:

Payment Request
Idempotency-Key: 123

Первый запрос  → платеж создан
Повторный      → возвращен существующий результат

Особенно это важно для:

  • Платежей.
  • Создания заказов.
  • Обработки сообщений.
  • Внешних интеграций.

Распределенные транзакции

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

В сервисной системе данные могут принадлежать разным компонентам:

Order Service
+
Payment Service
+
Warehouse Service

Обычная локальная транзакция уже не охватывает всю операцию.

Поэтому используются другие модели согласованности.

Saga

Saga представляет бизнес-операцию как последовательность локальных транзакций.

Например:

Создать заказ
      ↓
Зарезервировать товар
      ↓
Провести оплату
      ↓
Подтвердить заказ

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

Ошибка оплаты
      ↓
Отменить резерв
      ↓
Отменить заказ

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

Eventual Consistency

В распределенной системе данные разных сервисов не всегда обновляются одновременно.

Например:

Order Created
      ↓
Event
      ↓
Analytics обновится через некоторое время

Такая модель называется eventual consistency.

Она подходит не всем данным и должна учитывать допустимое время рассинхронизации.

Transactional Outbox

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

Один из подходов - Transactional Outbox.

Database Transaction
├── Business Data
└── Outbox Event
        ↓
Publisher
        ↓
Message Broker

Бизнес-изменение и запись будущего события сохраняются одной локальной транзакцией.

Наблюдаемость

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

В распределенной системе путь может выглядеть так:

Gateway
   ↓
Order Service
   ↓
Payment Service
   ↓
Database
   ↓
Message Broker
   ↓
Notification Service

Поэтому необходима общая система наблюдаемости.

Логи

Логи разных сервисов желательно собирать централизованно.

Для анализа одного запроса используется correlation identifier или trace identifier.

Например:

traceId=abc123

Gateway              → abc123
Order Service        → abc123
Payment Service      → abc123
Notification Service → abc123

Метрики

Для сервисов обычно контролируются:

  • Request Rate.
  • Latency.
  • Error Rate.
  • CPU.
  • Memory.
  • Saturation.
  • Database Latency.
  • Queue Lag.

Distributed Tracing

Distributed Tracing позволяет восстановить путь одного запроса через несколько компонентов.

Trace может содержать несколько spans:

HTTP Request
└── Order Service
    ├── Database
    ├── Payment Service
    │   └── Database
    └── Message Broker

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

SLI, SLO и SLA

Для контроля надежности могут использоваться формализованные показатели.

  • SLI - фактически измеряемый показатель качества.
  • SLO - целевое значение показателя.
  • SLA - формализованное соглашение об уровне сервиса.

Например, SLO может задавать допустимую доступность или процент запросов, которые должны укладываться в определенное время ответа.

Безопасность между сервисами

Внутренняя сеть не должна автоматически считаться доверенной.

В зависимости от требований используются:

  • Service-to-Service Authentication.
  • Authorization.
  • TLS или mTLS.
  • Управление сервисными учетными записями.
  • Управление секретами.
  • Network Policies.

Управление секретами

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

Для их хранения могут использоваться специализированные Secret Storage и механизмы платформы развертывания.

Также необходимо учитывать:

  • Разграничение доступа.
  • Ротацию.
  • Аудит доступа.
  • Отдельные секреты для разных окружений.

Контейнеризация

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

Контейнер содержит приложение и необходимое runtime-окружение:

Service
+
Runtime
+
Dependencies
↓
Container Image

Это упрощает воспроизводимый запуск компонентов на разных окружениях.

Kubernetes

Для управления большим количеством контейнерных сервисов может использоваться Kubernetes.

Он предоставляет механизмы:

  • Deployment.
  • Service Discovery.
  • Балансировку.
  • Health Checks.
  • Rolling Update.
  • Horizontal Scaling.
  • Configuration Management.

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

Health Checks

Каждый сервис должен позволять инфраструктуре определить его состояние.

Обычно различают:

  • Liveness - способен ли процесс продолжать работу.
  • Readiness - готов ли экземпляр принимать трафик.

Проверки должны отражать состояние самого сервиса и не создавать дополнительную нагрузку на зависимые системы.

CI/CD

Независимые сервисы обычно имеют собственные процессы сборки и развертывания.

Типовой pipeline:

Commit
 ↓
Build
 ↓
Static Analysis
 ↓
Tests
 ↓
Package
 ↓
Deploy
 ↓
Health Check
 ↓
Verification

Автоматизация особенно важна, когда система состоит из десятков компонентов и ручное управление релизами становится непрактичным.

Contract Testing

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

Например:

Consumer expectation
        ↓
API Contract
        ↓
Provider verification

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

Независимое масштабирование

Разные компоненты могут иметь различный профиль нагрузки.

Например:

Catalog Service     → 10 instances
Order Service       → 4 instances
Payment Service     → 3 instances
Notification Worker → 20 instances

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

Независимые релизы

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

Для этого необходимы:

  • Обратная совместимость контрактов.
  • Независимая сборка.
  • Автоматизированные тесты.
  • Управляемый deployment.
  • Возможность rollback.

Разные технологические стеки

Теоретически каждый сервис может использовать наиболее подходящие для него технологии.

Например:

Catalog   → Java + PostgreSQL
Search    → Java + OpenSearch
Analytics → Python
Frontend  → TypeScript

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

Поэтому технологическая неоднородность должна иметь практическое обоснование.

Основные преимущества

  • Возможность независимо развивать отдельные части системы.
  • Независимое масштабирование компонентов.
  • Возможность распределить ответственность между несколькими командами.
  • Более четкие границы крупных бизнес-областей.
  • Возможность локализовать часть технических отказов.
  • Независимые релизные циклы при правильно спроектированных контрактах.

Основные недостатки

  • Увеличение количества сетевых взаимодействий.
  • Более сложная диагностика ошибок.
  • Необходимость распределенной наблюдаемости.
  • Усложнение управления данными.
  • Отсутствие простой общей транзакции между сервисами.
  • Повышенные требования к CI/CD и инфраструктуре.
  • Необходимость контроля совместимости контрактов.
  • Более высокая стоимость эксплуатации небольших систем.

Модульный монолит как альтернатива

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

Например:

Application
├── Users Module
├── Catalog Module
├── Orders Module
├── Payments Module
└── Notifications Module

Модули имеют четкие границы, но развертываются как одно приложение.

Такой вариант сохраняет:

  • Простую локальную разработку.
  • Обычные транзакции базы данных.
  • Более простой deployment.
  • Меньшие требования к инфраструктуре.

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

Миграция от монолита

Переход к сервисной архитектуре не обязательно требует переписывания всей системы одновременно.

Часто используется постепенная схема:

Монолит
  ↓
Определение доменных границ
  ↓
Модульное разделение
  ↓
Выделение первого независимого сервиса
  ↓
Постепенный перенос следующих областей

Такой подход позволяет уменьшить риски и проверять архитектурные решения на реальной системе.

Strangler Pattern

Один из подходов к постепенной миграции - Strangler Pattern.

Новая функциональность постепенно выносится из старого приложения в отдельные компоненты.

Client
   ↓
Router / Gateway
   ├── Legacy Monolith
   └── New Services

По мере миграции все больше запросов обрабатывается новыми сервисами, пока соответствующая часть монолита не перестает использоваться.

Типовые ошибки проектирования

Слишком мелкие сервисы

Принцип "один сервис - одна функция" не означает, что каждый небольшой класс или CRUD-операция должны становиться самостоятельным приложением.

Слишком мелкое разделение увеличивает количество сетевых вызовов и усложняет сопровождение.

Общая база данных без границ ответственности

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

Длинные цепочки синхронных вызовов

Архитектура:

A → B → C → D → E

увеличивает latency и вероятность того, что отказ одного компонента нарушит весь пользовательский сценарий.

Отсутствие наблюдаемости

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

Микросервисы до появления необходимости

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

Где применяется сервисная архитектура

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

Например:

  • Финансовые платформы.
  • Крупные системы электронной коммерции.
  • SaaS-платформы.
  • Телекоммуникационные системы.
  • Логистические платформы.
  • Медиа-сервисы.
  • IoT-платформы.
  • Корпоративные информационные системы.

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

Что требуется для эксплуатации

По мере увеличения количества сервисов возрастает значение инженерных процессов.

Обычно необходимы:

  • CI/CD.
  • Автоматизированное тестирование.
  • Управление конфигурацией.
  • Управление секретами.
  • Централизованное логирование.
  • Метрики.
  • Distributed Tracing.
  • Алерты.
  • Документация API.
  • Регламенты реагирования на инциденты.

Выбор архитектуры

Мультисервисная архитектура не является следующим обязательным этапом после монолита.

Перед разделением системы необходимо определить конкретную проблему, которую оно должно решить:

  • Необходимость независимых релизов.
  • Разделение нескольких команд.
  • Независимое масштабирование.
  • Разные требования к доступности.
  • Слишком высокая связанность существующего приложения.
  • Четкие бизнес-границы между крупными функциональными областями.

Если таких требований нет, модульный монолит обычно проще разрабатывать и эксплуатировать.

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