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

Главная → Статьи → Таймаут-бюджет: сколько ждать на каждом шаге

Таймаут-бюджет: как распределить время ожидания по цепочке

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

Прямой ответ

Таймаут-бюджет — это общее время, выделенное на обработку запроса, распределённое между всеми шагами цепочки. Без него каждый сервис задаёт таймаут независимо, и сумма превышает ожидание клиента: клиент уже получил ошибку, а цепочка продолжает работать.

Типовая ошибка:

Уровень Таймаут
Клиент 2 с
Шлюз 5 с
Сервис A 10 с
Сервис B (вызов из A) 8 с

Клиент ждёт две секунды, сервис A — десять. Запрос, который не успел, продолжает потреблять ресурсы после того, как результат стал бесполезен.

Правильный подход — задавать бюджет на входе и передавать остаток вниз:

клиент: 2000 мс
  └─ шлюз: 1800 мс (резерв на сеть)
       └─ сервис A: 1500 мс
            ├─ сервис B: 400 мс
            ├─ база: 300 мс
            └─ кэш: 50 мс

Сумма параллельных вызовов определяется самым долгим, сумма последовательных — сложением. Резерв на каждом уровне учитывает сериализацию и сеть.

Распространение дедлайна в Python:

import asyncio, time

@app.get("/orders/{order_id}/card")
async def order_card(order_id: str, request: Request):
    deadline = request.state.deadline            # пришёл от вызывающей стороны
    remaining = deadline - time.monotonic()
    if remaining <= 0:
        raise HTTPException(504, "Бюджет времени исчерпан")

    async with asyncio.timeout(remaining):
        order = await orders.get(order_id, timeout=remaining * 0.4)
        payment = await payments.get(order_id, timeout=remaining * 0.3)
    return {"order": order, "payment": payment}

gRPC поддерживает дедлайны штатно: клиент задаёт срок, и он автоматически передаётся по всей цепочке вызовов. При исчерпании сервер прекращает работу и возвращает DEADLINE_EXCEEDED — о механизме в блоге gRPC.

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

Практика: как задавать и поддерживать бюджет

1. Начинайте от требования к пользователю. Если страница должна открыться за две секунды, бюджет API — около полутора, остальное уходит на сеть и отрисовку.

2. Оставляйте резерв на каждом уровне. Сериализация, сеть и постановка в очередь занимают время. Резерв в 10–20 % предотвращает ситуацию, когда вложенный вызов формально успел, а ответ не дошёл.

3. Проверяйте сумму последовательных вызовов. Если в сервисе три последовательных вызова по 400 миллисекунд, бюджет должен быть не меньше 1200 плюс резерв. Иначе последний вызов никогда не успеет.

4. Различайте таймаут соединения и таймаут ответа. Время на установку соединения и время на получение ответа ограничиваются отдельно; общий таймаут скрывает, какая фаза медленная.

5. Отменяйте работу при исчерпании бюджета. Если клиент ушёл, продолжение записи в базу или вычисления бесполезно. Отмена должна распространяться на дочерние задачи.

6. Не повторяйте запрос, если бюджет исчерпан. Повтор при отсутствии времени только увеличивает нагрузку; о правилах повторов — в статье про retry storm.

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

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

  1. Передавайте дедлайн, а не только таймаут. Таймаут отсчитывается с момента получения запроса каждым сервисом, поэтому накапливает задержку очереди. Абсолютный дедлайн этого недостатка лишён.
  2. Учитывайте очередь перед обработкой. Время, проведённое в очереди, расходует бюджет; запрос, ожидавший полторы секунды из двух, лучше отклонить сразу.
  3. Согласуйте таймауты в контракте. Клиент должен знать, сколько сервер будет работать, иначе политики не совпадут.
  4. Тестируйте медленные зависимости. Сценарий «зависимость отвечает на границе таймаута» выявляет накопление задержки; о приёмах — в статье про Locust.
  5. Сочетайте с частичным ответом. Если необязательный вызов не успел, лучше вернуть частичный результат, чем ошибку; о подходе — в статье про композицию API.

Практический сценарий: запрос продолжался после того, как клиент ушёл

Контекст: сервис формирования отчёта выполнялся до 25 секунд. Клиент обрывал соединение через 5 секунд, но сервер продолжал работу, нагружая базу.

  1. Диагностика. В логах базы были видны запросы, начатые после того, как клиент получил таймаут. Ресурсы расходовались на результат, который никто не получит.
  2. Причина. Таймауты задавались независимо на каждом уровне: клиент — 5 секунд, сервис — 30, запрос к базе — без ограничения. Дедлайн не передавался.
  3. Действия. (а) Установили бюджет 4 секунды на входе и передавали остаток вложенным вызовам; (б) запрос к базе получил ограничение 2 секунды и отменялся при исчерпании бюджета; (в) проверку остатка добавили перед каждым этапом, чтобы не начинать заведомо безуспешную работу; (г) формирование тяжёлых отчётов перевели в асинхронную задачу с последующим получением результата, поскольку в синхронный бюджет операция не укладывалась; (д) добавили метрику доли запросов, прерванных по бюджету.
  4. Результат. Нагрузка на базу от бесполезных запросов исчезла; тяжёлые отчёты выполняются фоново, а синхронные запросы укладываются в четыре секунды.

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

FAQ

Чем дедлайн отличается от таймаута? Таймаут — длительность ожидания, отсчитываемая с момента начала операции в каждом сервисе. Дедлайн — абсолютный момент времени, общий для всей цепочки, поэтому не накапливает задержки очередей.

Что возвращать при исчерпании бюджета? 504 Gateway Timeout для промежуточного сервиса и отмену работы для конечного. В gRPC используется код DEADLINE_EXCEEDED.

Нужно ли уменьшать бюджет при повторных попытках? Да. Повтор выполняется в пределах того же бюджета, поэтому на каждую следующую попытку остаётся меньше времени; о стратегии — в статье про circuit breaker.

Как быть с фоновыми задачами? Для них бюджет задаётся как максимальная длительность выполнения, а не как время ответа. Задача, превысившая лимит, прерывается и повторяется либо уходит в мёртвую очередь.

Источники: gRPC (deadlines), Google SRE Book, Python asyncio — ссылки в шапке.