"Мы разделили монолит на 47 микросервисов. Теперь на деплой уходит 4 часа вместо 20 минут, команда ненавидит меня, а CEO спрашивает, когда закончится 'этот эксперимент'."
Реальная цитата CTO компании, которая обратилась ко мне в начале 2024 года. Они потратили 8 месяцев на миграцию, $200k бюджета и теперь хотели вернуться обратно на монолит.
Спойлер: Мы не вернулись. Мы починили архитектуру за 6 недель, убили 32 из 47 сервисов и получили систему, которая работает лучше монолита. Но цена ошибок была высока.
В этой статье — честный разговор о миграции с монолита на микросервисы. Без евангелизма, без слепой веры в "Netflix делают так, значит и нам надо". Только практика, грабли и реальные цифры.
Правда о микросервисах, которую не говорят на конференциях
Начну с неудобной правды: микросервисы — это не эволюция монолита. Это другой класс проблем.
Монолит — это как жить в однокомнатной квартире. Тесно, всё под рукой, уборка занимает час.
Микросервисы — это как управлять многоквартирным домом. Каждая квартира независима, но теперь у вас проблемы с отоплением, электричеством, канализацией, и жильцы жалуются друг на друга.
История из жизни: "Мы стали как Google!"
2023 год. Стартап на Django, 12 человек команда, 50k DAU, стабильный монолит. На конференции CTO услышал доклад про микросервисы. Вернулся вдохновлённым.
План:
- Разделить монолит на 15 микросервисов
- Внедрить Kubernetes
- Service mesh (Istio)
- Event-driven архитектура (Kafka)
- "Стать как Netflix"
Реальность через 6 месяцев:
- Деплой с 15 минут вырос до 2 часов
- Новый feature занимает 3 недели вместо 1 (нужно координировать 5 команд)
- Debugging — кошмар (запрос проходит через 7 сервисов, непонятно где падает)
- Расходы на инфраструктуру выросли в 3 раза
- 3 senior разработчика уволились
Итог: Через 9 месяцев они вернулись к модульному монолиту. Потеряли $400k и лучших людей.
Микросервисы — это не серебряная пуля. Это обмен одного набора проблем (сложность монолита) на другой (сложность распределённой системы). Убедитесь, что вы готовы платить эту цену.
Когда реально пора разделять: чеклист без демагогии
После 15 миграций (в обе стороны) я вывел простое правило: микросервисы нужны, когда боль монолита дороже боли микросервисов.
Признаки, что монолит душит вас
1. Deploy Bottleneck (узкое горлышко деплоя)
Симптом: Деплой занимает > 30 минут и блокирует всю команду
Пример: 5 команд выстраиваются в очередь на деплой в пятницу вечером
Цена: Время разработчиков + риск конфликтов + slow time-to-market
Реальный кейс: SaaS-платформа, 80 разработчиков. Деплой монолита — 45 минут. Каждая команда могла делать релиз раз в неделю. Конфликты мерджей — еженедельно.
После разделения на 8 сервисов: Каждая команда деплоит независимо, 10-15 раз в день. Time-to-market упал с недели до дня.
2. Scaling Hell (ад масштабирования)
Симптом: Один endpoint жрёт 90% ресурсов, но приходится масштабировать весь монолит
Пример: API для мобилки генерирует PDF-отчёты (CPU-intensive)
Остальные 50 endpoints простые, но масштабируются вместе с PDF
Цена: $5k/месяц на серверы вместо $1k
Математика:
Монолит: 10 инстансов × 8GB RAM × $100/месяц = $1000
(Из них 8 инстансов нужны только для PDF-генератора)
Микросервисы: API (2 инстанса × 2GB × $50) + PDF Service (8 × 8GB × $100) = $900
Экономия: $100/месяц (или $1200/год)
3. Team Collision (столкновения команд)
Симптом: 3+ команды работают в одном репозитории и мешают друг другу
Пример: Team A меняет auth, Team B ломает заказы, Team C дебажит всю пятницу
Цена: Конфликты мерджей + медленные PR reviews + нервы
Признак: Если вы делаете daily sync meetings между командами, чтобы "не наступать на ноги" — вам нужны границы.
4. Technology Lock-in (технологическая тюрьма)
Симптом: Хотите попробовать новый язык/фреймворк, но это требует переписать весь монолит
Пример: Монолит на Django 2.2, хотите async FastAPI для WebSocket
Миграция всего монолита = 6 месяцев
Цена: Упущенные возможности + технический долг
Чеклист готовности к микросервисам
Ответьте честно:
- У вас 3+ команды, работающих в монолите?
- Деплой занимает > 30 минут?
- Различные части системы имеют разные требования к масштабированию?
- У вас есть dedicated DevOps/Platform team?
- Вы готовы внедрить distributed tracing и централизованное логирование?
- У вас нет проблем с наймом (микросервисы требуют senior-разработчиков)?
- Бизнес понимает, что миграция займёт 6-12 месяцев?
Если ответили "ДА" на 5+ вопросов — микросервисы имеют смысл.
Если меньше 5 — попробуйте модульный монолит сначала.
Модульный монолит как промежуточный шаг
Что это: Монолит, разделённый на независимые модули с чёткими границами.
monolith/
├── modules/
│ ├── auth/ # Отдельный модуль
│ │ ├── api/
│ │ ├── models/
│ │ └── services/
│ ├── orders/ # Отдельный модуль
│ │ ├── api/
│ │ ├── models/
│ │ └── services/
│ └── payments/ # Отдельный модуль
│ └── ...
└── shared/ # Общий код
Правила:
- Модули общаются только через публичные API (не прямые импорты)
- Каждый модуль можно вынести в сервис за неделю
- Shared код минимален и стабилен
Преимущества:
- ✅ Деплой всё ещё простой (один сервис)
- ✅ Debugging простой (один процесс)
- ✅ Команды работают независимо (разные модули)
- ✅ Готовность к миграции (границы уже есть)
80% компаний, с которыми я работал, решили свои проблемы модульным монолитом. Микросервисы понадобились только 20%. Не переоцениваемте сложность ваших проблем.
Strangler Fig Pattern: как мигрировать без downtime
Strangler Fig (удушающая фига) — паттерн постепенной миграции. Новая система растёт вокруг старой, постепенно заменяя её части. Как фиговое дерево обвивает старое дерево и в итоге заменяет его.
Почему не Big Bang Rewrite
Big Bang Rewrite — это когда вы останавливаете разработку и переписываете всё заново.
Проблемы:
- 📉 6-12 месяцев без новых features → бизнес теряет деньги
- 🐛 Вы забудете про edge cases старой системы → баги в продакшене
- 😰 Команда выгорает → увольнения
- 💸 Риск провала проекта → зря потраченные миллионы
Известные провалы:
- Netscape (1998) — переписали браузер с нуля, потеряли рынок
- Knight Capital (2012) — новая система запустилась с багом, потеряли $440M за 45 минут
Strangler Fig на практике
Идея: Новая система стоит рядом со старой. Постепенно переключаем трафик с монолита на микросервисы. Когда монолит опустеет — выключаем.
Архитектура миграции
┌─────────────────────────┐
│ API Gateway / Proxy │
│ (Nginx/Envoy) │
└───────────┬─────────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌────────────────────┐ ┌──────────────────┐
│ Монолит (Django) │ │ Микросервисы │
│ │ │ │
│ /api/orders ✅ │ │ Auth Service ✅ │
│ /api/users ✅ │ │ Orders Service │
│ /api/auth ❌ │◄─────────│ (в процессе) │
│ │ читает │ │
└────────┬───────────┘ данные └─────────┬────────┘
│ │
└────────────┬────────────────────┘
▼
┌───────────────┐
│ PostgreSQL │
│ (shared DB) │
└───────────────┘
Этапы:
- Прокси впереди всех — весь трафик идёт через API Gateway
- Первый сервис — выносим самый простой/изолированный модуль
- Переключаем трафик — меняем роутинг в прокси (без изменения кода)
- Мониторим — смотрим на метрики, латентность, ошибки
- Откатываемся если проблемы — переключаем обратно за 5 секунд
- Повторяем для следующего модуля
Пример: выносим Auth Service
Шаг 1: Дублируем функциональность
# Новый Auth Service (FastAPI)
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy import select
from database import async_session
app = FastAPI()
@app.post("/api/auth/login")
async def login(credentials: LoginRequest):
async with async_session() as db:
# Читаем из ОБЩЕЙ БД (та же, что у монолита)
result = await db.execute(
select(User).where(User.email == credentials.email)
)
user = result.scalar_one_or_none()
if not user or not verify_password(credentials.password, user.password):
raise HTTPException(401, "Invalid credentials")
token = create_jwt_token(user.id)
return {"access_token": token}Шаг 2: Настраиваем роутинг (Nginx)
upstream auth_service {
server auth-service:8000;
}
upstream monolith {
server django-app:8000;
}
server {
listen 80;
# 10% трафика на новый сервис (canary deployment)
location /api/auth/ {
if ($request_id ~* "[0-9]$") { # 10% запросов (заканчиваются на 0-9)
proxy_pass http://auth_service;
}
proxy_pass http://monolith;
}
# Остальное на монолит
location / {
proxy_pass http://monolith;
}
}Шаг 3: Постепенное увеличение трафика
День 1-3: 10% трафика → Auth Service
День 4-7: 50% трафика → Auth Service
День 8-10: 100% трафика → Auth Service
Шаг 4: Мониторим метрики
# Добавляем метрики в Auth Service
from prometheus_client import Counter, Histogram
auth_requests = Counter('auth_requests_total', 'Total auth requests', ['status'])
auth_latency = Histogram('auth_request_duration_seconds', 'Auth latency')
@app.post("/api/auth/login")
@auth_latency.time()
async def login(credentials: LoginRequest):
try:
# ... логика
auth_requests.labels(status='success').inc()
return response
except Exception as e:
auth_requests.labels(status='error').inc()
raiseШаг 5: Сравниваем с монолитом
# Grafana Dashboard
# Латентность монолита vs микросервиса
histogram_quantile(0.95,
rate(django_http_request_duration_seconds_bucket{endpoint="/api/auth/login"}[5m])
)
vs
histogram_quantile(0.95,
rate(auth_request_duration_seconds_bucket[5m])
)
# Error rate
rate(django_http_errors_total{endpoint="/api/auth/login"}[5m])
vs
rate(auth_requests_total{status="error"}[5m])Шаг 6: Убираем код из монолита
После 2 недель стабильной работы Auth Service на 100% трафика:
# Django монолит - удаляем auth views
# git rm apps/auth/views.py
# git rm apps/auth/serializers.py
# git commit -m "Remove auth - migrated to auth-service"Грабли Strangler Fig
Грабля #1: Shared Database
Во время миграции у вас общая БД. Это создаёт coupling.
# ❌ Плохо: Auth Service меняет схему
ALTER TABLE users ADD COLUMN last_login_ip VARCHAR(15);
# Монолит падает: Unknown column 'last_login_ip'Решение: Database View Pattern
-- Auth Service работает через VIEW
CREATE VIEW auth_users AS
SELECT id, email, password_hash, created_at
FROM users;
GRANT SELECT ON auth_users TO auth_service;
-- Монолит продолжает работать с таблицей
-- Auth Service работает с view
-- Миграция схемы не ломает монолитГрабля #2: Транзакции между сервисами
# ❌ Это НЕ сработает
def create_order(user_id, items):
with transaction.atomic():
# Вызов Auth Service
user = auth_service.get_user(user_id) # HTTP-запрос
# Создание заказа в монолите
order = Order.objects.create(user=user)
# Если здесь ошибка → откат не затронет Auth Service!Решение: Saga Pattern или eventual consistency
# ✅ Event-driven подход
def create_order(user_id, items):
# 1. Создаём заказ в статусе PENDING
order = Order.objects.create(user_id=user_id, status='PENDING')
# 2. Отправляем событие в очередь
event_bus.publish('order.created', {
'order_id': order.id,
'user_id': user_id
})
# 3. Auth Service слушает события и обновляет статистику
# 4. Если что-то пошло не так → compensating transactionDistributed transactions не работают в микросервисах. Примите eventual consistency как реальность. Используйте Saga, event sourcing или живите с дублированием данных.
Distributed Tracing с первого дня
Главная боль микросервисов: "Где упал запрос, если он прошёл через 7 сервисов?"
В монолите: смотрим стектрейс, видим всю цепочку вызовов.
В микросервисах: смотрим логи 7 сервисов, пытаемся найти запрос по timestamp. Удачи.
OpenTelemetry + Jaeger: must-have с первого сервиса
OpenTelemetry — стандарт для distributed tracing. Jaeger — UI для просмотра трейсов.
Архитектура трейсинга
Request ID: 7f3a9c12-4e8d-4f2a-a1b3-8d7e9f2c1a4b
API Gateway (span: 250ms)
↓
Auth Service (span: 45ms)
├─ DB query (span: 12ms)
└─ Redis cache (span: 3ms)
↓
Order Service (span: 180ms)
├─ DB query (span: 50ms)
├─ HTTP → Payment Service (span: 120ms)
│ ├─ DB query (span: 15ms)
│ └─ HTTP → Stripe API (span: 95ms) ← Виновник!
└─ Kafka publish (span: 8ms)
Один взгляд на Jaeger UI — и вы видите, что Stripe тормозит.
Настройка OpenTelemetry в Python
Установка:
pip install opentelemetry-api opentelemetry-sdk \
opentelemetry-instrumentation-fastapi \
opentelemetry-exporter-jaegerКод (FastAPI):
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor
# Настройка трейсера
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)
# Экспорт в Jaeger
jaeger_exporter = JaegerExporter(
agent_host_name="jaeger",
agent_port=6831,
)
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(jaeger_exporter)
)
app = FastAPI()
# Автоматическая инструментация
FastAPIInstrumentor.instrument_app(app)
SQLAlchemyInstrumentor().instrument(engine=engine)
RedisInstrumentor().instrument()
# Кастомные spans
@app.post("/orders")
async def create_order(order: OrderCreate):
with tracer.start_as_current_span("create_order"):
# Span автоматически включает:
# - request_id
# - http.method, http.url, http.status_code
# - duration
with tracer.start_as_current_span("validate_user"):
user = await auth_service.get_user(order.user_id)
with tracer.start_as_current_span("process_payment"):
payment = await payment_service.charge(order.total)
with tracer.start_as_current_span("save_to_db"):
result = await db.execute(insert(Order).values(**order.dict()))
return {"order_id": result.inserted_primary_key[0]}Межсервисное распространение trace ID:
import httpx
from opentelemetry.propagate import inject
async def call_payment_service(amount: float):
headers = {}
# Инжектим trace context в HTTP-заголовки
inject(headers)
async with httpx.AsyncClient() as client:
response = await client.post(
"http://payment-service/charge",
json={"amount": amount},
headers=headers # trace_id передаётся дальше
)
return response.json()Принимающая сторона (Payment Service):
from opentelemetry.propagate import extract
@app.post("/charge")
async def charge(request: Request, data: ChargeRequest):
# Извлекаем trace context из заголовков
context = extract(request.headers)
# Спан автоматически связывается с родительским
with tracer.start_as_current_span("charge_payment", context=context):
# ... логика оплаты
passDocker Compose для Jaeger
services:
jaeger:
image: jaegertracing/all-in-one:1.52
environment:
- COLLECTOR_OTLP_ENABLED=true
ports:
- "16686:16686" # Jaeger UI
- "6831:6831/udp" # Прием traces
- "4317:4317" # OTLP gRPC
networks:
- monitoring
auth-service:
build: ./auth-service
environment:
- JAEGER_AGENT_HOST=jaeger
- JAEGER_AGENT_PORT=6831
networks:
- monitoring
order-service:
build: ./order-service
environment:
- JAEGER_AGENT_HOST=jaeger
- JAEGER_AGENT_PORT=6831
networks:
- monitoring
networks:
monitoring:
driver: bridgeЧто показывает distributed tracing
Сценарий 1: Slow Request
Запрос на /api/orders занял 2.5 секунды. Почему?
Jaeger показывает:
┌─ API Gateway: 2500ms
│ ├─ Auth Service: 50ms ✅
│ └─ Order Service: 2400ms ⚠️
│ ├─ DB query: 2300ms ❌ ← Проблема здесь!
│ └─ Kafka publish: 80ms ✅
Решение: Добавить индекс на orders.user_id
Сценарий 2: Cascading Failures
Order Service возвращает 500. Что случилось?
Jaeger показывает:
┌─ Order Service: 500 Internal Server Error
│ └─ Payment Service: timeout after 30s ❌
│ └─ Stripe API: no response ❌
Решение: Stripe лежит. Добавить circuit breaker.
Сценарий 3: N+1 Problem в распределённой системе
Запрос на /api/orders?user_id=123 медленный
Jaeger показывает:
┌─ Order Service: 3200ms
│ ├─ DB query (orders): 50ms ✅
│ ├─ HTTP → Product Service: 150ms (×20 раз!) ❌
│ │ └─ DB query: 5ms ×20
│
Решение: Батчинг запросов к Product Service
Distributed tracing экономит часы debugging. Внедрите его ДО того, как запустите второй микросервис. Ретроспективное внедрение болезненно.
Куда девать shared код: 4 стратегии
Самый больной вопрос микросервисов: "У нас есть общий код (модели, утилиты, валидация). Что с ним делать?"
Стратегия 1: Shared Library (библиотека)
Идея: Общий код в отдельный пакет. Каждый сервис подключает как зависимость.
shared-lib/
├── models/
│ ├── user.py
│ └── order.py
├── utils/
│ ├── validators.py
│ └── formatters.py
└── setup.py
# Публикуем в private PyPI или Artifactory
Использование:
# requirements.txt каждого сервиса
company-shared-lib==1.2.3
# В коде
from company_shared.models import User
from company_shared.utils import validate_emailПлюсы:
- ✅ DRY (Don't Repeat Yourself)
- ✅ Версионирование (можно откатить)
- ✅ Единое место для изменений
Минусы:
- ❌ Coupling между сервисами (все зависят от одной либы)
- ❌ Обновление либы = деплой всех сервисов
- ❌ Breaking changes = кошмар
Грабля:
# Кто-то обновил shared-lib с 1.2.3 до 2.0.0
# Breaking change: User.full_name → User.get_full_name()
# Auth Service обновился → работает ✅
# Order Service не обновился → падает ❌Решение: Semantic versioning + deprecation warnings.
# shared-lib 1.3.0 (transitional release)
class User:
def full_name(self):
warnings.warn("Use get_full_name() instead", DeprecationWarning)
return self.get_full_name()
def get_full_name(self):
return f"{self.first_name} {self.last_name}"
# shared-lib 2.0.0 (breaking release)
# Удаляем full_name(), оставляем только get_full_name()Когда использовать:
- Stable код (редко меняется)
- Утилиты, константы, форматтеры
- Pydantic/Protobuf схемы для контрактов API
Стратегия 2: Code Generation (генерация кода)
Идея: Храним схемы в одном месте (OpenAPI, Protobuf), генерируем клиенты для каждого языка.
api-schemas/
├── openapi/
│ ├── auth.yaml
│ └── orders.yaml
└── generate.sh
# generate.sh
openapi-generator generate \
-i openapi/auth.yaml \
-g python \
-o clients/python/auth
openapi-generator generate \
-i openapi/auth.yaml \
-g typescript-fetch \
-o clients/typescript/auth
Результат:
# Auth Service (FastAPI)
# API автоматически генерирует OpenAPI схему
# Order Service использует сгенерированный клиент
from auth_client import AuthApi, Configuration
config = Configuration(host="http://auth-service")
api = AuthApi(config)
user = api.get_user(user_id=123)
print(user.email) # Type-safe!Плюсы:
- ✅ Type safety (IDE подсказывает методы)
- ✅ Автоматическая валидация
- ✅ Поддержка разных языков (Python, Go, TypeScript)
- ✅ Схема API = source of truth
Минусы:
- ❌ Дополнительный шаг в CI/CD
- ❌ Сгенерированный код иногда кривой
- ❌ Нужно синхронизировать схемы
Когда использовать:
- Polyglot микросервисы (Python + Go + Node.js)
- Строгие контракты API
- External API для партнёров
Стратегия 3: Duplication (дублирование)
Идея: Копируем код в каждый сервис. Да, серьёзно.
auth-service/
└── utils/
└── validators.py # Копия
order-service/
└── utils/
└── validators.py # Та же копия
payment-service/
└── utils/
└── validators.py # Та же копия
Плюсы:
- ✅ Полная независимость сервисов
- ✅ Нет coupling
- ✅ Можно изменять без риска сломать другие сервисы
Минусы:
- ❌ Нарушение DRY
- ❌ Баг придётся фиксить в 10 местах
- ❌ Расхождение версий
Когда использовать:
- Простой код (10-50 строк утилит)
- Код, который редко меняется
- Когда coupling дороже дублирования
"Duplication is far cheaper than the wrong abstraction" — Sandi Metz. Иногда скопировать 20 строк кода в 5 сервисов проще, чем поддерживать shared library.
Стратегия 4: Service as Source of Truth
Идея: Нет shared кода. Сервисы общаются только через API.
# ❌ Плохо: Order Service импортирует User из shared-lib
from shared.models import User
def create_order(user_id):
user = User.objects.get(id=user_id) # Direct DB access!
order = Order.create(user=user)
# ✅ Хорошо: Order Service вызывает Auth Service API
async def create_order(user_id):
user = await auth_service_client.get_user(user_id) # HTTP call
order = await Order.create(user_id=user_id, user_email=user.email)Плюсы:
- ✅ Нет coupling
- ✅ Каждый сервис владеет своими данными
- ✅ Легко менять реализацию внутри сервиса
Минусы:
- ❌ Network latency
- ❌ Нужен fallback если сервис недоступен
- ❌ Eventual consistency
Когда использовать:
- Высокая изоляция критична
- Сервисы на разных языках
- Разные команды владеют сервисами
Что выбрать: decision tree
Общий код:
├─ Stable, редко меняется?
│ ├─ Да → Shared Library
│ └─ Нет → Duplication или Service API
├─ Нужна type safety?
│ └─ Да → Code Generation
├─ Polyglot микросервисы?
│ └─ Да → Code Generation или Service API
└─ Хотите независимость любой ценой?
└─ Да → Service API
Мой выбор в 2025:
- Pydantic models для API контрактов → Shared Library (версионирование)
- Утилиты (форматтеры, валидаторы) → Duplication
- Бизнес-логика → Service API (каждый сервис владеет своей доменной логикой)
Real-World кейс: миграция E-commerce с метриками
Компания: E-commerce платформа (B2C)
До миграции: Django монолит, 120k строк кода, 35 разработчиков
Проблема: Деплой 45 минут, 5 команд мешают друг другу, scaling дорогой
Начальное состояние
Архитектура:
Django Monolith (10 инстансов × 8GB RAM)
├── Auth (5% CPU)
├── Catalog (15% CPU)
├── Orders (20% CPU)
├── Payments (10% CPU)
├── Recommendations (40% CPU) ← ML-модель, CPU-intensive
└── Admin Panel (10% CPU)
PostgreSQL (master + 2 read replicas)
Redis (cache + sessions)
Celery (background tasks)
Метрики (до):
| Метрика | Значение |
|---|---|
| Деплой | 45 минут |
| Deploys/неделя | 2-3 раза |
| Инстансы | 10 × c5.2xlarge ($340/мес) |
| Инфраструктура | $3400/мес |
| P95 latency | 350ms |
| Uptime | 99.5% (3.6 часа downtime/месяц) |
| Time to market | 2-3 недели |
Боль:
- Recommendations (ML) требовал 40% CPU, но приходилось масштабировать весь монолит
- 5 команд работали в одном репозитории → конфликты мерджей
- Деплой блокировал всех → очередь в пятницу вечером
- Изменение в Auth ломало Orders (coupling)
План миграции (6 месяцев)
Этап 1: Подготовка (месяц 1)
- ✅ Настроили OpenTelemetry + Jaeger
- ✅ Внедрили feature flags (LaunchDarkly)
- ✅ Настроили API Gateway (Kong)
- ✅ Создали shared library для моделей
- ✅ Разделили команды по доменам
Этап 2: Первый сервис - Recommendations (месяц 2)
Почему первый: Изолированный, CPU-intensive, не критичный для бизнеса.
# Recommendations Service (FastAPI + ML model)
from fastapi import FastAPI
from ml_model import RecommendationModel
app = FastAPI()
model = RecommendationModel.load()
@app.get("/recommendations/{user_id}")
async def get_recommendations(user_id: int, limit: int = 10):
# ML inference
products = await model.predict(user_id, limit=limit)
return {"products": products}Результат:
Экономия: $600/месяц (GPU инстансы дешевле для ML, чем раздувать монолит)
Этап 3: Auth Service (месяц 3)
Почему второй: Критичный, но простой. Чёткие границы.
# Auth Service (FastAPI + JWT)
from fastapi import FastAPI, Depends, HTTPException
from fastapi_jwt_auth import AuthJWT
app = FastAPI()
@app.post("/auth/login")
async def login(credentials: LoginRequest):
user = await authenticate(credentials)
access_token = create_access_token(user.id)
return {"access_token": access_token}
@app.get("/auth/me")
async def get_current_user(Authorize: AuthJWT = Depends()):
Authorize.jwt_required()
user_id = Authorize.get_jwt_subject()
user = await get_user_by_id(user_id)
return userРезультат:
- Деплой Auth: 8 минут (был 45)
- Auth team может релизить 5-10 раз в день
- Монолит стал легче (убрали 15k строк)
Этап 4: Orders Service (месяц 4)
Сложности: Транзакции с Payments, события для других сервисов.
# Orders Service (FastAPI + Event-driven)
from fastapi import FastAPI
import aio_pika # RabbitMQ
app = FastAPI()
@app.post("/orders")
async def create_order(order: OrderCreate):
# 1. Создаём заказ
order_entity = await db.create_order(order)
# 2. Отправляем событие
connection = await aio_pika.connect_robust("amqp://rabbitmq/")
channel = await connection.channel()
await channel.default_exchange.publish(
aio_pika.Message(
body=json.dumps({
"order_id": order_entity.id,
"user_id": order.user_id,
"total": order.total
}).encode()
),
routing_key="orders.created"
)
return order_entityЭтап 5: Payments Service (месяц 5)
Интеграция: Stripe, PayPal, internal wallet.
# Payments Service
@app.post("/payments/charge")
async def charge(payment: PaymentRequest):
# Слушаем события "orders.created"
# Списываем деньги
# Публикуем "payments.completed"
passЭтап 6: Catalog Service (месяц 6)
Особенность: Read-heavy (80% GET запросов).
# Catalog Service с агрессивным кешированием
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
@app.on_event("startup")
async def startup():
redis = aioredis.from_url("redis://localhost")
FastAPICache.init(RedisBackend(redis), prefix="catalog:")
@app.get("/products/{product_id}")
@cache(expire=3600) # 1 час
async def get_product(product_id: int):
return await db.get_product(product_id)Финальное состояние
Архитектура (после):
API Gateway (Kong)
├── Auth Service (2 инстансов)
├── Catalog Service (3 инстансов + Redis)
├── Orders Service (4 инстансов)
├── Payments Service (2 инстансов)
├── Recommendations Service (4 GPU инстансов)
└── Django Monolith (Admin Panel только, 2 инстансов)
Event Bus (RabbitMQ)
Distributed Tracing (Jaeger)
Centralized Logging (Loki)
Метрики (после):
| Метрика | До | После | Изменение |
|---|---|---|---|
| Деплой | 45 мин | 5-12 мин | -73% |
| Deploys/неделя | 2-3 | 40-50 | +1500% |
| Инфраструктура | $3400/мес | $2100/мес | -38% |
| P95 latency | 350ms | 180ms | -49% |
| Uptime | 99.5% | 99.9% | +0.4% |
| Time to market | 2-3 недели | 3-5 дней | -70% |
Экономия: $1300/месяц × 12 = $15,600/год только на инфраструктуре.
Выигрыш в productivity: Команды стали релизить в 20 раз чаще → фичи на рынок в 5 раз быстрее → ROI бесценен.
Грабли, которые мы поймали
Грабля #1: Забыли про N+1 в микросервисах
# ❌ Плохо: N+1 HTTP-запросов
async def get_order_details(order_id):
order = await db.get_order(order_id)
# Для каждого item делаем HTTP-запрос!
for item in order.items:
product = await catalog_service.get_product(item.product_id)
item.product_name = product.name
# 10 items = 10 HTTP-запросов × 50ms = 500ms latency!Решение: Batch API
# ✅ Хорошо: один запрос для всех продуктов
async def get_order_details(order_id):
order = await db.get_order(order_id)
product_ids = [item.product_id for item in order.items]
products = await catalog_service.get_products_batch(product_ids) # Один запрос!
products_map = {p.id: p for p in products}
for item in order.items:
item.product_name = products_map[item.product_id].nameГрабля #2: Distributed монолит
После 3 месяцев у нас было 15 сервисов, но они ВСЕ вызывали друг друга синхронно. Это distributed монолит, не микросервисы.
Решение: Event-driven архитектура. Сервисы общаются через события, не HTTP.
Грабля #3: Нет circuit breaker
Payments Service упал → Orders Service ждал 30s timeout на каждый запрос → весь сайт лёг.
Решение: Circuit Breaker Pattern.
from circuitbreaker import circuit
@circuit(failure_threshold=5, recovery_timeout=60)
async def call_payment_service(amount):
async with httpx.AsyncClient() as client:
response = await client.post("http://payment-service/charge", ...)
return response.json()
# После 5 неудач → circuit открыт → fast fail (не ждём 30s)Антипаттерны и как их избежать
Антипаттерн #1: Микросервис на каждую таблицу
Плохо:
User Service (таблица users)
Order Service (таблица orders)
Product Service (таблица products)
Cart Service (таблица cart_items)
...
Почему плохо: Создать заказ = 5 HTTP-запросов между сервисами. Транзакции невозможны.
Правильно: Сервисы по бизнес-доменам.
Auth & Users Service (вся auth логика)
Catalog Service (продукты, категории, поиск)
Order Management Service (заказы, корзина, checkout)
Антипаттерн #2: Shared Database
Плохо: Все сервисы пишут в одну PostgreSQL.
Почему плохо:
- Coupling на уровне схемы БД
- Один сервис меняет схему → другой падает
- Scaling проблематичен
Правильно: Database per service (или хотя бы schema per service).
-- Auth Service
CREATE SCHEMA auth;
CREATE TABLE auth.users (...);
-- Order Service
CREATE SCHEMA orders;
CREATE TABLE orders.orders (...);Антипаттерн #3: Нет API Gateway
Плохо: Frontend вызывает 10 микросервисов напрямую.
Почему плохо:
- CORS на каждом сервисе
- Auth на каждом сервисе
- Frontend знает внутреннюю топологию
- Нельзя изменить routing без изменения frontend
Правильно: API Gateway (Kong, Nginx, Envoy, AWS API Gateway).
# Kong routing
/api/auth/* → Auth Service
/api/products/* → Catalog Service
/api/orders/* → Orders ServiceАнтипаттерн #4: Distributed монолит
Признаки:
- Все сервисы синхронно вызывают друг друга
- Нельзя задеплоить один сервис без других
- Изменение API одного сервиса → изменения во всех остальных
Правильно: Loose coupling через события.
Чеклист перед стартом миграции
Инфраструктура:
- API Gateway настроен
- Service mesh (опционально, но желательно)
- Distributed tracing (OpenTelemetry + Jaeger)
- Centralized logging (Loki/ELK)
- Metrics & Monitoring (Prometheus + Grafana)
- CI/CD для каждого сервиса
- Container registry (Docker Hub, ECR, GCR)
Архитектура:
- Определили границы сервисов (DDD, bounded contexts)
- Спланировали стратегию для shared кода
- Выбрали паттерн для eventual consistency (Saga, events)
- Спроектировали API контракты (OpenAPI, gRPC)
- Спланировали data migration стратегию
Команда:
- Все понимают, зачем мигрируем
- Есть dedicated DevOps/Platform team
- Разработчики понимают distributed systems
- Есть владельцы для каждого сервиса
Бизнес:
- Бизнес понимает, что миграция займёт 6-12 месяцев
- Есть бюджет на дополнительную инфраструктуру
- Готовы к временному slowdown в features
Выводы
Микросервисы — это не про технологии. Это про людей, процессы и бизнес-цели.
Когда НЕ нужны микросервисы:
- Команда < 10 человек
- Startup в поиске product-market fit
- Нет проблем с deploy frequency
- Монолит справляется с нагрузкой
Когда нужны:
- 3+ команды работают в монолите
- Deploy bottleneck (> 30 минут)
- Разные части системы требуют разного scaling
- Есть DevOps/Platform team
Strangler Fig Pattern:
- Миграция без downtime
- Постепенное переключение трафика
- Возможность отката за секунды
- Минимизация рисков
Distributed Tracing:
- Must-have с первого микросервиса
- Экономит часы debugging
- OpenTelemetry + Jaeger — стандарт
Shared Code:
- Stable код → Shared Library
- API контракты → Code Generation
- Утилиты → Duplication (иногда это ОК)
- Бизнес-логика → Service API
Главный урок: Не делайте микросервисы, потому что это модно. Делайте, потому что монолит стал вашей болью. И делайте постепенно, измеряя каждый шаг.
Нужна помощь с архитектурой? Я провожу архитектурные ревью и помогаю командам принимать правильные решения. Пишите на почту — обсудим ваш кейс.
Полезные материалы:
- Мониторинг стек 2025 — как мониторить микросервисы
- Load Balancers — распределение нагрузки
- Technical Debt Metrics — как измерять архитектурный долг
