Прямой ответ
API-композиция собирает ответ из нескольких сервисов во время запроса. Композитор отправляет запросы владельцам данных, ждёт ответы и объединяет их в один результат. Это самый простой способ получить связанные данные после разделения базы по сервисам.
Три обязательных элемента корректной реализации:
- Параллельные запросы. Последовательные вызовы складывают задержки; параллельные дают время самого медленного.
- Таймаут на каждый вызов. Без него один медленный сервис определяет время ответа всего экрана.
- Частичный результат. Отказ одного источника не должен обесценивать остальные данные.
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.
Рекомендации:
- Начинайте с параллельных запросов. Это самая дешёвая оптимизация: время ответа определяется самым медленным вызовом, а не суммой.
- Проектируйте ответ с учётом отказа. Поля, которые могут отсутствовать, должны быть опциональными в контракте с первого дня.
- Логируйте время каждого вызова. Без этого невозможно понять, какой сервис определяет задержку.
- Проверяйте согласованность идентификаторов. Композиция по внешнему ключу ломается, если владельцы используют разные форматы идентификаторов.
- Не композируйте на запись. Агрегация подходит для чтения; запись должна идти напрямую владельцу данных.
Практический сценарий: экран заказа отвечал три секунды и падал целиком
Контекст: карточка заказа собиралась последовательными вызовами четырёх сервисов. При недоступности сервиса доставки экран показывал ошибку, хотя остальные данные были получены.
- Диагностика. Замеры показали: заказы — 120 мс, оплата — 90 мс, доставка — 2600 мс (таймаут не был задан), рекомендации — 180 мс. Сумма составляла около трёх секунд.
- Вторая проблема. Сбой доставки приводил к исключению, которое прерывало обработку, и клиент получал 500 без каких-либо данных.
- Третья проблема. Список заказов на странице содержал 20 позиций, и для каждой отдельно запрашивалась оплата — 20 дополнительных сетевых вызовов.
- Действия. (а) Перевели вызовы на параллельное выполнение с
return_exceptions=True; (б) задали таймаут 400 мс на каждый вызов; (в) разделили источники на обязательные (заказ) и необязательные (доставка, рекомендации), добавив полеpartial; (г) добавили пакетный метод получения оплаты по списку заказов; (д) ввели метрику доли частичных ответов и алерт при превышении пяти процентов. - Результат. Время ответа сократилось до 450 миллисекунд; при недоступности доставки экран показывает заказ и оплату с пометкой о неполноте вместо ошибки.
Вывод: композиция сама по себе не является проблемой — проблемы создают последовательные вызовы, отсутствие таймаутов и обработка частичного отказа как полной ошибки.
FAQ
Чем композиция отличается от локальной копии данных? Композиция запрашивает данные во время обращения, копия хранит их локально и обновляется асинхронно. Композиция даёт свежие данные ценой задержки, копия — быстрый ответ ценой устаревания.
Что делать, если сервисов больше десяти? Композиция из десяти вызовов ненадёжна: вероятность отказа хотя бы одного высока. Нужна локальная проекция либо пересмотр границ сервисов.
Нужен ли отдельный сервис для агрегации? Если композиция используется несколькими клиентами — да. Для одного клиента достаточно BFF или даже логики в клиенте.
Как обеспечить согласованность собранного ответа? Строго — никак: данные получены в разные моменты. Практически — минимизировать окно, выполняя запросы параллельно, и показывать время формирования данных, если свежесть критична.
Источники: microservices.io (API Composition), Python asyncio docs — ссылки в шапке.