지난주에 롤링 업데이트 도중 5분 동안 502가 쭉쭉 떨어졌다. 트래픽 피크 시간대였고, 알림 채널이 몇 초 간격으로 울렸다. 새벽도 아닌 오후 3시였는데, 오히려 그게 더 뼈아팠다. 다들 사무실에 있었으니까.
우리 팀은 EKS 위에 AWS Load Balancer Controller로 ALB를 붙여서 쓴다. 노드 24대, 파드 수는 서비스마다 다르지만 문제가 된 서비스는 파드 12개짜리였다. 배포 스크립트도 평범한 Deployment 롤링 업데이트, maxSurge: 25%, maxUnavailable: 0. 이론적으로는 무중단이어야 하는데, 현실은 그렇지 않았다.
처음 의심한 건 헬스체크였다
로그를 열어보니 502가 뜬 요청들은 전부 upstream connect error 계열이었다. ALB는 살아있다고 판단해서 트래픽을 보냈는데, 뒤쪽 파드는 이미 죽어있는 상황. 이거 딱 봐도 데레지스트레이션(deregistration) 타이밍 문제다. 근데 나는 처음에 헬스체크 인터벌을 의심했다. ALB target group의 healthy threshold를 낮추면 되지 않을까 싶어서 3에서 2로 내려봤다. 별 차이 없었다.
그다음엔 terminationGracePeriodSeconds를 30에서 90으로 올렸다. 이것도 소용없었다. 왜냐하면 문제가 파드가 늦게 죽는 게 아니라 너무 빨리 트래픽을 못 받게 되는 쪽이었기 때문이다. 이 사실을 깨닫는 데 한 시간 정도 걸렸다. 그동안 슬랙에는 계속 502 알림이 쌓였다. 멘탈이 좀 나갔다.
진짜 문제는 kube-proxy와 ALB의 시간차
파드가 Terminating 상태로 바뀌면 두 가지가 동시에 일어난다. 하나는 kubelet이 SIGTERM을 컨테이너에 보내는 것, 다른 하나는 endpoints controller가 endpoint에서 파드를 지우는 것. 앱은 SIGTERM 받자마자 종료 준비에 들어간다. 근데 ALB 쪽은? Load Balancer Controller가 endpoint 변화를 감지해서 target group에서 대상을 deregister하는 데 짧게는 몇 초, 길게는 20-30초까지 걸린다.
즉 이런 순서다.
kubectl rollout시작- 파드가 Terminating으로 바뀜
- 앱이 SIGTERM 받고 커넥션 안 받기 시작
- ALB는 아직 그 파드를 healthy로 알고 트래픽 계속 보냄
- 502 발생
- 몇 초 뒤에야 ALB가 파드를 deregister
이 3~6번 사이의 gap이 502의 진짜 원인이었다. AWS 공식 문서에도 나와있고 aws-load-balancer-controller 이슈 트래커에도 사람들이 계속 언급해 온 문제인데, 우리 팀에서는 그동안 트래픽이 적어서 안 터졌던 것뿐이다.
preStop 슬립이 정답이었다
해결은 허무할 정도로 간단했다. 파드 스펙에 preStop 훅으로 sleep 30을 넣었다.
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 30"]
terminationGracePeriodSeconds도 preStop 시간 + 앱 종료 시간을 커버하도록 60초로 잡았다. ALB target group의 deregistration_delay.timeout_seconds는 30초. 이 세 값의 관계가 중요하다.
- preStop sleep(30s) ≤ ALB deregistration delay(30s)
- terminationGracePeriod(60s) > preStop sleep + 앱 graceful shutdown 여유
preStop이 도는 동안 파드는 여전히 커넥션을 받는다. 이 시간에 ALB가 target을 draining으로 바꾸고 새 요청은 다른 파드로 보낸다. sleep이 끝나면 그제서야 SIGTERM이 앱에 전달되고, 이미 트래픽이 안 오는 상태에서 조용히 죽는다. 이렇게 하니 502가 완전히 사라졌다.
몇 가지 더 배운 것들
sleep 명령어가 컨테이너 이미지에 없을 수도 있다. distroless나 scratch 기반이면 없다. 우리는 두 개 이미지에서 이거 때문에 preStop이 무시된 걸 나중에 발견했다. distroless는 앱 프로세스에 SIGTERM 유예 로직을 직접 넣어야 한다. 아니면 busybox 계열 sidecar를 붙이거나, 그냥 non-distroless로 바꾸거나. 팀 내부 논의 끝에 이 두 서비스는 alpine 기반으로 롤백했다.
또 하나, deregistration_delay.timeout_seconds를 너무 크게 잡으면 배포가 하염없이 느려진다. 30초가 우리한테는 적당했는데, 서비스 특성마다 다를 것 같다. 롱리빙 요청이 많은 서비스라면 60-90초까지 봐야 할 수도. 반대로 요청이 짧은 API라면 15초로도 충분하다.
그리고 이번에 알게 된 건데, AWS Load Balancer Controller가 최근 버전(2.13.x대)부터 pod readiness gate를 기본 옵션 중 하나로 밀고 있다. namespace에 라벨 하나 붙이면 파드 준비 상태를 ALB target group healthy 상태와 연동해준다. 배포 시점 502 방지에는 이쪽이 더 정공법인데, 우리는 아직 롤아웃 안 했다. 다음 스프린트에 시도해볼 계획이다.
회고
돌이켜 보면 이 이슈는 진작 잡았어야 했다. 지난 반년간 트래픽이 두 배로 늘면서 배포 중 스파이크가 계속 눈에 띄었는데, 개별 알림으로 뜨진 않으니까 다들 넘겼다. 이번엔 그게 5분 동안 지속되면서 SLO를 살짝 갉아먹었고, 그제서야 원인 조사가 시작됐다. 관측성이 있으면 뭐하나. 사람이 그래프를 안 보면 소용없다는 걸 다시 배웠다.
혹시 비슷한 증상 겪으시는 분 있으면 preStop sleep부터 확인해 보시라. 그다음이 target group deregistration delay, 그다음이 pod readiness gate. 이 순서로 접근하면 대부분 잡힌다.
'IT > AWS' 카테고리의 다른 글
| EKS Auto Mode 실전 도입 가이드 (0) | 2026.08.29 |
|---|---|
| EKS Pod Identity로 IRSA 걷어내다 만난 것들 (0) | 2026.08.25 |
| Karpenter WhenEmptyOrUnderutilized 켰다가 밤새운 이야기 (0) | 2026.08.04 |
| Cluster Autoscaler에서 Karpenter로, 우리는 정말 이득이었나 (0) | 2026.07.25 |
| ALB deregistration_delay를 30초로 뒀더니 배포마다 5xx가 튀던 이야기 (0) | 2026.07.16 |