메뉴
HN
Hacker News • 8일 전

우버의 재시도 폭풍(Retry Storm) 방지 기법

IMP
6/10
핵심 요약

우버는 깊은 의존성 체인에서 서비스 하나의 장애가 재시도 트래픽 증폭으로 이어져 전체 시스템 장애로 번지는 '재시도 폭풍' 문제를 다룹니다. 기존의 수동 재시도 설정과 재시도 예산(retry budget)만으로는 교차 서비스 증폭을 막기 어려워, 오류를 발생시킨 서비스와 전달만 하는 서비스를 구분하는 '오류 소유권(error ownership)' 기반의 컨텍스트 인식 메커니즘을 공유 인프라에 도입했습니다. 이를 통해 오류 발생 지점 근처에서만 재시도를 허용해 기준 대비 10% 이내의 트래픽 증가로 가용성을 유지할 수 있습니다.

번역된 본문

2026년 9월 17일 우버는 재시도 폭풍(Retry Storm)을 어떻게 방어하는가 DM Deepanshu Mehndiratta 수석 스택 엔지니어 AS Alok Srivastava 프린시펄 엔지니어 VD Vibhor Dhingra 시니어 소프트웨어 엔지니어 1+ 이 기사 공유하기 페이스북 링크드인 X 소셜 링크

소개

재시도 폭풍은 역사적으로 비즈니스 운영과 브랜드 신뢰에 영향을 미쳐왔습니다. 재시도 구성 튜닝과 재시도 예산(retry budget)은 서비스 수준에서 의미 있는 완화책을 제공하지만, 이는 수동으로 구성되며 깊은 의존성 체인과 팬아웃(fan-out) 패턴으로 인한 교차 서비스 증폭에 대한 가시성이 부족합니다. 그 결과, 스택 깊숙이 위치한 단일 서비스 장애가 촉발하는 도미노 효과로부터 인프라를 보호하기가 어려울 수 있습니다.

핵심 이유는 오늘날의 재시도 동작이 컨텍스트를 인식하지 못한다는 점입니다. 재시도가 몇 번 발생하는지는 제어할 수 있지만, 언제 발생하는지는 정확하게 제어할 수 없습니다. 이는 서비스가 발생시킨 오류와 그 서비스를 통과해 전달된 오류를 신뢰성 있게 구분하는 데 따르는 어려움에서 비롯됩니다. 그 결과, 재시도가 조건부가 아닌 균일하게 적용됩니다. 이러한 접근 방식은 일시적이거나 낮은 비율의 장애에서는 잘 작동하지만, 중간 정도 또는 심각한 성능 저하 상황에서는 역효과를 낳습니다. 이미 어려움을 겪고 있는 서비스에 대해 공격적으로 재시도하는 것은 부하를 증가시키고, 장애를 가속화하며, 상위 의존성 전반에 재시도 트래픽을 증폭시킵니다. 국지적인 장애로 시작된 것이 빠르게 스택 전체의 사고로 확대되어 궁극적으로 최종 사용자 경험을 저하시키거나, 최악의 경우 완전히 망가뜨릴 수 있습니다.

다운스트림 서비스의 오류 코드를 업스트림으로 변환해 재시도에 컨텍스트를 제공할 수 있다고 반론할 수 있습니다. 이론적으로 가능하지만, 우버의 규모에서는 대규모 팬인(fan-in) 및 팬아웃(fan-out), 계속 변화하는 호출 흐름, 빈번한 적응형 변경 필요성 때문에 확장성이 없습니다. 따라서 우리는 오류를 더 효율적으로 처리하기 위해 공유 인프라에 컨텍스트 인식 메커니즘을 개발했습니다. 이 블로그에서 그 메커니즘을 설명합니다.

배경

그림 1과 같은 간단한 호출 체인을 생각해보면, 노드 A에 도착하는 총 요청 수를 Ƞ라고 할 때, 추론에 따라 모든 노드 B, C, D, E, F, G는 정상 상태(어떤 노드도 오류를 내지 않을 때)에서 Ƞ개의 요청을 처리합니다.

그림 1: 1:1 팬아웃 호출 체인. 각 노드는 들어오는 요청당 다운스트림을 정확히 한 번 호출합니다.

서비스 D가 오류를 내기 시작하고 각 서비스가 한 번 재시도하도록 구성된 경우(1회 정상 시도 + 다운스트림 실패 시 1회 재시도), 각 노드가 처리하는 총 요청 수를 살펴보겠습니다.

그림 2: 서비스가 오류를 내는 호출 체인.

노드: A B C D E F G 깊이: 0 1 2 3 4 5 6 처리 요청 수: Ƞ, 2×Ƞ, 4×Ƞ, 8×Ƞ, 8×Ƞ, 8×Ƞ, 8×Ƞ

모든 홉에서 재시도 수 R이 동일하다고 가정하면, 이는 간단한 공식으로 정리할 수 있습니다. ɗ는 호출 체인에서 노드의 깊이를 나타내며, 노드가 오류를 만들거나 통과시키는 경우 해당 노드가 처리하는 요청 수는 Rɗ × Ƞ입니다.

재시도 예산(Retry Budgets)

재시도 예산을 도입해 이를 최적화할 수 있습니다. 모든 홉에서 동일한 재시도 예산을 B라고 하면 새 공식은 (1+B)ɗ × Ƞ가 됩니다.

이제 재시도 예산이 10%일 때 처리되는 요청 수를 살펴보겠습니다:

노드: A B C D E F G 깊이: 0 1 2 3 4 5 6 처리 요청 수: Ƞ, 1.1×Ƞ, 1.21×Ƞ, 1.33×Ƞ, 1.33×Ƞ, 1.33×Ƞ, 1.33×Ƞ

위 예시에서 오류는 노드 D에서 발생하며, 재시도를 노드 D와 노드 C 사이에서만 허용하고 노드 A와 노드 B가 이 오류에 대해 재시도하는 것을 완전히 제한하면, 노드 D, E, F, G에 과부하를 주지 않으면서 호출 경로의 유사한 가용성을 보장할 수 있습니다.

오류 소유권(Error Ownership)

오류가 발생한 노드 D와 노드 C 사이의 엣지로 재시도를 제한하면서 동일한 재시도 예산 예시를 생각해보겠습니다.

노드: A B C D E F G 깊이: 0 1 2 3 4 5 6 처리 요청 수: Ƞ, Ƞ, Ƞ, 1.1×Ƞ, 1.1×Ƞ, 1.1×Ƞ, 1.1×Ƞ

여기서 D부터 리프 노드 G까지 모든 노드가 처리하는 총 요청 수를 기준치 대비 단 10% 초과 수준으로 억제하면서, 오류가 처음 발생한 시점에 최대 10%의 오류에 대해 최소 1회의 재시도를 허용합니다.

원문 보기
원문 보기 (영어)
September 17, 2026 How Uber Protects Against Retry Storms DM Deepanshu Mehndiratta Senior Staff Engineer AS Alok Srivastava Principal Engineer VD Vibhor Dhingra Sr Software Engineer 1+ Share this article Facebook Linkedin X social Link Introduction Retry storms historically impact business operations and brand trust. While retry configuration tuning and retry budgets provide meaningful mitigation at the service level, they’re manually configured and lack visibility into cross-service amplification caused by deep dependency chains and fan-out patterns. As a result, it can be difficult to shield infrastructure against the domino effect triggered by a single service outage deeper in the stack. A key reason is that retry behavior today isn’t context-aware. While we can control how many retries occur, we can’t precisely control when they occur. This stems from the challenge of reliably distinguishing between errors generated by a service and those merely propagated through it. As a result, retries are applied uniformly rather than conditionally. This approach works for transient or low-rate failures. However, during moderate or severe degradation, it becomes counterproductive. Aggressively retrying against an already struggling service increases load, accelerates failure, and amplifies retry traffic across upstream dependencies. What begins as a localized outage can quickly escalate into a stack-wide incident—ultimately degrading, or in the worst case, completely breaking, the end user experience. One might argue that error codes from downstream services could be translated upstream to provide context for retries. While theoretically possible, this approach doesn't scale at Uber due to large fan-in and fan-out, evolving call flows, and the need for frequent adaptive changes. Therefore, we developed a context-aware mechanism in shared infrastructure to handle errors more efficiently. This blog explains the mechanism. Background Consider a simple call chain as shown in Figure 1, where the total number of requests arriving at Node A is Ƞ. By deduction, all nodes B, C, D, E, F, and G serve Ƞ requests in the steady state (when no node errors out). Figure 1: Call-chain with 1:1 fan-out, where a node calls its downstream exactly once for any incoming request. If service D starts erroring out and each service is configured to retry once (1 regular attempt and another attempt if the downstream fails), let’s look at the total number of requests served by each node. Figure 2: Call chain where a service errors out. Node A B C D E F G Depth 0 1 2 3 4 5 6 Requests Served Ƞ 2 × Ƞ 4 × Ƞ 8 × Ƞ 8 × Ƞ 8 × Ƞ 8 × Ƞ This can be distilled down to a simple formula, assuming the number of retries R is the same at every hop. ɗ denotes the depth of the node in the call chain, the number of requests served by the node if the node creates or passes through an error: Rɗ × Ƞ Retry Budgets We can optimize this by introducing retry budgets. Let’s assume the same retry budget at every hop represented by B. The new formula becomes: (1+B)ɗ × Ƞ Now, let’s try to see the number of requests served with a retry budget of 10%: Node A B C D E F G Depth 0 1 2 3 4 5 6 Requests Served Ƞ 1.1 × Ƞ 1.21 × Ƞ 1.33 × Ƞ 1.33 × Ƞ 1.33 × Ƞ 1.33 × Ƞ In the above example, the error originates at Node D, and if we limit the retry to only between Node D and Node C , and restrict entirely Node A and Node B from retrying on this error, we can guarantee a similar availability of the call-path without overburdening Node D, Node E, Node F , and Node G. Error Ownership Consider the same example of retry budgets while restricting retries between the edge from Node C to Node D , where the error originates. Node A B C D E F G Depth 0 1 2 3 4 5 6 Requests Served Ƞ Ƞ Ƞ 1.1 × Ƞ 1.1 × Ƞ 1.1 × Ƞ 1.1 × Ƞ Here, we clamp down the total number of requests served by all nodes from D till the leaf node G to just 10% over baseline, while allowing at least once retry for up to 10% of errors when they’re first returned. But what about the availability of Node D as seen by Node C ? Let’s run some numbers for various availability scenarios, and try to calculate ‌availability after retry. Base Availability % Base Error Rate % Retry Budget Error Rate after Retries % Availability after Retries % 99.9 0.1 10% 0.0001 99.9999 99 1 10% 0.01 99.99 95 5 10% 0.25 99.75 90 10 10% 1 99 80 20 10% 12 88 70 30 10% 23 77 As shown in the table above, for availability drops up to 10% in the callee node, even a single retry is helpful in getting the perceived availability by the caller node up to 99%. Beyond this, perceived availability drops significantly as a good chunk of requests are never retried due to the retry budget in place. This calculation assumes the errors from the callee are independent and that retries will lead to recovery. However, in many real-world scenarios like service overload, bad database hosts, database overload, or sharding issues, the probability of retries remains high even with retries. This contradicts the idea that retries to callee always increase perceived availability to the caller. It’s also this intuition that forms the basis of error ownership. During periods of high error rates from a service, the errors are less likely to be randomized, and wouldn’t benefit from a higher number of retries, and instead might be responsible for further degradation. Architecture The solution is about establishing error ownership, which can be explained using the symptom versus cause analogy. If a service calls N outbounds for fulfilling a request, and if an outbound error-out causes it to return an error, then the error returned by that service is only a symptom. Simultaneously, in the context of the service, the cause is the incoming error from its downstream. However, if no outbound of the service errors out while fulfilling the request and it still returns an error, the service is the cause of the returned error, and is the owner. In the next section, we discuss some possible solutions that can leverage this. Simple Correlation Claiming Error Ownership We use the Service Dependency Analysis Solution to correlate an inbound failure with an outbound failure and use the ruleset shown in Figure 3 for making or refuting error claims. Figure 3: Decision logic for claiming error ownership. Retrying with Error Ownership The caller uses the logic shown in Figure 4 to determine if it should retry the request. Figure 4: Decision logic for allowing retries. While the scenario of missing error claim headers is an uncooperative environment, it could occur because the downstream service doesn’t have the service dependency analysis solution, and is unable to correlate outbound and inbound errors. Or, the downstream service has missing context propagation, resulting in an incomplete correlation between outbound and inbound errors. Here, the first node to see a missing error claim from a downstream unclaims the error, limiting the impact radius of the retry disturbance (it’s no longer a storm), while still allowing sufficient retries to the error-returning service. Decision Matrix Callee Error Caller Error Callee Error Claim Caller Should Retry (Retry Middleware) Caller Propagated Error Claim No Yes NA NA Claim Yes Yes Missing Yes Unclaim Yes Yes Claimed Yes Unclaim Yes Yes Unclaimed No Unclaim Figure 5: 3 nodes used to demonstrate caller errors. When the edges A -> B and B -> C are fail-close, and the Node C returns an internal error that it’s claimed, Node B upon seeing the claimed error from C should retry to C. However, if the retry fails, it’d propagate the error to Node A, but while returning the error it must unclaim it. Node A upon seeing the error from B and the unclaimed error header shouldn’t retry the request to Node B. Figure 6: Decision logic for Error claim propagation. Coincidental Errors and Why We Need Service Dependency Analysis The decision matrix above covers cases where the downstream call fails or there’s an internal server er