Как распределить монолитное приложение на микросервисы?

«Как распределить монолитное приложение на микросервисы?» — вопрос из категории Архитектура, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Миграция с монолита на микросервисы — это сложный инженерный процесс, а не просто механическое разделение кода. Вот стратегический подход:

1. Анализ и проектирование (самый важный этап)

  • Выявление bounded context (ограниченных контекстов): Используйте принципы Domain-Driven Design (DDD). Найдите в монолите логически связанные группы функциональности, которые минимально зависят от других частей. Например: Управление пользователями, Каталог товаров, Оформление заказов, Оплата, Доставка.
  • Определение границ сервисов: Каждый bounded context становится кандидатом в отдельный микросервис. Ключевой вопрос: "Можно ли разрабатывать, развертывать и масштабировать эту часть независимо?"

2. Стратегия разделения данных

  • База данных на сервис: Каждый микросервис должен владеть своей схемой БД. Прямой доступ к БД одного сервиса из другого запрещен.
  • Шаблон Saga для распределенных транзакций: Замените транзакции БД монолита на цепочки компенсирующих операций.
    // Пример сценария Saga для создания заказа:
    // 1. OrderService: Создает заказ в статусе 'Pending', публикует OrderCreatedEvent.
    // 2. PaymentService (подписчик): Обрабатывает платеж, публикует PaymentCompletedEvent.
    // 3. OrderService (подписчик): Обновляет статус заказа на 'Confirmed'.
    // 4. Если платеж не прошел, публикуется PaymentFailedEvent и OrderService отменяет заказ.

3. Определение API и коммуникации

  • Синхронное взаимодействие (REST/gRPC): Для операций, требующих немедленного ответа. Используйте для запросов, не изменяющих состояние (GET) или для простых команд.
    // REST API для сервиса каталога
    [HttpGet("api/products/{id}")]
    public async Task<IActionResult> GetProduct(int id) { ... }
  • Асинхронное взаимодействие (Message Broker): Для событий и сложных рабочих процессов. Используйте RabbitMQ, Apache Kafka или Azure Service Bus. Это повышает отказоустойчивость и развязывает сервисы.
    // Публикация события в шину
    public async Task PublishOrderCreatedEvent(Order order)
    {
        var event = new OrderCreatedEvent { OrderId = order.Id, ... };
        await _messageBus.PublishAsync(event);
    }

4. Поэтапная миграция (Strangler Fig Pattern) Не переписывайте всё сразу. Постепенно "отрезайте" функциональность от монолита:

  1. Создайте новый микросервис для одного bounded context (например, NotificationService).
  2. Настройте маршрутизацию (API Gateway). Новые запросы идут в микросервис, старые — пока в монолит.
  3. Перенесите данные и настройте синхронизацию, если нужно.
  4. Повторяйте для следующего контекста.

Структура после миграции:

API Gateway
├───> UserService (отдельная БД пользователей)
├───> ProductService (отдельная БД каталога)
├───> OrderService (отдельная БД заказов, слушает события из PaymentService)
└───> Legacy Monolith (постепенно уменьшается)

Ключевые сложности, к которым нужно быть готовым:

  • Распределенная отладка и мониторинг (трейсинг, централизованное логирование).
  • Повышенная сложность сетевых взаимодействий (latency, временные сбои).
  • Согласованность данных в конечном счете (Eventual Consistency).