왜 드레이닝이 필요한가
Blue-Green 배포는 신규(Green) 환경을 띄운 뒤 트래픽을 한 번에 전환하는 방식이다. 문제는 전환 순간에 Blue로 향하던 기존 커넥션이다. 로드밸런서 타깃에서 Blue를 즉시 제거하면 처리 중이던 요청이 잘려 5xx나 커넥션 리셋이 발생한다. 특히 keep-alive 커넥션은 클라이언트가 재사용하려는 순간 상대가 사라져 있어 간헐적 오류로 나타난다. 커넥션 드레이닝(deregistration delay)은 "타깃을 등록 해제 상태로 두되 진행 중 요청은 끝까지 처리"하게 만들어 이 구간을 무손실로 넘긴다.
드레이닝의 동작 원리
드레이닝이 시작되면 LB는 해당 타깃으로 새 요청은 보내지 않고, 이미 열린 커넥션의 in-flight 요청만 완료시킨다. 정해진 타임아웃까지 완료되지 않으면 강제 종료한다. 그래서 타임아웃은 "정상 요청의 최대 처리시간 + 여유"로 잡아야 한다. 값이 너무 짧으면 롱 리퀘스트가 잘리고, 너무 길면 배포가 느려지고 좀비 커넥션이 남는다.
# AWS ALB 타깃 그룹: 등록 해제 지연을 요청 특성에 맞춰 설정
aws elbv2 modify-target-group-attributes \
--target-group-arn $TG_ARN \
--attributes \
Key=deregistration_delay.timeout_seconds,Value=120 \
Key=deregistration_delay.connection_termination.enabled,Value=false
애플리케이션 측 graceful shutdown
LB 드레이닝만으로는 부족하다. 앱이 SIGTERM을 받고 즉시 죽으면 in-flight 요청도 함께 죽는다. 핵심 순서는 (1) 헬스체크 실패로 전환 → (2) 신규 요청 거부 → (3) 진행 요청 완료 → (4) 종료다. 특히 LB가 타깃을 빼기 전에 헬스체크를 먼저 실패시켜야 트래픽이 자연스럽게 빠진다.
import signal, asyncio
from aiohttp import web
shutting_down = False
async def health(request):
# 종료 신호를 받으면 헬스체크를 먼저 죽여 LB가 트래픽을 끊게 한다
if shutting_down:
return web.Response(status=503)
return web.Response(text="ok")
async def on_shutdown(app):
global shutting_down
shutting_down = True
await asyncio.sleep(10) # LB 헬스체크가 unhealthy를 인지할 시간
# 이후 aiohttp가 진행 중 핸들러 완료를 기다린 뒤 종료
app = web.Application()
app.router.add_get("/health", health)
app.on_shutdown.append(on_shutdown)
web.run_app(app, shutdown_timeout=120) # 드레이닝 타임아웃과 정렬
Kubernetes에서의 정렬
K8s는 Pod 종료 시 엔드포인트 제거와 SIGTERM 전송이 비동기라 순서가 어긋난다. preStop 훅으로 지연을 넣어 서비스에서 빠질 시간을 벌고, terminationGracePeriodSeconds를 드레이닝 타임아웃보다 크게 둔다.
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 15"] # 엔드포인트 제거 전파 대기
terminationGracePeriodSeconds: 140 # preStop + 드레이닝 여유
세션 이관: 상태를 어디 두느냐
드레이닝이 끝나도 Blue에만 있던 세션 상태는 사라진다. 근본 해법은 세션을 인스턴스 밖으로 빼는 것이다. 인메모리 세션을 쓰면 전환 후 사용자가 재로그인하거나 장바구니가 비는 문제가 생긴다.
| 방식 | 이관 난이도 | 주의점 |
|---|---|---|
| 인메모리(로컬) | 불가 | 배포 시 세션 유실 |
| 스티키 세션 | 부분 | Blue 종료 시 해당 사용자만 유실 |
| 외부 스토어(Redis) | 쉬움 | 스토어 자체가 SPOF·버전 호환 |
| JWT(무상태) | 불필요 | 즉시 무효화 어려움 |
-- 세션 스키마에 버전 태그를 둬 Green의 새 포맷과 공존시킨다
CREATE TABLE sessions (
sid TEXT PRIMARY KEY,
payload JSONB NOT NULL,
schema_ver SMALLINT NOT NULL DEFAULT 1, -- 읽는 쪽이 버전 분기
expires_at TIMESTAMPTZ NOT NULL
);
스키마·직렬화 호환성
Green이 세션 구조를 바꾸면 Blue가 아직 살아있는 드레이닝 구간에서 상호 읽기가 깨진다. 규칙은 N-1 호환: 새 코드는 옛 포맷을 읽을 수 있어야 하고, 필드는 추가만 하며 삭제·의미 변경은 최소 한 배포 뒤로 미룬다. 직렬화는 pickle 같은 코드 결합형 대신 JSON처럼 버전 독립적인 포맷을 쓴다.
실무 체크리스트와 함정
- 드레이닝 타임아웃 ≤ 앱 graceful timeout ≤ 종료 grace period 순서로 정렬한다.
- WebSocket·SSE·gRPC 스트림은 HTTP 요청 단위 드레이닝으로 안 끊긴다. 서버가 재연결을 유도하는 close 프레임을 명시적으로 보내야 한다.
- 롤백 대비로 Blue를 즉시 삭제하지 말고 축소(scale-in) 상태로 유지한다.
- DB 커넥션 풀·큐 컨슈머도 종료 시퀀스에 포함한다. HTTP만 드레이닝하면 처리 중 백그라운드 작업이 유실된다.
- 드레이닝이 실제로 무손실인지 배포 중 5xx·리셋 카운트로 검증한다. 설정만으로 가정하지 않는다.