원치 않았던 강제 롤백
장애가 난 버전을 이전 버전으로 되돌리려는데, 복구하던 배포가 다시 중단됐습니다.
Argo Rollouts의 AnalysisRun이 높은 오류율을 감지했고, 트래픽은 다시 장애 버전으로 돌아갔습니다.
복구하려고 내린 변경을 배포의 안전장치가 막아버린 셈입니다.
여기서 말하는 “강제 롤백”은 AnalysisRun의 실패로 Rollout이 abort되고, 트래픽이 stable로 돌아간 상황입니다.
이때 Git의 이미지 태그와 Rollout의 배포 대상은 그대로 남아, 배포하려던 버전과 실제 트래픽을 받는 버전이 달라질 수 있습니다.
왜 이런 일이 생겼는지 보려면, 먼저 평소 Canary 배포에서 무엇을 확인하고 넘어가는지부터 볼 필요가 있습니다.
이 글은 당시 사용한 Argo Rollouts v1.8.2와 Istio traffic routing 구성을 기준으로 합니다.
파드 수와 트래픽 비율을 따로 다룬다
새 버전의 파드가 Ready라고 해서 실제 요청도 정상적으로 처리한다고 볼 수 있을까요?
Deployment의 롤링 업데이트는 파드 준비 상태를 확인하며 버전을 교체하지만, 서비스 오류율을 보고 배포를 중단하는 판단은 별도로 구성해야 합니다.
우리 환경에서는 Argo Rollouts와 Istio로 새 버전에 보낼 트래픽을 나누고, AnalysisRun으로 오류율과 지연 시간을 확인했습니다.
Rollout도 Deployment처럼 ReplicaSet으로 파드를 관리합니다.
배포 순서는 step으로 지정하고, 기존 버전인 stable과 배포할 버전인 canary에 보낼 트래픽은 Istio로 조절합니다.
setWeight: 5를 지정하면 전체 트래픽의 5%를 새 버전으로 보냅니다.
파드 수와 트래픽 비율을 따로 다루므로, canary가 2대여도 트래픽은 5%만 받을 수 있습니다.
5%를 보내고, 10분을 본다
당시 공통 차트의 기본 steps는 다음과 같았습니다.
steps:
- setWeight: 5
- pause:
duration: 10m
- setWeight: 100새 버전에 5%를 보낸 뒤 10분 동안 관찰하고, 다음 단계에서 전체 트래픽을 넘기는 구성입니다.
관찰하는 동안에는 background AnalysisRun이 메트릭을 계속 확인합니다.
서비스가 자체 steps를 선언하면 공통 차트의 리스트를 통째로 대체합니다.
setWeight: 100 하나만 선언한 서비스는 5%로 관찰하는 단계와 10분 대기를 생략합니다.
이 흐름을 전체 replica 20대, minPodsPerReplicaSet: 2인 경우로 살펴보겠습니다.
트래픽을 늘리기 전에 파드부터 준비한다
처음 5%에 필요한 파드는 계산상 20 × 5% = 1대입니다.
하지만 최소 파드 수가 2이므로 canary는 2대로 시작합니다.
10분 뒤 마지막 setWeight: 100 단계로 넘어가면 컨트롤러는 새 ReplicaSet의 목표를 20대로 늘립니다.
그림의 ③~⑤처럼 마지막 단계에 도착해도, 20대가 모두 준비되기 전까지는 트래픽을 5%만 보냅니다.
이때 보는 것은 Available replica 수입니다.
minReadySeconds를 설정했다면 Ready가 된 뒤에도 그 시간 동안 준비 상태를 유지해야 가용한 파드로 인정합니다.
v1.8.2의 weight 계산 을 이 상황에 맞춰 풀면 다음과 같습니다.
새 ReplicaSet이 현재 step에서 요구하는 만큼 준비되지 않았다면 이전 weight를 사용한다.
실제로는 stable의 목표 파드 수까지 함께 검사하지만, 이 예시에서 기다리는 것은 새 ReplicaSet의 가용 파드입니다.
⇒ 파드를 20대로 늘리겠다는 목표만으로 트래픽까지 먼저 넘기지는 않습니다.
변경한 상태는 다음 reconcile에서 확인한다
컨트롤러는 현재 상태를 읽고 필요한 변경을 적용하는 reconcile을 반복합니다.
한 번의 reconcile에서 traffic weight와 ReplicaSet 목표를 계산한 뒤 step 진행 여부를 기록합니다.
다음 reconcile에서는 바뀐 step index를 읽고, 파드가 그 단계에서 요구하는 만큼 준비됐는지 다시 확인합니다.
마지막 step과 승격
모든 step을 통과하고 새 ReplicaSet 전체가 준비되면 새 버전을 stable로 승격(promotion)합니다.
step이 더 없을 때 컨트롤러는 최종 weight를 100으로 계산합니다.
따라서 마지막 setWeight: 100을 명시하지 않아도 정상 배포는 100%까지 진행됩니다.
이 동작은 뒤에서 롤백 설정을 바꿀 때 다시 보게 됩니다.
파드가 준비돼도 AnalysisRun은 배포를 멈출 수 있다
측정할 메트릭과 판정 기준을 AnalysisTemplate에 지정하면, 배포 중 생성되는 AnalysisRun이 이 설정에 따라 측정합니다.
우리 환경에서는 background analysis를 사용해 배포 중 오류율과 지연 시간을 계속 측정했습니다.
오류율이 0.001(0.1%) 이상이거나 지연 시간이 0.3s 이상이면 해당 메트릭의 측정을 실패로 판정하는 설정입니다.
interval: 20s마다 측정하고, 한 메트릭에서 실패가 failureLimit: 2를 초과해 누적 세 번이 되면 AnalysisRun이 Failed가 됩니다. (v1.8.2 실패 횟수 판정 )
중간에 성공한 측정이 있어도 실패 횟수는 줄지 않습니다.
첫 측정부터 세 번 연속 실패하면, 측정을 시작한 지 약 40초 만에도 이 조건에 도달할 수 있습니다.
컨트롤러는 Rollout을 abort하고, canary weight를 0으로 내려 stable로 트래픽을 되돌립니다.
새 버전에 문제가 생겼고 stable이 정상이라면, 기대했던 보호 동작입니다.
그런데 이 오류율은 어느 버전의 것일까
당시 Prometheus 쿼리는 destination_service로 요청을 모아 서비스 전체 메트릭을 보고 있었습니다.
canary에서 오류가 늘어도 트래픽 비중이 5%라면 서비스 전체 오류율에는 작게 반영될 수 있습니다.
반대로 stable에서 발생한 오류도 같은 AnalysisRun의 실패 횟수에 들어갑니다.
파드는 canary와 stable로 나눴지만, 분석에 쓰는 요청은 두 버전을 합쳐 보고 있었던 것입니다.
복구 배포에서 stable은 장애 버전이었다
GitOps로 이미지 태그를 이전 값으로 되돌리면 pod template이 바뀝니다.
빠른 롤백 조건이 적용되지 않으면, 컨트롤러는 이 변경에도 Canary 단계와 AnalysisRun을 실행합니다.
이때 stable과 canary가 가리키는 버전을 다시 봐야 합니다.
| 역할 | 일반적인 새 버전 배포 | 이번 복구 배포 |
|---|---|---|
| stable | 기존에 서비스하던 버전 | 장애가 난 현재 버전 |
| canary | 검증할 새 버전 | 복구하려는 이전 버전 |
stable이라는 이름이 지금도 정상이라는 뜻은 아닙니다.
복구 배포의 첫 weight가 1%라면, 요청의 99%는 아직 장애 버전으로 갑니다.
복구 버전 canary 1% ─┐
├─ 서비스 전체 오류율 → AnalysisRun 실패
장애 버전 stable 99% ─┘
abort → canary 0% / 장애 버전 stable 100%복구 버전이 정상이어도 서비스 전체 오류율은 높게 나올 수밖에 없는 상황입니다.
AnalysisRun은 그 오류율로 복구 배포를 중단했고, 트래픽은 다시 장애 버전으로 돌아갔습니다.
결론 : 서비스 전체 오류율로 배포를 판정하면, 장애 버전에서 발생한 오류가 정상 버전으로의 복구까지 막을 수 있습니다.
분석을 건너뛰면 복구할 수 있지 않을까
복구 버전의 메트릭만 따로 측정하면 이 문제를 줄일 수 있습니다.
다만 당시 메트릭 구성에서는 두 리비전을 쉽게 구분하기 어려웠습니다.
그래서 최근 버전으로 돌아갈 때는 배포 단계와 분석을 건너뛰는 rollbackWindow를 사용했습니다.
이미 사용했던 버전으로 빠르게 돌아가면 복구가 가능할 것이라고 봤습니다.
그런데 분석을 건너뛴 롤백에서는, 파드가 준비되기 전에 트래픽부터 넘어가는 문제가 드러났습니다.
2부 — 왜 rollbackWindow는 안전하지 않았을까?에서 그때의 흐름을 따라가 보겠습니다.