АрхитектураНадёжность и производительностьИнженер по надёжности платформы

Сервис начал отвечать медленно под нагрузкой; после добавления автоматических повторных запросов доля ошибо...

Сервис начал отвечать медленно под нагрузкой; после добавления автоматических повторных запросов доля ошибок выросла. Объясните механизм этого эффекта.

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Автоматические повторы могут усилить перегрузку: один исходный запрос превращается в несколько попыток, которые дополнительно занимают потоки, соединения, CPU и ресурсы зависимостей. Если повторы выполняются без ограничения, задержки и тайм-ауты растут, из-за чего запускается ещё больше повторов — возникает положительная обратная связь, известная как retry storm.

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

Исторический контекст

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

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

Постановка проблемы

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

При повторах на нескольких уровнях эффект перемножается. Например, один внешний запрос может породить несколько попыток на уровне клиента, а каждая из них — несколько попыток в библиотеке доступа к зависимости. В результате кратковременная перегрузка превращается в каскадный отказ.

Неверная политика особенно опасна для операций с побочными эффектами. Если сервер выполнил действие, но ответ потерялся, повтор может создать дубликат платежа, заказа или сообщения.

Подробное решение

Повтор должен иметь ограниченный бюджет: максимум попыток и общий deadline операции. Общий deadline важнее отдельного числа попыток, потому что бесконечные или слишком долгие повторы ухудшают пользовательскую задержку и удерживают ресурсы.

Между попытками применяют экспоненциальную задержку: последующие попытки выполняются не сразу. К задержке добавляют случайный разброс, или jitter, чтобы тысячи клиентов не повторяли запрос одновременно после общего сбоя.

Повторять стоит только ошибки, которые классифицированы как временные, например кратковременную недоступность или перегрузку, если контракт сервиса допускает такую реакцию. Ошибки валидации, запрета доступа и некорректного запроса повтором обычно не исправляются.

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

Для операций с побочными эффектами применяют идемпотентность: повтор того же логического запроса не должен создавать новое действие. Этого добиваются, например, уникальным ключом операции и сохранением результата первой обработки на стороне сервиса.

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

Ситуация из практики

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

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

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

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

Что кандидаты часто упускают

  1. Почему увеличение числа повторов не всегда повышает доступность сервиса?

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

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

  2. Как случайный разброс задержки между повторами предотвращает синхронную перегрузку?

    Если все клиенты получили ошибку одновременно и ждут одинаковое время, они почти одновременно отправят следующую волну запросов. Это создаёт периодические пики нагрузки даже после восстановления зависимости.

    Jitter случайно распределяет моменты повторов по интервалу. Средняя нагрузка может не измениться, но пиковая синхронная нагрузка уменьшается, поэтому зависимость получает возможность обрабатывать запросы более равномерно. Jitter не заменяет ограничение числа попыток и deadline: он лишь устраняет синхронизацию.

  3. Почему тайм-аут ответа не доказывает, что операция не была выполнена?

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

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