배포할 때마다 502 가 조금씩 난다

무중단 배포를 설정했는데 배포할 때마다 502 가 몇십 건씩 나요. 설정은 다 맞는데도요. 서버가 내려가는 순간과 트래픽이 끊기는 순간 사이의 빈틈을 메우는 법을 정리했어요.

무중단 배포를 설정해 뒀어요. 새 버전을 띄우고 헬스체크가 통과하면 옛 버전을 내리는 순서요.

그런데 배포할 때마다 502 가 몇십 건씩 나요. 많지는 않아서 오래 방치했는데, 배포 빈도가 늘면서 무시할 수 없는 수가 됐어요.

로그를 보면 옛 버전이 내려가는 시점에 몰려 있어요. 이미 종료된 프로세스로 요청이 갔다는 뜻이에요.

"무중단"이라고 설정했는데 왜 끊기는지가 이 편의 질문이에요. 답은 누가 언제 무엇을 알게 되는가의 타이밍에 있어요.

설정이 틀린 게 아니에요. 설정들 사이에 생기는 시간차를 아무도 메우지 않은 겁니다.

왜 그런가

프로세스가 종료 신호를 받는 시점과, 로드 밸런서가 그 서버를 목록에서 빼는 시점이 다르기 때문이에요.

보통 이런 순서로 일어나요. 오케스트레이터가 파드에 종료 신호를 보내요. 동시에 그 파드를 서비스 목록에서 빼라는 지시가 로드 밸런서 쪽으로 전파돼요. 그런데 이 전파는 즉시가 아니에요. 여러 노드의 규칙을 갱신해야 하고, 그 사이 짧은 시간이 걸려요.

문제는 애플리케이션이 신호를 받자마자 종료하는 경우예요. 로드 밸런서는 아직 그 서버가 살아 있다고 알고 트래픽을 보내는데, 받을 프로세스가 없어요. 그게 502 예요.

여기서 핵심은 삭제 지시와 실제 반영 사이의 시간차예요. 이 시간차 동안에도 요청이 오니까, 애플리케이션은 신호를 받고도 잠시 더 살아서 받아 줘야 해요.

또 하나가 진행 중인 요청이에요. 종료 신호를 받은 시점에 처리 중이던 요청이 있어요. 그것들이 끝날 때까지 기다려야 하는데, 기다리지 않고 바로 죽으면 그 요청들도 502 가 돼요.

그래서 필요한 게 둘이에요. 신호를 받은 뒤 잠시 대기하는 것, 그리고 진행 중인 요청을 끝까지 처리하는 것. 둘 다 없으면 배포마다 조금씩 끊깁니다.

코드로 보면

신호를 받고도 잠시 더 산다:

코드
 SIGTERM 수신 → 즉시 종료
 로드 밸런서는 아직 이 서버를 살아 있다고 안다 → 502

· 1) 종료 훅으로 전파 시간을 벌어 준다
 preStop: sleep 5~15초
 그동안에도 새 요청을 정상 처리한다

· 2) 그다음 우아한 종료 — 진행 중 요청을 끝낸다
 SIGTERM → 새 연결 거부, 진행 중 요청 완료 대기 → 종료
 terminationGracePeriodSeconds > preStop + 최대 요청 시간

· 3) 준비됨 검사를 먼저 실패시키는 방법도 있다
 SIGTERM 수신 → /healthz/ready 를 즉시 실패로 전환
 → 로드 밸런서가 빼는 동안 살아 있는 요청은 계속 처리

 유예 시간이 preStop 보다 짧으면 강제 종료된다

✕ 문제 지점 · ✓ 해법 · 그대로 돌려 보기 전에 자기 환경에 맞게 고쳐 주세요.

실전에서는

순서를 정확히 잡는 게 핵심이에요.

가장 흔한 실수가 유예 시간 설정을 서로 안 맞추는 거예요. 종료 훅에서 15초를 자는데 전체 유예가 10초면, 자다가 강제 종료돼요. 진행 중 요청은 그대로 끊기고요. 전체 유예 시간은 훅 대기 시간과 최대 요청 처리 시간을 합친 것보다 커야 해요.

두 번째는 애플리케이션이 종료 신호를 아예 안 다루는 경우예요. 프레임워크가 기본으로 우아한 종료를 지원하는지 확인해야 해요. 지원하더라도 진행 중 요청 대기 시간이 기본값으로 짧게 잡혀 있을 수 있어요.

세 번째는 신호가 애플리케이션까지 안 가는 경우예요. 셸 스크립트로 감싸서 실행하면 셸이 신호를 받고 자식 프로세스에 전달하지 않을 수 있어요. 컨테이너 안에서 프로세스가 1번이 아니면 이런 문제가 생깁니다.

새 버전 쪽에도 대칭적인 문제가 있어요. 준비됨 판정이 너무 빨리 나면, 아직 캐시도 안 채워지고 커넥션 풀도 준비 안 된 서버로 트래픽이 가요. 그러면 느리고, 앞 편에서 본 대로 다시 제외될 수도 있어요. 기동 유예를 충분히 주는 게 필요합니다.

그리고 장수 연결은 별도 문제예요. 웹소켓이나 서버 전송 이벤트처럼 오래 유지되는 연결은 "진행 중 요청 완료"를 기다릴 수 없어요. 클라이언트가 재연결하도록 설계해야 하고, 한꺼번에 끊기지 않게 흩어야 합니다.

배포 속도도 봐야 해요. 한 번에 너무 많은 파드를 교체하면 남은 용량이 부족해져요. 앞 편의 헬스체크 연쇄 제외와 만나면 배포 자체가 장애가 됩니다.

처방

  1. 종료 훅으로 대기 시간을 두세요. 로드 밸런서 전파가 끝날 때까지 살아 있어야 합니다. 5~15초가 흔한 범위예요.
  2. 우아한 종료를 켜세요. 새 연결은 거부하되 진행 중 요청은 끝내고 나가야 해요.
  3. 유예 시간 계산을 맞추세요. 전체 유예 > 훅 대기 + 최대 요청 처리 시간. 안 맞으면 강제 종료됩니다.
  4. 종료 신호가 애플리케이션까지 가는지 확인하세요. 셸로 감싸면 전달이 끊길 수 있어요.
  5. 새 버전의 기동 유예를 충분히 주세요. 준비되기 전에 트래픽을 받으면 그 서버가 바로 나빠집니다.
  6. 배포 속도를 조절하세요. 한 번에 너무 많이 교체하면 남은 용량이 부족해져요.
  7. 장수 연결은 재연결 전략을 따로 설계하세요. 끊길 때 한꺼번에 몰리지 않게 흩어야 합니다.
  8. 배포 중 5xx 를 지표로 보세요. "조금씩 난다"를 숫자로 만들지 않으면 고쳐졌는지도 모릅니다.

지금 확인해 보세요

배포 중 무엇이 끊기는가:

코드
· 배포 시각과 5xx 를 겹쳐 본다
 sum(rate(http_responses_total{code=~'5..'}[1m]))
 배포 시각에 봉우리가 생기면 이 문제다

· 상태 코드별로 나눈다 — 원인이 다르다
 502 → 뒤쪽이 없거나 끊겼다 (종료 타이밍)
 504 → 응답이 늦다 (기동 직후 느림)

· 설정 정합성 — 이 부등식이 성립해야 한다
 terminationGracePeriod > preStop + 최대 요청 처리 시간

· 애플리케이션이 종료 신호를 실제로 받는가
 종료 로그가 찍히는지 확인한다
 안 찍히면 신호가 프로세스까지 안 간 것이다

· 워커·배치도 같이 확인한다
 웹 서버만 설정하고 워커는 그냥 죽이는 경우가 흔하다

핵심 한 줄무중단은 설정이 아니라 타이밍이다. 신호를 받고도 잠시 더 살아라.

브루니가 적어요

색을 바꾸며 숨는 카멜레온이에요. 운영에서 실제로 터지는 문제를 한 편씩 적어요. 같은 실수를 두 번 하지 않으려고 쓰는 기록이에요.

배포·관측 글 16편 모두 보기 →