Zettelkasten

변수 하나만 바꾼 카나리를 같은 타깃그룹에 동시 투입하면 부하 교란 없이 회귀 원인을 격리한다

·수정 1

요약

  • 배포가 여러 변수(런타임 버전 + OS + 의존성)를 한 번에 바꾸면 "배포 전 vs 후" 비교로는 원인을 못 가린다. 시간대·부하가 교란 변수로 섞이기 때문.
  • 변수 하나만 바꾼 카나리를 운영 트래픽과 같은 로드밸런서 타깃그룹에 넣어 동시에 돌리면, 모든 변형이 같은 부하·같은 시각의 트래픽을 받으므로 교란이 상쇄되고 그 변수의 순수 효과만 남는다.
  • 단, gevent처럼 부하에 따라 측정값이 증폭되는 시스템은 회귀가 나타난 부하 조건(피크) 에서 비교해야 한다. 저부하 카나리는 효과가 노이즈에 묻혀 거짓 음성을 낸다.

본문

왜 before/after가 안 되나. 한 배포에 런타임(3.10→3.11) + OS(Debian bullseye→bookworm) + 의존성(wrapt 등)이 동시에 바뀌면, "어제 vs 오늘" 비교는 (1) 어느 변수 탓인지 못 가리고 (2) 어제와 오늘의 시간대별 트래픽이 달라 부하가 교란된다. 특히 gevent는 I/O 시간(DB/external)을 "소켓 wall-clock = 실제 시간 + hub가 다른 greenlet 처리하느라 콜백 재개를 미룬 시간"으로 재므로, 측정값이 동시성(부하)에 비례해 부풀어 오른다. 그래서 부하가 다르면 같은 코드도 다른 수치가 나온다.

핵심 기법 — 동일 타깃그룹 동시 카나리. ECS(Fargate) 기준:

  • 프로덕션과 딱 한 가지 변수만 다른 이미지를 빌드한다 (예: OS만 bullseye로, 또는 greenlet 핀만 bump — pyproject에 핀 추가 후 uv lock으로 lock의 해당 패키지만 갱신, diff가 그 한 줄인지 확인).
  • 프로덕션 task definition을 복제해 이미지만 교체한 새 서비스를 만들고, 기존과 같은 ALB 타깃그룹에 등록한다(--load-balancers targetGroupArn=...). ALB가 prod N대 + 카나리 M대에 라운드로빈하므로 카나리는 운영 트래픽의 M/(N+M)을 동시에 받는다.
  • 여러 변형(예: py310-bullseye, py311-bullseye, py311-bookworm)을 동시에 띄우면 한 화면에서 비교된다. 이때 변형 간 차이는 시각·부하·요청믹스가 모두 같으므로 순수 변수 효과다.

New Relic에서 변형 구분 — FACET host. APM 에이전트가 같은 appName으로 보고하면 FACET CASES(WHERE host IN (...))로 그룹을 가른다. host는 컨테이너의 FQDN(ip-<dashed-private-ip>.<region>.compute.internal)이라, ECS task의 privateIPv4Address를 조회해 매핑한다. NEW_RELIC_APP_NAME 환경변수로 appName을 분리하려 했으나 newrelic.iniapp_name을 못 덮어써서, host 식별로 fall back했다.

측정 함정들 (각각 사유 분리):

  1. 워밍업 — 갓 뜬 task는 ~10–15분간 app·DB·external이 전부 부풀려진다. 코드 캐시·커넥션 풀·(3.11) 적응형 인터프리터가 콜드라서다. 신호: per-call의 median은 낮은데 average가 큼(꼬리가 무거움). 대응: average 말고 median을 보고, median-avg 격차가 닫힐 때까지 기다린다.

  2. n=1 task 노이즈 — task 1대는 ALB 라우팅·Fargate 배치 운에 따라 들쭉날쭉하다. 2–3대로 늘리면 수렴한다.

  3. 공정성 기준의 함정 — 같은 런타임끼리 비교할 땐 app_median(순수 CPU 시간)이 같아야 공정하다. 그러나 다른 Python 버전을 비교하면 app은 원래 다르다(3.11이 CPU ~30% 빠름). 이땐 "app 같음"을 워밍 완료 기준으로 쓰면 안 되고, 그룹 자체 수치가 시간에 따라 안정됐는지로 판단한다.

  4. 부하 의존성 — 가장 중요 — 회귀가 부하 증폭으로 나타난 거라면(gevent attribution), 저부하 시간대 카나리는 효과를 못 본다. 실제로 로컬 마이크로벤치(arm64)에서 greenlet 3.2.4→3.5.1이 switch +60%를 해소했지만, 같은 bump를 prod(amd64) 오후 저부하에 카나리로 띄우니 DB per-call이 stock과 동일했다 — switch 절약(ns)이 쿼리 시간(ms) 옆에서 무의미하고 hub가 한가해 증폭이 안 일어나서다. 즉 로컬 벤치 예측이 prod에서 재현 안 됨. 회귀가 난 조건(아침 피크)에서 비교해야 결론이 난다.

부산물 — 서버 측 교차검증. 클라이언트 측 측정(NR DB time)이 늘었을 때, DB 서버 자체가 느린지(RDS storage latency/CPU/Performance Insights wait events)와 대조하면 "서버가 느림 vs 클라이언트 I/O 경로 회귀"를 가른다. 서버 지표가 평행이면 회귀는 앱 컨테이너 쪽이다.

관련 노트

참고