Uber가 재시도 폭풍(Retry Storm)으로부터 보호하는 방법 — 에러 소유권으로 연쇄 장애 차단
Uber가 깊은 서비스 의존성 체인에서 재시도 폭풍을 막기 위해 '에러 소유권(error ownership)' 메커니즘을 서비스 메시지에 도입한 경험을 공유합니다. 에러를 실제로 발생시킨 서비스의 직접 호출자에게만 재시도를 허용해, 최대 재시도 폭풍 반경을 25에서 3으로 줄였고 실제 장애에서 약 950만 건의 불필요한 요청을 차단했습니다.
본문
Uber Engineering 블로그에 게시된 이 글은 재시도 폭풍(retry storm)이 깊은 의존성 체인과 팬아웃 패턴을 통해 서비스 간에 증폭되는 문제를 다룹니다. 기존의 재시도 설정 튜닝과 재시도 버짓(retry budget)은 서비스 수준에서는 유효하지만 수동 설정에 의존하고, 서비스 간 증폭에 대한 가시성이 없어 스택 깊은 곳의 단일 서비스 장애가 연쇄 장애로 번지는 '도미노 효과'를 막기 어려웠습니다. 근본 원인은 재시도가 문맥 인식(context-aware)이 아니라는 점인데, 어떤 에러가 서비스 자체에서 발생했는지 아니면 다운스트림에서 단순히 전파되어 온 것인지 구분하기 어렵기 때문입니다. 에러 코드를 상위로 변환해 전달하는 방법도 이론적으로 가능하지만, Uber 규모의 거대한 팬인·팬아웃과 변화하는 호출 흐름에서는 확장되지 않는다고 설명합니다.
재시도 폭풍의 수학
A부터 G까지 7개 노드의 호출 체인에서 모든 서비스가 재시도 1회를 수행한다고 가정하면, D에서 에러가 발생했을 때 B는 2Ƞ, C는 4Ƞ, D~G는 8Ƞ의 요청을 처리하게 됩니다. 홉당 재시도 횟수 R이 동일하면 깊이 d의 노드 부하는 R^d × Ƞ 형태로 지수 증가합니다. 재시도 버짓 B를 도입하면 (1+B)^d × Ƞ이 되어 버짓 10% 기준으로 D~G는 1.33Ƞ까지 줄어들지만 여전히 증폭이 남습니다. 반면 에러 발생 지점인 C→D 간선에서만 재시도를 허용하고 나머지 상위 노드의 재시도를 막으면 A~C는 Ƞ, D~G는 1.1Ƞ로 부하를 제한할 수 있습니다. 가용성 계산상 callee의 가용성 하락이 10% 이내라면 단 한 번의 재시도로도 호출자 입장의 체감 가용성을 99%까지 끌어올릴 수 있지만, 서비스 과부하·DB 호스트 이상·샤딩 문제처럼 에러가 상관된 실제 상황에서는 재시도가 회복으로 이어지지 않고 오히려 상태를 악화시킨다는 점이 에러 소유권 개념의 직관적 근거가 됩니다.
에러 소유권(Error Ownership)
해법은 증상(symptom)과 원인(cause)의 구분입니다. 요청을 처리하는 동안 아웃바운드 호출이 실패해 자신도 에러를 반환했다면 그 에러는 '증상'이고, 아웃바운드 실패 없이 자체적으로 에러를 냈다면 해당 서비스가 에러의 '원인' 즉 소유자입니다. Uber는 Service Dependency Analysis Solution으로 인바운드 실패와 아웃바운드 실패를 상관관계 분석해 에러 클레임(claim) 여부를 결정하고, 재시도 미들웨어는 다운스트림이 반환한 에러가 클레임되지 않은(unclaimed) 경우에만 재시도를 수행합니다. 에러 발생 지점에 가장 가까운 노드로부터 에러 클레임이 빠진 것을 처음 확인한 노드가 에러를 unclaim하면서, 재시도의 영향 반경이 국소화됩니다.
우연한 에러와 엣지 케이스
fail-open 의존성 에러와 동시에 발생한 정당한 서버 에러가 잘못 unclaim되어 합법적 재시도 기회를 잃는 우연한 에러(coincidental error) 문제가 있습니다. 6개월치 데이터 분석 결과 이런 동시 발생은 극히 드물었고(분당 callee 실패 100건 이상인 엣지의 80%에서 최악의 경우 약 2%), 분석 솔루션이 실패 패턴의 '기억'을 활용해 fail-close 의존성에서 인바운드·아웃바운드 실패가 상관될 때만 unclaim하도록 해 이 위험을 제거했습니다. 또한 재시도가 아예 설정되지 않은 서비스를 위해 '최소 1회 재시도'를 보장하는 플래그를 도입해 가용성 하락을 방지했고, unclaimed 에러 헤더를 상위로 전파해 컨텍스트가 끊긴 상황에서도 불필요한 재시도를 차단합니다. 노드 A 왼쪽에 5개 노드가 더 있었다면, 전파가 없을 때 최악의 경우 32배 많은 요청이 발생했을 것이라고 분석합니다.
프로덕션 적용과 결과
이 메커니즘은 이론이 아니라 Uber 서비스 메시지의 재시도 미들웨어와 Service Dependency Analysis Solution에 실제 배포되어 동작 중이며, 공유 인프라에 내장되어 서비스별 개별 에러 처리 로직 없이 보호가 적용됩니다. 2025년 11월 18일 호출 체인 5단계 이상 깊이의 Core Entity 서비스 장애 당시, 단순 재시도 버짓만 있었다면 성능 저하된 서비스에 46~135%의 트래픽 증가가 발생해 장애가 장기화됐을 것이지만, 에러 소유권이 즉시 상위 재시도를 차단해 직접 호출자 단계에서 최대 20만 건, 전체 서비스 메시지에서 약 950만 건의 불필요한 요청을 막았습니다. 그 결과 사용자 대면 API 전체에서 최대 재시도 폭풍 반경(retry storm radius)이 25에서 3으로, 평균은 20에서 2로 감소했습니다.
Hacker News 반응
- @aftbit — 이 분야의 다른 전략들도 듣고 싶습니다. 저는 모든 곳에서 재시도를 허용하는 순진한 방식을 해봤다가 재시도 폭풍에 빠졌습니다. 다음에 같은 문제를 마주했을 때는 반대로 최상위 서비스에서만 재시도를 허용하는 순진한 방식을 시도했는데, 실패할 때마다 엄청난 양의 작업을 다시 해야 했습니다. 복잡성을 너무 추가하지 않는 좋은 중간 지점은 무엇일까요?
- @tregoning — 지수 백오프(Exponential backoff) 위키백과 링크
- @anonymars — Microsoft Old New Thing 블로그 링크(2005년 글)
- @applfanboysbgon — 이건 좋은 개발자 경험을 나쁜 사용자 경험과 맞바꾸는 것입니다. 수동 재시도를 강제하는 게 말이 되는 상황도 있지만, 모든 상황에 하나의 보편적 규칙을 적용할 이유는 없습니다. 상황의 뉘앙스를 고려하지 않는 것은 그저 지적 게으름입니다.
- @anonymars — 제 요지는 재시도 문제에 지수 백오프를 던져넣는 것이 만능 해결책이 아니라는 것이었습니다. 백오프의 곱셈적 계층을 피하려고 조심하는 게 어떻게 좋은 개발자 경험을 나쁜 사용자 경험과 맞바꾸는 것인지 이해가 안 됩니다. 설명된 상황은 끔찍한 사용자 경험이고, 재시도 하나를 그냥 추가하고 끝내는 것이야말로 사용자 경험을 희생해 쉬운 개발자 경험을 취하는 것처럼 들립니다.
원문: Uber Engineering / 번역·요약: Trawling