В интеграции с поставщиком настроили повторную отправку документов. Если запрос не прошёл, приложение пробует ещё раз. Пока ошибки редкие, это удобно: сотрудник даже не замечает короткого сбоя. Но однажды поставщик начинает отвечать медленнее, и почти каждый исходный запрос порождает дополнительные. Механизм восстановления сам становится заметной частью нагрузки.
У повторов есть две разные задачи: довести конкретную операцию до результата и не мешать системе восстановиться. Настройка «попробовать несколько раз» решает первую лишь при определённых условиях. Вторую легко испортить, даже если все запросы совершенно легитимны.
Сначала нужно понять, что изменится к следующей попытке
Если соединение временно недоступно, новая попытка может попасть уже на работающий сервис. Если документ отклонён из-за отсутствующего обязательного поля, ожидание ничего не исправит. Отказ в доступе тоже требует изменения условий, а не бесконечного повторения прежнего запроса.
Причину нельзя надёжно вывести из одной общей надписи «ошибка сервера». Нужен контракт конкретного API: какие ответы обозначают временный отказ, какие — окончательный, как запрашивать состояние операции и какие ограничения действуют после перегрузки. Поставщик может рекомендовать время следующего обращения, например через Retry-After. Игнорировать такой сигнал ради собственного короткого интервала бессмысленно.
Повтор также должен оставаться полезным. Если клиент уже получил окончательный отказ, скрытая отправка документа позднее может удивить его не меньше, чем потеря запроса. Для фонового процесса допустим более длинный период восстановления, но с явным состоянием и правилами остановки.
Ответ потерялся, а документ уже принят
Таймаут не сообщает, на какой стороне оборвалось взаимодействие. Поставщик мог зарегистрировать документ, а подтверждение не дошло. Повторная отправка как новой операции тогда создаст второй документ.
Для таких действий нужен устойчивый идентификатор намерения и согласованное поведение при повторе. Один и тот же ключ используется при новых попытках той же операции; он не должен меняться после каждого сетевого сбоя. При неизвестном результате иногда правильнее сначала запросить состояние, а не немедленно отправлять всё заново.
Это задача идемпотентности. Пауза между попытками не решает её. Даже идеально распределённые по времени запросы могут многократно выполнить одно действие, если получатель не распознаёт повтор. У внешнего API нужно проверять не только поддержку ключа, но и срок его хранения, область уникальности и поведение при изменённых параметрах.
Почему одинаковые паузы создают новую очередь
Backoff означает, что после ошибки клиент ждёт, прежде чем повторять запрос. При экспоненциальной схеме пауза растёт с каждой неудачей до установленного предела. Это даёт зависимости время восстановиться и снижает частоту обращений от одного клиента.
Однако все экземпляры приложения могут получить сбой одновременно и одинаково отсчитать паузу. Следующая волна придёт почти так же дружно, как предыдущая. Jitter добавляет случайный разброс в ожидание, чтобы попытки распределились во времени. Способ расчёта бывает разным; важно, чтобы разброс действительно разводил запросы, а не исчезал после достижения максимальной паузы.
Ни backoff, ни jitter не уменьшают число клиентов, которым нужна услуга. Если исходная нагрузка всё равно превышает возможности поставщика, одних задержек мало. Потребуется ограничивать повторный трафик и объём принимаемой работы, а часть операций откладывать или завершать отказом.
Кто ещё повторяет этот же запрос?
Повторы могут быть включены в клиентской библиотеке, шлюзе, сервисе и обработчике очереди. Каждый слой выглядит разумно в отдельности. Вместе они способны умножить обращения к последней зависимости.
Для наглядности: если три последовательных слоя разрешают до трёх попыток каждый, включая исходную, нижний сервис в худшем случае получит до 27 вызовов на одно верхнее обращение. Это условный расчёт при повторяемом отказе на всех уровнях, а не обычная цена любого запроса. Он показывает, зачем проверять всю цепочку, включая настройки библиотек по умолчанию.
Стоит определить, какой слой отвечает за повтор бизнес-операции, и ограничить самостоятельные повторы остальных. Все попытки и паузы должны укладываться в общий срок выполнения. Если времени уже не хватает даже на вероятный успешный ответ, запускать очередную попытку незачем.
Лимит на операцию не защищает от общей перегрузки
Даже небольшой предел попыток может дать большой поток, если одновременно ошибаются многие клиенты. Поэтому отдельно полезен общий бюджет повторов: сколько дополнительной работы приложение разрешает себе отправить в эту зависимость за период. После исчерпания бюджета часть запросов завершается без очередной попытки, сохраняя ресурсы для новых обращений и восстановления.
Для длительной фоновой обработки пауза не должна удерживать соединение с базой или транзакцию. Состояние работы можно сохранить и запланировать следующую попытку отдельно. При этом нужны срок актуальности задания, защита от дублей и разбор операций, которые не удалось завершить. Перенос в очередь не даёт разрешения пробовать вечно.
Если зависимость остаётся неработоспособной, Circuit Breaker может временно прекратить обычные вызовы. Цикл повторов должен учитывать этот отказ, иначе одна часть приложения пытается защитить сервис, а другая упорно обходит защиту.
Как увидеть пользу, а не только количество попыток
Сравнивайте число исходных операций, число дополнительных обращений и итоговые успешные результаты. Система, которая скрывает растущий процент первых отказов за повторами, может выглядеть здоровой вплоть до серьёзного инцидента. Нужны также полное время операции, случаи дублирования и возраст незавершённой фоновой работы.
Проверять стоит и короткий сбой, и длительную недоступность, и потерю подтверждения после успешного приёма документа. Затем восстановить поставщика и убедиться, что накопленная очередь не отправляется к нему одним залпом. Именно поведение после восстановления показывает, помогает ли выбранная политика выйти из аварии.