Реализация Microservices Architecture для веб-приложения
Микросервисная архитектура — разбиение монолита на независимо деплоящиеся сервисы, каждый из которых отвечает за свою бизнес-область. Каждый сервис имеет собственную БД, собственный деплой и собственную команду. Это не про масштаб запросов — это про масштаб команды и частоту изменений.
Когда переходить на микросервисы
Микросервисы решают организационные проблемы, а не технические. Признаки готовности:
- 3+ команды разрабатывают один монолит и тормозят друг друга
- Разные части системы требуют разного масштабирования
- Критические части (платежи, уведомления) нужно деплоить независимо
- Разные технологические стеки оправданы для разных задач
Монолит с хорошей архитектурой часто лучше преждевременной декомпозиции.
Декомпозиция на сервисы
По бизнес-возможностям (Business Capability):
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User Svc │ │ Product Svc │ │ Order Svc │ │ Payment Svc │
│ │ │ │ │ │ │ │
│ Auth │ │ Catalog │ │ Cart │ │ Stripe │
│ Profiles │ │ Search │ │ Checkout │ │ Refunds │
│ Permissions │ │ Inventory │ │ History │ │ Invoices │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│ │ │ │
└─────────────────┴─────────────────┴─────────────────┘
Message Bus (Kafka)
Каждый сервис — своя PostgreSQL (или MongoDB, Redis, где уместно). Никаких общих БД между сервисами.
Межсервисная коммуникация
Синхронная (REST/gRPC) — запрос-ответ, подходит для запросов пользователя:
// Order Service вызывает Product Service для проверки наличия
const productClient = new ProductServiceClient(process.env.PRODUCT_SERVICE_URL);
async function createOrder(items: OrderItem[]) {
// Проверяем наличие товаров синхронно
const availability = await productClient.checkAvailability(
items.map(i => ({ productId: i.productId, quantity: i.quantity }))
);
if (availability.some(a => !a.available)) {
throw new InsufficientStockError();
}
// ...
}
Асинхронная (Events/Kafka) — для операций, не требующих немедленного ответа:
// Order Service публикует событие после создания заказа
await kafka.producer.send({
topic: 'order.events',
messages: [{
key: order.id,
value: JSON.stringify({
type: 'OrderCreated',
orderId: order.id,
customerId: order.customerId,
items: order.items,
total: order.total,
occurredAt: new Date().toISOString()
})
}]
});
// Notification Service подписан на 'order.events'
kafka.consumer.on('order.events', async (event) => {
if (event.type === 'OrderCreated') {
await notificationService.sendConfirmationEmail(event.customerId, event.orderId);
}
});
Паттерн Strangler Fig для миграции монолита
Постепенная миграция без «большого переписывания»:
- Идентифицировать наиболее изолированный модуль монолита (обычно — уведомления, поиск или аутентификация)
- Поставить прокси (API Gateway) перед монолитом
- Вынести модуль в отдельный сервис
- Переключить прокси на новый сервис
- Удалить код из монолита
- Повторить для следующего модуля
# API Gateway (nginx) маршрутизирует по пути
location /api/auth/ {
proxy_pass http://auth-service:3001;
}
location /api/notifications/ {
proxy_pass http://notification-service:3002;
}
location /api/ {
proxy_pass http://monolith:8080; # остальное — в монолит
}
Data Management
Database per Service — каждый сервис владеет своими данными:
# docker-compose.yml
services:
user-db:
image: postgres:15
environment:
POSTGRES_DB: users
order-db:
image: postgres:15
environment:
POSTGRES_DB: orders
product-db:
image: postgres:15
environment:
POSTGRES_DB: products
notification-db:
image: redis:7
Saga Pattern для распределённых транзакций (см. отдельную страницу).
Shared Data через API — если Order Service нужны данные о пользователе, он запрашивает User Service через API, не пишет в его БД.
Инфраструктура
| Компонент | Инструмент |
|---|---|
| Container orchestration | Kubernetes |
| API Gateway | Kong, Traefik, AWS API Gateway |
| Service Discovery | Consul, Kubernetes DNS |
| Config Management | Consul KV, Vault |
| Message Broker | Apache Kafka, RabbitMQ |
| Distributed Tracing | Jaeger, Zipkin |
| Centralized Logging | ELK Stack, Loki + Grafana |
| Health Checks | Kubernetes liveness/readiness probes |
Observability
Каждый сервис должен экспортировать:
- Метрики в Prometheus (RED: Rate, Errors, Duration)
- Трейсы в Jaeger (OpenTelemetry SDK)
- Логи в структурированном JSON → Loki или Elasticsearch
// OpenTelemetry трейсинг в Node.js
import { trace, context } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service');
async function processOrder(orderId: string) {
const span = tracer.startSpan('processOrder');
span.setAttribute('order.id', orderId);
try {
await context.with(trace.setSpan(context.active(), span), async () => {
await validateOrder(orderId); // дочерний span
await chargePayment(orderId); // дочерний span
await notifyCustomer(orderId); // дочерний span
});
span.setStatus({ code: SpanStatusCode.OK });
} catch (err) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR });
throw err;
} finally {
span.end();
}
}
Сроки реализации
- Декомпозиция монолита и выделение первого сервиса — 3–6 недель
- Настройка инфраструктуры (Kubernetes + Kafka + трейсинг) — 2–4 недели параллельно
- Полная миграция среднего монолита (5–10 сервисов) — 4–8 месяцев
- Постепенная миграция через Strangler Fig — 1–2 года для крупного монолита







