RSS
웹 인프라

무중단 배포의 드레인: 연결을 비우는 시간과 버전이 섞이는 시간

작성자
듀오랩스 대표·10분 읽기

배포 버튼을 누르고 얼마 뒤 모니터링에 502 가 몇 개 찍힙니다. 새 버전은 헬스체크를 통과했고, 옛 버전은 로드밸런서에서 뺐습니다. 순서대로 했는데 요청이 끊겼습니다. 같은 시각 브라우저 콘솔에는 ChunkLoadError 가 뜹니다. 방금 받은 페이지가 존재하지 않는 자바스크립트 파일을 달라고 한 것입니다.

두 증상 모두 드레인(draining)이라는 구간에서 생깁니다. 옛 서버를 내리기 전에 새 요청은 막고 이미 받은 요청만 마저 처리하게 두는 시간입니다.

「빼고 기다리면 끝」이라는 이해

흔히 드레인을 이렇게 이해합니다. 로드밸런서에서 옛 서버를 빼면 새 요청은 더 오지 않고, 남은 요청이 끝날 때까지 기다렸다가 끄면 안전하다고요. 틀린 설명은 아닙니다. 다만 세 군데가 빠져 있어서 위의 두 증상을 설명하지 못합니다.

첫째, 「빼는 일」은 즉시 일어나지 않고, 끄는 신호와 동시에 시작되기도 합니다. 둘째, 어떤 연결은 기다려도 끝나지 않습니다. 셋째, 드레인이 비우는 것은 연결이지 버전이 아닙니다. 드레인 동안에는 옛 버전과 새 버전이 함께 살아 있고, 그 사이에 오간 요청은 서로 다른 버전을 만납니다.

드레인의 정의와 기본값

AWS 의 Application Load Balancer 는 이 과정을 「등록 취소 지연(deregistration delay)」이라고 부릅니다. 문서의 설명은 짧습니다.

Elastic Load Balancing stops sending requests to targets that are deregistering. By default, Elastic Load Balancing waits 300 seconds before completing the deregistration process, which can help in-flight requests to the target to complete.

빼는 서버의 상태는 draining 이 되고, 기본 300초가 지나면 unused 가 됩니다. 처리 중인 요청도 열린 연결도 없으면 기다리지 않고 바로 끝납니다. 같은 문서의 다음 문장이 첫머리의 502 를 설명합니다.

If a deregistering target terminates the connection before the deregistration delay elapses, the client receives a 500-level error response.

드레인 시간이 아무리 넉넉해도, 서버 프로세스가 그 전에 스스로 연결을 끊으면 사용자는 5xx 를 받습니다. 드레인은 로드밸런서가 기다려 주는 시간이지, 서버가 살아 있기로 약속한 시간이 아닙니다.

빼는 일과 끄는 일이 동시에 시작되는 쿠버네티스

쿠버네티스에서는 이 틈이 더 분명하게 드러납니다. 파드를 지우면 kubelet 은 컨테이너에 종료 신호(SIGTERM)를 보내고, 기본 30초 유예 뒤에도 살아 있으면 SIGKILL 로 끕니다. 트래픽에서 빼는 일은 그 다음 순서가 아닙니다. 파드 종료 흐름은 이렇게 적습니다.

At the same time as the kubelet is starting graceful shutdown of the Pod, the control plane evaluates whether to remove that shutting-down Pod from EndpointSlice objects

동시에 시작되니, 신호를 받은 순간에도 엔드포인트 목록이 아직 갱신되지 않은 로드밸런서나 프록시가 요청을 보낼 수 있습니다. 앱이 SIGTERM 을 받자마자 종료하면 그 요청들이 끊깁니다. 그래서 흔히 preStop 훅에 몇 초 기다리는 명령을 넣어, 목록에서 빠질 시간을 먼저 줍니다. 몇 초가 적당한지는 클러스터와 프록시마다 달라서 저도 정해진 숫자를 말하기 어렵습니다.

기다려도 끝나지 않는 연결

드레인은 「진행 중인 요청이 끝나기를」 기다립니다. 웹소켓, SSE(Server-Sent Events), 긴 폴링은 요청 하나가 몇 분에서 몇 시간 이어지는 연결입니다. 이런 연결이 하나라도 있으면 드레인은 제한 시간까지 가고, 제한 시간이 오면 연결은 끊깁니다. 배포할 때마다 몇 분씩 늘어지고, 그 끝에 실시간 화면이 한 번씩 끊기는 이유입니다.

HTTP keep-alive 도 한때 같은 모양을 만들었습니다. 프록시와 앱 사이의 재사용 연결은 한가해도 열려 있어서, 「열린 연결이 다 닫히면 종료」가 끝나지 않았습니다. Node.js 는 19 부터 HTTP 서버의 server.close()가 요청을 보내고 있지 않은 연결을 닫도록 바뀌었습니다. 그래도 SSE 나 웹소켓처럼 계속 흐르는 연결은 남으니, 종료 처리에는 상한 시간이 따로 있어야 합니다.

저는 이 경우 드레인 시간을 늘리는 쪽보다, 클라이언트가 끊기면 다시 붙도록 만드는 쪽을 먼저 봅니다. 서버가 먼저 끊어도 되는 연결이면 드레인이 짧아도 됩니다.

드레인이 막지 못하는 버전 스큐

첫머리의 ChunkLoadError 는 연결 문제가 아닙니다. 옛 버전이 내준 HTML 에는 옛 자바스크립트 파일 이름이 적혀 있습니다. 브라우저가 그 파일을 나중에 달라고 할 때 요청이 새 버전 서버로 가면, 새 서버에는 그 파일이 없습니다. 드레인이 아무리 깔끔해도 이 요청은 생깁니다. 페이지는 옛 버전이 줬고 파일 요청은 드레인이 끝난 뒤에 오기 때문입니다.

이것을 버전 스큐(version skew)라고 부릅니다. Vercel 의 Skew Protection 문서는 「클라이언트와 서버에서 서로 다른 버전이 돌 때 생기는 오류」라고 정의하고, 새 배포가 필수 필드를 추가했는데 옛 클라이언트가 그 필드 없이 요청을 보내는 예를 듭니다. 해법은 요청마다 배포 ID 를 붙여, 그 요청을 처음 페이지를 준 배포로 보내는 것입니다. 기본값으로 배포 후 하루 동안 옛 배포를 살려 둡니다.

플랫폼이 이 일을 해 주지 않는 환경에서는 두 가지를 직접 해야 합니다. 정적 파일은 이름에 내용 해시를 붙이고 이전 배포분을 한동안 지우지 않습니다. API 와 DB 스키마는 옛 코드와 새 코드가 동시에 읽어도 되는 방향으로만 바꿉니다. 칼럼은 먼저 더하고, 옛 코드가 다 빠진 다음 배포에서 지웁니다.

설계할 때 달라지는 것

드레인을 「기다리는 시간」이 아니라 「두 버전이 겹치는 시간」으로 보면, 챙길 것이 서버·클라이언트·데이터 세 곳으로 나뉩니다.

어디 해야 할 일
서버 SIGTERM 을 받으면 준비 상태(readiness)부터 실패 → 목록에서 빠질 시간 → 새 연결 차단 · 열린 요청 마무리. 전체에 상한 시간
클라이언트 오래 붙는 연결은 끊기면 다시 붙기. 청크 로드 실패는 새로고침으로 복구
데이터 정적 파일은 해시 이름 + 이전 배포분 보존. 스키마는 더하기 먼저, 지우기는 다음 배포

서버 쪽의 모양은 대략 이렇습니다.

let ready = true; // 헬스체크(readiness)가 이 값을 돌려준다

process.on("SIGTERM", () => {
  ready = false; // 로드밸런서가 먼저 빼 가게 한다
  setTimeout(() => {
    server.close(() => process.exit(0)); // 새 연결을 막고, 열린 연결이 끝나면 종료
    setTimeout(() => process.exit(1), 20_000).unref(); // 끝나지 않는 연결을 위한 상한
  }, 5_000); // 목록에서 빠질 시간. 환경마다 다르다
});

이 숫자들을 합한 값이 플랫폼의 종료 유예(쿠버네티스라면 terminationGracePeriodSeconds, 기본 30초)보다 작아야 합니다. 그렇지 않으면 종료 처리가 끝나기 전에 SIGKILL 이 옵니다.

여기까지가 확실한 부분

AWS 의 300초, 쿠버네티스의 30초와 동시 시작, Vercel 의 하루는 문서에 적힌 기본값과 동작입니다. 그 밖의 숫자, 예를 들어 preStop 에 몇 초를 넣을지, 드레인을 몇 초로 줄일지는 프록시가 목록을 얼마나 빨리 갱신하는지와 가장 긴 정상 요청이 얼마인지에 달려 있어서 환경마다 직접 재야 합니다. 컨테이너를 통째로 바꿔 끼우는 배포(예: docker compose up 으로 다시 만들기)는 드레인 단계가 아예 없어서, 짧게라도 끊기는 배포라고 보는 편이 정확합니다.

참고: AWS ALB 등록 취소 지연 · 쿠버네티스 파드 종료 흐름 · Vercel Skew Protection

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.