🏠 목록 Envoy Graceful Drain 📄 MD 원본 🌓 테마
istioenvoygraceful-terminationdrain-listeners

Envoy Graceful Drain

NOTE

Pod 종료 시 502를 없애는 Envoy 데이터플레인 진입점은 admin API POST /drain_listeners?graceful&skip_exit 하나다. 핵심 오해를 먼저 깬다: 이 API는 in-flight 요청을 "보호"하지 않는다. 새 연결 유입만 discourage해서 downstream_rq_active 곡선을 단조감소시키는 유입 차단기일 뿐이다. 실제 in-flight 보호는 상위 hc FSM이 health 신호를 200으로 유지해 LB가 backend를 빼지 않게 막는 데서 온다. 이 문서는 그 차단기 자체 — graceful 모드의 신규 연결 신호 방식(H1 Connection: close / H2 GOAWAY), skip_exit, 완료 판정이 왜 cx가 아니라 rq인지 — 에 집중한다.

대상환경 Istio 1.30 / Envoy sidecar · 대상독자 graceful termination을 메커니즘 수준에서 이해하려는 DevOps/SRE · 범위 Envoy admin drain_listeners API 한 계층 · 선행개념 sidecar(15000 admin), HTTP keepalive, HTTP/2 GOAWAY · 상위 계층 drain.sh 오케스트레이션 · hc FSM

1. 배경: "누가 in-flight를 지키는가"라는 질문

Pod가 죽을 때 가장 단순한 종료는 listener를 즉시 close하는 것이다. 그러면 그 순간 처리 중이던 요청이 TCP RST로 끊겨 클라이언트가 502를 본다. graceful termination의 목표는 "신규 연결은 받지 않으면서 진행 중인 요청은 끝까지 처리"하는 것이고, 이를 달성하려면 두 개의 독립된 일을 해야 한다.

여기서 핵심 함정: drain_listeners?graceful(a)만 한다. Envoy listener에 "이제 신규는 discourage하라"고 지시할 뿐이다. (b)는 Envoy admin API의 일이 아니라 상위 hc FSM의 일이다 — drain.sh가 /health_check.html을 200으로 유지해서 외부 LB가 backend를 endpoint pool에서 빼지 않게 붙잡아둔다. LB가 backend를 UP으로 보는 한 기존 연결은 살아 있고, 그래서 in-flight가 보호된다.

이 분리를 못 잡으면 "drain_listeners를 부르면 알아서 무중단이 된다"고 오해하게 된다. 실제로는 (a) 없이 (b)만 하면 폴링이 영원히 안 끝나고, (b) 없이 (a)만 하면 LB가 backend를 빼버려 in-flight가 RST된다. 둘 다 있어야 한다. 이 문서는 (a)를 책임지는 한 계층을 정확히 본다.

2. 멘탈모델 앵커 + admin API

머릿속 그림 하나: drain_listeners?graceful은 "입구 수도꼭지를 잠그는" 동작이다. 물(in-flight rq)이 다 빠질 때까지 기다리는 건 폴링하는 쪽의 몫이고, 빠지는 동안 통(LB endpoint)을 안 비우는 건 hc FSM의 몫이다. 그래서 완료 판정도 통에 든 물의 양 = request(rq)로 하지, 수도관이 연결돼 있는지(connection cx)로 하지 않는다.

API는 Envoy admin endpoint다. Istio 기본 바인딩은 127.0.0.1:15000으로 Pod 내 sidecar에서만 접근 가능하다(localhost-only — 외부 노출 금지가 보안 모범 사례).

# graceful drain 시작 → 응답 본문은 "OK"
curl -X POST "http://127.0.0.1:15000/drain_listeners?graceful&skip_exit"
# OK

# active request 계측 → gauge 라인 형태로 출력
curl -s "http://127.0.0.1:15000/stats?filter=downstream_rq_active|upstream_rq_active"
# http.inbound_0.0.0.0_8080.downstream_rq_active: 1
# cluster.outbound|80||backend.svc.cluster.local.upstream_rq_active: 0

호출 한 번이 곧 "유입 차단" 신호고, 그 뒤로는 위 stats gauge가 0이 되는지를 호출자가 폴링한다. API 자체는 "기다려준다"가 없다 — fire-and-forget으로 listener 상태만 바꾼다.

3. graceful 모드: 신규 연결을 "어떻게" 막는가

graceful 파라미터 없이 호출하면 listener를 즉시 close한다(= 위험한 그 동작, 502 유발). graceful을 붙이면 drain period 동안 새 연결을 discourage하는 형태로 바뀐다. 여기서 비자명한 디테일은 "discourage"가 프로토콜마다 다른 신호로 구현된다는 점이다 — TCP 레벨에서 강제로 끊는 게 아니라, 애플리케이션 프로토콜 신호로 클라이언트가 스스로 새 연결로 옮기게 유도하기 때문에 진행 중 요청이 안 깨진다.

프로토콜 신규 유입 discourage 신호 메커니즘
HTTP/1.1 응답에 Connection: close 헤더 keepalive 재사용을 막아 다음 요청은 새 연결로 가게 함
HTTP/2 (gRPC 포함) GOAWAY 프레임 Connection 헤더가 없는 프로토콜이라, GOAWAY로 graceful close를 신호 → 클라이언트가 새 stream을 다른 connection으로

drain 기간은 누가 정하나 — 등치하면 안 되는 두 값

drain period의 길이는 Envoy 자체 파라미터가 결정한다: --drain-time-s와 drain strategy(gradual/immediate)가 점진 종료 시간을 정한다. 반면 Istio terminationDrainDuration(default 5s, 운영에선 상향)은 istio-agent가 SIGTERM 후 sidecar drain 완료를 기다리는 상한이다 — 즉 "Envoy가 drain을 끝낼 때까지 agent가 프로세스를 안 죽이고 버티는 최대 시간"이지, drain_listeners?graceful이 실제로 discourage하는 기간과 1:1로 같지 않다. 둘을 등치하면 타임아웃 계산이 어긋난다.

아래는 Envoy admin 계층 관점의 drain 시퀀스다. hc FSM 전이는 hc FSM 정본이 다루므로, 여기서는 admin-API 흐름만 표현한다.

preStop / Op Envoy admin :15000 Listener Client POST /drain_listeners?graceful&skip_exit Op OK --> OK enter graceful drain (faucet OFF) 신규 유입 discourage H1: Connection: close / H2: GOAWAY 기존 in-flight rq 계속 처리 loop: poll until drained GET /stats downstream_rq_active rq_active = N (단조감소) rq_active == 0 도달 listener close (skip_exit → 프로세스 유지)
그림 1. graceful drain 시퀀스. drain_listeners 후 listener는 신규 유입을 discourage(Connection: close / GOAWAY)하되 in-flight는 끝까지 처리하고, rq_active가 0이 되면 listener만 닫는다 — skip_exit라 프로세스는 살아 종료 주도권을 kubelet에 넘긴다.

4. skip_exit: 왜 프로세스를 살려두는가

skip_exit를 붙이면 drain이 끝나도 Envoy 프로세스가 exit하지 않는다. 이게 필요한 이유는 종료의 주도권을 K8s에 넘기기 위해서다 — preStop hook에서 Envoy를 직접 죽이지 않고 kubelet의 자연스러운 SIGTERM → grace period → SIGKILL 흐름을 그대로 유지하려는 것이다.

skip_exit 없이 호출하면 drain 완료 직후 Envoy가 스스로 종료해버린다. 그러면 §3 폴링이 아직 active=0을 확인하기 전에 데이터플레인이 사라지거나, kubelet의 SIGTERM 타이밍과 충돌해 시퀀스가 어긋날 수 있다. skip_exit는 "drain은 하되 죽는 건 K8s가 정한다"를 보장하는 스위치다.

5. 완료 판정: cx가 아니라 rq인 이유

폴링이 보는 게 무엇이냐가 drain 정확성을 좌우한다.

metric 의미
http.<config_name>.downstream_rq_active Envoy로 들어온 HTTP request 중 응답 미완료 개수
cluster.<cluster_name>.upstream_rq_active Envoy → upstream(backend)으로 보낸 request 중 응답 미완료 개수

rq인가: graceful drain은 §3에서 봤듯 클라이언트에게 "다음부터 새 연결로 가라"고 신호할 뿐, 기존 keepalive connection을 즉시 끊지 않는다. 그래서 요청이 다 끝나도 connection은 한동안 살아 있을 수 있다. cx 기반으로 완료를 판정하면 keepalive connection이 살아 있는 한 영원히 0에 안 닿아 무한 대기에 빠진다. 실제 종료해도 되는 시점 = "처리 중 요청이 없다" = rq_active == 0이므로, drain.sh도 downstream_rq_active + upstream_rq_active == 0을 폴링한다.

streaming/gRPC 함정

WebSocket·gRPC streaming은 long-lived 요청이라 downstream_rq_active떨어지지 않는다. 하나의 stream이 분 단위로 살아 있으면 rq_active는 계속 1 이상이고, 폴링은 영영 안 끝난다. 그래서 rq 판정에도 상한(drain timeout) 이 필요하다 — drain.sh의 DRAIN_TIMEOUT이 그 역할로, 초과 시 남은 in-flight를 끊는 걸 감수하고 진행한다(산정: runbook).

6. drain.sh 오케스트레이션 (정본 분리)

drain.sh 7단계 preStop 오케스트레이션의 라인별 분석(환경변수·do_curl 래퍼·awk 합산·LB_BUFFER 순서)은 중복이므로 여기서 다시 쓰지 않는다 — 정본은 drain.sh walkthrough §3. 본 문서는 그 스크립트가 호출하는 Envoy admin API 자체(§2~5)에 집중한다. 한 줄 매핑만 남기면: drain.sh step [2] POST /drain_listeners?graceful&skip_exit가 곧 §2~4이고, step [3] *_rq_active==0 폴링이 곧 §5다.

7. 워크드 예시: in-flight가 끊기지 않는지 직접 본다 (S2 improved)

주의: 이 검증은 drain_listeners API 자체가 아니라 그 위 hc FSM(drain.sh) 거동(CLOSING 진입 여부, /health_check.html=200 유지, HAProxy backend UP)이다. 여기서 in-flight가 보호되는 근본 이유는 §1·§3 그대로 — graceful 모드가 기존 rq를 유지하고, FSM은 rq_active>0이라 CLOSING으로 전이하지 않아 health를 200으로 유지하기 때문이다. FSM 전이 상세는 hc FSM.

시나리오와 관측 결과:

이 한 실험이 §1의 분리를 그대로 보여준다: graceful 모드가 기존 rq를 살려두고(차단기는 신규만 막음), hc가 health 200을 유지해 LB가 backend를 안 빼서(보호는 상위 계층), 그래서 60초짜리 한 건이 502 없이 완주한다.

8. 추천 설정 (정본 분리)

terminationDrainDuration/terminationGracePeriodSeconds 권장값과 산출 공식(max_request_duration + LB_detection_time + LB_BUFFER + drainDuration + 여유)은 중복이므로 생략 — 정본: 프로덕션 적용, runbook. 핵심 제약 하나만 남긴다: terminationGracePeriodSeconds가 전체 drain 시퀀스 합계보다 짧으면 kubelet이 SIGKILL을 먼저 보내 in-flight 요청을 끊는다. 그러면 §7의 보호가 무너지므로, grace period는 drain 합계보다 반드시 길게 잡는다.

핵심 정리

What you might be missing

See also