노트

커넥션 드레이닝과 무중단 재시작

Connection Draining

인프라#aws#network · 연결된 개념 6개

쉽게 말하면

커넥션 드레이닝은 가게를 잠시 닫기 전에 입구에 '마감' 팻말부터 걸고, 이미 앉은 손님의 식사는 끝까지 마치게 해 주는 거예요. 그래야 배포 중에 문 앞에서 502를 받고 쫓겨나는 손님이 없어요.

비유가 깨지는 곳 팻말을 걸어도 손님을 받아 줄 옆 가게가 있어야 해요. 서버가 한 대뿐이면 빼는 순간 갈 곳이 없어요. ALB 등록 해제 지연은 가장 긴 정상 요청보다 길게 잡고, 그 시간이 끝난 뒤에 재시작해요.

커넥션 드레이닝(connection draining)은 서버를 내리기 전에 새 요청은 더 보내지 않고, 이미 받은 요청은 끝까지 처리하게 기다려 주는 과정이다. 이게 없으면 배포 때마다 짧은 502 구간이 생긴다.

배포 중 502가 나는 원리

흔한 구성은 이렇다.

브라우저 → 로드밸런서(ALB) → 서버마다 nginx → 앱 서버(gunicorn :8000) → Django

배포 스크립트가 앱 서버 프로세스를 그냥 재시작하면, 몇 초 동안 :8000에 아무도 없다. 그 사이에도 그 서버는 로드밸런서의 대상 목록에 남아 있으니 요청이 계속 들어오고, nginx는 업스트림 연결에 실패해 자기가 만든 502 Bad Gateway를 돌려준다. 서버가 여러 대이고 한 대씩 배포해도, 재시작 중인 그 한 대로 간 요청은 실패한다.

이 502는 Django가 만든 응답이 아니라서 앱이 붙이던 CORS 헤더가 없다. 그래서 다른 출처에서 호출한 브라우저 콘솔에는 CORS 오류로 보이기 쉽다(CORS).

고치는 순서

1. 로드밸런서 대상 그룹에서 등록 해제 → 드레이닝 시작(새 요청 중단)
2. 진행 중인 요청이 끝날 때까지 대기
3. 앱 서버 재시작
4. 준비 확인(헬스 체크 엔드포인트가 200을 줄 때까지)
5. 다시 등록 → 헬스 체크를 연속 통과하면 트래픽 재개

남은 서버가 트래픽을 받아 주므로 사용자는 실패를 보지 않는다. 대상이 한 대뿐인 환경(QA 등)에서는 빼는 순간 받을 곳이 없으니 이 절차를 건너뛰거나 다른 방법을 써야 한다.

AWS ALB의 등록 해제 지연 (2026 기준)

  • 대상을 등록 해제하면 ALB는 그 대상으로 새 요청 보내기를 멈추고, 대상 상태가 draining이 된다
  • 진행 중인 요청을 위해 등록 해제 지연(deregistration delay)만큼 기다린다. 기본 300초이고 대상 그룹 속성 deregistration_delay.timeout_seconds로 바꾼다. 진행 중인 요청과 연결이 없으면 바로 끝난다
  • 지연이 끝나기 전에 대상이 연결을 끊으면 클라이언트는 5xx 오류를 받는다. 그래서 지연 시간은 가장 긴 정상 요청보다 길게, 재시작은 드레이닝이 끝난 뒤에 한다

앱 서버 쪽: graceful shutdown

로드밸런서에서 빼는 것과 별개로, 프로세스도 받은 요청을 마치고 내려가야 한다.

  • gunicorn 마스터는 TERM을 받으면 graceful shutdown을 한다. 워커가 처리 중인 요청을 graceful_timeout(기본 30초)까지 기다린 뒤 남은 워커를 강제로 끝낸다. INT·QUIT는 바로 끈다
  • HUP은 설정을 다시 읽고 새 워커를 띄운 뒤 옛 워커를 차례로 내린다. 마스터 프로세스는 내려가지 않고 워커만 교체하므로, 프로세스를 통째로 재시작할 때처럼 포트가 비는 구간이 생기지 않는다. 다만 앱을 미리 로드(preload_app)했다면 코드가 다시 로드되지 않는다
  • USR2는 새 마스터를 띄우는 바이너리 업그레이드다

WSGI와 gunicorn의 역할

WSGI(Web Server Gateway Interface, PEP 3333)는 파이썬 웹 서버와 앱 사이의 호출 규약이다. 앱은 application(environ, start_response) 형태의 호출 가능한 객체만 내놓으면 되고, 소켓·워커 프로세스·타임아웃 관리는 gunicorn 같은 WSGI 서버가 맡는다. Django의 manage.py runserver는 개발용이고, 운영에서는 이 조합을 nginx 뒤에 둔다. 비동기(WebSocket 등)가 필요하면 후속 규약인 ASGI(Asynchronous Server Gateway Interface)와 uvicorn 같은 서버를 쓴다(Django 미들웨어와 로그인 흐름).

쿠버네티스 핵심 개념에서도 같은 문제가 있다. kubelet이 파드 종료를 시작하는 것과 컨트롤 플레인이 그 파드를 서비스의 EndpointSlice에서 빼는 것이 동시에 진행되므로, 프로세스는 신호를 받은 뒤에도 잠깐 요청을 받아 줄 수 있어야 한다. 서버를 여러 대 두는 이유는 수직 확장과 수평 확장, 헬스 체크 엔드포인트는 Spring Boot Actuator를 참고한다.

출처: AWS — ALB target group attributes: Deregistration delay · Gunicorn — Signal Handling · Gunicorn — Settings: graceful_timeout · PEP 3333 — Python Web Server Gateway Interface · Kubernetes — Pod termination

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • loading 경계와 레이아웃 끌어올리기

    Next.js App Router에서 loading.tsx는 같은 세그먼트의 page와 그 아래(하위 layout 포함)를 suspense로 감싸지만, **같은 세그먼트의 layout은 감싸지 않는다**. 그래서 데이터를 기다릴 필요가 없는 공통 UI(탭바, 필터 헤더)를 page에서 layout으로 끌어올리면 스켈레톤에 덮이지 않고 처음부터 실제 모습으로 보인다.

  • 업스트림과 다운스트림

    흐름의 방향을 강물에 빗댄 말이다. 다만 무엇의 흐름을 기준으로 하느냐에 따라 방향이 반대로 쓰인다. 서비스 간 호출을 말할 때는 요청이 흘러가는 쪽을 기준으로 다운스트림 = 내가 호출하는 쪽, 업스트림 = 나를 호출하는 쪽이라 부르는 경우가 많다. 아래 설명은 이 용법을 따른다.

  • 동기·비동기와 블로킹·논블로킹

    동기·비동기는 결과를 언제 어떻게 받느냐, 블로킹·논블로킹은 기다리는 동안 호출한 스레드가 멈추느냐의 문제다.

  • 요청 제한 (Throttling)

    요청 제한(rate limiting, throttling)은 일정 시간 동안 한 클라이언트가 보낼 수 있는 요청 수를 묶어 두는 장치다. 한도를 넘으면 429 Too Many Requests로 거절하고, 보통 Retry-After 헤더로 언제 다시 시도하면 되는지 알려 준다.

  • 이벤트 루프

    자바스크립트를 실행하는 하나의 스레드가 할 일을 큐에서 하나씩 꺼내 처리하는 반복 구조. JS 엔진 자체에는 타이머도 네트워크도 없고, 브라우저(또는 Node.js)가 이 루프를 돌리면서 콜백을 엔진에 넘겨준다.

보기 옵션