본문으로 건너뛰기
목록으로 돌아가기
기술
18

5% 카나리를 자동 롤백하려면: p99·오류율을 판정 가능한 신호로 바꾸기

baseline 상대 비교, 정렬된 관측 창, 최소 표본, 연속 성공·실패 한도, no-data 처리와 DB migration 경계를 결합해 재현 가능한 카나리 판정기를 설계합니다.

박효열 (Hyoyoul Park)
#Canary Deployment#Argo Rollouts#Prometheus#SRE#Observability#자동 롤백

이 글의 문제 발견 출처는 Threads의 카나리 자동 롤백 질문입니다. 원문 캡션·이미지·영상은 복제하지 않고 직접 링크만 provenance로 남겼습니다. 구현 의미는 Argo Rollouts 공식 문서의 Canary 전략, Analysis, Prometheus provider, Prometheus 공식 histogram 지침으로 교차 검증했습니다.

먼저 결론

“카나리에 트래픽 5%를 보내고 p99가 높으면 롤백한다”는 정책은 아직 실행 가능한 명세가 아니다. 다음 질문에 답해야 자동화가 우연한 spike와 실제 회귀를 구분할 수 있다.

  1. 무엇과 비교하는가: 고정 SLO인가, 동시에 실행 중인 baseline인가?
  2. 어떤 시간 창을 비교하는가: warm-up, scrape 지연, 배포 시점을 어떻게 맞추는가?
  3. 표본이 충분한가: 5% 트래픽에서 p99를 믿을 요청 수가 있는가?
  4. 몇 번 실패해야 중단하는가: 한 점인가, 연속 창인가?
  5. no data·NaN·timeout은 성공, 실패, inconclusive 중 무엇인가?
  6. rollback이 복구하는 범위는 어디까지인가: traffic, Pod, schema, queue consumer를 구분했는가?

좋은 카나리 정책은 metric query가 아니라 관측과 행동을 잇는 유한 상태 머신이다.

보드는 정상 카나리와 p99·오류율 회귀를 같은 5% 시작점에서 재생한다. 실패 한도에 도달한 뒤 traffic을 0%로 돌리는 것과 DB schema까지 원복하는 것이 별개의 문제임을 마지막 상태에서 확인할 수 있다.

절대값보다 baseline 상대 비교가 강한 이유

고정 임계값 canary_p99 > 300ms만 쓰면 배포와 무관한 전체 시스템 변화에 흔들린다. upstream 지연, 시간대별 트래픽, cache hit rate, region 장애가 stable과 canary를 동시에 느리게 만들 수 있다.

동시에 실행하는 baseline과 canary를 비교하면 공통 변동을 일부 제거할 수 있다.

p99_ratio   = canary_p99 / baseline_p99
error_ratio = canary_error_rate / max(baseline_error_rate, floor)

candidate failure:
  p99_ratio > 1.20
  OR canary_error_rate - baseline_error_rate > 0.005

오류율 baseline이 0에 가까울 때 단순 비율은 폭발하므로 absolute difference나 작은 floor를 함께 둔다. ratio 1.20도 보편적인 정답이 아니다. 서비스 SLO, 과거 변동성, rollback 비용으로 결정하고 staging replay와 과거 incident 데이터로 calibration한다.

baseline 비교에도 함정이 있다. 두 version이 같은 node pool, region, request class를 공정하게 나눠 받지 않으면 version 효과와 인프라 효과가 섞인다. sticky session과 tenant skew도 5% 표본을 편향시킬 수 있다. routing key와 label cardinality를 설계 단계에서 정한다.

p99는 요청 수가 적으면 거의 한 요청의 이야기다

5% traffic이 분당 50건이면 카나리는 분당 2~3건만 받을 수 있다. 그 창의 p99는 안정적인 tail 분포라기보다 가장 느린 한두 요청에 가깝다. 반대로 초당 수천 건이면 1분 창도 충분할 수 있다.

따라서 시간만 지정하지 말고 최소 표본도 둔다.

evaluate(window) only if:
  canary_requests >= minimum_requests
  baseline_requests >= minimum_requests
  telemetry_freshness <= scrape_delay_budget
otherwise:
  INCONCLUSIVE, do not promote

Prometheus classic histogram에서 p99를 계산할 때는 histogram_quantile 전에 bucket rate를 필요한 label로 올바르게 합쳐야 한다. instance별 quantile을 평균내는 방식은 수학적으로 같은 결과가 아니다. native histogram을 쓰더라도 schema와 aggregation contract를 배포 전 검증한다.

한 번의 spike가 아니라 관측 sequence를 판정한다

Argo Rollouts의 AnalysisTemplate은 interval, count, failureCondition, failureLimit, consecutiveSuccessLimit으로 반복 측정을 정책화할 수 있다. 예시는 다음 의도를 표현한다.

metrics:
- name: p99-ratio
  interval: 1m
  count: 10
  failureLimit: 1
  consecutiveSuccessLimit: 3
  failureCondition: result[0] > 1.20
  provider:
    prometheus:
      address: http://prometheus.monitoring:9090
      query: <canary p99 divided by aligned baseline p99>
  • 첫 실패는 transient로 보고 관측을 계속한다.
  • failure limit을 위반하면 AnalysisRun은 Failed가 되고 rollout을 중단하는 근거가 된다.
  • 연속 성공 수를 채우면 승격 조건을 만족한다.
  • count가 끝났는데 성공·실패 어느 조건도 확정하지 못하면 Inconclusive로 멈출 수 있다.

failureLimit: 1은 실패 측정 한 번을 허용하고 두 번째 실패에서 한도를 위반한다. 이 경계 의미는 controller version과 공식 문서의 정의로 테스트해야 한다. “한 번 허용”과 “두 번째에 실패”를 말로만 두면 off-by-one이 생긴다. synthetic AnalysisRun으로 정확한 phase 전이를 CI 또는 pre-production에서 확인한다.

no data는 성공이 아니다

카나리 Pod가 crash loop에 빠져 요청을 전혀 받지 않으면 latency query가 빈 vector를 반환할 수 있다. 빈 결과를 0ms로 바꾸면 최악의 배포가 가장 빠른 배포로 판정된다.

정책은 최소한 다음을 구분해야 한다.

관측권장 기본 상태이유
충분한 표본, guardrail 안Success 후보유효한 관측
충분한 표본, threshold 초과Failure 후보회귀 증거
요청 수 부족Inconclusivetail 판단 불가
query timeout·Prometheus 오류Inconclusive 또는 Failure관측 시스템 실패
canary Ready=0Failure트래픽을 받을 대상이 없음
NaN·Inf·빈 vector명시적 조건암묵적 형 변환 금지

관측 시스템 장애를 무조건 rollback으로 처리하면 monitoring outage가 배포 전체를 멈춘다. 반대로 성공으로 처리하면 blind promotion이 된다. critical service는 보통 fail closed로 pause하고 사람이 확인하도록 하며, low-risk service는 제한 시간 뒤 다른 신호로 결정할 수 있다.

traffic rollback의 정확한 상태 머신

최소 상태를 다음처럼 둔다.

Idle → Warmup → Measuring → {Promote | Abort | Inconclusive}
                          Abort → RouteCanaryToZero
                                → VerifyStableRecovery
                                → ScaleDownCanary

순서가 중요하다. 먼저 traffic을 stable로 돌리고, stable의 error와 saturation이 회복되는지 확인한 뒤 canary를 scale down한다. stable이 95%만 받던 상태에서 갑자기 100%를 받으면 자체 capacity가 부족할 수 있다. rollback 경로도 load test 대상이다.

또한 rollout controller의 Abort가 “이전 container image로 모든 외부 상태가 되돌아갔다”는 뜻은 아니다. 다음은 별도 복구 영역이다.

  • 비호환 DB migration
  • 이미 발행한 queue message와 side effect
  • cache key·serialization format 변경
  • 외부 API에 전송한 요청
  • background job이 갱신한 데이터

DB는 expand/contract로 두 version이 동시에 읽고 쓸 수 있는 호환 구간을 만든다. destructive migration은 traffic 승격과 분리하고 검증 뒤 실행한다. consumer schema에는 versioning과 idempotency를 둔다.

운영에 필요한 guardrail 묶음

하나의 p99만으로는 부족하다. 최소한 다음 계층을 조합한다.

  • availability: Ready replica, request success rate, crash loop
  • latency: p50·p95·p99와 timeout, endpoint별 분리
  • saturation: CPU throttling, connection pool, queue lag, thread pool
  • correctness: business invariant, validation error, mismatch shadow metric
  • telemetry health: scrape freshness, request count, missing series

여러 metric 중 하나만 실패해도 abort할지, 특정 조합일 때만 실패할지 명시한다. metric마다 표본과 변동성이 다르므로 같은 interval과 threshold를 복사하지 않는다.

배포 전 검증 시나리오

  1. canary에 의도적으로 300ms delay를 넣으면 정해진 창 뒤 traffic이 0%가 되는가?
  2. 한 번의 2초 spike 뒤 정상화하면 false rollback 없이 계속되는가?
  3. 요청량이 최소치 아래면 승격하지 않고 Inconclusive가 되는가?
  4. Prometheus query가 timeout이면 정한 fail-open/closed 정책을 따르는가?
  5. stable도 동시에 느려질 때 ratio와 absolute SLO가 서로 어떤 결론을 내는가?
  6. abort 직후 stable이 100% traffic을 감당하고 recovery를 확인하는가?
  7. 이전 version과 새 schema가 동시에 존재해도 읽기·쓰기가 호환되는가?

자동 롤백의 목적은 사람을 완전히 제거하는 것이 아니다. 판단 가능한 증거가 모였을 때 blast radius를 빠르게 줄이고, 애매할 때는 승격을 멈추는 것이다.