Overview
1편(EKS 무중단 배포의 4가지 값)에서 preStop · terminationGracePeriodSeconds · ALB deregistration_delay 세 시간의 관계를 다뤘다. 그런데 그 글을 다시 읽어보면 한 가지 질문이 남는다.
"preStop 35초, grace 60초, dereg 30초 — 이 숫자들은 대체 어떻게 정한 것인가? 그리고 정한 값이 실제로 그렇게 도는지는 어떻게 확인하는가?"
1편이 "세 값이 왜 이런 부등식을 이뤄야 하는가"였다면, 이 글은 그 숫자의 근거(배포 전 측정) 와 검증(배포 후 확인) 을 다룬다. 감으로 35·60·30을 박는 것과, 측정해서 박는 것은 완전히 다른 일이다.
대상은 1편과 동일하게 라이브 트래픽을 받는 게임 서버 파드(이하 A 서비스, Stateless HTTP / Fastify 기반)다.

0. 종료 순서와 역산 공식
먼저 파드가 종료될 때의 순서를 시간축으로 정리한다.
t=0 파드 Terminating → Service Endpoint에서 제외 + preStop sleep 시작
(파드는 살아서 기존 요청을 계속 처리한다)
t=P preStop 끝 → 컨테이너 PID 1로 SIGTERM 전달
t=G grace 만료 → SIGKILL 강제 종료
이 순서에서 세 시간의 관계는 두 개의 부등식으로 압축된다.
preStop sleep ≥ ALB deregistration_delay (+ 여유)
grace > preStop + 앱 자체 drain(p99) + 여유
- preStop ≥ ALB dereg — ALB가 종료 중인 IP 타겟의 등록 해제를 끝낼 때까지 파드를 살려둬, 새 요청 유입을 0으로 만든다.
- grace > preStop — preStop이 끝난 뒤 SIGTERM이 정상 전달되고, 남은 시간 동안 앱이 in-flight 요청을 마저 처리한다.
이 흐름이 성립하려면 ingress에 `target-type: ip` 와 `deregistration_delay` 가 선행돼야 한다는 점은 1편에서 다룬 그대로다.
핵심은 이 부등식의 우변에 들어가는 세 숫자(ALB dereg, preStop, 앱 drain)를 측정으로 채워야 한다는 것이다. 그래야 좌변의 grace가 비로소 정당한 값이 된다.
1. 설정값을 정하기 위한 측정 (배포 전)
목적은 단순하다. 세 숫자를 감이 아니라 측정 근거로 정하는 것이다.
ⓐ ALB deregistration_delay — preStop의 하한을 정한다
"종료 중인 타겟으로 새 요청이 끊기는 데 걸리는 시간"이다. 기본값은 300초(5분)지만, 긴 keepalive 연결을 쓰지 않는 짧은 HTTP 트래픽이라면 더 짧게 잡을 수 있다.
A 서비스는 이 값을 30초로 결정했고, 이 30초가 곧 `preStop sleep` 의 하한이 된다. ingress 어노테이션으로 설정한다.
# Ingress (AWS Load Balancer Controller)
metadata:
annotations:
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/target-group-attributes: deregistration_delay.timeout_seconds=30
- ALB 타겟 그룹의 `deregistration_delay.timeout_seconds` 기본값은 300초이므로, 위처럼 명시적으로 낮춰주지 않으면 preStop을 아무리 잡아도 부등식이 깨진다.
ⓑ 앱이 SIGTERM에 graceful한가 — in-flight 유지 vs 끊김
다음으로 확인할 것은 "이 앱이 SIGTERM을 받았을 때 처리 중이던 요청을 끝까지 처리하는가, 아니면 그냥 끊는가"다. 단독 컨테이너에 느린 응답을 in-flight 상태로 만들어 놓고 SIGTERM을 쏴서, 200으로 끝나는지 / 끊기는지를 본다.
# 컨테이너 안에서: 느린 엔드포인트를 in-flight 로 만들고 SIGTERM 전달
curl -s -o /dev/null -w '%{http_code} %{time_total}s\n' localhost:8080/<느린API> &
sleep 1; kill -15 1; wait # 200=graceful / 000·Empty reply=끊김
# 또는 컨테이너 외부에서 종료 소요시간 측정
/usr/bin/time -f '%E' docker stop <container>
주의: 컨테이너 기본 셸(dash/busybox)의 kill은 - SIGTERM 표기를 받지 못한다.
반드시 `kill -15` 또는 `kill -TERM` 으로 보내야 한다.
여기서 한 가지 더 말해보자면, SIGTERM을 받았을 때의 행동은 런타임마다 천차만별이다. ASP.NET Core처럼 알아서 ~30초 drain 하는 것도 있고, Node.js·Go처럼 핸들러가 없으면 즉시 죽는 것도 있고,
nginx·PHP-FPM처럼 SIGTERM을 fast shutdown으로 해석해 오히려 in-flight를 끊어버리는 함정도 있다. 런타임별 SIGTERM 행동표는 1편에 정리해 두었으니, 측정 전에 자기 런타임이 어느 부류인지 먼저 확인하는 것을 권한다.
ⓒ 앱 drain이 몇 초인가 — graceful일 때만, p99 기준
앱이 graceful하다면(ⓑ가 200으로 끝났다면), 그 drain이 몇 초나 걸리는지를 재야 한다. 이게 grace 부등식의 마지막 항이다. K8s에서 grace를 넉넉히 둔 뒤, 앱 로그의 SIGTERM 수신 시각 ~ 마지막 요청 완료 시각 차이를 잰다. 동시에 지속 부하를 흘리며 실측한다.
hey -z 90s -c 20 https://app.example.com/health &
kubectl -n app rollout restart deployment/app
kubectl -n app logs -l app.kubernetes.io/name=app --prefix | grep -i 'SIGTERM\|shutdown\|closed'
- `preStop sleep` 값은 worst-case가 아니라 p99 처리시간을 기준으로 역산하는 것이 정석이다. 극단적인 outlier 한두 건 때문에 전체 종료 윈도우를 늘리면, 매 배포가 그만큼 느려진다.
도출
세 측정을 모으면 숫자가 나온다.
preStop sleep = ALB dereg + 여유 = 30 + 5 = 35
grace > preStop + 앱 drain + 여유 = 35 + drain + buffer
2. 정한 값이 맞는지 검증 (배포 후)
값을 박았으면 끝이 아니다. 실제로 그렇게 도는지를 확인해야 한다. 이때 가장 중요한 원칙은 각 구간을 독립적으로 본다는 것이다.
흔한 실수가 "파드 삭제에 걸린 총시간"만 보고 안심하는 것이다. 그런데 총 삭제시간 하나로는 preStop과 grace를 구분할 수 없다. 앱의 SIGTERM 반응에 따라 같은 총시간도 해석이 완전히 갈리기 때문이다. (이 부분은 4장 실측에서 극적으로 드러난다.)
| 검증 대상 | 방법 | 합격 기준 |
| preStop 35s | `delete --wait=false` 후 종료 중 파드에서 `ps -ef | grep '[s]leep 35'` | 0~35s 존재, 이후 소멸 |
| grace 60s | `time kubectl -n app delete pod $POD` | 총 ≈ 60s (SIGKILL 천장) |
| 앱 SIGTERM | `kubectl exec $POD -- sh -c 'kill -15 1; sleep 5; kill -0 1 && echo IGNORED || echo EXITED'` + RESTARTS 확인 | IGNORED / EXITED 판정 |
| ALB dereg 30s | `aws elbv2 describe-target-health --target-group-arn <TG-ARN>` | podIP가 draining → ~30s 내 제거 |
| 종단 (zero-502) | `hey -z 90s -c 20 https://app.example.com/health` 중 `kubectl rollout restart` | 5xx = 0 ← 최종 합격 |
구간 분해의 핵심
`time kubectl delete` 의 총시간은 grace 천장(SIGKILL) 만 보여준다. 여기서 preStop(35)을 따로 떼어내려면, 종료 중 파드 안의 `sleep 35` 프로세스 생존 구간을 직접 들여다봐야 한다.
POD=$(kubectl -n app get pod -l app.kubernetes.io/name=app -o name | head -1 | cut -d/ -f2)
kubectl -n app delete pod $POD --wait=false
# 0~35초: 보임 / 35초 후: 사라짐 → preStop 이 정확히 35초 실행됨을 증명
kubectl -n app exec $POD -- ps -ef | grep '[s]leep 35'
- 총시간이라는 "블랙박스"를 preStop 구간과 grace 구간으로 쪼개 보는 것 — 이게 검증의 전부다.
3. 실측 결과
현재 배포된 값은 `terminationGracePeriodSeconds: 60`, `preStop: sleep 35`, `ALB deregistration_delay=30` 이다.
| 측정 | 명령 | 결과 | 의미 |
| grace 천장 | `time kubectl delete pod` | 62s | grace 60 + 오버헤드 — SIGKILL 천장 확인 |
| 앱 SIGTERM | `kill -15 1` + RESTARTS 확인 | IGNORED, RESTARTS=0 | 앱이 SIGTERM을 무시(핸들러 없음) |
두 결과를 합치면 62초의 정체가 확정된다.
t=0 Terminating + preStop sleep 35 시작
t=35 preStop 끝 → SIGTERM 전달 ← 앱이 무시
t=60 grace 만료 → SIGKILL
───── 총 ≈ 62초
(preStop 실패가 아니라, SIGTERM 무시로 grace 천장까지 버틴 것)
여기서 2장에서 강조한 "구간 분해"의 가치가 드러난다.
만약 `time kubectl delete` 의 62초만 봤다면 "음, grace 60초가 잘 도는구나" 하고 넘어갔을 것이다.
하지만 앱 SIGTERM 반응(IGNORED)을 따로 측정했기 때문에, 그 62초의 정체가 "정상적인 graceful drain"이 아니라 "SIGTERM 무시로 SIGKILL을 기다린 죽은 시간" 이었음을 알 수 있었다.
결론
- preStop 35 > ALB dereg 30 → 맞음. 502 방지(트래픽이 빠질 때까지 파드 생존)는 이 부등식으로 정확히 작동한다. 종료 중 파드로 들어오는 새 요청은 0이 된다.
- grace 60 > preStop 35 → 유효하나 현재는 과대. 앱이 SIGTERM을 무시하므로 t=35~60의 25초는 그저 SIGKILL을 기다리는 죽은 시간이다. 502에는 무해하지만, 파드 삭제가 매번 60초씩 걸린다.
- 이 60초가 의미를 가지려면 Fastify에 SIGTERM 핸들러(enableShutdownHooks + graceful close) 를 추가해야 한다. 그러면 측정 ⓒ의 "앱 drain"이 비로소 0이 아닌 값으로 생기고, 35 + drain + 여유 < 60 으로 grace가 정당해진다. 핸들러를 추가하지 않을 거라면 grace를 40초 정도로 줄여 teardown을 빠르게 할 수도 있다(502 정확성에는 영향 없다).
한 줄 요약
측정(ⓑ·ⓒ)으로 앱 drain을 먼저 알아내야 grace 숫자가 정해지고, 검증(2·3장)으로 그 숫자가 실제로 그렇게 도는지 + 502가 0인지 확인한다.
A 서비스는 "앱 drain = 0(SIGTERM 무시)"인 상태라서, `preStop=35` 만으로 502는 이미 막혀 있고, `grace=60` 은 미래의 SIGTERM 핸들러를 위한 여유로 남아 있는 셈이다. 숫자를 감으로 박았다면 결코 알 수 없었을 사실이다.
Reference
- 1편: EKS 무중단 배포의 4가지 값 — lifecycle(preStop) · Pod Readiness Gate · strategy(RollingUpdate) · PodDisruptionBudget
- Kubernetes — Pod Lifecycle (Termination)
- Kubernetes — Container Lifecycle Hooks (preStop)
- AWS — Target groups for your Application Load Balancers (deregistration_delay)
- AWS Load Balancer Controller — Ingress annotations
Somaz | DevOps Engineer | Kubernetes & Cloud Infrastructure Specialist
'Container Orchestration > Kubernetes' 카테고리의 다른 글
| Unity MCP Server를 Kubernetes에 배포하기 (0) | 2026.07.30 |
|---|---|
| Kubernetes Ingress에서 APK 파일의 Content-Type 올바르게 설정하기 (0) | 2026.07.02 |
| Kagent: Kubernetes에 AI Agent를 도입하기! (0) | 2026.06.18 |
| Kubernetes OOMKilled 대응 전략: 무작정 메모리만 늘리지 말자! (0) | 2026.06.10 |
| Kubernetes 클러스터로의 외부 트래픽 흐름 완벽 가이드 (0) | 2026.06.04 |