Skip to Content
Blog원치 않았던 강제 롤백

원치 않았던 강제 롤백

#Argo Rollouts#GitOps#Istio#메트릭

장애가 난 버전을 이전 버전으로 되돌리려는데, 복구하던 배포가 다시 중단됐습니다.

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로 조절합니다.

Istio를 통해 설정 반영요청95%5%Rollout 컨트롤러VirtualService 가중치 ·DestinationRule subset설정서비스로 보내는 요청같은 서비스 주소로 접근Istio · Envoy반영된 라우팅 설정에 따라요청 분배기존 ReplicaSet의파드stable subset · 기존버전새 ReplicaSet의파드canary subset · 새 버전
Rollout 컨트롤러가 VirtualService의 가중치와 DestinationRule의 subset을 설정하면 Envoy가 각 ReplicaSet의 파드로 요청을 전달합니다. 점선은 설정 변경을, 실선은 요청 흐름을 나타냅니다. 95%와 5%는 각 버전으로 보내는 트래픽 비율입니다.

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을 반복합니다.

현재 상태 확인Rollout · ReplicaSettraffic weight결정현재 step이 요구하는 비율ReplicaSet 목표계산stable · canary 파드 수다음 step 판단진행 가능하면 인덱스를옮긴다상태 기록효과는 다음 바퀴에나타난다
컨트롤러는 weight와 ReplicaSet 목표를 계산한 뒤 step 진행 여부를 기록합니다. 다음 reconcile에서는 바뀐 index로 계산합니다(rollout/canary.go).

한 번의 reconcile에서 traffic weight와 ReplicaSet 목표를 계산한 뒤 step 진행 여부를 기록합니다.

다음 reconcile에서는 바뀐 step index를 읽고, 파드가 그 단계에서 요구하는 만큼 준비됐는지 다시 확인합니다.

마지막 step과 승격

모든 step을 통과하고 새 ReplicaSet 전체가 준비되면 새 버전을 stable로 승격(promotion)합니다.

setWeight: 5index 0pause: 10mindex 1 · AnalysisRun측정남은 step 없음모든 step 통과최종 weight 100%승격setWeight: 100없어도 된다
모든 step을 통과하면 컨트롤러는 최종 weight를 100으로 계산합니다. 마지막 setWeight: 100을 명시하지 않아도 전체 파드가 준비되면 승격합니다.

step이 더 없을 때 컨트롤러는 최종 weight를 100으로 계산합니다.

따라서 마지막 setWeight: 100을 명시하지 않아도 정상 배포는 100%까지 진행됩니다.

이 동작은 뒤에서 롤백 설정을 바꿀 때 다시 보게 됩니다.

파드가 준비돼도 AnalysisRun은 배포를 멈출 수 있다

측정할 메트릭과 판정 기준을 AnalysisTemplate에 지정하면, 배포 중 생성되는 AnalysisRun이 이 설정에 따라 측정합니다.

우리 환경에서는 background analysis를 사용해 배포 중 오류율과 지연 시간을 계속 측정했습니다.

오류율이 0.001(0.1%) 이상이거나 지연 시간이 0.3s 이상이면 해당 메트릭의 측정을 실패로 판정하는 설정입니다.

alt[result.Failed <= failureLimit 2][else] Failed 가 3 이 되는 순간아래 2·3이 interval 20s 로 반복된다count 를 주지 않으면 Rollout 이 끝날 때까지4. failureCondition: result[0] >= 0.001참이면 result.Failed 를 1 올린다7. status.abort = trueGetCurrentSetWeight 가 Abort 를 먼저 보고 0 을 반환한다→ 가중치 0, stable RS 가 전량을 받는다1. 생성 — background, 리비전마다 하나2. errorRateQuery3. result 배열5. 20초 뒤 다시 측정6. phase = Failedrollout 컨트롤러AnalysisRunPrometheus
20초마다 측정하고, 실패가 누적 세 번이면 Rollout을 abort합니다. 중간에 성공한 측정이 있어도 실패 횟수는 줄지 않습니다.

interval: 20s마다 측정하고, 한 메트릭에서 실패가 failureLimit: 2를 초과해 누적 세 번이 되면 AnalysisRun이 Failed가 됩니다. (v1.8.2 실패 횟수 판정 )

중간에 성공한 측정이 있어도 실패 횟수는 줄지 않습니다.

첫 측정부터 세 번 연속 실패하면, 측정을 시작한 지 약 40초 만에도 이 조건에 도달할 수 있습니다.

컨트롤러는 Rollout을 abort하고, canary weight를 0으로 내려 stable로 트래픽을 되돌립니다.

새 버전에 문제가 생겼고 stable이 정상이라면, 기대했던 보호 동작입니다.

그런데 이 오류율은 어느 버전의 것일까

당시 Prometheus 쿼리는 destination_service로 요청을 모아 서비스 전체 메트릭을 보고 있었습니다.

95%5%stable트래픽 95%canary트래픽 5%서비스 전체 메트릭두 버전 요청을 합쳐 계산AnalysisRun오류율 · 지연 시간 판정
두 버전의 요청을 함께 집계하므로 AnalysisRun은 stable 트래픽까지 포함한 오류율로 판정합니다.

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는 안전하지 않았을까?에서 그때의 흐름을 따라가 보겠습니다.