W1. Big Picture — 트래픽 경로 + 6 events + 4 시나리오
graceful-termination 학습 시리즈(W1~W6)의 첫 hub 노트. 무중단 Pod 종료를 한 문장으로 환원한다: 우리는 LB를 직접 제어할 수 없고 오직 health 신호(200/503) 하나만 LB에 줄 수 있으므로, “끊김 없는 종료"는 6개 timestamp를 옳은 순서로 정렬해 LB가 backend를 늦게 빼게 만드는 타이밍 문제다. 이 문서는 그 무대(트래픽 5-hop 경로), 그 변수(종료 시 6개 시점), 그 측정(4 시나리오)을 세운다.
시리즈 위치: 대응 코드 워크스루는 코드 워크스루(quickstart). 다음 단계는 W2 hc FSM, 전체 인덱스는 graceful-termination MOC. FSM 명명: 2026-04-26 이후
OPEN/DRAINING/CLOSING/CLOSED/FAULT(게이트 비유). 옛 명칭(READY/DRAINING/DRAINED_WAIT_LB/TERMINATING/FAILED)은 옛 artifacts에만 남아 있음. 신규 endpoint:POST /reopen— DRAINING/CLOSING → OPEN abort. CLOSED는 unrecoverable(409). CLOSING→OPEN 응답엔Warning: 199 ...헤더. 이때 LB는inter 2s × rise 2 = +4s후 backend를 다시 UP 마킹(재투입). reopen 시나리오·전이 상세는 W2 hc FSM §전이/reopen.
1. 배경 지식 — 왜 Pod 종료가 어려운가
kubectl delete pod 한 줄이면 끝날 것 같지만, 그 순간 in-flight 요청이 끊긴다. 원인을 이해하려면 K8s Pod 종료가 **여러 독립 주체의 비동기 신호 경쟁(async signal race)**이라는 사실에서 출발해야 한다.
Pod가 사라질 때 최소 네 주체가 서로의 진행을 모른 채 동시에 움직인다.
- kubelet — 종료를 결정하고 preStop hook을 돌린 뒤 SIGTERM을 보낸다.
- K8s control plane — readiness가 깨지면 EndpointSlice에서 이 Pod를 비동기로 뺀다.
- istio-proxy 컨테이너(PID 1 = istio-agent) — SIGTERM을 받는 것은 Envoy 자신이 아니라 컨테이너의 PID 1인 istio-agent(pilot-agent)다. istio-agent가 Envoy admin API(
/drain_listeners?graceful)를 호출해 drain을 지시하고,terminationDrainDuration만큼 대기한 뒤/quitquitquit로 Envoy를 종료시킨다 — Envoy 프로세스 자체가 SIGTERM에 반응해 자율적으로 drain을 시작하는 것이 아니다. - 외부 LB(HAProxy/Citrix) — health check 응답만 보고 backend를 UP/DOWN으로 판정한다.
위 “네 주체가 완전히 비동기"라는 프레이밍은 sidecar가 일반 컨테이너로 주입되던 시절의 논의다. Istio 1.27부터 ENABLE_NATIVE_SIDECARS가 기본 true로 승격되어 istio-proxy가 Kubernetes native sidecar(initContainers + restartPolicy: Always, K8s 1.29+ beta로 기본 활성화)로 주입되는 것이 기본값이다. 이 경우 kubelet은 앱 컨테이너가 모두 종료된 뒤에야 istio-proxy에 SIGTERM을 보내는 순서를 보장하므로, kubelet↔istio-proxy 사이의 “서로 모른 채 동시에 움직인다"는 서술은 Istio 1.30 기본 프로필에서는 더 이상 정확하지 않다. 다만 외부 LB가 health 신호 하나로만 판단한다는 핵심 제약(§1의 결론)은 native sidecar 여부와 무관하게 그대로 유효하다 — 아래 논의는 이 제약 위에서 성립한다.
여기서 두 가지 제약이 모든 설계를 지배한다. 이 둘이 W1~W6 전체의 출발점이다.
- 권한 제약 — 서비스 팀은 보통 LB에
backend down같은 명령을 내릴 권한이 없다. LB를 직접 조작할 수 없다. - 판단 근거 제약 — LB는 backend의 내부 상태를 모른다. 오직 health check 응답 200 vs 503 하나로만 UP/DOWN을 판단한다.
두 제약을 합치면 결론이 강제된다: 우리가 LB에 줄 수 있는 입력은 health 신호 한 채널뿐이다. 따라서 “in-flight를 안 끊는다"는 목표는 LB를 멈추는 문제가 아니라, health 신호의 타이밍으로 LB가 backend를 빼는 순간을 미루는 문제로 바뀐다. LB가 backend를 빼는 순간(= on-marked-down shutdown-sessions로 기존 connection을 RST하는 순간)이 in-flight 요청이 다 끝난 뒤에 오도록 신호를 늦추면 무중단이 된다.
선행 개념: K8s preStop hook / SIGTERM / readiness probe / EndpointSlice, Envoy listener drain, HAProxy backend health check(
inter/rise/fall,on-marked-down). 각 부품의 디테일은 후속 W2~W6에 위임하고, W1은 이들이 어떤 순서로 맞물려야 하는가만 본다.대상 환경: Istio 1.30 IGW + 외부 HAProxy(L7 TLS offload) + bare-metal worker NodePort. 대상 독자: 무중단 배포/종료를 직접 튜닝하는 DevOps/SRE. 범위: W1은 문제 정의와 측정 골격까지; 코드·FSM·운영 산출식은 cross-ref.
2. 멘탈모델 + 트래픽 경로 — 무대 세우기
ANCHOR (머리에 하나만 담을 그림): LB는 눈먼 backend pool 관리자다. 그가 보는 신호는 health 200/503 하나, 그가 할 수 있는 행동은 “backend를 빼고 그 connection을 끊는다” 하나. 그러므로 graceful termination = “backend의 in-flight가 0이 될 때까지 health=200을 거짓말처럼 유지하다가, 0이 된 그때 비로소 503으로 떨어뜨려 LB가 뒤늦게 빼게 만드는” 타이밍 조율. 나머지 모든 디테일은 이 한 그림에서 따라 나온다.
이 조율이 어디서 일어나는지 보려면 트래픽이 지나는 hop을 알아야 한다. health 신호를 읽는 주체(HAProxy)와 drain 대상(IGW Envoy)이 경로상 어디에 앉아 어떤 포트로 분리돼 있는지가 §3 events의 무대다.
Mac client (curl)
| HTTPS, --resolve example.local:443:203.0.113.211
v
[ lb-haproxy 203.0.113.211 ]
| HAProxy bind *:443 ssl alpn h2,http/1.1
| L7 TLS offload, X-Forwarded-Proto/Host/For 주입
| backend istio-http-backend balance roundrobin
| option httpchk GET /health_check.html
| check port 30180 inter 2s rise 2 fall 2
| on-marked-down shutdown-sessions
v
[ worker NodePort 30080 (traffic) / 30180 (health) ]
| externalTrafficPolicy: Local (이 노드에 ready endpoint 있어야 응답)
v
[ IGW Pod (replicas=1 or 2, podAntiAffinity required hostname) ]
| +- container istio-proxy (Envoy, port 8080, admin 15000)
| | proxy.istio.io/config terminationDrainDuration: <drain 합계보다 길게>
| | (Istio default 5s에서 상향; 산출식은 W6/runbook)
| +- container hc (port 18180, FSM 5-state)
v
[ backend Service ClusterIP 8080 ] -> Deployment replicas=2, podAntiAffinity preferred
v
[ backend Pod ]
/fast : 즉시 200
/sleep?N : N초 sleep 후 200
/stream?N&M : M초마다 chunk flush, 총 N초
이 경로에서 두 채널이 분리돼 있다는 점이 핵심이다. 트래픽은 30080 → envoy:8080으로 흐르고, health probe는 30180 → hc:18180으로 따로 흐른다. 즉 hc 사이드카는 트래픽 경로 밖에서 LB에게 “이 backend 살아있어?“의 답만 쥐고 있다 — 바로 §1에서 말한 “유일한 입력 채널”. hc가 health를 언제 503으로 떨어뜨리느냐가 LB가 backend를 빼는 시점을 결정한다.
핵심 포트 맵:
| 호스트:포트 | 역할 |
|---|---|
Mac → 203.0.113.211:443 |
HAProxy L7 TLS offload (example.local 인증서) |
Mac → 203.0.113.211:6443 |
apiserver (TCP passthrough, 별도 frontend) |
HAProxy → worker:30080 |
IGW Envoy traffic (TLS termination 후 plaintext) |
HAProxy → worker:30180 |
hc 사이드카 health check |
HAProxy → master1:30080/30180 |
2026-04-25 master1 untaint 후 추가 |
| 외부 → Pod | (a) envoy:8080 트래픽 진입, (b) hc:18180 헬스 probe |
| Pod 내부 단방향 | hc → envoy admin:15000 (drain.sh의 graceful drain 제어. 역방향 없음) |
마지막 줄이 메커니즘의 심장이다: hc → envoy admin 15000이 단방향으로 연결돼 있어, hc가 Envoy에게 “drain 시작해” / “active 몇 개야?“를 물을 수 있다. 이 채널이 있어야 §3 improved 모드의 “active=0 폴링 후 health를 떨어뜨린다"가 가능해진다.
15000 vs 15021 포트 역할 구분:
15000은 Envoy admin API(관리:/drain_listeners,/stats,/config_dump) — drain 제어·active count 폴링이 여기로 간다.15021은 status/health 포트(별도)로, W3에서 IGW가 NodePort 32021로 노출한다(W3 IGW deployment §status 노출 참조). 본 시리즈의 hc graceful drain은 15000(admin)만 쓰며, 15021은 istio-proxy 자체 readiness/health 용도다 — 두 포트를 혼동하지 말 것.
3. 핵심 메커니즘 — 6 events를 정렬 문제로 보기
종료 순간 §1의 네 주체가 만드는 사건을 측정 가능한 6개 timestamp로 못 박는다. 이 6개의 상대 순서가 곧 무결성의 전부다.
| # | 이벤트 | 측정 위치 |
|---|---|---|
| 1 | health_fail: hc /health_check.html 200→503 flip |
hc.log event=transition |
| 2 | readiness_fail: hc /health 200→503 (K8s endpoint 제거 트리거) |
hc.log + EndpointSlice |
| 3 | envoy_drain_start: /drain_listeners?graceful&skip_exit 호출 |
envoy.log “drain” 라인 |
| 4 | active_zero: downstream_rq_active + upstream_rq_active == 0 |
Envoy /stats?filter=... |
| 5 | lb_down: HAProxy show stat에서 backend status UP→DOWN |
stat-timeline.csv |
| 6 | rst: tcpdump 첫 TCP RST (또는 HTTP/2 stream CANCEL) |
tcpdump pcap |
skip_exit의 “종료 방지” 효과는 admin API 레벨만으로는 관측되지 않았다Envoy 공식 문서는 skip_exit이 있으면 drain 기간이 끝나도 Envoy 프로세스를 종료시키지 않는다고 설명하지만, homelab 클러스터 실측(T27, 상세는 문서 하단 검증 기록)에서는 skip_exit 유무와 무관하게 terminationDrainDuration이 한참 지나도 istio-proxy의 Envoy PID가 그대로 살아 있었다. 위 §1 정정에서 다뤘듯 실제 프로세스 종료(/quitquitquit)는 drain_listeners 호출 자체가 아니라 istio-agent가 별도로 트리거하는 후속 단계이기 때문으로 보인다 — Envoy 혼자서는 drain 종료 후 자동으로 종료(exit)하지 않으므로, 표준 종료 경로(istio-agent가 관리)에서는 skip_exit의 차이가 admin API만 보고는 드러나지 않는다. in-flight 요청이 drain 도중에도 끝까지 완주한다는 “graceful” 효과 자체는 실측으로 지지된다.
왜 “순서” 문제인가: 이벤트 5(LB DOWN)는 항상 이벤트 6(RST)을 유발한다 — HAProxy on-marked-down shutdown-sessions가 backend를 빼는 즉시 그 backend로 가던 모든 connection을 RST하기 때문이다. 따라서 우리가 통제할 수 있는 단 하나의 레버는 **“5가 4(active=0)보다 뒤에 오게 하는 것”**이다. 4보다 5가 먼저 오면(=in-flight가 남았는데 LB가 backend를 뺌) 6의 RST가 살아있는 요청을 죽인다. 4 다음에 5가 오면 RST가 일어나도 끊을 게 없다.
flowchart LR
subgraph "통제 대상: 이 순서를 만든다"
E3["3. drain start"] --> E4["4. active=0"]
E4 --> E1["1. health 503"]
E1 --> E5["5. LB DOWN"]
end
E5 -->|"on-marked-down<br/>shutdown-sessions"| E6["6. RST"]
E6 -.->|"active=0이면<br/>끊을 게 없음 = 무결"| OK["no disruption"]
E5 -.->|"active>0인데 먼저 DOWN<br/>= in-flight 죽음"| BAD["disruption"]핵심은 이벤트 5는 우리가 직접 못 누른다는 것. LB는 health 503을 두 번(fall 2) 본 뒤에야 DOWN을 찍으므로, 이벤트 1(health 503)을 언제 발생시키느냐로 이벤트 5를 간접 제어한다. 그래서 통제 가능한 사슬은 3 → 4 → 1이고, 5 → 6은 그 사슬이 정렬돼 있는 한 무해해진다.
current 모드 — 순서 어긋남으로 끊김
기존 preStop은 “drain → close-lb → sleep 30"인데, health를 먼저 떨어뜨려 버린다(이벤트 1을 4보다 앞에 둔다). Envoy drain(이벤트 3·4)은 SIGTERM 이후에야 시작돼 너무 늦다.
sequenceDiagram
autonumber
participant K as kubelet
participant HC as hc
participant E as Envoy
participant LB as HAProxy
participant C as Client
K->>HC: preStop "/drain -> /close-lb -> sleep 30"
HC->>HC: state=DRAINING -> CLOSING (즉시)
Note over HC: /health_check.html 200->503 (event 1)
Note over HC: /health 200 그대로 (event 2 미발생)
LB->>HC: GET /health_check.html -> 503 (1번째 fail)
LB->>HC: GET /health_check.html -> 503 (2번째 fail, fall=2)
LB->>LB: backend DOWN (event 5)
LB-->>E: shutdown-sessions: in-flight TCP RST (event 6)
LB-->>C: 502 Bad Gateway (S1 long-request)
Note over E: SIGTERM은 실제로 istio-agent(PID 1)가 받아 Envoy에 drain을 지시하며<br/>terminationDrainDuration(상향값, W6 산출식)간 대기하지만 connection은 이미 LB가 끊은 상태실제 순서: 1 → 5 → 6. event 3·4(Envoy drain)가 빠진 채 LB가 먼저 backend를 빼서 in-flight가 RST된다.
시나리오별 client 표현 차이 — 위 시퀀스는 long-request(S1), 결과는 502. streaming(S4)은 같은 RST 사건이지만 200 + chunk 일부가 client에 도달한 후 끊김 → curl
exit 92(HTTP/2 stream CANCEL)또는 HTTP/1.1이면exit 18(transfer closed). HAProxyalpn h2,http/1.1협상 결과로 표현이 달라짐. 자세한 차이는 W5 테스트 시나리오 설계 §HTTP/2 vs HTTP/1.1 RST 표현.
improved 모드 — 순서 정렬로 무결
핵심 뒤집기: health=200을 유지한 채 먼저 Envoy를 drain하고(이벤트 3), active=0을 폴링으로 확인한 뒤에야(이벤트 4) health를 503으로 떨어뜨린다(이벤트 1). 이게 §2 anchor의 “거짓말처럼 200을 유지하다가 0이 된 그때 떨어뜨린다”.
sequenceDiagram
autonumber
participant K as kubelet
participant HC as hc
participant E as Envoy
participant LB as HAProxy
participant C as Client
K->>HC: preStop /opt/hc/graceful-drain.sh
HC->>HC: state=DRAINING (/health_check.html 200 그대로)
HC->>E: POST /drain_listeners?graceful&skip_exit (event 3)
loop 폴링 until active=0
HC->>E: GET /stats?filter=...rq_active
end
Note over E: 60초 응답 완료
HC-->>C: 200 OK (curl 정상 수신)
Note over E: active=0 (event 4)
HC->>HC: state=CLOSING
Note over HC: /health_check.html 503 (event 1)
LB->>HC: 503 (4초 후) -> backend DOWN (event 5)
LB-->>E: shutdown-sessions 트리거되지만 active 이미 0이라 끊을 게 없음
HC->>HC: sleep LB_BUFFER (10s)
HC->>HC: state=CLOSED (/health 503, event 2)
Note over K,HC: K8s가 readiness 503 관측 -> endpoint 비동기 제거<br/>(SIGTERM 타이밍과 독립; active 이미 0이라 무결성에 non-critical)
K->>E: SIGTERM (preStop 종료 후 — 실제 수신자는 istio-agent, Envoy는 그 지시를 받음)실제 순서: 3 → 4 → 1 → 5 → (6 무력) → 2. event 6이 발생해도 active=0이라 끊을 게 없다. drain → active=0 → health 503 사슬이 정렬됐으므로 5 → 6은 무해.
event 2(readiness 503)의 위치 주의: 위 시퀀스에서 event 2를 마지막에 그렸지만, 이는 이미 active=0인 상태에서의 후행 정리 단계다. readiness 503 → K8s endpoint 제거는 SIGTERM과 인과적으로 묶인 순서가 아니라 비동기로 일어나며(preStop과 SIGTERM은 거의 동시 시작), 무결성에 critical하지 않다. CLOSED 상태에서만
/health가 503을 반환하는 응답 매핑은 W2 hc FSM §응답표와 정합된다.
4. 예시·결과 — 4 시나리오로 변수 격리 측정
메커니즘이 맞는지는 측정으로 증명한다. 4 시나리오(S1~S4)는 각각 한 변수만 격리한다 — 같은 종료 사건을 long-request / drain 단독 / rollout / streaming 네 각도로 본다.
| # | replicas | 시나리오 | current | improved | 시사점 |
|---|---|---|---|---|---|
| S1 | 1 | long-request /sleep?seconds=60 + Pod delete @ T+5s |
502 / 8.25s (HAProxy retry exhausted) | 200 / 60.01s | LB가 backend 빼는 순간 in-flight RST |
| S2 | 1 | improved 단독 측정 (drain 거동) | — | active=1 60초 유지, hc OPEN→DRAINING | drain.sh가 active 폴링으로 health 200 유지 |
| S3 | 2 | continuous traffic 90s + rollout restart | conn_err=9 / 5xx=0 / p50=5.7ms | conn_err=0 / 5xx=0 / p50=5.1ms | HAProxy retries 3이 5xx 흡수 — disruption은 conn_err로 봐야 |
| S4 | 1 | streaming /stream?seconds=60&interval=1 + Pod delete @ T+8s |
chunks=12 / curl_exit=92 (HTTP/2 CANCEL) | chunks=59/60 / curl_exit=0 | T+8s delete + ~4s detect = chunk 12 시점 끊김 |
검증의 결론: S1이 가장 깨끗한 대조군이다 — current는 502 / 8.25s(LB가 5번 → 6번을 만들어 in-flight를 죽임), improved는 200 / 60.01s(60초 sleep 응답이 온전히 완료). 같은 종료 사건에서 6 events 정렬 여부 하나만 바뀌었는데 결과가 끊김↔무결로 갈린다.
왜 S1·S2·S4는 replicas=1, S3만 replicas=2? roundrobin 환경에서 multi-replica + 한 pod만 delete하면 curl traffic이 다른 pod으로 가서 영향 격리가 안 된다 → single-pod로 통제. S3는 운영 rollout 시나리오라 multi-replica가 필요하다. (시나리오별로 어느 변수를 격리하고 무엇을 못 잡는지의 매트릭스: W5 테스트 시나리오 설계 §1.)
S3의 함정 — 측정 지표 자체가 거짓말한다: 양 모드 모두 5xx=0이라 current가 멀쩡해 보이지만, 실제 disruption은 conn_err=9에 숨어 있다. HAProxy retries 3(defaults block)이 backend DOWN 시 다른 worker로 자동 retry해 5xx를 흡수하기 때문. disruption은 5xx가 아니라 connection/전송 레벨 오류로 측정해야 한다. 다만 예시로 든 curl exit 코드는 정정이 필요하다 — exit 7(connect 실패)은 신규 TCP 연결 수립 자체의 실패를 뜻하는 코드라 이미 맺어진 연결이 RST되는 상황과는 실패 지점이 다르므로 이 목록에서 빼야 한다. homelab 실측(T33, 상세는 문서 하단 검증 기록)에서는 backend를 --grace-period=0으로 강제 종료했을 때 56/92/18이 아니라 curl exit 28(timeout) — backend가 응답 없이 사라져 client가 그대로 행(hang)하다 타임아웃되는 패턴이 관측됐다. 즉 정확히 어떤 exit code가 뜨는지는 종료 경로(강제 kill vs graceful RST vs HTTP/2 스트림 종류)에 따라 달라지며, 핵심은 특정 코드 하나로 단정하는 것이 아니라 “5xx로는 안 보인다"는 방향이다.
회상 quiz
Q1. T+5s에 `kubectl delete pod` 시 current 모드에서 HAProxy가 backend DOWN 마킹하는 시점은?
A: 약 T+9s. preStop이 즉시 hc 503 flip → HAProxy inter 2s 첫 fail check → 4초 후(fall 2) DOWN. 정확히는 probe 타이밍에 따라 ±2초 변동.
Q2. improved 모드에서 60초 동안 Envoy `downstream_rq_active`가 1로 유지된 이유는?
A: backend의 /sleep?seconds=60 핸들러가 60초 sleep 후 응답하므로, Envoy 입장에서 in-flight HTTP request 1건이 60초 내내 미완료. drain.sh 폴링 루프가 active>0 조건에서 health 200 유지 → HAProxy backend UP 유지 → in-flight 안전.
Q3. S3에서 양 모드 모두 5xx=0인데 왜 current가 broken인가?
A: HAProxy retries 3(defaults block)이 backend DOWN 시 다른 backend(UP인 worker)로 자동 retry → client는 200. 진짜 disruption은 connection level에서 RST된 9건의 connection_err로 나타남(curl exit 코드는 종료 경로에 따라 다르며 7은 아님 — §4 정정 참조). disruption 측정 시 5xx + connection_err 둘 다 봐야.
핵심 정리
- 한 문장 멘탈모델: LB는 health 신호만 보는 눈먼 pool 관리자다. graceful termination = active=0이 될 때까지 health=200을 유지하다 0이 된 그때 503으로 떨어뜨려, LB가 backend를 뒤늦게 빼게 만드는 타이밍 조율.
- Pod 종료의 무결성은 6개 timestamp(health 503 → readiness 503 → Envoy drain → active=0 → LB DOWN → RST)의 정렬 문제이며, 통제 가능한 사슬은
3(drain) → 4(active=0) → 1(health 503)하나뿐이다. - 이벤트 5(LB DOWN)→6(RST)은
on-marked-down shutdown-sessions로 항상 묶여 있다. 4(active=0) 뒤에 5가 오면 RST가 끊을 게 없어 무해, 앞에 오면 in-flight가 죽는다. - 변수 격리를 위해 S1·S2·S4는 replicas=1, rollout 모사용 S3만 replicas=2.
- HAProxy
retries 3이 5xx를 흡수하므로 disruption은 5xx가 아니라 connection error로 측정해야 한다.
What you might be missing
terminationDrainDuration는 W1에서 단정하지 않는다. 이 값은 drain 합계보다 길게(Istio default 5s에서 상향) 잡되, 구체 수치 산출식은 W6 프로덕션 적용·runbook에 위임된다. 한 문서에 옛 실험값이 박제되면 다른 문서와 어긋나므로 정본을 cross-ref로만 참조하라.- 15000(admin) ≠ 15021(status). drain 제어·active 폴링은 admin 15000으로 가고, 15021은 istio-proxy의 status/health(W3에서 별도 NodePort 노출)다. 두 포트를 같은 “health"로 묶으면 진단이 어긋난다.
- event 2(readiness 503)는 무결성의 인과 경로가 아니다. improved 모드에서 무중단을 보장하는 것은 event 3·4(drain → active=0)와 event 1(health 503)의 순서이며, K8s endpoint 제거는 이미 active=0인 뒤의 비동기 후행 정리일 뿐이다. SIGTERM과 endpoint 제거의 선후를 인과로 오해하지 말 것.
- disruption은 5xx로 안 보인다. HAProxy
retries 3가 backend DOWN 시 다른 worker로 재시도해 5xx를 흡수한다. 진짜 끊김은 connection/전송 레벨 오류(강제종료 실측에서는 exit 28/timeout, RST 시에는 56/92 계열 — exit 7은 신규 connect 실패라 해당 없음)로만 드러난다. - 두 채널의 분리가 전제다. health probe(30180→hc:18180)와 traffic(30080→envoy:8080)이 따로이고, hc→envoy admin 15000이 단방향으로 연결돼 있어야 “active를 폴링해 health를 늦게 떨어뜨리는” 트릭이 성립한다. 이 토폴로지가 없으면 메커니즘 자체가 불가능하다.
이어 보기
- 코드 워크스루: quickstart 코드 워크스루
- 다음: W2 hc FSM 멘탈 모델 — hc FSM 5-state + drain.sh 7단계
- 프로젝트 인덱스: graceful-termination MOC
- 운영 매핑: HAProxy 워크스루(on-marked-down shutdown-sessions), Envoy drain listeners(graceful drain)
검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6)
검증 방법: 각 주장을 공식 문서(Envoy/Istio/HAProxy/curl)와 대조하고, homelab 클러스터(Istio 1.30 default profile, k8s 1.30.6)에서 재현 가능한 항목은 실측으로 교차 검증함.
| 주장 | 판정 | 근거 |
|---|---|---|
| C1. 핵심 멘탈모델 — health 신호 하나로 LB의 backend 제거 타이밍을 조율한다 | ✅ 실측 확인 | envoyproxy.io/…/draining · apps-walkthrough T27 |
C2. drain_listeners?graceful&skip_exit가 신규 요청을 억제하고 skip_exit로 프로세스를 유지시킨다 |
🔬 실측 반증 — 본문 교정 | envoyproxy.io/…/admin · apps-walkthrough T27 |
C3. downstream_rq_active/upstream_rq_active는 in-flight 판정용 게이지다 |
✅ 실측 확인 | envoyproxy.io/…/http_conn_man/stats · apps-walkthrough T27 |
| C4. 15000=Envoy admin API, 15021=status/health로 역할이 분리된다 | ✅ 실측 확인 | envoyproxy.io/…/admin · envoy-admin-api-diagnosis T62 |
C5. terminationDrainDuration 기본값은 5초다 |
✅ 실측 확인 | github.com/istio/api/…/proxy.proto · runbook T30 |
| C6. “Envoy가 SIGTERM을 받아 스스로 drain을 시작한다” | ❌ 오류 — 본문 교정 | github.com/istio/istio/…/agent.go · runbook T30 |
| C7. kubelet/제어plane/Envoy/LB 네 주체가 완전히 비동기로 움직인다 | ⚠️ 구버전 서술 — 갱신 | istio.io/…/1.27 change-notes · runbook T30 |
C8. on-marked-down shutdown-sessions는 DOWN 마킹 즉시 기존 세션을 RST한다 |
✅ 문헌 확인 | discourse.haproxy.org/…/on-marked-down |
C9. HAProxy retries 기본값은 3이다 |
✅ 문헌 확인 | haproxy.com/…/retries |
C10. inter/rise/fall의 의미(간격/UP 판정/DOWN 판정 횟수) |
✅ 문헌 확인 | haproxy.com/…/health-checks |
| C11. disruption은 5xx가 아니라 connection error(curl exit 7/92/18)로 측정해야 한다 | ❌ 오류 — 본문 교정 | curl.se/libcurl/…/errors · tests-walkthrough T33 |