← Назад к вопросам

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

3.0 Senior🔥 132 комментариев
#DevOps и инфраструктура#Soft skills и опыт работы#Архитектура и паттерны

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Миграция монолита в микросервисную архитектуру

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

Контекст проблемы

У нас был монолит на Node.js + Express:

  • 5 разработчиков в команде
  • 2000+ строк кода в основном файле
  • Разные домены (orders, payments, inventory, notifications)
  • Развертывание: вся система целиком, даже если менялась одна фича
  • Масштабирование: приходилось плодить экземпляры всего приложения
  • Проблема: один разработчик мешал другим (merge conflicts, багс одного ломали других)

Фаза 1: Анализ и планирование (2 недели)

Шаг 1: Сделать топологию монолита

// старый монолит
app.js
├── routes/
│   ├── orders.ts
│   ├── payments.ts
│   ├── inventory.ts
│   └── notifications.ts
├── controllers/
├── services/
├── database/
└── utils/

Шаг 2: Найти границы сервисов Использовал Domain-Driven Design (DDD):

  • Order Service — создание заказов, управление статусом
  • Payment Service — обработка платежей
  • Inventory Service — управление складом
  • Notification Service — отправка email, SMS
  • API Gateway — единая точка входа

Шаг 3: Нарисовать граф зависимостей

Order Service → Payment Service (проверка платежа)
           ↓
      Inventory Service (резервирование товара)
           ↓
    Notification Service (письма)

Шаг 4: Выделить бизнес-события (Event-driven)

  • order.created → оповестить inventory и payment
  • payment.completed → обновить статус заказа
  • inventory.reserved → отправить уведомление

Фаза 2: Синхронная коммуникация (месяц 1)

Сначала я НЕ стал использовать очереди. Стартовал с REST API.

Создание Order Service:

// order-service/src/app.ts
import express from 'express';
import { OrderController } from './controllers/OrderController';
import { OrderService } from './services/OrderService';
import { PaymentService } from './clients/PaymentService';
import { InventoryService } from './clients/InventoryService';

const app = express();
const paymentServiceUrl = process.env.PAYMENT_SERVICE_URL;
const inventoryServiceUrl = process.env.INVENTORY_SERVICE_URL;

const paymentClient = new PaymentService(paymentServiceUrl);
const inventoryClient = new InventoryService(inventoryServiceUrl);
const orderService = new OrderService(paymentClient, inventoryClient);
const orderController = new OrderController(orderService);

app.post('/api/v1/orders', (req, res) => orderController.create(req, res));
app.get('/api/v1/orders/:id', (req, res) => orderController.get(req, res));

app.listen(3001, () => console.log('Order Service running on 3001'));

Order Service вызывает Payment Service:

// order-service/src/services/OrderService.ts
export class OrderService {
  constructor(
    private paymentService: PaymentService,
    private inventoryService: InventoryService,
    private db: Database,
  ) {}

  async createOrder(createOrderDto: CreateOrderDto) {
    const order = await this.db.orders.create({
      userId: createOrderDto.userId,
      items: createOrderDto.items,
      status: 'pending',
    });

    try {
      // Синхронный вызов Payment Service
      const payment = await this.paymentService.processPayment({
        orderId: order.id,
        amount: this.calculateTotal(createOrderDto.items),
      });

      if (!payment.success) {
        await this.db.orders.update(order.id, { status: 'payment_failed' });
        throw new Error('Payment failed');
      }

      // Синхронный вызов Inventory Service
      await this.inventoryService.reserve({
        orderId: order.id,
        items: createOrderDto.items,
      });

      await this.db.orders.update(order.id, { status: 'confirmed' });
      return order;
    } catch (error) {
      await this.db.orders.update(order.id, { status: 'failed' });
      throw error;
    }
  }
}

HTTP Client для Payment Service:

// order-service/src/clients/PaymentService.ts
import axios from 'axios';

export class PaymentService {
  constructor(private baseUrl: string) {}

  async processPayment(request: ProcessPaymentRequest) {
    const response = await axios.post(
      `${this.baseUrl}/api/v1/payments`,
      request,
      { timeout: 5000 },
    );
    return response.data;
  }
}

API Gateway (первая версия):

// api-gateway/src/app.ts
import express from 'express';

const app = express();

const orderServiceUrl = process.env.ORDER_SERVICE_URL || 'http://order-service:3001';
const paymentServiceUrl = process.env.PAYMENT_SERVICE_URL || 'http://payment-service:3002';

// Прокси
app.post('/api/v1/orders', async (req, res) => {
  const response = await axios.post(`${orderServiceUrl}/api/v1/orders`, req.body);
  res.json(response.data);
});

app.listen(3000);

Фаза 3: Асинхронная коммуникация (месяц 2)

После первого месяца я понял, что синхронные вызовы создают проблемы: если Payment Service падает, весь Order Service зависает. Перешел на Event-driven архитектуру.

RabbitMQ для событий:

// shared/events.ts
export type DomainEvent = 
  | { type: 'order.created'; orderId: string; items: Item[] }
  | { type: 'payment.completed'; orderId: string }
  | { type: 'payment.failed'; orderId: string }
  | { type: 'inventory.reserved'; orderId: string };

Order Service публикует события:

// order-service/src/services/OrderService.ts
export class OrderService {
  constructor(
    private db: Database,
    private eventBus: EventBus,
  ) {}

  async createOrder(createOrderDto: CreateOrderDto) {
    const order = await this.db.orders.create({
      userId: createOrderDto.userId,
      items: createOrderDto.items,
      status: 'pending',
    });

    // Опубликовать событие
    await this.eventBus.publish('order.created', {
      orderId: order.id,
      items: createOrderDto.items,
    });

    return order;
  }
}

Payment Service слушает события:

// payment-service/src/handlers/OrderCreatedHandler.ts
export class OrderCreatedHandler implements EventHandler {
  constructor(private paymentService: PaymentService) {}

  async handle(event: OrderCreatedEvent) {
    try {
      const payment = await this.paymentService.process({
        orderId: event.orderId,
        amount: this.calculateTotal(event.items),
      });

      // Опубликовать событие платежа
      await this.eventBus.publish('payment.completed', {
        orderId: event.orderId,
      });
    } catch (error) {
      await this.eventBus.publish('payment.failed', {
        orderId: event.orderId,
      });
    }
  }
}

Inventory Service также слушает events:

// inventory-service/src/handlers/PaymentCompletedHandler.ts
export class PaymentCompletedHandler implements EventHandler {
  async handle(event: PaymentCompletedEvent) {
    await this.inventoryService.reserve(event.orderId);
    await this.eventBus.publish('inventory.reserved', { orderId: event.orderId });
  }
}

Фаза 4: Docker и Docker Compose (месяц 3)

# docker-compose.yml
version: '3.8'
services:
  api-gateway:
    build: ./api-gateway
    ports:
      - "3000:3000"
    depends_on:
      - order-service
      - payment-service

  order-service:
    build: ./order-service
    ports:
      - "3001:3001"
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/orders
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - postgres
      - rabbitmq

  payment-service:
    build: ./payment-service
    ports:
      - "3002:3002"
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/payments
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - postgres
      - rabbitmq

  inventory-service:
    build: ./inventory-service
    ports:
      - "3003:3003"
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/inventory
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - postgres
      - rabbitmq

  postgres:
    image: postgres:14
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - postgres_data:/var/lib/postgresql/data

  rabbitmq:
    image: rabbitmq:3.12-management
    ports:
      - "15672:15672"
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq

volumes:
  postgres_data:
  rabbitmq_data:

Фаза 5: Миграция данных (2 недели)

Самая критичная часть. Стратегия:

  1. Дублирование данных — синхронизировать БД во время работы монолита
  2. Параллельная работа — оба пути (монолит и сервисы) работают одновременно
  3. Переключение — перенаправить трафик на микросервисы
  4. Откат — быстро вернуться к монолиту если проблемы
-- migrations/001_sync_orders_to_new_db.sql
CREATE OR REPLACE FUNCTION sync_orders_to_new_db()
RETURNS void AS $$
DECLARE
BEGIN
  INSERT INTO new_orders_db.orders 
  SELECT * FROM old_monolith.orders
  WHERE created_at > NOW() - INTERVAL '1 day';
END;
$$ LANGUAGE plpgsql;

Фаза 6: Мониторинг и откат (месяц 4)

Prometheus метрики:

// shared/metrics.ts
import prometheus from 'prom-client';

const httpRequestDuration = new prometheus.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['service', 'method', 'route', 'status_code'],
});

const eventProcessingDuration = new prometheus.Histogram({
  name: 'event_processing_duration_seconds',
  help: 'Duration of event processing',
  labelNames: ['event_type', 'service'],
});

Алерты в Alertmanager:

  • Если Payment Service недоступен > 1 минуты
  • Если queue RabbitMQ растет быстрее, чем обрабатывается
  • Если ошибки в логах нового сервиса > 5% от всех запросов

Ключевые вызовы и решения

Проблема: Распределенные транзакции

  • Решение: Saga pattern (хореография через события)

Проблема: Консистентность данных

  • Решение: Eventually consistent, идемпотентные операции

Проблема: Отладка и логирование

  • Решение: Distributed tracing (Jaeger), correlationId во всех сервисах

Проблема: Управление зависимостями

  • Решение: API contracts (OpenAPI specs), версионирование

Результаты

После 4 месяцев миграции:

  • Deployment время: с 30 мин до 5 мин (каждый сервис отдельно)
  • MTTR (Mean Time To Recovery): с 2 часов до 15 минут
  • Масштабирование: Payment Service масштабируется отдельно от Order
  • Независимость команд: каждая команда владеет своим сервисом

Что я бы сделал по-другому

  1. Раньше стартовал с асинхронной коммуникацией — это сложнее, но стоит оно того
  2. Тестировал отказы — хаос-инженерия на раннем этапе
  3. Документировал контракты — OpenAPI был очень полезен
  4. Инвестировал в observability — логи, метрики, трейсы с самого начала