Микросервисы — архитектура и паттерны
Микросервисная архитектура делит приложение на небольшие независимые сервисы, у каждого — своя база, свой цикл деплоя и своя команда. В этом уроке ты разберёшь, когда это оправдано, какие паттерны применять и как не превратить проект в распределённый хаос.
1. Монолит vs микросервисы
Монолит — одно приложение: весь код, база данных и интерфейс находятся в одном развёртываемом блоке. Микросервисы — набор небольших независимых сервисов, каждый из которых отвечает за одну бизнес-возможность и общается с остальными по сети.
В монолите app.py объединяет каталог, корзину и оплату; в микросервисах это три отдельных сервиса со своими базами и деплоями:
# Монолит: один процесс
app.py (catalog + cart + payment)
db.sql
# Микросервисы: N процессов
catalog/ -> PORT 8081, база catalog_db
cart/ -> PORT 8082, база cart_db
payment/ -> PORT 8083, база payment_db
orders/ -> PORT 8084, база orders_db| Критерий | Монолит | Микросервисы |
|---|---|---|
| Развёртывание | одна единица | каждый сервис отдельно |
| База данных | общая на всё | у каждого своя |
| Связь | вызовы функций | HTTP / очереди |
| Отказ | падает всё приложение | падает один сервис |
| Команда | одна на всё | по сервису на команду |
2. Плюсы микросервисов
Зачем вообще делить приложение? У микросервисной архитектуры есть измеримые преимущества.
- Независимое масштабирование — тяжело нагруженный сервис оплаты масштабируется отдельно от остальных.
- Изолированный деплой — релиз каталога не требует перезапуска корзины.
- Технологическая свобода — один сервис на Python, другой на Go; выбор под задачу.
- Владение командой — команда отвечает за свой сервис от кода до продакшена.
- Локальные сбои — деградация одного сервиса не роняет всё приложение.
service catalog:
build: ./catalog
ports: ["8081:8081"]
scale: 2 # только каталог ловит пик трафика
service payment:
build: ./payment
ports: ["8083:8083"]3. Минусы микросервисов
Плата за гибкость честная и немалая. Основные минусы — это не «сложно», а стабильные дополнительные затраты.
- Сетевая сложность — вызовы по HTTP медленнее и ненадёжнее функций в памяти.
- Распределённые транзакции — нет единой транзакции на несколько баз: нужны компенсации и saga.
- Трудная отладка — запрос идёт через 5 сервисов, отслеживание требует трассировки.
- Операционная зрелость — нужны CI/CD, логи, мониторинг, квотирование, управление версиями.
- Рост числа движущихся частей — каждый сервис надо мониторить, обновлять, защищать.
| Проблема монолита | Цена решения микросервисами |
|---|---|
| громоздкий деплой | N деплоев и пайплайнов |
| единая база-бутылочное горлышко | N баз + синхронизация данных |
| одна команда всё трогает | межсервисные контракты и версионирование |
| падает целиком | отказоустойчивость требует circuit breaker, retry, k8s |
4. Архитектурные паттерны
Совокупность практик превращает набор сервисов в архитектуру. Ключевые паттерны декомпозиции и согласованности:
| Паттерн | Решает | Суть |
|---|---|---|
| Бизнес-возможности | что делить | границы по бизнес-процессам (заказ, склад) |
| Database per service | несвязанность данных | база принадлежит одному сервису |
| Saga | распределённые транзакции | шаги + компенсирующие откаты |
| Strangler Fig | постепенная миграция | новые функции выносятся, старые «удушаются» |
| API Gateway | единая точка входа | роутинг, аутентификация, агрегация |
| Sidecar | вспомогательное рядом | прокси/тулинг рядом с сервисом |
Пример Saga на оркестрации заказа: сервис резервирует товар, затем списывает оплату, а при сбое оплаты вызывает компенсирующий шаг возврата товара:
orders: "Создать заказ"
-> inventory: "Зарезервировать товар"
-> payment: "Списать оплату"
-> [сбой оплаты]
-> inventory: "Отменить резерв" # компенсация
-> orders: "Заказ отменён"5. API Gateway
Клиенты (браузер, мобильное приложение) не должны знать адреса всех сервисов. API Gateway — единственная точка входа: принимает запрос, роутит его по сервисам, выполняет аутентификацию и лимиты, отдаёт клиенту единый ответ.
Простой gateway на базе Nginx разводит запросы по путям на сервисы:
server {
listen 80;
location /users/ { proxy_pass http://users-service:8081; }
location /orders/ { proxy_pass http://orders-service:8082; }
location /payments/{ proxy_pass http://payment-service:8083; }
}Готовые gateway-продукты (Kong, Traefik, Ambassador) добавляют сверху aутентификацию, rate limiting, кэширование и метаданные по маршрутам.
| Функция | Зачем нужна |
|---|---|
| Роутинг | один домен вместо N адресов сервисов |
| Аутентификация | проверка токена до попадания в сервис |
| Rate limiting | защита от всплесков и перебора |
| Агрегация | объединение нескольких вызовов в один ответ |
| SSL termination | шифрование наружу, простой трафик внутри |
5. Сервис-дискавери
Сервисы меняют адреса при масштабировании, а друг друга узнают через service registry — реестр доступных инстансов. Без него каждый сервис хранит жёсткие IP, которые устаревают с каждым рестартом.
# Регистрация сервиса в реестре (пример, Consul HTTP API)
curl -X PUT \
http://consul:8500/v1/agent/service/register \
-d '{
"ID": "cart-1",
"Name": "cart",
"Address": "10.0.1.5",
"Port": 8082,
"Check": {"HTTP": "http://10.0.1.5:8082/health", "Interval": "10s"}
}'При старте сервис регистрируется в реестре, при остановке — снимается. Потребитель спрашивает реестр: «какие инстансы cart есть?» — и получает актуальный список.
| Подход | Как работает | Инструменты |
|---|---|---|
| Реестр (registry) | сервисы сами регистрируются, клиенты спрашивают | Consul, Eureka, Zookeeper |
| Client-side | клиент сам выбирает инстанс из реестра | Ribbon, feign + consul |
| Server-side | балансировщик смотрит в реестр | Nginx + consul-template, Traefik |
| DNS (в k8s) | имя сервиса резолвится в балансировщик | CoreDNS + Service |
6. Очереди сообщений
Синхронные HTTP-вызовы делают сервисы зависимыми: если оплата лежит, весь запрос падает. Асинхронная связь через очереди развязывает сервисы и сглаживает пики нагрузки.
order_service -> queue "orders" -> warehouse_service
order_service -> queue "emails" -> email_service
# Пример сообщения (JSON) в очереди "orders":
{ "order_id": 1001, "items": ["sku-42"], "customer": "user@example.com" }Распространённые брокеры: Kafka (потоки событий, миллионы сообщений в секунду) и RabbitMQ (гибкие очереди и задачи).
| Тип связи | Блокировка | Отказ отправителя | Пропускная способность |
|---|---|---|---|
| HTTP-вызов (sync) | да, ждёт ответ | запрос падает | ниже |
| Очередь (async) | нет, fire-and-forget | сообщение остаётся в очереди | выше, сглаживает пики |
| Событийная шина | нет | событие в топике для подписчиков | максимальная |
7. Observability — наблюдаемость
Запрос, проходящий через 5 сервисов, нельзя отлаживать по логам одного. Observability — три кита: логи, метрики и трассировка, собранные в одну систему.
# Структурированный лог сервиса — его удобно искать по полям
{"level":"error","service":"payment","order_id":1001,
"trace_id":"7f3a...","error":"insufficient funds","ts":"2026-09-19T12:00:01Z"}- Логи — что произошло: ELK, Loki.
- Метрики — сколько и как часто: Prometheus, Grafana.
- Трассировка — каким путём прошёл запрос: Jaeger, Zipkin, OpenTelemetry.
| Столп | Вопрос | Типичный инструмент |
|---|---|---|
| Logs | что случилось в этот момент? | ELK, Loki |
| Metrics | где узкое место, насколько загружен сервис? | Prometheus + Grafana |
| Traces | сколько времени занял каждый шаг запроса? | Jaeger, OpenTelemetry |
trace_id). Один и тот же идентификатор в логах всех сервисов позволяет собрать путь запроса целиком.8. Когда НЕ стоит переходить
Микросервисы — не серебряная пуля, и есть явные сигналы, когда переход принесёт больше боли, чем пользы.
| Ситуация | Что разумнее |
|---|---|
| Маленькая команда (до 5–7 человек) | модульный монолит с чистыми границами |
| Дедлайн и нет автоматизации | сначала наладить CI/CD на монолите |
| Единая база и сильная связанность данных | разобрать модули, потом думать о сети |
| Нет чётких границ бизнес-функций | выделить домены, дождаться ясности |
| Монолит работает и быстро развивается | оставить как есть, чинить только реальные пробки |
Правильный триггер для перехода — конкретная боль: бутылочное горлышко одного модуля, недельные деплои из-за связанности, команда топчется друг у друга по ногам.
# Проверка перед "распиливанием":
# 1. Деплой монолита занимает больше 1 дня? только тогда ...
# 2. Один модуль создаёт основную долю пиковой нагрузки? ... выделяем его в сервис
# 3. Команды конфликтуют в одной ветке? ... границы функций готовы
# Если 3 раза "нет" — пилить рано.9. Деплой и оркестрация
Сервисы живут в контейнерах: один контейнер — один процесс, конфигурация через переменные окружения, а не через код. Стартовый вариант — Docker Compose:
services:
gateway:
image: gateway:1.2.0
ports: ["80:80"]
auth:
image: auth:1.0.3
environment:
AUTH_DB_URL: postgres://auth:pass@db/authДля продакшена поверх контейнеров строят оркестрацию — Kubernetes берёт на себя масштабирование, обновления и восстановление после сбоев.
- Реестры образов (Registry) хранят артефакты с тегами версий.
- Health-пробы определяют, когда сервис готов принимать трафик.
- Конфигурация выносится в ConfigMap/Secret, а не в образ.
10. Консистентность и saga
Классическая транзакция не живёт поверх нескольких баз. В распределённой системе используют компенсирующие паттерны:
- Saga — серия локальных транзакций с компенсациями при отказе шага.
- Event sourcing — события становятся источником истины, состояние воспроизводится из них.
- 2PC — двухфазный коммит; теоретически надёжен, но блокировки и неудачи координатора делают его редким гостем.
Для заказа с оплатой и складом шаги Saga: «зарезервировать товар → списать деньги → отгрузить», а при падении — компенсация: «вернуть деньги → снять резерв».
11. Тестирование микросервисов
Интеграция сервисов проверяется на уровне контрактов. Consumer-driven контракты (Pact) фиксируют, что отправитель ожидает от получателя:
{
"provider": "auth",
"consumer": "api-gateway",
"interactions": [
{ "description": "get user by token",
"request": { "method": "GET", "path": "/user" },
"response": { "status": 200, "body": { "id": 1 } } }
]
}Тестовую пирамиду логично строить так:
- Юнит-тесты внутри сервиса — самый быстрый слой.
- Контрактные тесты проверяют формат общения без поднятия всех окружений.
- E2E-тесты — редкие, но незаменимые на критических путях.
12. Изоляция сбоев
Сеть и сервисы ненадёжны, поэтому каждый вызов обязан быть ограничен. Четыре базовых паттерна устойчивости:
| Паттерн | Что даёт |
|---|---|
| Timeout | запрос не висит вечно, ошибка приходит быстро |
| Retry | повтор с backoff спасает от транзиентных сбоев |
| Circuit breaker | отключает поток к падающему сервису и даёт ему восстановиться |
| Bulkhead | изолирует пулы потоков и памяти одного вызова |
Каскадные гонки из-за одного медленного сервиса — самая частная авария: таймауты и breaker сокращают время реагирования до секунд.
13. Версионирование API
Микросервисы живут дольше, чем клиенты, которые их используют. Если изменять контракт без плана, один из потребителей сломается. Поэтому API версионируется осмысленно:
GET /v1/users # первая версия контракта
GET /v2/users?active # вторая — расширение- URL-версионирование простое и заметное:
/v1/…. - Header-версионирование (например,
Accept: application/vnd.api+json;version=2) не меняет URL, но прямее для внутренних вызовов. - Обратная совместимость: добавление полей — ок, удаление — нет. Мягкое deprecation-сообщение лучше внезапного 404.
14. Безопасность между сервисами
Внутренний трафик тоже атакуем: попасть в сеть проще, чем кажется. Три уровня защиты — транспорт, идентичность, авторизация:
mTLS: каждый сервер проверяет сертификат клиента
OAuth 2.0: сервис-клиенты получают токены
RBAC/Vault: доступ к секретам по ролям- mTLS гарантирует, что говорит именно ваш сервис, а не чужак в сети.
- OAuth 2.0 / OpenID Connect — единый вход для пользователя и маршрутизация токена между сервисами.
- Секреты (пароли, ключи API) хранят в Vault — не в конфиг-файлах и не в репозитории.
15. Продакшен-чеклист запуска
Запуск сервиса в бой — событие, а не случайность. До релиза пройдись по контрольному списку, чтобы прод не стал «первым тестом»:
health: GET /health # жив ли процесс
ready: GET /ready # готов принимать трафик
metrics: /metrics # метрики для Prometheus- Health + ready раздельно: контейнер может быть жив, но не готов (миграция БД, прогрев кэша).
- Graceful shutdown: сигнал SIGTERM — доделать текущие запросы, закрыть соединения, отдать 503 на новые.
- Ретраи и backoff наружу настроены, таймауты вниз — меньше секунды на каждый вызов.
- Логи в stdout в структурированном виде: JSON с
trace_idи уровнем.
16. Миграция монолита: стратегия вырезания
Переход с монолита на микросервисы «разом» почти всегда проваливается. Набирает очки другой путь — Strangler шаблон: функциональность вырезается кусками, пока монолит не «умирает» постепенно:
1. Выделить домен (DDD) для первого сервиса
2. За фасадом временный роутинг: часть запросов — новый сервис
3. Накопить уверенность, выключить старый путь- Роутер/шлюз направляет выбранные маршруты в новый сервис, остальное — в монолит.
- Один сервис за раз: риски локализуются, а регресс понятен.
- Целевая архитектура строится от границ доменов (bounded contexts) Domain-Driven Design: не «всё разбить на микросервисы», а «разделить по границам домена».
Что дальше
Ты разобрался в основах микросервисной архитектуры: паттерны, API Gateway, дискавери, очереди, observability и критерии «за» и «против». Практический следующий шаг — собрать два сервиса в Docker с общим gateway на Nginx, а затем попробовать оркестрацию в Kubernetes. Темы очередей и метрик раскрыты в уроках Kafka и Prometheus.