Skip to Content
Blog왜 rollbackWindow는 안전하지 않았을까?

왜 rollbackWindow는 안전하지 않았을까?

#Argo Rollouts#GitOps#Istio#트러블슈팅

1부 — 원치 않았던 강제 롤백에서는 서비스 전체 오류율 때문에 복구 배포까지 중단되는 상황을 봤습니다.

최근 버전으로 돌아갈 때 분석을 건너뛰도록 rollbackWindow를 사용했지만, 이번에는 다른 문제가 생겼습니다.

2026-08-21, 프로덕션에서 이전 버전으로 롤백하던 중 파드 2대에 트래픽 100%가 몰렸습니다.

목표는 20대였는데, 나머지 파드가 준비되기도 전에 트래픽부터 넘어간 것입니다.

일반적인 Canary 배포에서는 파드가 준비될 때까지 낮은 weight를 유지했는데, 롤백에서는 왜 기다리지 않았을까요?

당시 사용한 Argo Rollouts v1.8.2의 처리 순서를 따라가 보겠습니다.

20대가 준비되기 전에 넘어간 트래픽

사고가 난 서비스는 공통 차트의 기본 steps를 덮어쓰고, 다음 순서로 배포하고 있었습니다.

runtime replicas: 20 minPodsPerReplicaSet: 2 rollbackWindow.revisions: 3 steps: 1% → pause 5m → 5% → pause 5m → 100%
시각Ready / 20RS 목표가중치상태
13:58:56020%롤백 sync, 이전 RS 기동
수 초 후020%step index가 끝으로 이동, Analysis 취소
13:59:45220100%마지막 weight 선택, RS 확장 시작
13:59:50~14:00:252 → 0 → 2 → 020100%소수 파드에 전체 트래픽 집중
14:00:30~14:01:002020100%20대 준비 후 정상화

13:59:45에 RS 목표는 이미 20대로 바뀌었지만, 실제로 요청을 받을 수 있는 파드는 2대였습니다.

확인한 영향값
healthy endpoint 0 구간30초
2대에 집중된 부하96 rps
기대치 대비 요청 결손(추정)약 3,058건
확인된 UH 50328건

전체 용량의 일부만 준비된 상태에서 모든 요청을 받으니, 먼저 올라온 파드부터 과부하가 걸렸습니다.

이후 readiness probe 실패로 파드가 endpoint에서 빠졌다가 복귀하기를 반복하면서, 요청을 받을 수 있는 파드 수도 2 → 0 → 2 → 0으로 바뀌었습니다.

분석을 건너뛰기 위해 넣었던 설정

앞서 복구를 막았던 것은 서비스 전체 오류율을 읽는 AnalysisRun이었습니다.

복구할 버전이 정상이더라도, 요청 대부분을 받던 장애 버전 때문에 실패 판정이 누적됐습니다.

이 분석을 생략하면 이전 버전으로 돌아갈 수 있다고 봤습니다.

그때 사용한 설정이 다음과 같습니다.

rollbackWindow: revisions: 3

공식 문서의 rollbackWindow 설명 처럼, 범위 안의 이전 ReplicaSet으로 돌아갈 때 배포 단계를 생략하는 기능입니다.

revisions: 3은 시간 단위가 아니라, 최근 ReplicaSet 중 빠른 롤백을 허용할 범위를 뜻합니다.

대상 ReplicaSet이 남아 있어야 하므로 revisionHistoryLimit에 따라 이미 삭제된 버전에는 이 경로를 사용할 수 없습니다.

분석과 함께 step도 건너뛴다

빠른 롤백은 AnalysisRun만 끄는 방식으로 동작하지 않습니다.

대상이 window 안에 있으면 컨트롤러는 실행 중인 AnalysisRun을 취소하고, step index를 전체 step 수인 stepCount로 바꿉니다. (v1.8.2의 step 처리 )

사고 당시 steps에서는 다음과 같습니다.

0: setWeight 1 1: pause 5m 2: setWeight 5 3: pause 5m 4: setWeight 100 rollbackWindow → currentStepIndex = 5

남은 단계를 모두 통과한 위치로 바로 옮기는 것입니다.

분석 때문에 복구가 중단되는 일은 피했지만, 이 위치가 다음 weight 계산에도 사용됐습니다.

“이전 weight”가 100이 되는 순간

ReplicaSet들이 현재 단계의 목표 상태에 도달하지 못했다면, 컨트롤러는 현재 index 앞에서부터 setWeight를 거꾸로 찾습니다. (v1.8.2의 weight 계산 )

이번에는 새 ReplicaSet의 가용 파드가 목표에 못 미친 상태였습니다.

같은 steps라도 어디서 탐색을 시작하느냐에 따라 결과가 달라집니다.

정상 배포: index 4의 setWeight 100에 도착 → index 3은 pause → index 2의 weight 5 유지 빠른 롤백: index 5로 건너뜀 → index 4의 weight 100 선택

일반적인 배포에서는 5를 선택하던 역탐색이, 빠른 롤백에서는 건너뛴 마지막 setWeight: 100을 선택합니다.

여기서 “이전 weight”는 실제로 직전에 보냈던 트래픽 비율을 기억한 값이 아닙니다.

현재 step index보다 앞에 선언된 setWeight를 찾은 값입니다.

⇒ 마지막 단계 뒤로 건너뛰었으니, 그 바로 앞의 100이 선택됩니다.

파드 목표는 20대인데 왜 2대만 받았나

되돌아갈 과거 ReplicaSet은 이번 배포에서 canary 역할을 하며, 컨트롤러 안에서는 newRS로 다뤄집니다.

첫 weight 1%에서 필요한 파드는 ceil(20 × 1%) = 1대지만, minPodsPerReplicaSet: 2가 적용돼 2대로 시작했습니다.

이후 step index가 끝으로 이동하면 최종 목표가 20대로 올라갑니다.

RS 크기 경로 — 파드 수를 정한다가중치 경로 — 트래픽 비율을 정한다index = stepCountcurrentStep = nilL75 RS 크기reconcileCanaryReplicaSetsGetCurrentSetWeightnil 이면MaxTrafficWeight목표 20대하한은 이미 넘어 무효L57 가중치reconcileTrafficRouting:245 역탐색i := index−1; i >= 0;i−−100%Ready 2대가 받는다게이트stable 만 검사 · canary미검증
컨트롤러는 ReplicaSet 목표와 weight를 각각 계산합니다. 목표를 20대로 늘려도 파드가 준비되기까지는 시간이 걸립니다.

컨트롤러는 ReplicaSet 목표를 20대로 늘리지만, 파드가 준비되기 전에 역탐색으로 찾은 weight 100을 적용합니다.

ReplicaSet의 목표를 바꾸는 것과 파드가 실제 요청을 받을 수 있게 되는 것 사이에는 시간이 필요합니다.

그동안 Available 2대 · Desired 20대인 상태에서 트래픽은 100%가 됐습니다.

다른 가용성 검사는 왜 막지 못했나

컨트롤러는 step에서 weight를 찾기 전에 abort, canary 0대, promote --full 등의 조건을 먼저 확인합니다.

①②③④⑤⑥가중치 결정 진입reconcileTrafficRouting:187 stable 복귀동적 복귀 중:194 완전 승격IsFullyPromoted:199 abortIsAborted():217 canary 0대Available 0:229 PromoteFullstatus 필드:243 그 밖index != nil가중치 재계산stable 가용량 기준가중치 0라우팅 정리 · 승격 시 한번에가중치 0stable 100%가중치 0canary 로 안 보냄가중치 동결RS 만 전량 확장:245 역탐색미도달 & !PromoteFull마지막 setWeight우리 형태면 100
컨트롤러는 위에서부터 조건을 확인합니다. abort와 promote --full은 step에서 weight를 찾기 전에 별도로 처리합니다(rollout/trafficrouting.go:187–263).

이번 롤백은 abort 상태도, promote --full로 진행한 상태도 아니었습니다.

canary도 이미 2대가 준비돼 있었으므로, step에서 weight를 찾는 경로까지 내려갔습니다.

그 뒤의 가용성 검사는 stable이 나머지 트래픽을 감당할 수 있는지 확인합니다.

canary로 100%를 보내면 stable로 보낼 트래픽은 0%라 이 검사도 통과합니다.

canary가 아예 없는 경우는 막았지만, 준비된 2대가 트래픽 100%를 감당할 수 있는지를 확인하는 조건은 이 경로에 없었던 것입니다.

promote --full은 왜 다르게 움직이나

promote --full도 step과 Analysis를 건너뜁니다.

다만 앞의 그림처럼 별도 분기에서 weight를 계산합니다.

dynamicStableScale을 사용하지 않는 이 구성에서는 새 ReplicaSet이 준비될 때까지 현재 weight를 유지합니다.

5%에서 실행했다면 트래픽을 5%만 보내면서 새 ReplicaSet을 20대로 늘립니다.

사고 당시 rollbackWindow는 이 분기를 사용하지 않았습니다.

step index를 끝으로 바꾼 뒤, 일반 step 계산에서 마지막 weight를 가져왔습니다.

같이 단계를 건너뛰는 기능이라도, 파드가 준비되는 동안 트래픽을 어떻게 다루는지는 달랐습니다.

빠른 롤백에서 빠져 있었던 확인

이번 문제는 rollbackWindow, 마지막 setWeight: 100, 그리고 전체보다 적은 초기 파드 수가 겹치면서 드러났습니다.

결론 : v1.8.2의 이 롤백 경로에서는 step을 생략한 뒤 마지막 weight 100을 선택했고, 전체 파드가 준비되기 전에 트래픽부터 넘어갔습니다.

이미 검증했던 버전이라는 사실이, 지금 트래픽을 받을 용량도 준비됐다는 뜻은 아니었습니다.

그럼 steps 끝의 100을 지우고 낮은 weight만 남기면 어떨까요?

3부 — Canary로 안전하게 배포하는 방법에서는 이 변경의 효과와 한계, 실제 롤백에서 확인할 항목을 정리하겠습니다.