rollbackWindow는 왜 그 안전장치를 깨는가
2026-08-21, prod에서 이전 버전으로 롤백하던 중 파드 2대에 트래픽 100%가 몰렸습니다.
전체 목표는 20대였지만 나머지가 준비되기 전에 트래픽부터 넘어갔고, 과부하된 파드가 readiness probe에 실패하면서 요청을 받을 endpoint가 사라졌습니다.
1부의 정상 Canary에서는 파드가 준비될 때까지 이전의 낮은 weight를 유지했습니다.
롤백에서는 왜 준비된 파드가 감당할 수 있는 것보다 많은 트래픽을 보냈을까요?
20대가 준비되기 전에 넘어간 트래픽
사고 당시 설정은 다음과 같았습니다.
runtime replicas: 20
minPodsPerReplicaSet: 2
rollbackWindow.revisions: 3
steps:
1% → pause 5m → 5% → pause 5m → 100%| 시각 | Ready / 20 | RS 목표 | 가중치 | 상태 |
|---|---|---|---|---|
| 13:58:56 | 0 | 2 | 0% | 롤백 sync, 이전 RS 기동 |
| 수 초 후 | 0 | 2 | 0% | step index가 끝으로 이동, Analysis 취소 |
| 13:59:45 | 2 | 20 | 100% | 마지막 weight 선택, RS 확장 시작 |
| 13:59:50~14:00:25 | 2 → 0 → 2 → 0 | 20 | 100% | 소수 파드에 전체 트래픽 집중 |
| 14:00:30~14:01:00 | 20 | 20 | 100% | 20대 준비 후 정상화 |
13:59:45에 RS 목표는 이미 20대로 바뀌었지만, 실제로 요청을 받을 수 있는 파드는 2대였습니다.
| 확인한 영향 | 값 |
|---|---|
| healthy endpoint 0 구간 | 30초 |
| 2대에 집중된 부하 | 96 rps |
| 기대치 대비 요청 결손 | 약 3,058건 |
| 확인된 UH 503 | 28건 |
용량을 확보하기 전에 트래픽을 보내면서 과부하가 시작됐습니다.
이후 readiness probe 실패로 파드가 endpoint에서 빠졌다가 복귀하기를 반복하면서, 요청을 받을 수 있는 파드 수도 2 → 0 → 2 → 0으로 바뀌었습니다.
장애 버전의 오류율 때문에 롤백도 중단됐다
rollbackWindow를 도입하기 전에는 Analysis가 롤백을 중단하는 문제가 있었습니다.
GitOps로 이미지 태그를 이전 값으로 되돌리면 pod template이 바뀝니다.
컨트롤러는 이전 버전으로 되돌리는 변경도 새 배포로 처리해 Canary 절차와 background AnalysisRun을 다시 시작합니다.
이때 복구하려는 이전 버전이 canary, 장애가 난 현재 버전이 stable입니다.
첫 weight가 1%이면 대부분의 요청은 아직 장애 버전으로 갑니다.
복구 버전 canary 1% ─┐
├─ 서비스 전체 오류율 → AnalysisRun
장애 버전 stable 99% ─┘우리 Prometheus 쿼리는 리비전을 나누지 않고 destination_service로 요청을 모읍니다.
복구 버전이 정상이더라도 AnalysisRun은 장애 버전의 요청까지 합산한 높은 오류율로 판정합니다.
interval: 20s, failureLimit: 2이므로 실패가 누적 세 번이면 abort됩니다.
첫 측정부터 세 번 연속 실패하면 약 40초 만에도 이 조건에 도달할 수 있습니다.
abort되면 canary weight가 0으로 내려가 복구 버전은 요청을 받지 못하고, 모든 트래픽이 다시 장애 버전으로 갑니다.
Analysis 쿼리에서 복구 버전의 메트릭만 측정하도록 바꾸면 되지만, 당시 메트릭 구성에서는 두 리비전을 쉽게 구분하기 어려웠습니다.
그래서 최근 버전으로 돌아갈 때 분석을 건너뛰는 rollbackWindow를 사용했습니다.
빠른 롤백은 step도 건너뛴다
rollbackWindow:
revisions: 3이 설정을 사용하면 최근 ReplicaSet으로 돌아갈 때 Canary 단계를 생략합니다.
3은 시간이 아니라 현재 stable과 롤백 대상 사이의 ReplicaSet을 기준으로 판단하는 범위입니다.
대상 ReplicaSet이 남아 있어야 하므로 revisionHistoryLimit에 따라 이미 삭제된 버전으로는 빠른 롤백을 할 수 없습니다.
롤백 대상이 이 범위에 포함되면 컨트롤러는 실행 중인 AnalysisRun을 취소하고, step index를 전체 step 수인 stepCount로 바꿉니다.
사고 당시 steps에서는 다음과 같습니다.
0: setWeight 1
1: pause 5m
2: setWeight 5
3: pause 5m
4: setWeight 100
rollbackWindow → currentStepIndex = 5Analysis로 인한 중단은 피했지만, 바뀐 index가 weight 계산에도 영향을 줬습니다.
“이전 weight”가 100이 되는 순간
새 ReplicaSet이 충분히 준비되지 않았다면 컨트롤러는 현재 index 앞에서부터 setWeight를 거꾸로 찾습니다.
같은 steps라도 어디서 탐색을 시작하느냐에 따라 결과가 달라집니다.
정상 배포: index 4의 setWeight 100에 도착
→ index 3은 pause → index 2의 weight 5 유지
빠른 롤백: index 5로 건너뜀
→ index 4의 weight 100 선택정상 배포에서는 5를 선택하던 역탐색이, 롤백에서는 건너뛴 마지막 setWeight: 100을 선택합니다.
파드 목표는 20대인데 왜 2대만 받았나
되돌아갈 과거 ReplicaSet은 이번 Rollout의 newRS가 됩니다.
첫 weight 1%에서 필요한 파드는 ceil(20 × 1%) = 1대지만, minPodsPerReplicaSet: 2가 적용돼 2대로 시작했습니다.
이후 step index가 끝으로 이동하면 최종 목표가 20대로 올라갑니다.
컨트롤러는 ReplicaSet 목표를 20대로 늘리지만, 파드가 준비되기 전에 역탐색으로 찾은 weight 100을 적용합니다.
파드는 즉시 준비되지 않으므로 Available 2대 · Desired 20대인 동안 트래픽은 100%가 됩니다.
가용성 검사도 통과한 이유
컨트롤러는 step에서 weight를 찾기 전에 abort, canary 0대, promote --full 등의 조건을 먼저 확인합니다.
이번 롤백은 abort 상태가 아니었고 canary도 2대 있었으므로, 이 조건들에 해당하지 않아 step에서 weight를 찾았습니다.
그 뒤에는 stable이 나머지 트래픽을 감당할 수 있는지 확인합니다.
canary로 100%를 보내면 stable로 보낼 트래픽은 0%라 이 검사도 통과합니다.
canary가 0대인 경우는 확인했지만, 준비된 2대가 트래픽 100%를 감당할 수 있는지는 확인하지 않은 것입니다.
promote --full은 왜 다르게 움직이나
promote --full도 step과 Analysis를 건너뜁니다.
다만 앞의 그림처럼 별도로 weight를 계산하며, dynamicStableScale을 사용하지 않는 이 구성에서는 새 ReplicaSet이 준비될 때까지 현재 weight를 유지합니다.
5%에서 실행했다면 트래픽을 5%만 보내면서 새 ReplicaSet을 20대로 늘립니다.
사고 당시 rollbackWindow에는 이 처리가 없어서, step index를 끝으로 바꾼 뒤 일반 step 계산으로 weight를 정했습니다.
steps 끝의 100을 지우면 롤백 중에도 이전의 낮은 weight를 유지할 수 있지 않을까요?