왜 롤링 배포가 느려지는가
롤링 배포는 인스턴스를 하나씩 교체하며 무중단을 노린다. 그런데 실제로는 배포 한 대에 몇 분씩 걸리는 경우가 흔하다. 원인의 대부분은 로드밸런서(LB) 헬스체크 설정이다. LB는 새 인스턴스를 트래픽 대상 그룹에 넣기 전, 헬스체크가 연속으로 성공하기를 기다린다. 이 "성공 판정" 시간이 곧 배포 시간이다.
예를 들어 헬스체크 주기 30초, 정상 판정 임계치 5회라면 한 대당 최소 150초가 든다. 인스턴스 20대면 산술적으로 50분이다. 반대로 종료 시에는 비정상 판정을 기다리느라 또 시간을 쓴다. 설정값 하나가 배포 파이프라인 전체를 느리게 만든다.
헬스체크 타이밍의 실제 비용
등록(register) 지연은 interval × healthy_threshold에 가깝고, 제거(deregister) 지연은 interval × unhealthy_threshold + deregistration_delay에 가깝다. 기본값을 그대로 쓰면 아래처럼 누적된다.
| 설정 | 보수적 기본값 | 튜닝값 |
|---|---|---|
| interval | 30s | 5s |
| healthy_threshold | 5 | 2 |
| 등록까지 최소 시간 | 150s | 10s |
| deregistration_delay | 300s | 30s |
대부분의 웹 애플리케이션은 등록에 10~15초, 제거에 30초면 충분하다. 300초 draining은 장시간 커넥션(대용량 다운로드, 웹소켓)이 있을 때만 의미가 있다.
얕은 헬스체크와 깊은 헬스체크 분리
흔한 실수는 헬스체크 엔드포인트에서 DB, 캐시, 외부 API까지 전부 점검하는 것이다. 이러면 의존성 하나가 느려질 때 멀쩡한 인스턴스가 대량으로 빠지고 연쇄 장애가 난다. LB 헬스체크는 "이 프로세스가 트래픽을 받을 수 있는가"만 판단하는 얕은 체크로 두고, 의존성 점검은 별도 엔드포인트로 분리한다.
from fastapi import FastAPI, Response
app = FastAPI()
ready = False # 워밍업 완료 플래그
# LB용: 프로세스가 살아있고 준비됐는지만 (얕은 체크)
@app.get("/healthz")
def healthz(response: Response):
if not ready:
response.status_code = 503
return {"status": "warming_up"}
return {"status": "ok"}
# 운영 모니터링용: 의존성까지 (깊은 체크, LB에 연결하지 않음)
@app.get("/healthz/deep")
def healthz_deep():
return {"db": check_db(), "cache": check_cache()}
워밍업과 readiness의 분리
새 인스턴스가 헬스체크는 통과하지만 JIT 컴파일, 커넥션 풀 생성, 캐시 로딩이 끝나기 전에 트래픽을 받으면 초기 요청이 느리거나 실패한다. 위 코드처럼 애플리케이션이 완전히 준비됐을 때만 ready = True로 바꿔 200을 반환하게 하면, LB는 진짜 준비된 시점부터 트래픽을 보낸다. 헬스체크 통과 = 트래픽 수신 가능이라는 등식을 애플리케이션이 직접 통제하는 것이 핵심이다.
graceful shutdown으로 무중단 보장
제거 지연을 줄이면서 502를 막으려면 종료 순서가 중요하다. SIGTERM을 받으면 먼저 헬스체크를 실패로 돌려 LB가 등록 해제를 시작하게 하고, draining 시간만큼 기다린 뒤 실제 종료한다.
import signal, time, threading
shutting_down = False
def handle_sigterm(signum, frame):
global ready, shutting_down
ready = False # 이후 /healthz 는 503 반환 → LB가 제거 시작
shutting_down = True
signal.signal(signal.SIGTERM, handle_sigterm)
# 워커 종료 전, LB가 트래픽을 끊을 시간을 확보
def graceful_exit():
while not shutting_down:
time.sleep(1)
time.sleep(30) # deregistration_delay 와 맞춤
# 이 시점에 남은 요청 처리 후 프로세스 종료
ALB Target Group 설정 예시
AWS ALB 기준으로 위 원칙을 반영하면 다음과 같다. 짧은 interval과 낮은 임계치로 등록을 빠르게 하고, draining은 애플리케이션의 종료 대기와 일치시킨다.
aws elbv2 modify-target-group \
--target-group-arn $TG_ARN \
--health-check-path /healthz \
--health-check-interval-seconds 5 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 2 \
--health-check-timeout-seconds 3
# draining(등록 해제 지연)을 30초로
aws elbv2 modify-target-group-attributes \
--target-group-arn $TG_ARN \
--attributes Key=deregistration_delay.timeout_seconds,Value=30
주의점
- interval을 과도하게 낮추면(1~2초) 대상이 많을 때 헬스체크 트래픽 자체가 부하가 된다. 5초 전후가 무난하다.
- timeout은 interval보다 반드시 작게 둔다. 그렇지 않으면 체크가 겹쳐 오판정이 생긴다.
- 얕은 헬스체크는 절대 DB에 의존시키지 않는다. 의존성 장애가 인스턴스 전멸로 번지지 않게 하는 안전장치다.
- graceful shutdown 대기 시간과 오케스트레이터의 강제 종료 유예(예: 쿠버네티스
terminationGracePeriodSeconds)를 반드시 정렬한다. 후자가 짧으면 draining 중에 프로세스가 강제 종료돼 502가 난다.
결국 배포 속도와 안정성은 트레이드오프가 아니라, 헬스체크의 의미를 명확히 나누고 타이밍을 의존성이 아닌 애플리케이션 상태에 맞추는 설계 문제다.