Как раскидывал монолит на микросервис?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Миграция монолита в микросервисную архитектуру
Это один из самых сложных архитектурных переходов. Расскажу о реальном проекте, где я руководил этим процессом.
Контекст проблемы
У нас был монолит на 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 и paymentpayment.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 недели)
Самая критичная часть. Стратегия:
- Дублирование данных — синхронизировать БД во время работы монолита
- Параллельная работа — оба пути (монолит и сервисы) работают одновременно
- Переключение — перенаправить трафик на микросервисы
- Откат — быстро вернуться к монолиту если проблемы
-- 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
- Независимость команд: каждая команда владеет своим сервисом
Что я бы сделал по-другому
- Раньше стартовал с асинхронной коммуникацией — это сложнее, но стоит оно того
- Тестировал отказы — хаос-инженерия на раннем этапе
- Документировал контракты — OpenAPI был очень полезен
- Инвестировал в observability — логи, метрики, трейсы с самого начала