Тестирование и деплой микросервисов
Сложность тестирования микросервисов
В монолите интеграционный тест запускается в одном процессе. В микросервисах каждый тест должен учитывать сетевые вызовы, eventual consistency и независимый деплой.
Монолит: один процесс, один тест
┌──────────────────────────────────┐
│ Test Runner │
│ ┌────────┐ ┌────────┐ ┌──────┐ │
│ │ Orders │ │ Users │ │ Pay │ │
│ │ (in │ │ (in │ │ (in │ │
│ │ memory)│ │ memory)│ │ mem) │ │
│ └────────┘ └────────┘ └──────┘ │
│ │
│ Всё в одном процессе -- просто │
└──────────────────────────────────┘
Микросервисы: N процессов, N проблем
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Orders │ │ Users │ │ Payments │
│ (test) │──│ (mock?) │──│ (mock?) │
│ │ │ (real?) │ │ (real?) │
└──────────┘ └──────────┘ └──────────┘
Запускать всё? Дорого и медленно.
Мокать всё? Тесты не отражают реальность.
Тестовая пирамида для микросервисов
/\
/ \
/ E2E \ < 5% -- Smoke tests (happy path)
/ Tests\ Медленные, хрупкие, дорогие
/──────────\
/ Contract \ ~15% -- Контрактные тесты
/ Tests \ Проверяют API-совместимость
/────────────────\
/ Integration \ ~30% -- Тесты с реальной БД
/ Tests \ и внешними зависимостями
/──────────────────────\
/ Unit Tests \ ~50% -- Быстрые, изолированные
/ (domain logic) \ Ядро бизнес-логики
/────────────────────────────\
Unit Tests
Тестируют бизнес-логику сервиса в изоляции. Внешние зависимости -- моки.
Что тестировать:
✅ Доменная логика (расчёт цены, валидация, state machine)
✅ Бизнес-правила (скидки, лимиты, workflow)
✅ Чистые функции (преобразования данных)
Чего НЕ тестировать unit-тестами:
❌ HTTP endpoints (это интеграционные тесты)
❌ SQL запросы (это интеграционные тесты)
❌ Внешние API (это контрактные тесты)
Integration Tests
Тестируют взаимодействие с инфраструктурой (БД, Redis, Kafka) внутри одного сервиса.
┌──────────────────────────────────────┐
│ Integration Test │
│ │
│ ┌──────────────┐ │
│ │ Order Service│ │
│ │ (real code) │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────┴───────┐ ┌──────────────┐ │
│ │ PostgreSQL │ │ Redis │ │
│ │ (Testcontainer)│ (Testcontainer)│ │
│ └──────────────┘ └──────────────┘ │
│ │
│ Внешние сервисы замокированы: │
│ User Service -> WireMock │
│ Payment Service -> WireMock │
└──────────────────────────────────────┘
Testcontainers: реальная БД в Docker для тестов
WireMock: HTTP-сервер, имитирующий внешний API
Contract Testing
Проблема
Order Service вызывает User Service по HTTP. Как убедиться, что API совместимы, не запуская оба сервиса?
Без Contract Tests:
Order Service ожидает:
GET /users/123 -> { "id": 123, "name": "Ivan" }
User Service возвращает (после рефакторинга):
GET /users/123 -> { "id": 123, "fullName": "Ivan" }
Баг! "name" переименовали в "fullName".
Узнаём только на staging или production.
Consumer-Driven Contract Testing
┌──────────────────────────────────────────────────────┐
│ Consumer-Driven Contract Testing │
│ │
│ 1. Consumer (Order Svc) определяет ожидания: │
│ "Мне нужно GET /users/{id} с полями id, name" │
│ │
│ 2. Contract сохраняется как файл (Pact): │
│ { "consumer": "OrderService", │
│ "provider": "UserService", │
│ "interactions": [{ │
│ "request": { "method": "GET", "path":...}, │
│ "response": { "body": { "id": 123, │
│ "name": "Ivan" } } │
│ }] │
│ } │
│ │
│ 3. Provider (User Svc) верифицирует контракт: │
│ - Запускает свой API │
│ - Прогоняет запросы из контракта │
│ - Проверяет что ответы соответствуют │
│ │
│ 4. Если контракт нарушен -- тест падает ДО деплоя │
└──────────────────────────────────────────────────────┘
Workflow Contract Testing
Consumer Provider
(Order Svc) (User Svc)
│ │
1. Написать │ │
Pact тест │ │
│ │
2. Запуск ────│────> Pact Broker ─────────│
генерирует │ (хранилище контрактов) │
контракт │ │
│ │
│ 3. CI/CD Pipeline
│ │
│ 4. Загрузить контракт
│ из Pact Broker
│ │
│ 5. Верифицировать
│ контракт
│ │
│ Успех: │ Контракт ОК
│ Провал: │ Breaking change!
│ │ Блокировать деплой
Инструменты Contract Testing
| Инструмент | Протокол | Подход |
|---|---|---|
| Pact | HTTP, messaging | Consumer-driven |
| Spring Cloud Contract | HTTP, messaging | Provider-driven |
| Protovalidate | gRPC/Protobuf | Schema validation |
| Schemathesis | OpenAPI | Property-based |
CI/CD для микросервисов
Один репозиторий vs Mono-repo
Polyrepo (один репозиторий = один сервис):
github.com/company/order-service
github.com/company/user-service
github.com/company/payment-service
+ Независимые CI/CD пайплайны
+ Чёткое владение (одна команда = один repo)
+ Изоляция зависимостей
- Сложно делать cross-service refactoring
- Дублирование CI/CD конфигураций
Monorepo (все сервисы в одном репозитории):
github.com/company/platform
/services/order/
/services/user/
/services/payment/
/shared/libs/
+ Atomic cross-service commits
+ Переиспользование CI конфигов
+ Единый code review
- Нужен Bazel/Nx для инкрементальных билдов
- Права доступа сложнее
CI Pipeline для микросервиса
┌──────────────────────────────────────────────────────┐
│ CI Pipeline (per service) │
│ │
│ ┌─────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │
│ │ Build │─>│ Unit │─>│ Integr. │─>│Contract│ │
│ │ & Lint │ │ Tests │ │ Tests │ │ Tests │ │
│ └─────────┘ └────────┘ └──────────┘ └────┬───┘ │
│ │ │
│ ┌──────────────┐ ┌───────────────┐ │ │
│ │ Security Scan│ │ Docker Build │<────────┘ │
│ │ (SAST, deps) │ │ & Push │ │
│ └──────────────┘ └───────┬───────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ Artifact │ │
│ │ Registry │ │
│ └─────────────┘ │
└──────────────────────────────────────────────────────┘
Стратегии деплоя
Blue-Green Deployment
┌─────────────────────────────────────────────────────┐
│ Blue-Green Deployment │
│ │
│ Текущее состояние: │
│ ┌──────────┐ ┌──────────────┐ │
│ │ Router │────────>│ BLUE (v1) │ ← Live │
│ │ (LB) │ │ 3 pods │ │
│ └──────────┘ └──────────────┘ │
│ ┌──────────────┐ │
│ │ GREEN (v2) │ ← Idle │
│ │ 3 pods │ │
│ └──────────────┘ │
│ │
│ Деплой v2: │
│ 1. Деплоить v2 в GREEN │
│ 2. Прогнать smoke tests на GREEN │
│ 3. Переключить Router: BLUE -> GREEN │
│ 4. GREEN стал Live, BLUE стал Idle │
│ │
│ Откат: переключить Router обратно на BLUE (секунды)│
│ │
│ + Мгновенный откат │
│ + Zero downtime │
│ - Двойные ресурсы │
│ - БД миграции нужно делать backward-compatible │
└─────────────────────────────────────────────────────┘
Canary Deployment
┌─────────────────────────────────────────────────────┐
│ Canary Deployment │
│ │
│ Этап 1: 5% трафика на canary │
│ ┌──────────┐ 95% ┌──────────────┐ │
│ │ Router │─────────>│ Stable (v1) │ │
│ │ │ 5% │ 10 pods │ │
│ │ │─────────>┌──────────────┐ │
│ └──────────┘ │ Canary (v2) │ │
│ │ 1 pod │ │
│ └──────────────┘ │
│ │
│ Мониторинг метрик canary: │
│ - Error rate < 0.1%? ✅ │
│ - P99 latency < 200ms? ✅ │
│ - CPU/Memory нормальные? ✅ │
│ │
│ Этап 2: 25% трафика │
│ Этап 3: 50% трафика │
│ Этап 4: 100% -- canary стал stable │
│ │
│ Если метрики плохие на любом этапе: │
│ -> Автоматический rollback (0% на canary) │
│ │
│ + Постепенный rollout, ранее обнаружение проблем │
│ + Минимальный blast radius │
│ - Сложнее настроить │
│ - Нужна хорошая observability │
└─────────────────────────────────────────────────────┘
Rolling Update
┌─────────────────────────────────────────────────────┐
│ Rolling Update (Kubernetes default) │
│ │
│ Начало: [v1] [v1] [v1] [v1] │
│ │
│ Шаг 1: [v1] [v1] [v1] [v2] ← новый pod │
│ Шаг 2: [v1] [v1] [v2] [v2] │
│ Шаг 3: [v1] [v2] [v2] [v2] │
│ Шаг 4: [v2] [v2] [v2] [v2] ← все обновлены │
│ │
│ Kubernetes: maxSurge=1, maxUnavailable=0 │
│ Гарантия: всегда есть 4 готовых пода │
│ │
│ + Не нужны двойные ресурсы │
│ + Встроено в Kubernetes │
│ - Нет чистого rollback (нужно откатить деплой) │
│ - Во время rollout -- разные версии одновременно │
└─────────────────────────────────────────────────────┘
Сравнение стратегий
| Стратегия | Downtime | Rollback | Ресурсы | Сложность |
|---|---|---|---|---|
| Blue-Green | Нет | Мгновенный | 2x | Средняя |
| Canary | Нет | Быстрый | 1.05-1.5x | Высокая |
| Rolling | Нет | Медленный | 1.25x | Низкая |
| Recreate | Да | Медленный | 1x | Минимальная |
Feature Flags
Деплой кода =/= включение фичи. Feature Flags позволяют деплоить код и включать функциональность отдельно.
┌──────────────────────────────────────────────────┐
│ Feature Flags │
│ │
│ Деплой: код v2 содержит новый алгоритм поиска │
│ Флаг: new_search_algorithm = false │
│ │
│ Включение: │
│ 1. new_search_algorithm = true (для 1% юзеров) │
│ 2. Мониторинг метрик │
│ 3. new_search_algorithm = true (для 10%) │
│ 4. Если ОК -> 100% │
│ 5. Удалить старый код и флаг │
│ │
│ Откат: new_search_algorithm = false │
│ Мгновенно, без деплоя! │
└──────────────────────────────────────────────────┘
Инструменты: LaunchDarkly, Unleash, Flagsmith, самописный
Database Migrations
Backward-Compatible Migrations
При Blue-Green и Canary одновременно работают две версии кода. Миграции БД должны быть совместимы с обеими.
Добавление колонки (безопасно):
v1: SELECT id, name FROM users
Migration: ALTER TABLE users ADD COLUMN email VARCHAR
v2: SELECT id, name, email FROM users
v1 продолжает работать (игнорирует email)
Удаление колонки (ОПАСНО -- 3 шага):
Шаг 1 (деплой v2): перестать читать колонку old_field
Шаг 2 (миграция): ALTER TABLE DROP COLUMN old_field
Шаг 3: убедиться что всё ОК
Переименование колонки (ОПАСНО -- expand/contract):
Шаг 1: добавить новую колонку, дублировать данные
Шаг 2: переключить код на новую колонку
Шаг 3: удалить старую колонку
Observability в CI/CD
Deployment Metrics
Ключевые метрики после деплоя:
DORA Metrics:
1. Deployment Frequency -- как часто деплоим
2. Lead Time for Changes -- от коммита до production
3. Change Failure Rate -- % деплоев с проблемами
4. Mean Time to Recovery -- время восстановления
Canary Metrics:
- Error rate (5xx)
- Latency (p50, p95, p99)
- Saturation (CPU, Memory)
- Traffic (requests per second)
Реальные примеры
Amazon -- Deployment Pipeline
Amazon деплоит каждые 11.7 секунд (данные 2015). Их pipeline:
- Разработчик пушит код
- Автоматические тесты (unit, integration, contract)
- Canary deployment в одном регионе
- Мониторинг 15-30 минут
- Автоматическое расширение на все регионы
- Автоматический rollback при аномалиях
Google -- Borg/Kubernetes
Google обрабатывает 2 миллиарда контейнеров в неделю. Rolling updates с health checks -- стандарт. Каждый сервис проходит через canary (1% pods) перед полным rollout.
Spotify -- Squad-based CI/CD
Каждый Squad (команда из 6-8 человек) владеет полным CI/CD пайплайном для своих сервисов. Они сами решают когда и как деплоить. Автономия команд -- ключевой принцип.