ONE-PAGER

All playbooks

One-pager

gRPC Deadlines and Retry Budgets Across a Service Chain

A one-page model for passing a shared deadline, keeping time for the caller, and capping retries without cascading load.

The full text is in Russian.

GogRPCDistributed SystemsHighloadObservability

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

Памятка относится к синхронной цепочке gRPC на Go. Она не обещает безопасный повтор изменяющей операции: DEADLINE_EXCEEDED и UNAVAILABLE не доказывают, что эффект не состоялся — эта граница явно указана в описании кодов gRPC. Для такого метода отдельно нужен идемпотентный контракт или выяснение неизвестного исхода.

Рабочая модель

Верхняя граница задаётся один раз на входе из бюджета пользовательской операции. Каждый сервис передаёт оставшийся deadline зависимости, но сохраняет резерв на обработку ответа и завершение собственного шага.

flowchart LR
    C[Клиент: общий deadline D] --> A[Сервис A]
    A -->|остаток минус резерв A| B[Сервис B]
    B -->|остаток минус резерв B| P[(PostgreSQL или сервис C)]
    A -. один владелец повторов .-> R[Общий бюджет повторов]
    B -. попытки и паузы расходуют тот же deadline .-> R
    P -->|ответ до локальной границы| B
    B -->|время на сборку ответа| A
    A -->|время на сериализацию и сеть| C

В gRPC deadline по умолчанию отсутствует, поэтому клиент должен задавать реалистичную границу явно. Go автоматически распространяет входящий deadline в следующий gRPC-вызов, если обработчик передаёт полученный context.Context; gRPC пересылает оставшийся тайм-аут с учётом уже прошедшего времени и тем самым не полагается на одинаковые часы узлов. Это граница реализации из официального руководства gRPC, а не повод использовать исходный контекст без проверки остатка.

Рабочая формула для дочернего вызова:

B_child = min(time.Until(D_parent) − reserve_parent, cap_child)

Если остатка не хватает на полезную работу и возврат ответа, сервис отказывает до захвата дорогого ресурса. reserve_parent и cap_child берут из распределения задержки и проверяют под нагрузкой, а не копируют между методами. В Go дочерний контекст создают от родительского через context.WithDeadline или context.WithTimeout и обязательно вызывают cancel; пакет context прямо требует распространять отмену по цепочке.

Отмена сообщает, что результат больше не ждут, но сама не откатывает уже выполненную запись и не останавливает произвольную горутину. Серверный код обязан проверять ctx.Done() между этапами и передавать контекст в запросы к базе и зависимостям. Для необратимого эффекта решение о продолжении задаёт предметный протокол, а не факт закрытого RPC.

Как распределять бюджет повторов

У попыток один общий deadline. В настроенной политике повторов gRPC параметр maxAttempts включает исходную попытку, пауза между попытками расходует то же время, а новая попытка запускается только для перечисленных кодов. После получения заголовков ответа вызов считается зафиксированным для механизма повторов, и библиотека больше его не повторяет. Низкоуровневые прозрачные повторы возможны и без политики только тогда, когда gRPC знает, что обработчик сервера запрос не видел. Эти правила описаны в gRPC Retry и gRFC A6.

Практические правила:

  • повтором владеет один уровень цепочки; независимые повторы на каждом уровне перемножают обращения к нижнему сервису;
  • повтор разрешён только для конкретного метода, кода и доказанной семантики операции;
  • до новой попытки должны помещаться пауза, сама попытка и резерв вызывающего сервиса;
  • помимо лимита на логический вызов нужен агрегатный предел дополнительных попыток, иначе массовый сбой превращает клиентов в генератор нагрузки.

Встроенный retryThrottling gRPC использует токены успешных вызовов и ошибок, разрешённых политикой повторов, на имя сервера. Это полезный локальный предохранитель, но не глобальная квота кластера: разные клиентские процессы не делят один счётчик. Для общего ограничения могут понадобиться отсечение нагрузки или собственный бюджет, выраженный как допустимая доля дополнительных попыток. Google SRE отдельно рекомендует ограничивать повторы на запрос и на процесс и не повторять на нескольких уровнях (разбор каскадных отказов).

Типичные поломки

Каждый сервис назначает новый тайм-аут

Сервис B создаёт context.Background() и получает полный локальный тайм-аут, хотя клиентский deadline почти истёк. Зависимость продолжает бесполезную работу после ухода клиента. Дочерний контекст должен наследовать родительский; локальная граница может только сократить остаток.

Все ошибки считаются временными

Повтор INVALID_ARGUMENT не изменит запрос. Повтор RESOURCE_EXHAUSTED без паузы и ограничения способен усилить перегрузку. Даже UNAVAILABLE разрешает транспортную попытку только после проверки идемпотентности: сервер мог выполнить эффект и потерять ответ.

Последняя попытка съедает резерв

Клиент успевает получить ответ зависимости ровно к общему deadline, но не успевает преобразовать его и отправить наверх. Успех нижнего вызова превращается в ошибку всей операции. Проверка должна учитывать не только time.Until(deadline), но и стоимость оставшегося локального пути.

Метрики считают только логические вызовы

Один успешный RPC может скрывать несколько попыток. Нужны отдельно логические вызовы, grpc.client.attempt.started, длительность попыток, число предыдущих попыток, паузы, остаток deadline на входе, ранние отказы и итоговый код. Идентификаторы запросов остаются в трассировке, а не в метках.

Вопросы для ревью

  • Где задаётся верхний deadline и чем подтверждено его значение?
  • Какой резерв оставляет каждый узел после вызова зависимости?
  • Кто единственный владелец повторов в цепочке?
  • Какие коды повторяются и почему операция выдерживает неизвестный исход?
  • Сколько дополнительных попыток допускается при массовом отказе, а не для одного запроса?
  • Останавливаются ли база, горутины и следующие RPC после отмены контекста?

Когда подход вреден

Жёсткое дробление deadline вредно для фоновой работы, которая не обязана завершаться в пользовательском RPC. Такой процесс лучше принять как отдельную операцию с долговечным состоянием и вернуть идентификатор. Автоматические повторы вредны для неидемпотентных изменений, длительных потоков и перегруженной зависимости без серверной команды отложить или запретить повтор (pushback) либо отсечения нагрузки. Если команда не измеряет попытки отдельно от вызовов, сложная политика повторов лишь скрывает дополнительную работу.