Повторный запрос после ошибки выглядит безобидно: одна неудачная попытка заменяется ещё одной. Но когда цепочка вызовов уходит на десяток уровней вглубь и каждый уровень повторяет неудачу своего соседа, число запросов растёт по экспоненте. Разбираем, как Uber описывает механику этого усиления и что противопоставляет ему — от бюджетов повторов до передачи «владения ошибкой» вверх по цепочке вызовов.
Почему ретраи превращаются в шторм
Если на каждом хопе разрешено одинаковое число повторов R, то узел на глубине ɗ обслуживает Rɗ × Ƞ запросов, где ɗ — номер уровня в цепочке вызовов (корень — уровень 0), R — сколько раз каждый узел повторяет неудавшийся запрос, а Ƞ — базовый поток запросов, приходящий на корень. Множители перемножаются, потому что повторы каждого уровня умножаются на повторы всех уровней ниже. Это и есть усиление нагрузки при ретраяхрост числа запросов из-за того, что каждый уровень цепочки повторяет неудачи своего соседа, и вклад уровней перемножается. Ограничение вводится через retry budgetдолю запросов, которую разрешено повторять на каждом узле: при одинаковом бюджете B на каждом хопе формула принимает вид (1+B)ɗ × Ƞ.
Для бюджета 10% на глубинах 0–6 множители нагрузки выглядят так:
Ƞ 1.1 × Ƞ 1.21 × Ƞ 1.33 × Ƞ 1.33 × Ƞ 1.33 × Ƞ 1.33 × ȠРост прекращается на 1.33 × Ƞ. Если же ограничить повторы только между узлами D и C, где возникла ошибка, и запретить их на A и B, множители становятся Ƞ, Ƞ, Ƞ, 1.1 × Ƞ, 1.1 × Ƞ, 1.1 × Ƞ, 1.1 × Ƞ. Это удерживает суммарное число запросов, обслуживаемых всеми узлами от D до листового узла G, всего на 10% выше базового уровня.
Насколько это критично, показывает инцидент 18 ноября 2025 года. Проблема возникла в Core Entity service — сервисе, который находится более чем на 5 уровней вглубь цепочки вызовов и критичен для бизнес-операций. Из-за проблемы в нижележащей инфраструктуре сервис начал возвращать очень высокую долю ошибок, и ошибка быстро распространилась вверх по цепочке. Многие вышестоящие сервисы-вызыватели успели повторно отправить неудавшиеся запросы; при простых бюджетах повторов это дало бы рост трафика на деградировавший сервис на 46%–135%, продлевая сбой и снижая шансы на восстановление.
Retry Budgets: как ограничивается доля повторов
Бюджет задаётся одинаковой долей B на каждом узле цепочки, и число обслуживаемых запросов на глубине ɗ описывается формулой (1+B)ɗ × Ƞ. Что это даёт при разной доступности, видно из таблицы доступности: при базовой доступности 99.9% и доле ошибок 0.1% бюджет 10% даёт доступность 99.9999%, а при базовой доступности 70% и доле ошибок 30% — 23% ошибок и 77% доступности.
Здесь и лежит ключевой предел: при падении доступности вызываемого узла до 10% даже одного ретрая достаточно, чтобы воспринимаемая доступность у вызывающего выросла до 99%. Дальше она заметно падает, так как значительная часть запросов не повторяется из-за действующего бюджета. Расчёт при этом предполагает независимость ошибок вызываемого узла и то, что ретраи приведут к восстановлению. В реальных сценариях — перегрузка сервиса, плохие хосты БД, перегрузка БД, проблемы шардирования — вероятность повторных ошибок остаётся высокой. Это противоречит идее, что ретраи к вызываемому узлу всегда повышают воспринимаемую доступность у вызывающего, и лежит в основе концепции error ownershipпередачи ответственности за ошибку тому сервису, который действительно её породил: при высоких частотах ошибок ошибки менее случайны, и большее число ретраев не помогает, а может усугубить деградацию.
Бюджет считается на стороне клиента, потому что именно клиент решает, повторять ли запрос. В Google SRE описан per-client retry budgetбюджет повторов, который клиент считает по доле своих запросов-повторов: клиент отслеживает долю запросов, которые являются повторами, и повторяет только пока эта доля ниже 10%. Без такого бюджета число запросов может вырасти почти до 3X, а с бюджетом 10% — всего до 1.1x. Но у клиентского бюджета есть компромисс: при частичном отказе, когда перегружена лишь часть задач, он может ограничивать повторные попытки, и тогда воспринимаемая доступность падает. Кроме того, клиентский бюджет не различает повторные и новые запросы на стороне балансировщика, поэтому повтор может попасть на ту же перегруженную задачу — вероятность попасть на другую существует лишь за счёт количества участников.
Архитектура: error claims и распространение ошибок
error claimмеханизм установления владельца ошибки, при котором сервис-причина «заявляет» ошибку, а сервис-симптом её не заявляет строится на различении симптома и причины. Если сервис вызывает N исходящих вызовов и ошибка одного из них заставляет его вернуть ошибку, то возвращаемая ошибка — только симптом, а причина — входящая ошибка от нижележащего сервиса. Если же ни один исходящий вызов не завершился ошибкой, а сервис всё равно вернул ошибку, то он сам является причиной и владельцем.
Владелец определяется через Service Dependency Analysis Solution, которая коррелирует входящий сбой с исходящим и применяет набор правил для создания или отклонения error claim. Вызывающий использует отдельную логику, чтобы решить, повторять ли запрос: при получении claimed ошибки от вызываемого узла он должен ретраить, но если ретрай не удался, при передаче ошибки выше он обязан её unclaim, и тогда вышестоящий узел не должен ретраить. В продакшене схема работает в retry middleware и Service Dependency Analysis Solution, которые находятся в пути запроса пользовательских API и непрерывно claim/unclaim ошибки по мере прохождения трафика через глубокие цепочки зависимостей.
Правило «первый узел, увидевший отсутствующий error claim от downstream, снимает ошибку» работает так: узел, который видит ошибку от нижестоящего узла, помеченную как unclaimed, сам не должен повторять запрос к нему. Это ограничивает радиус поражения retry-возмущения — оно перестаёт быть штормом, — но всё же позволяет достаточное число ретраев к сервису, вернувшему ошибку. Если retry от B к C не удался, B возвращает ошибку A, но при возврате обязан снять с неё claim; A, увидев ошибку от B с заголовком unclaimed, не повторяет запрос к B. При одном retry на каждом узле это сокращает общее число запросов к узлам B–D вдвое.
Условия обработки claim задаются матрицей решений, где состояние claim отражает, кто несёт ответственность за ошибку. Состояние Claimed означает, что нижележащий сервис признал ошибку своей и способен её устранить, поэтому вызывающий повторяет запрос; состояние Unclaimed означает, что ошибка нижележащему сервису не принадлежит, поэтому вызывающий не повторяет запрос; состояние Missing означает, что claim не был выставлен — например, из-за отсутствия у нижестоящего сервиса service dependency analysis solution или из-за missing context propagation, что даёт неполную корреляцию между исходящими и входящими ошибками, — и тогда вызывающий повторяет запрос. Во всех трёх случаях при собственной ошибке вызывающий снимает claim (Unclaim). Конкретные таймауты и срок жизни claim не определены.
Пограничные случаи
Совпадающие ошибки — это легитимная серверная ошибка или fail-close ошибка зависимостиошибка зависимости, при которой сбой одного вызова заставляет сервис вернуть ошибку вверх, происходящая во время fail-open ошибки зависимостиошибки зависимости, при которой сервис продолжает работу, несмотря на сбой вызова. Решение анализа зависимостей хранит память о паттернах сбоев и снимает claim только когда входящие сбои коррелируют с исходящими в fail-close зависимостях, что устраняет риск подавления ретраев.
Для гарантии at-least-once ретраев вводится флаг, сигнализирующий, удовлетворены ли критерии ретрая для ошибки от нижележащего сервиса. Retry middleware при получении ошибки от downstream вычисляет это и передаёт в решение анализа зависимостей, которое решает, владеть ли ошибкой на входящем.
При потере внутрисервисного контекста решение не может сопоставить исходящий запрос с входящим, поэтому узел claim'ит ошибку, а retry middleware продолжает развязанные ретраи. Если разрыв произошёл в узле C, разделённые повторы продолжают происходить между C и D, а также между B и C, так как C должен присвоить возвращаемую им ошибку. Когда повторы терпят неудачу и B возвращает ошибку в A, он также возвращает заголовок unclaimed error, что предотвращает повтор из A в B. Общее число запросов к узлам B–D сокращается вдвое при конфигурации с одним повтором (1 попытка и 1 повтор) на всех узлах. Если бы слева от A было 5 узлов, в худшем случае было бы в 32 раза больше запросов без распространения владения ошибкой.
При перегрузке бэкенда клиент получает ответ «task overloaded» и решает, повторять ли запрос. Чтобы избежать лавины, применяется per-request retry budgetограничение числа попыток на один запрос: не более трёх попыток, после чего ошибка передаётся вызывающему. Дополнительно действует per-client retry budget с порогом 10%. Третий механизм: клиент передаёт в метаданных счётчик попыток, а бэкенд по гистограммам решает, возвращать ли «overloaded; don't retry» вместо «task overloaded». В архитектуре Uber error claims и retry budgets ограничивают распространение повторов: при ошибке на NodeD повтор разрешён только между NodeD и NodeC, а NodeA и NodeB не повторяют, что удерживает нагрузку на уровне 1.1×Ƞ. При этом возможны потерянные легитимные повторы: при совпадающих ошибках NodeA может ошибочно unclaim до 1.9 из 10 запросов, а в худшем случае — 0.396 из 10.
Как сравнивали и что получилось
Сравнение строится на двух наборах данных. Первый — 6 месяцев данных решения по анализу зависимостей сервисов. По ним установлено, что совпадающие ошибки крайне редки: в худшем случае для 80% рёбер с более чем 100 отказами вызываемого сервиса в минуту примерно в 2% случаев вызывающий сервис также отказывал. На примере это дало бы 0.396 из 10 запросов, ошибочно не заявленных узлом Node A. Даже при таком худшем сценарии решение создаёт память о паттернах отказов и может не заявлять ошибки только тогда, когда входящие отказы коррелируют с исходящими в fail-close зависимостях, — это устраняет риск подавления повторных попыток.
Второй набор — эффект от включения error ownership в продакшене. На инциденте 18 ноября 2025 года система автоматически ограничила зону поражения: немедленные повторы были остановлены для непосредственных вызывателей деградировавшего сервиса, причём некоторых вызывателей остановили от совершения до 200 000 дополнительных запросов. Подсчитав остановленные повторы у предков этих непосредственных вызывателей и агрегировав их в корневом узле, Uber выяснил, что смог остановить 9,5 млн ложных запросов в сервисной сети, которые иначе могли бы продлить сбой, постоянно нагружая деградировавший сервис.
По всем пользовательским API максимальное значение снижено до 3, тогда как ранее оно достигало 25; среднее значение снижено до 2 с прежних 20.
Компромиссы и альтернативы
Подход Uber строится на владении ошибкой: сервис, вернувший ошибку без сбоя исходящего вызова, — владелец, и повторять должен только вызывающий непосредственно над владельцем. Это реализуется корреляцией входящих и исходящих сбоев и заголовками error claim: когда retry не удался, ошибка unclaim'ится, и вышестоящие вызывающие её не повторяют.
Google SRE использует клиентское регулирование с per-request и per-client бюджетами: повторы не отличаются от новых запросов и повторяются, пока доля повторов ниже 10%. Клиентское регулирование принимает решение локально на основе вероятности отклонения, которая вычисляется из соотношения requests и accepts и множителя K: requests — это число запросов, а accepts — число принятых запросов, и их соотношение вместе с множителем K задаёт вероятность отклонения запроса. утилизацияизмерение загрузки задачи, например текущей скорости CPU относительно зарезервированных CPU по мере приближения к порогам приводит к отклонению запросов в зависимости от критичностиодно из четырёх значений, приписываемых запросу к бэкенду, с более высокими порогами отклонения для более высокой критичности.
Uber не полагается только на эти сигналы, потому что в реальных сценариях — перегрузка сервиса, плохие хосты БД, перегрузка БД, проблемы шардирования — вероятность повторных попыток остаётся высокой даже при повторах, а ошибки не рандомизируются, поэтому большее число повторов может усугубить деградацию.
Цена подхода Uber — потеря легитимных повторов: в сценарии с совпадающими ошибками 1.9 из 10 запросов были бы ошибочно unclaim'лены, и повтор не состоялся бы. Кроме того, бюджет ограничивает повторы 10% ошибок, поэтому при падении доступности за пределами 10% воспринимаемая доступность значительно снижается — многие запросы вообще не повторяются.
Применение в Uber
Service dependency analysis применяется для установления владения ошибкой в цепочках вызовов: оно сопоставляет входящий сбой с исходящим, чтобы определить, какой сервис — причина, а какой — лишь симптом. Оно работает в паре с retry middleware и встроено в service mesh, через которую проходят пользовательские API, поэтому сервисы получают защиту от retry storm без собственной логики обработки ошибок. Связь с защитой в том, что повторы разрешаются только сервисам-владельцам ошибки, способным реально устранить проблему, что предотвращает экспоненциальное разрастание запросов.
Что из этого следует на практике
- Бюджет повторов сам по себе не спасает от шторма: при одинаковом бюджете на каждом хопе нагрузка всё равно растёт как (1+B)ɗ × Ƞ. Ограничивать повторы нужно там, где возникла ошибка, а не на каждом уровне цепочки.
- При падении доступности вызываемого узла до 10% одного ретрая достаточно, чтобы поднять воспринимаемую доступность до 99%; дальше дополнительные повторы не помогают, а при неслучайных ошибках вредят.
- Клиентский бюджет защищает от шторма, но не различает повторы и новые запросы на балансировщике — повтор может снова попасть на перегруженную задачу.
- Владение ошибкой работает как распределённый протокол: владелец заявляет ошибку, вызывающий над ним повторяет, а при неудаче снимает claim, чтобы выше по цепочке повторов не было. Это ограничивает радиус шторма, сохраняя повторы к сервису, который реально может восстановиться.
- Потеря claim или контекста трактуется как Missing — повтор разрешается, но при возврате ошибки выше она снова unclaim'ится, что не даёт шторму распространиться.
- Цена подхода — потерянные легитимные повторы при совпадающих ошибках: в разобранном примере до 1.9 из 10 запросов могли быть ошибочно unclaim'лены.