Паттерны микросервисов
Когда монолит распадается на десятки сервисов, появляются новые проблемы — и набор паттернов, которые их решают.
Паттерны микросервисов — это типовые решения задач, возникающих, когда приложение разбито на множество независимо развёртываемых сервисов: как клиенту обращаться к ним (API Gateway, BFF), как провести транзакцию через несколько сервисов (Saga), как пережить отказ соседа (Circuit Breaker) и как сервисам находить друг друга (Service Discovery).
Слоистые архитектуры из прошлого урока наводят порядок внутри одного процесса. Микросервисы режут систему на отдельные процессы, общающиеся по сети, — и это рождает класс проблем, которых в монолите не было: сеть ненадёжна, у каждого сервиса своя база, а вызов метода превращается в сетевой запрос, который может зависнуть. Перечисленные паттерны — это коллективный опыт индустрии по укрощению распределённости. Здесь мы разбираем их концептуально: важно понять, какую боль каждый лечит.
API Gateway: единая дверь
Если у вас 20 сервисов, заставлять мобильное приложение знать адреса всех двадцати — катастрофа: клиент завязан на внутреннюю топологию, а аутентификацию и лимиты пришлось бы дублировать в каждом сервисе. API Gateway — единая точка входа: клиент стучится только к нему, а шлюз маршрутизирует запрос нужному сервису и берёт на себя сквозные задачи.
Клиент ──> [API Gateway] ──> Users Service
│ └─────> Orders Service
│ └─────> Catalog Service
│
делает по пути: аутентификация, rate limiting,
TLS-терминация, логирование, маршрутизация
Плюсы: клиент знает один адрес; сквозная логика (авторизация, ограничение частоты) — в одном месте; внутреннюю структуру можно перекраивать, не трогая клиентов. Риск — шлюз становится узким местом и единой точкой отказа, поэтому его делают тонким (без бизнес-логики!) и масштабируют горизонтально.
BFF: бэкенд под каждый фронт
У мобильного приложения и веб-сайта разные потребности: мобильному нужен компактный ответ ради экономии трафика, вебу — побольше данных за один запрос. Паттерн Backend for Frontend предлагает отдельный шлюз-агрегатор под каждый тип клиента. BFF не просто проксирует — он агрегирует: собирает данные из нескольких сервисов в одну удобную клиенту форму.
| Паттерн | Сколько входов | Главная задача |
| API Gateway | один общий | маршрутизация + сквозные задачи |
| BFF | по одному на тип клиента | агрегация под нужды конкретного фронта |
BFF — это, по сути, специализированный gateway. Команда мобильного фронта владеет своим BFF и подгоняет ответы под себя, не мешая веб-команде.
Saga: транзакция через сервисы
В монолите перевод денег — одна транзакция БД: либо всё, либо ничего. Но когда «склад» и «оплата» — это разные сервисы с разными базами, общей транзакции нет. Saga заменяет её последовательностью локальных транзакций: каждый шаг фиксируется в своём сервисе, а если какой-то шаг падает, выполняются компенсирующие действия, откатывающие уже сделанное.
def reserve_stock(order):
print(" склад: товар зарезервирован")
return True
def charge_payment(order):
print(" оплата: СБОЙ платежа")
raise RuntimeError("карта отклонена")
def compensate_stock(order):
print(" склад (компенсация): резерв снят")
def run_saga(order):
done = []
steps = [
("stock", reserve_stock, compensate_stock),
("payment", charge_payment, None),
]
try:
for name, action, _ in steps:
action(order)
done.append(name)
print("Saga: заказ оформлен")
except Exception as err:
print(f"Saga: ошибка на шаге -> откат ({err})")
for name, action, compensate in reversed(steps):
if name in done and compensate:
compensate(order)
run_saga({"id": 42})
Вывод:
склад: товар зарезервирован оплата: СБОЙ платежа Saga: ошибка на шаге -> откат (карта отклонена) склад (компенсация): резерв снят
Резерв на складе прошёл, а оплата сорвалась — Saga вызывает компенсацию и снимает резерв, возвращая систему в согласованное состояние. Существуют два стиля: оркестрация (центральный координатор дирижирует шагами, как в примере) и хореография (сервисы реагируют на события друг друга без дирижёра). Важное следствие: Saga даёт не мгновенную, а итоговую согласованность — между шагами система временно «недосогласована».
Circuit Breaker: предохранитель
Если сервис оплаты лёг, а сервис заказов продолжает слать к нему запросы, каждый из которых висит до таймаута, — потоки заканчиваются, и падает уже сервис заказов. Сбой расползается каскадом. Circuit Breaker («предохранитель») считает ошибки и после порога «размыкается»: на время перестаёт пускать запросы к больному сервису, мгновенно отвечая отказом, — давая тому шанс восстановиться.
class CircuitBreaker:
def __init__(self, threshold=3):
self.threshold = threshold # сколько подряд сбоев до размыкания
self.failures = 0
self.state = "CLOSED" # CLOSED -> работаем, OPEN -> не пускаем
def call(self, func):
if self.state == "OPEN":
return "отказ сразу (предохранитель разомкнут)"
try:
result = func()
self.failures = 0 # успех сбрасывает счётчик
return result
except Exception:
self.failures += 1
if self.failures >= self.threshold:
self.state = "OPEN"
return "ошибка сервиса"
def broken_service():
raise RuntimeError("сервис недоступен")
breaker = CircuitBreaker(threshold=3)
for attempt in range(1, 6):
answer = breaker.call(broken_service)
print(f"Попытка {attempt}: state={breaker.state} -> {answer}")
Вывод:
Попытка 1: state=CLOSED -> ошибка сервиса Попытка 2: state=CLOSED -> ошибка сервиса Попытка 3: state=OPEN -> ошибка сервиса Попытка 4: state=OPEN -> отказ сразу (предохранитель разомкнут) Попытка 5: state=OPEN -> отказ сразу (предохранитель разомкнут)
После трёх сбоев предохранитель разомкнулся, и попытки 4–5 отбиваются мгновенно, не нагружая упавший сервис. В реальных реализациях есть ещё состояние HALF-OPEN: спустя паузу пропускается один пробный запрос, и по его исходу breaker либо замыкается обратно, либо снова размыкается.
Service Discovery: как найти друг друга
В облаке сервисы запускаются и гаснут, их IP-адреса меняются, экземпляров может быть пять или пятьдесят. Прописывать адреса в конфиге бессмысленно. Service Discovery — это «телефонная книга»: каждый сервис при старте регистрируется в реестре (Consul, etcd, Eureka), а клиент спрашивает реестр «где сейчас orders-service?» и получает актуальный список адресов, заодно распределяя нагрузку между ними.
Как это работает под капотом
За всеми этими паттернами стоит одно фундаментальное допущение, перевёрнутое с ног на голову по сравнению с монолитом: сеть ненадёжна, а вызов — не мгновенный. В монолите вызов метода либо происходит, либо нет; в распределённой системе он может зависнуть, прийти дважды или потеряться. Отсюда вырастает каждый паттерн. Saga существует, потому что нет распределённой ACID-транзакции через разные базы — и согласованность приходится собирать вручную из локальных транзакций и компенсаций, соглашаясь на итоговую (eventual) согласованность вместо немедленной. Circuit Breaker существует, потому что синхронный вызов по сети может зависнуть и исчерпать ресурсы вызывающего — нужен механизм быстро сдаться. Service Discovery — потому что в эфемерной облачной среде адреса не статичны. API Gateway и BFF — потому что прямое обращение клиента к десяткам меняющихся сервисов хрупко. Поэтому проектирование микросервисов — это во многом проектирование поведения при частичных отказах: не «как сделать, чтобы всё работало», а «как вести себя корректно, когда часть системы недоступна». Тот, кто это усвоил, перестаёт относиться к сетевому вызову как к локальному — и в этом половина успеха.
Частые ошибки
- Распределённый монолит. Сервисы нарезали, но они синхронно дёргают друг друга цепочкой и не разворачиваются по отдельности. Получили минусы распределённости без её плюсов. Границы сервисов проводят по бизнес-возможностям, минимизируя связность.
- Двухфазный коммит вместо Saga. Попытка натянуть распределённую блокирующую транзакцию (2PC) на сервисы убивает доступность и масштабируемость. В мире микросервисов выбирают Saga и итоговую согласованность.
- Бизнес-логика в Gateway. Шлюз обрастает правилами предметной области и становится новым монолитом и узким местом. Gateway держат тонким: маршрутизация и сквозные задачи, не более.
- Retry без Circuit Breaker. Слепые повторы к упавшему сервису усиливают шторм запросов и роняют его окончательно. Повторы сочетают с предохранителем и экспоненциальной задержкой.
- Забыли про компенсации. Saga написали для «счастливого пути», а откат не продумали — при сбое система зависает в полусогласованном состоянии. Каждый шаг саги обязан иметь компенсирующее действие (или быть последним).
Итоги
- API Gateway — единая дверь в систему: маршрутизация и сквозные задачи (auth, лимиты), клиент знает один адрес.
- BFF — отдельный шлюз-агрегатор под каждый тип клиента, подгоняющий данные под нужды конкретного фронта.
- Saga — замена распределённой транзакции цепочкой локальных транзакций с компенсациями; даёт итоговую согласованность (оркестрация или хореография).
- Circuit Breaker — предохранитель, который после серии сбоев перестаёт пускать запросы к больному сервису, предотвращая каскадный отказ.
- Service Discovery — реестр-«телефонная книга», где сервисы регистрируются, а клиенты находят их актуальные адреса в эфемерной среде.
- Общий корень всех паттернов — ненадёжная сеть и проектирование поведения при частичных отказах.