Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Композиция API: агрегация ответов нескольких сервисов

Композиция API: как собрать ответ из нескольких сервисов

API-композиция: параллельные запросы, таймауты и частичный результат, где размещать композитор и когда вместо него нужна локальная копия.

Прямой ответ

API-композиция собирает ответ из нескольких сервисов во время запроса. Композитор отправляет запросы владельцам данных, ждёт ответы и объединяет их в один результат. Это самый простой способ получить связанные данные после разделения базы по сервисам.

Три обязательных элемента корректной реализации:

  1. Параллельные запросы. Последовательные вызовы складывают задержки; параллельные дают время самого медленного.
  2. Таймаут на каждый вызов. Без него один медленный сервис определяет время ответа всего экрана.
  3. Частичный результат. Отказ одного источника не должен обесценивать остальные данные.
import asyncio

@router.get("/orders/{order_id}/card")
async def order_card(order_id: str):
    results = await asyncio.gather(
        orders.get(order_id),
        payments.get_by_order(order_id),
        delivery.get_by_order(order_id),
        return_exceptions=True,          # сбой не прерывает остальные
    )
    order, payment, shipment = results

    if isinstance(order, Exception):
        raise HTTPException(502, "Основной сервис недоступен")

    return {
        "order": order,
        "payment": None if isinstance(payment, Exception) else payment,
        "delivery": None if isinstance(shipment, Exception) else shipment,
        "partial": isinstance(payment, Exception) or isinstance(shipment, Exception),
    }

Флаг return_exceptions=True принципиален: без него первое исключение отменяет остальные задачи, и экран не показывает даже те данные, которые получены.

Поле partial сообщает клиенту, что часть блоков не заполнена. Клиент решает, показать заглушку или запросить обновление.

Где размещать композитор:

Место Когда подходит
Клиент Мало источников, быстрый канал, полный контроль у клиента
BFF Ответ нужен под конкретный экран; см. статью про BFF
Отдельный сервис агрегации Композиция используется несколькими клиентами
Локальная копия по событиям Данные читаются часто, задержка согласованности допустима

Выбор зависит от частоты чтения и допустимой задержки. Композиция во время запроса проще, но платит сетевой задержкой при каждом обращении.

Практика: надёжная агрегация

1. Не допускайте N+1 между сервисами. Запрос списка заказов и последующий цикл с запросом оплаты по каждому — это та же проблема, что и N+1 в ORM, но с сетевой задержкой. Нужен пакетный метод: payments.get_by_orders([ids]). О проблеме в рамках одной базы — в статье про N+1.

2. Задавайте таймаут короче общего. Если экран обязан ответить за две секунды, таймаут отдельного вызова — 500 миллисекунд, иначе один медленный сервис исчерпает бюджет.

3. Ограничивайте число повторных попыток. Повтор внутри композитора умножается на повторы клиента и шлюза; о настройке — в статье про retry и backoff.

4. Различайте обязательные и необязательные источники. Отказ источника с основными данными — ошибка; отказ блока рекомендаций — пустой блок.

5. Кэшируйте с явным сроком. Кэш сокращает число вызовов, но добавляет устаревание. Срок выбирают исходя из того, насколько критична свежесть.

6. Измеряйте долю частичных ответов. Метрика показывает, как часто пользователи видят неполные данные; рост доли — сигнал о деградации одного из сервисов.

7. Используйте копию для частых чтений. Если композиция выполняется на каждый запрос и данные меняются редко, локальная проекция, обновляемая событиями, дешевле — о подходе в статье про CQRS.

Рекомендации:

  1. Начинайте с параллельных запросов. Это самая дешёвая оптимизация: время ответа определяется самым медленным вызовом, а не суммой.
  2. Проектируйте ответ с учётом отказа. Поля, которые могут отсутствовать, должны быть опциональными в контракте с первого дня.
  3. Логируйте время каждого вызова. Без этого невозможно понять, какой сервис определяет задержку.
  4. Проверяйте согласованность идентификаторов. Композиция по внешнему ключу ломается, если владельцы используют разные форматы идентификаторов.
  5. Не композируйте на запись. Агрегация подходит для чтения; запись должна идти напрямую владельцу данных.

Практический сценарий: экран заказа отвечал три секунды и падал целиком

Контекст: карточка заказа собиралась последовательными вызовами четырёх сервисов. При недоступности сервиса доставки экран показывал ошибку, хотя остальные данные были получены.

  1. Диагностика. Замеры показали: заказы — 120 мс, оплата — 90 мс, доставка — 2600 мс (таймаут не был задан), рекомендации — 180 мс. Сумма составляла около трёх секунд.
  2. Вторая проблема. Сбой доставки приводил к исключению, которое прерывало обработку, и клиент получал 500 без каких-либо данных.
  3. Третья проблема. Список заказов на странице содержал 20 позиций, и для каждой отдельно запрашивалась оплата — 20 дополнительных сетевых вызовов.
  4. Действия. (а) Перевели вызовы на параллельное выполнение с return_exceptions=True; (б) задали таймаут 400 мс на каждый вызов; (в) разделили источники на обязательные (заказ) и необязательные (доставка, рекомендации), добавив поле partial; (г) добавили пакетный метод получения оплаты по списку заказов; (д) ввели метрику доли частичных ответов и алерт при превышении пяти процентов.
  5. Результат. Время ответа сократилось до 450 миллисекунд; при недоступности доставки экран показывает заказ и оплату с пометкой о неполноте вместо ошибки.

Вывод: композиция сама по себе не является проблемой — проблемы создают последовательные вызовы, отсутствие таймаутов и обработка частичного отказа как полной ошибки.

FAQ

Чем композиция отличается от локальной копии данных? Композиция запрашивает данные во время обращения, копия хранит их локально и обновляется асинхронно. Композиция даёт свежие данные ценой задержки, копия — быстрый ответ ценой устаревания.

Что делать, если сервисов больше десяти? Композиция из десяти вызовов ненадёжна: вероятность отказа хотя бы одного высока. Нужна локальная проекция либо пересмотр границ сервисов.

Нужен ли отдельный сервис для агрегации? Если композиция используется несколькими клиентами — да. Для одного клиента достаточно BFF или даже логики в клиенте.

Как обеспечить согласованность собранного ответа? Строго — никак: данные получены в разные моменты. Практически — минимизировать окно, выполняя запросы параллельно, и показывать время формирования данных, если свежесть критична.

Источники: microservices.io (API Composition), Python asyncio docs — ссылки в шапке.