Паттерны микросервисов

Когда монолит распадается на десятки сервисов, появляются новые проблемы — и набор паттернов, которые их решают.

Паттерны микросервисов — это типовые решения задач, возникающих, когда приложение разбито на множество независимо развёртываемых сервисов: как клиенту обращаться к ним (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 — реестр-«телефонная книга», где сервисы регистрируются, а клиенты находят их актуальные адреса в эфемерной среде.
  • Общий корень всех паттернов — ненадёжная сеть и проектирование поведения при частичных отказах.
Проверьте себя
1. Зачем нужны компенсирующие действия в паттерне Saga?
AЧтобы откатить уже выполненные локальные транзакции, когда один из последующих шагов распределённой операции завершился неудачей
BЧтобы ускорить выполнение всех шагов саги
CЧтобы зашифровать данные между сервисами
DЧтобы заменить API Gateway
2. Какую проблему решает Circuit Breaker?
AКаскадный отказ: он перестаёт слать запросы к уже упавшему сервису, чтобы зависающие вызовы не исчерпали ресурсы вызывающего и не уронили его тоже
BПоиск адресов сервисов в облаке
CАгрегацию данных под мобильный клиент
DШифрование трафика между клиентом и сервером
3. Чем BFF (Backend for Frontend) отличается от обычного API Gateway?
ABFF делают отдельным под каждый тип клиента и он агрегирует данные под нужды именно этого фронта, тогда как Gateway — один общий вход с маршрутизацией
BBFF работает только с базами данных, а Gateway — только с кэшем
CBFF полностью заменяет все микросервисы одним приложением
DМежду ними нет никакой разницы, это синонимы