Istio Graceful Termination 테스트 하니스 코드 워크스루 (출처: 홈랩 실험 코드 정리)
홈랩 Istio graceful-termination 실험의 tests/ 디렉터리(5개 실행 스크립트 + 공통 라이브러리)를 코드 레벨로 해부한다. 이 하니스가 측정하려는 단 하나의 명제 — "in-flight 요청 drain이 LB의 backend DOWN 마킹보다 먼저 끝나는가" — 를 축으로, 각 스크립트가 "왜 그렇게 짰는가"(측정 격리, exit code 분리, bash 관용구의 macOS 3.x 호환)를 메커니즘 수준에서 설명하고 Envoy/HAProxy 동작과 연결한다. 결론: S1(current)은 502/≈8.25s로 끊김을 입증하고 S2(improved)는 drain 덕에 200/60s로 살아남으며, 5종 스크립트가 6개 이벤트 소스를 timeline.jsonl로 통합해 그 차이를 시간축에서 증명한다.
이 문서는 코드/bash 관용구 정본(스크립트가 어떻게 동작하는가). 시나리오 변수격리 매트릭스·artifacts 해석·재현 회상 Q&A는 W5 테스트 시나리오 설계가 정본이다. 아래 replicas=1·05 통합 표는 "코드 구현 관점"만 남기고 설계 철학은 W5로 위임한다.
이 문서가 해부하는 tests/{lib/common.sh, 01-baseline-long-request.sh, 02-improved-long-request.sh, 03-continuous-traffic.sh, 04-rst-capture.sh, 05-collect-timestamps.sh}는 현재 레포 스냅샷 어디에도 실파일로 존재하지 않는다(전체 디스크 검색으로 확인). 아래 인라인 발췌(cleanup trap, Envoy poller, pull_pcap 등)는 부분 fragment이자 유일하게 남은 기록이며, 완전한 스크립트 전문은 아니다.
1. 배경: 이 하니스는 무엇을 증명하려고 존재하나
graceful termination은 "pod이 죽을 때 이미 받아 처리 중이던(in-flight) 요청을 끝까지 살려보내는가"의 문제다. K8s가 pod을 지우면 두 시계가 동시에 돈다 — 하나는 앱이 in-flight 요청을 처리·배수(drain)하는 시계, 다른 하나는 상위 로드밸런서(여기선 HAProxy)가 그 backend를 죽었다고 판정하고 세션을 끊는 시계다. 이 둘의 순서가 모든 것을 가른다. drain이 먼저 끝나면 LB가 끊어도 잃을 요청이 없다(안전). LB가 먼저 끊으면 아직 처리 중인 요청 위로 TCP RST가 날아가 502가 된다(끊김).
말로는 자명하지만, 실제 분산 시스템에서 이 순서를 증명하기는 어렵다. 두 시계는 서로 다른 호스트(앱 pod vs lb-haproxy 노드)에서 돌고, 각자 다른 로그·메트릭·패킷에 흔적을 남긴다. "끊겼다/안 끊겼다"는 curl 한 줄로 보이지만 왜 그 순서였는지는 6군데 증거를 하나의 시간축에 겹쳐야만 읽힌다. 이 tests/ 하니스 전체가 바로 그 작업 — 흩어진 증거를 한 시간축으로 모아 순서의 부호를 읽는 측정 장치다.
선행 개념은 셋이다: (1) K8s pod termination 라이프사이클(preStop hook → SIGTERM → terminationGracePeriodSeconds 후 SIGKILL), (2) HAProxy active health check가 backend를 DOWN으로 마킹하는 detection window, (3) Envoy sidecar가 in-flight 요청 수를 노출하는 admin /stats. 이 셋이 위 "두 시계"의 구체적 구현체다. 대상독자는 이 실험을 재현·디버그하려는 SRE이며, 범위는 스크립트 코드의 동작 원리다(시나리오 설계 철학은 W5).
2. 핵심 mental model: 하나의 판정선, 두 시계, 6개 프로브
머릿속에 담을 한 장면: 이 하니스 전체는 결국 단 하나의 판정선을 측정하기 위한 장치다 — active=0(in-flight drain 완료)이 HAProxy backend DOWN보다 먼저 오느냐 나중에 오느냐. 먼저면 drain 성공(S2), 나중이면 끊김(S1). 5개 스크립트는 이 두 이벤트의 순서·부호를 시간축에 찍기 위한 6개 측정 프로브이고, 05가 그것을 하나의 timeline으로 합쳐 부호를 읽어낸다.
이 한 문장에서 나머지 모든 설계가 따라 나온다. "판정선이 단 하나"이므로 측정은 그 선 양쪽 이벤트의 타임스탬프만 정확하면 된다 — 그래서 모든 프로브가 UTC ISO-8601로 시각을 찍고(사전순=시간순), 로그 줄 순서가 OS 스케줄링에 흔들려도 05가 타임스탬프 기준으로 다시 정렬한다. "두 시계의 순서"가 본질이므로 다른 pod의 트래픽이 섞이면 인과가 깨진다 — 그래서 replicas=1 격리가 전제가 된다. "in-flight 카운트"가 판정선의 한쪽 축이므로 그 수를 노출하는 Envoy admin /stats(15000)가 핵심 프로브가 된다.
measurement target = sign of one interval
------------------------------------------------------------
drain start
|
clock A: |====== in-flight draining ======> active=0
clock B: |=== HAProxy detection window ===> backend DOWN
|
S2 (safe): active=0 THEN backend DOWN (positive gap)
S1 (broken): backend DOWN while active>0 (RST over live rq)
판정선의 양쪽을 찍는 데 6개 이벤트 소스가 동원되고, 그 소스를 만들어내는 게 5개 스크립트다. 의존 관계는: common.sh(공통 헬퍼·artifacts 골격) → 01/02(S1/S2 본실험, in-flight 1건의 운명) → 03(continuous load, disruption rate) → 04(RST 패킷 증거, 01/02와 병렬) → 05(post-test, 6 소스 → 한 timeline). 아래는 이 순서대로 코드를 해부한다.
tests/
├── lib/common.sh — 공통 헬퍼 (init_artifacts, log, kctx, pick_target_pod, cleanup trap)
├── 01-baseline-long-request.sh — S1: current 모드 + /sleep?60 + pod delete → 502 입증
├── 02-improved-long-request.sh — S2: improved 모드 + drain → 200 + FSM grep 검증
├── 03-continuous-traffic.sh — S3: continuous load + rollout restart → disruption rate
├── 04-rst-capture.sh — S4: tcpdump RST 패킷 캡처 (S1/S2와 병렬)
├── 05-collect-timestamps.sh — post-test: 6 소스 → timeline.jsonl + intervals.json
└── artifacts/<YYYYMMDD-HHMMSS>/<scenario>/
3. replicas=1 격리 — 왜 측정의 전제인가 (코드 관점)
판정선이 "단일 pod에 묶인 in-flight 요청의 운명"인 이상, 그 pod 하나의 인과만 깨끗이 추적돼야 한다. roundrobin 분산이 살아 있으면(replicas≥2) 삭제되는 pod의 in-flight가 다른 pod 트래픽과 섞여 인과 추적이 불가능하다 — 이것이 S1·S2·S4가 replicas=1을 요구하는 이유다.
코드 관점의 핵심은 스크립트가 replicas를 강제하지 않는다는 점이다: pick_target_pod()이 head -1로 한 pod만 골라 delete할 뿐, replicas=1 보장은 운영자가 사전에 kubectl scale로 세팅해야 한다. 더구나 replicas≥2에서는 head -1이 "항상 같은 pod"을 고른다는 보장이 없어(아래 §4 참조) 격리가 이중으로 깨진다. 격리 함정의 전체 서사(S1 1·2차 미입증 → 3차 replicas=1 재현)와 변수격리 매트릭스는 W5에서 다룬다.
4. common.sh — bash 관용구의 의도 (왜 이렇게 짰나)
공통 라이브러리의 모든 관용구는 두 제약에서 나온다: macOS 기본 bash 3.x 호환(개발 머신이 Mac)과 비결정적 종료 경로 대응(시그널로 죽는 프로세스의 exit code는 못 믿는다). 이 두 렌즈로 보면 아래가 전부 일관된다.
init_artifacts <scenario>는 tests/artifacts/<ts>/<scenario>/를 만들고 ART_DIR을 export한다. source된 스크립트가 공유 컨텍스트이므로 export 변수가 호출 스크립트에 그대로 보인다. timestamp가 초 단위라 같은 초에 두 스크립트가 시작하면 충돌 위험이 있으나, scenario 서브디렉터리가 분리돼 있어 S1·S4 동시 실행도 안전하다.
log()는 UTC ISO-8601 + [SCENARIO] 태그를 stdout과 run.log에 tee -a로 동시 기록한다. 여러 배경 프로세스(log follower, poller)가 모두 같은 run.log에 흔적을 남기는 설계지만, tee가 서브쉘에서 돌기 때문에 로그 줄 순서는 OS 스케줄링에 의존한다 — 그래서 정확한 시간 정렬은 05-collect-timestamps.sh가 타임스탬프 기준으로 다시 한다(판정선이 타임스탬프이지 줄 순서가 아니므로).
kctx()는 --context homelab 문자열만 반환하는 함수로, kubectl $(kctx) ... 패턴으로 쓰인다. $()는 매번 fork+exec를 일으켜 hot loop에서는 비효율적이지만 테스트에선 무시 가능하고, context 변경 지점을 한 곳으로 모으는 이점이 더 크다.
pick_target_pod()는 kubectl get pod -l app=... -o name | head -1 | sed 's|^pod/||'로 첫 pod을 고른다. kubectl 출력은 대체로 이름순으로 오지만 보장된 사전순은 아니다 — 정렬은 server-side 반환 순서에 의존하며 버전/캐시에 따라 흔들릴 수 있다. 엄밀한 결정성이 필요하면 --sort-by=.metadata.name을 붙여야 한다. replicas≥2에서는 head -1이 "항상 같은 pod"을 고른다는 보장이 없어 측정 격리가 깨질 수 있으므로(이것이 replicas=1 전제의 또 다른 이유), 어느 worker의 어느 pod이 선택됐는지를 실험자가 의식해야 한다.
haproxy_show_stat()은 Mac → homelab → lb-haproxy(203.0.113.211)의 double-hop SSH로 socat을 통해 HAProxy admin socket에 직접 접근한다. 비대화식 동작을 위해 원격에 sudo NOPASSWD 설정이 필요하고, 실패해도 || true로 진행한다(stat snapshot은 부가 데이터).
cleanup trap의 핵심은 빈 배열 안전 확장이다:
_CLEANUP_CMDS=()
cleanup() {
local cmd
for cmd in "${_CLEANUP_CMDS[@]+"${_CLEANUP_CMDS[@]}"}"; do
eval "$cmd" || true
done
}
trap cleanup EXIT
"${array[@]+"${array[@]}"}"는 set -u(nounset)에서 빈 배열을 참조해도 unbound 오류가 나지 않게 하는 관용구다. mapfile 대신 += append를 쓰는 것도 macOS 기본 bash 3.x 호환 때문. 배경 프로세스 PID를 register_cleanup "kill $PID 2>/dev/null || true"로 등록하면 정상 종료·실패·Ctrl-C 어느 경로든 정리된다 — 비결정적 종료 경로에서도 poller/follower 좀비를 안 남긴다.
5. S1 (01-baseline-long-request.sh) — current 모드 끊김 입증
목적: current 모드에서 in-flight long request가 끊김(expected_failure=true)을 입증. 즉 §2 판정선에서 LB DOWN이 active=0보다 먼저 오는 쪽을 의도적으로 재현한다.
- Pre-flight (이중 체크): ① Deployment의
metadata.labels.mode != "current"면 즉시 exit 1 (모드 오염 방지), ②READY_COUNT -lt 2등 deployment 정상 가동 확인. 두 검사는 목적이 다르다 — mode 라벨은 "current/improved 중 어느 manifest가 떠 있나"를, READY_COUNT는 "측정 가능한 상태로 떠 있나"를 본다. 단, 어느 쪽도 replicas=1을 강제하진 않는다(격리는 운영자 책임). S1·S2는 변수 오염 위험이 커 엄격히 실패시키고(S3는 경고만). READY_COUNT 체크의 의미와 한계는 W5 회상 Q&A에서 더 다룬다. - Log followers: hc·istio-proxy 컨테이너 로그를
--follow --tail=0으로 백그라운드 캡처(hc.log,envoy.log).tail=0이라 시작 이전 로그는 제외. - Long request 발사: 서브쉘로
curl /sleep?seconds=60을 백그라운드 실행하고echo "exit=$?"를 curl.out에 append.wait $PID의 반환값은 프로세스가 signal로 죽으면 불안정하므로, exit code를 파일에 직접 기록해 신뢰성을 확보(비결정적 종료 경로 대응). - T+5s pod delete:
--grace-period=210 --wait=false. grace-period는 Deployment의terminationGracePeriodSeconds: 210과 일치(아래 correction 참고),--wait=false로 즉시 반환해 curl 흐름으로 복귀. - Summary: HAProxy stat before/after diff를
awk -F, '{print $1","$2","$18}'로 추출(컬럼 18 = status).
S1 예상 결과: curl_http_code=502, time_total≈8.25s. 메커니즘 체인 — T+5s delete → preStop hc가 즉시 /health_check.html=503 → HAProxy detection window(inter 2s × fall 2 = 4s) 후 backend DOWN → on-marked-down shutdown-sessions → in-flight TCP RST → retry pool에 살아있는 backend 없어 502.
inter 2s/fall 2/on-marked-down shutdown-sessions 값은 HAProxy 설정의 http-backend server 라인에서 나온다 — 출처와 detection window 산식은 HAProxy walkthrough에 정의돼 있다. 타이밍 산식: curl 발사(T+0) → T+5 delete → 503 flip 직후 detection window ~4s → ~T+9 DOWN 마킹 → 즉시 RST. curl 입장의 time_total≈8.25s는 발사부터 RST까지의 벽시계 시간으로, "delete까지 5s + DOWN 감지·RST까지 ~3~4s" 합과 일치한다(probe 위상에 따라 ±2s).
FSM 상태명 매핑 — 2026-04-26에 게이트 비유로 개명됐고 옛 artifacts는 옛 명칭을 보존하므로, 로그/artifacts 해석 시 다음 대응을 의식해야 한다:
| 옛 명칭 (artifacts) | 새 명칭 (현재 코드/로그) |
|---|---|
| READY | OPEN |
| DRAINING | DRAINING |
| DRAINED_WAIT_LB | CLOSING |
| TERMINATING | CLOSED |
| FAILED | FAULT |
원본 회상 Q1의 "kubectl delete pod 기본값은 terminationGracePeriodSeconds(210s)를 따른다"는 표현은 부정확하다. kubectl delete pod의 클라이언트 측 기본 grace period는 spec 값이 아니라 30초로 하드코딩돼 있다(--grace-period를 생략하면 kubectl이 30을 보냄). spec의 terminationGracePeriodSeconds는 grace period를 명시하지 않은 다른 경로(예: API로 직접 삭제, controller의 pod 교체)에서 적용된다. 그래서 이 스크립트가 --grace-period=210을 명시하는 것이다 — 명시하지 않으면 30초로 잘려 drain이 끝나기 전에 SIGKILL이 나간다.
6. S2 (02-improved-long-request.sh) — improved 모드 보호 입증
S2는 §2 판정선에서 active=0이 LB DOWN보다 먼저 오는 쪽, 즉 보호 성공을 입증한다. S1과 골격은 같고 3가지가 다르다.
차이 1: pre-flight의 mode 라벨 체크가 mode != "improved"면 exit 1 (READY_COUNT deployment-health 체크는 S1과 동일).
차이 2 — Envoy active-request poller: 판정선 한쪽 축(active count)을 실시간으로 찍는 프로브다. pod이 사라질 때까지 2초 간격으로 Envoy admin API를 폴링한다.
(
while kubectl $(kctx) -n service-a get pod "${TARGET}" >/dev/null 2>&1; do
STAT="$(kubectl $(kctx) exec -n service-a "${TARGET}" -c istio-proxy -- \
curl -s 'http://127.0.0.1:15000/stats?filter=downstream_rq_active%7Cupstream_rq_active' \
2>/dev/null || echo 'exec-failed')"
printf '%s\t%s\n' "$(date -u +%FT%TZ)" "$STAT" >> "${ART_DIR}/envoy-active.tsv"
sleep 2
done
) &
%7C는 URL-encoded |. 폴링 대상은 Envoy admin(istio-proxy 컨테이너 내부 127.0.0.1:15000, localhost-only bind)의 /stats다. 15000은 sidecar admin 포트로 localhost에만 바인드되므로 메시·외부에서 직접 닿지 않고, 그래서 kubectl exec로 컨테이너 안에 들어가야만 접근된다. 메트릭 수집용 포트(15090 merged prometheus, 15020 agent merged metrics·probe)와 혼동하지 말 것 — 15000은 진단·디버그용 admin이다. downstream_rq_active(클라이언트→Envoy in-flight)와 upstream_rq_active(Envoy→backend in-flight)를 따로 노출하며, 이 tsv가 05의 envoy_active_count 이벤트 소스다.
차이 3 — FSM 전이 검증: grep 'event=transition' hc.log로 전이 타임라인을 뽑고, from=CLOSING to=CLOSED 전이가 hc.log에 있고 동시에 http_code=200이어야 validation_ok=true. 두 조건 중 하나라도 실패하면 exit 2. exit 1(pre-flight 환경 문제)과 exit 2(graceful drain 미작동)를 구분해 CI에서 원인을 분리할 수 있게 한 설계 — "환경이 잘못됐다"와 "drain이 안 됐다"는 다른 버그이고, exit code로 갈라야 자동화에서 분기된다.
S2 예상 결과: 200 / 60.008s. drain.sh가 active=0까지 /health_check.html=200을 유지해 HAProxy가 backend를 DOWN 마킹하지 않으므로 60초 요청이 끝까지 살아남는다. 즉 §2 판정선이 양수(active=0 → 그 후 DOWN)로 찍힌다.
7. S3 (03-continuous-traffic.sh) — rolling update disruption rate
S1/S2가 in-flight 1건의 운명을 본다면, S3는 지속 부하 하에서 rolling update의 총 disruption rate를 본다(판정선의 집계 버전). continuous load 중 kubectl rollout restart를 걸어 5xx + connection error를 측정한다.
부하 도구는 hey → wrk → curl-fallback 우선순위로 탐지한다. 각자 한계가 다르다:
- hey: --resolve/--cacert 없음 → IP 직접 URL + -host 헤더 + -insecure.
- wrk: --resolve 없음 → Lua로 Host 헤더 주입.
- curl-fallback: --resolve+--cacert로 TLS 검증은 정확하나, 20 loop가 serial이라 실효 throughput이 낮음.
warm-up 30s 후 rollout restart 트리거 + rollout status --timeout=300s를 foreground 대기(이때 부하 generator는 백그라운드 유지).
S3의 deadlock 함정: anti-affinity required + maxUnavailable=0 + replicas=N_workers면 rollout이 데드락한다. 새 ReplicaSet의 pod를 스케줄할 빈 노드가 없고(모든 노드가 점유), 기존 pod를 먼저 죽일 수도 없다(maxUnavailable=0). 홈랩에서는 master1 untaint + HAProxy backend 추가로 여유 노드를 만들어 해소했다.
python3가 있으면 인라인 Python으로 haproxy-stat-timeline.csv를 파싱해 worker DOWN 구간을 interval 배열로 계산(istio in pxname + worker in svname 필터, 연속 DOWN bucket 병합).
8. S4 (04-rst-capture.sh) — RST 패킷 캡처
S4는 판정선의 "끊김"을 패킷 증거로 못박는 프로브다. lb-haproxy, worker1, worker2에서 동시에 tcpdump를 돌려 TCP RST를 pcap으로 저장하고, S1/S2와 병렬 실행해 끊김이 관찰된 그 시점의 패킷 증거를 확보한다. 각 호스트마다 sudo -n true를 먼저 확인하고, 3곳 모두 실패해야 graceful skip(exit 0).
pcap pull은 base64 우선, dd fallback:
pull_pcap() {
if b64="$(eval "${ssh_cmd}" "\"base64 ${remote_path}\"")"; then
printf '%s' "$b64" | base64 -d > "${local_path}"; return 0
fi
eval "${ssh_cmd}" "\"sudo dd if=${remote_path}\"" > "${local_path}"
}
binary pcap을 SSH stdout으로 직송하면 SSH multiplex나 터미널 제어 문자가 바이트를 오염시킬 수 있다. base64 → decode가 안전하고, base64 명령이 없는 환경을 위해 dd를 둔다. 디코딩은 로컬 tcpdump/tshark → 둘 다 없으면 ssh homelab "tcpdump -r /dev/stdin" < pcap 원격 디코딩.
9. S5 (05-collect-timestamps.sh) — 6 소스 타임라인 통합 (판정선을 읽는 단계)
여기서 판정선이 실제로 읽힌다. 6개 이벤트 소스를 공통 시간축으로 정렬해 timeline.jsonl을 만들고, 거기서 핵심 구간(intervals)을 뽑는다:
| 이벤트 | 소스 | 파싱 |
|---|---|---|
hc_state |
hc.log | event=transition 줄 → ts/from/to 정규식 |
envoy_drain_listeners |
envoy.log | drain_listeners 포함 줄 |
envoy_active_count |
envoy-active.tsv | ISO-ts + downstream + upstream |
haproxy_backend_down/up |
haproxy-stat-timeline.csv | snapshot 경계 diff |
tcp_rst |
rst-*.pcap | tcpdump/tshark 파싱 |
k8s_pod_terminated |
kubectl-events.txt | service-a-igw + Kill/Terminat 필터 |
핵심 interval 계산에서 active_zero 조건은 downstream + upstream == 0이다. 둘 중 하나만 0인 과도 상태(upstream은 정리됐으나 downstream이 남음)를 active=0으로 오판하지 않기 위해서다 — 판정선 한쪽 축의 정의를 엄격히 한 것. python3가 없으면 awk fallback으로 전환하되, sort -t'"' -k4,4로 "ts":"..." 4번째 필드 기준 정렬한다(ISO-8601은 사전순=시간순이라 정확). 단 intervals.json은 null로 채우고 pcap은 tshark fallback 없이 tcpdump만 시도한다.
통합 결과 읽는 법 — timeline.jsonl은 한 줄당 하나의 정규화된 이벤트(JSON Lines)다:
{"ts":"2026-05-31T07:12:34Z","event":"hc_state","from":"OPEN","to":"DRAINING"}
{"ts":"2026-05-31T07:12:36Z","event":"envoy_drain_listeners"}
{"ts":"2026-05-31T07:12:51Z","event":"envoy_active_count","downstream":0,"upstream":0}
{"ts":"2026-05-31T07:12:53Z","event":"haproxy_backend_down","svname":"worker2"}
{"ts":"2026-05-31T07:12:53Z","event":"tcp_rst","host":"worker2"}
intervals.json은 이 timeline에서 뽑은 핵심 구간(초)이다. current(S1)에서는 끊김이, improved(S2)에서는 안전 순서가 드러난다:
{
"drain_to_active_zero": 17.2,
"active_zero_to_lb_down": 2.1,
"lb_down_to_first_rst": 0.0
}
S2(improved)는 drain → active=0 → (그 후에야) HAProxy DOWN 순서라 active_zero_to_lb_down이 양수여서 in-flight가 보호되고, S1(current)은 active>0인 상태에서 DOWN+RST가 나 lb_down_to_first_rst≈0으로 끊김이 찍힌다. 이 세 간격의 부호·순서가 "drain이 작동했는가"의 판정선이다 — 바로 §2 mental model이 숫자로 환원된 형태다.
두 경로의 이벤트 순서를 나란히 두면 판정선이 한눈에 들어온다 — current는 LB DOWN과 RST가 active>0 위에서 겹치고, improved는 active=0이 LB DOWN보다 먼저 온다:
10. 검증·재현
# S1: 502 재현
kubectl --context homelab -n service-a scale deploy/service-a-igw --replicas=1
bash tests/01-baseline-long-request.sh
# S2: 200 + FSM 전이 검증
kubectl --context homelab -n service-a scale deploy/service-a-igw --replicas=1
bash tests/02-improved-long-request.sh
# S3: continuous + rollout restart (replicas=2, master1 untaint)
bash tests/03-continuous-traffic.sh improved
# S4: RST 캡처 (별도 터미널)
bash tests/04-rst-capture.sh S1-current 120
# Post-test: 타임라인 통합
bash tests/05-collect-timestamps.sh tests/artifacts/<YYYYMMDD-HHMMSS>/<scenario>/
기대 판정: S1은 intervals.json의 lb_down_to_first_rst≈0 + curl 502/≈8.25s로 "active>0 위에 RST"가 찍히고, S2는 active_zero_to_lb_down이 양수 + curl 200/60.008s + hc.log에 from=CLOSING to=CLOSED 전이가 떠야 성공이다.
핵심 정리
- 하니스 전체의 측정 대상은 단 하나의 판정선 —
active=0이HAProxy backend DOWN보다 먼저(S2 보호) 오나 나중에(S1 끊김) 오나. 5개 스크립트 = 6개 프로브,05가 한 timeline으로 합쳐 부호를 읽음. - replicas=1 격리는 "in-flight 요청의 운명"을 단일 pod 인과로 추적하기 위한 필수 전제. 스크립트는 강제하지 않으므로 운영자가 사전에 scale. S3만 disruption rate 목적상 replicas≥2.
- pre-flight는 mode 라벨 체크 + READY_COUNT deployment-health 체크의 이중 구조. exit code 분리(1=pre-flight, 2=validation 실패)와
expected_failure/validation_ok이중 검증으로 환경 문제와 drain 미작동을 구분. - bash 관용구(빈 배열 안전 확장, exit code 파일 기록, cleanup trap)는 모두 macOS bash 3.x 호환 + 비결정적 종료 경로 대응이 목적.
--grace-period=210을 명시하는 이유는 kubectl 기본값 30초가 drain을 자르기 때문(spec의 terminationGracePeriodSeconds는 명시 없는 경로에만 적용).
What you might be missing
time_total≈8.25s는 결정값이 아니다. HAProxy detection은 probe 위상(phase)에 의존한다 — 503 flip이 직전 probe 직후에 떨어지면 거의 풀 4s를 기다리고, 직전이면 거의 0s에 잡힌다. 그래서 8.25s는 "delete 5s + DOWN 감지 ~3.25s"의 한 표본일 뿐 ±2s 변동이 정상이다.- 15000 vs 15090/15020 혼동. S2 poller가 15000 admin을 쓰는 건
_active게이지(in-flight 카운트)가 prometheus merged metrics(15090/15020)에는 그 형태로 안 나오기 때문이다. admin은 localhost-only라exec필수 — 메시 네트워크로 긁으려다 실패하는 함정. head -1비결정성. kubectl-o name순서는 보장된 사전순이 아니다. replicas≥2에서 head -1을 믿으면 매 실행 다른 pod을 delete할 수 있어 격리가 깨진다. 엄밀하게는--sort-by=.metadata.name.- FSM 명칭 시점 의존. 옛 artifacts(READY/DRAINED_WAIT_LB/...)와 현재 로그(OPEN/CLOSING/...)를 섞어 grep하면
from=CLOSING to=CLOSED검증이 옛 로그에선 매칭되지 않는다. 위 매핑 표로 정규화 후 비교할 것.
이어 보기
- 설계 철학·시나리오 변수격리 매트릭스·회상 Q&A: W5 테스트 시나리오
- 재현 quickstart: graceful-termination quickstart
- drain·listener 메커니즘 세부: Envoy drain listeners
- HAProxy on-marked-down·detection window 출처: HAProxy walkthrough
- 허브: Graceful Termination MOC