무중단 배포는 진짜 0.00001초의 공백도 없을까?
글 재료를 좀 찾다가... 프로비저닝이라는 단어를 봐버림. 이중화, 무중단 배포, Blue-Green, Canary에 대해서...
오늘은 뭘 써야하나… 소재 고갈에 고민하다가, 문득 Azure에서 컨테이너 앱을 올릴 때 콘솔 한구석에 떠 있는 “프로비저닝 중…” 이라는 문장이 눈에 들어왔다.
이중화, 프로비저닝은 정말로… 1초의 공백도 없는 무중단일까?
“무중단 배포”라는 말을 수도 없이 들어봤고, 공부도 해봐서 나름 잘 구현도 할 수 있다. 그런데, 막상 “그래서 진짜 단 1초도 안 끊긴다는 게 가능한 거야?” 라고 누가 물어보면 나는 자신 있게 답할 자신이 없었다.
마침 글 소재도 없고 해서, 오늘은 이 한 문장을 파보기로 했다.
1. 그래서 프로비저닝이 뭔데?
처음에 좀 헷갈렸다. “프로비저닝(Provisioning)” 이라는 단어는 맥락에 따라 다르게 쓰인다.
- 인프라 프로비저닝 — VPC, 서브넷, 보안그룹 같은 걸 만드는 과정 (Terraform 같은 IaC)
- 서버 프로비저닝 — VM이나 컨테이너 instance를 띄우고 OS/런타임 셋팅하는 과정
- 사용자 프로비저닝 — SSO 시스템에서 새 유저 계정 생성하는 과정
내가 본 Azure 콘솔의 “프로비저닝 중…”은 두 번째에 해당한다.
새 컨테이너 instance를 클러스터 어딘가에 띄우고, 이미지 받고, 헬스체크 통과시키고, 서비스 디스커버리에 등록하는 일련의 과정.
요점은, 이 시간 동안 그 새 instance는 traffic을 받지 않는다는 것.
받을 준비가 안 됐으니까.
근데 그러면 — 그동안 사용자 요청은 누가 처리하고 있는가? 여기서 이중화 얘기가 나온다.
2. 무중단의 시작은 결국 이중화
당연한 얘기지만, 서버 한 대 띄워놓고 그 한 대를 새 버전으로 갈아끼우는 동안엔 무조건 다운타임이 생긴다. 죽이고 띄우는 사이의 몇 초~몇 분이 그대로 사용자한테 노출된다.
그래서 무중단 배포의 첫 번째 전제는 “항상 두 대 이상이 동시에 살아 있다” 다.
[사용자] → [Load Balancer] → [Replica 1] (옛 버전, 살아있음)
→ [Replica 2] (새 버전, 막 떴음)
이 구조에서 LB가 health check 통과한 replica한테만 traffic을 보내면, 한 대씩 차례로 갈아끼우면서도 사용자는 끊김 없이 응답을 받게 된다.
내가 지난 글에서 Container App을 만들 때 --min-replicas 1 로 둔 게 좀 마음에 걸리는데, 정말 무중단을 보장하려면 최소 2가 안전하다는 게 이런 이유였다.
(1대 운영 + 새 버전 1대 띄우는 동안의 그 짧은 순간은 사실 100% 무중단이라고 보기 어렵다.)
3. 무중단 배포의 3대 패턴
이제 본격적으로, 새 버전을 어떻게 옛 버전과 바꿔 끼우는가. 패턴이 크게 셋이다. 정처기, 보안기사, 기술사 공부에 무진장 반복되지만 매번 새로운 녀석들…ㅋㅋ
Rolling Deployment
[Replica 1: v1] → 죽이기 → [Replica 1: v2 새로 생성]
[Replica 2: v1] → 그동안 살아있음
다음 차례:
[Replica 1: v2] → 살아있음
[Replica 2: v1] → 죽이기 → [Replica 2: v2 새로 생성]
- 장점: 자원 적게 씀 (전체 replica 수가 거의 그대로)
- 단점: 배포 중에 v1과 v2가 동시에 응답한다. 사용자가 새로고침하면 v1, v2가 번갈아 보일 수도 있음. API 스키마가 바뀌었으면 문제 생김.
Kubernetes의 default 배포 방식이고, ECS의 rolling update도 이거다.
Blue-Green Deployment
[Blue 환경: v1, 100% traffic] (현재 운영)
[Green 환경: v2, 0% traffic] (새로 띄움, 검증 대기)
검증 끝 → traffic switch (LB 설정 변경)
[Blue 환경: v1, 0% traffic] (대기, 롤백용)
[Green 환경: v2, 100% traffic] (운영)
- 장점: switch 순간이 거의 atomic. v1과 v2가 섞이지 않음. 롤백이 빠름 (LB 다시 돌리면 끝).
- 단점: 자원 두 배 필요. 짧은 시간이지만 v1 + v2 양쪽이 다 떠 있어야 함.
Canary Deployment
[v1: 95% traffic]
[v2: 5% traffic] ← 일부만 v2로 보내서 모니터링
문제 없으면 점진적으로:
[v1: 50%]
[v2: 50%]
↓
[v1: 0%]
[v2: 100%]
- 장점: 새 버전 문제를 일부 사용자한테만 노출하면서 점진적 배포. 모니터링 지표 보면서 자동 롤백 가능.
- 단점: 셋업 복잡. trace ID 기반 샘플링이나 사용자 segmentation 같은 게 필요.
대규모 서비스(넷플릭스, 페이스북 등)에서 자주 쓰는 패턴.
4. Azure Container Apps의 revision 모델은 어디에 속하나?
내가 직접 쓰는 Container Apps 기준으로 보면, 사실상 Blue-Green에 가깝다.
새 이미지 push → 새 revision 자동 생성 → health check 통과
→ traffic이 새 revision 으로 shift
→ 옛 revision deactivate (--revisions-mode single 일 때)
내가 봤던 그 “프로비저닝 중…” 이 바로 새 revision의 instance를 띄우고 health check를 통과시키는 시간. 이 시간 동안 옛 revision은 멀쩡히 응답하고 있다.
그러니까 사용자 입장에선 옛 버전이 끊김 없이 동작하다가, 어느 순간부터 새 버전이 응답하는 구조.
겉으로 보면 무중단이 맞다.
5. 근데 진짜로 무중단인가? — 숨겨진 빈틈들
여기서부터가 오늘 진짜 궁금했던 부분이다. 결론부터 말하면 — “99.9X% 무중단”이지 “0초 단 한 번도 안 끊김” 은 환상에 가깝다. 빈틈들이 꽤 있더라.
빈틈 1. In-flight 요청과 Graceful Shutdown
옛 replica를 종료시키는 그 순간, 이미 그 replica가 처리 중이던 요청은 어떻게 되는가?
- LB가 traffic을 안 보내는 것과
- 이미 들어온 요청을 마저 처리하는 것
이 두 개는 다른 얘기다. 그래서 graceful shutdown(또는 connection draining) 이 필요하다. 종료 신호 받으면:
- 새 요청은 받지 않는다
- 처리 중이던 요청은 마저 끝낸다 (보통 30초~몇 분)
- 그 다음에야 진짜로 죽는다
이게 안 되어 있으면 — 그 순간 처리 중이던 요청이 끊겨서 사용자한테 502/504가 떨어진다. 사용자 1명한테 1번만 끊겨도 그건 무중단이 아니다.
내가 평소에 nginx 설정에서 proxy_read_timeout 같은 걸 30초 정도로 둔 것도 사실 이 graceful 종료의 안전 마진과 관계가 있다고 들었다.
빈틈 2. Long-lived Connection
WebSocket, gRPC streaming, SSE 같은 연결을 오래 유지하는 통신은 진짜 골치다.
옛 replica가 graceful 종료를 시작했어도, 그 위에 연결된 WebSocket은 결국 끊어야 한다. 클라이언트는 그 끊김을 받고 재연결을 시도해야 하는데, 이게 잘 짜여있지 않으면:
- 사용자 화면이 잠깐 멈추거나
- 메시지가 일부 누락되거나
- “연결이 끊겼습니다” 토스트가 뜨거나
이런 식의 사용자 인지 가능한 끊김이 발생한다. 이게 진짜 무중단의 가장 큰 적이라고 보면 될 것 같다.
빈틈 3. DNS TTL
이건 인프라 단 변경(예: LB 자체를 바꾸는 경우)에서 발생한다.
DNS 레코드를 바꿨을 때, 클라이언트의 resolver가 그 변경을 보기까지 TTL만큼 시간이 걸린다. TTL이 60초로 짧게 박혀있어도 그동안엔 일부 사용자가 옛 IP로 들어온다.
다행히 Container Apps의 revision 전환은 LB 안에서 이루어지니까 DNS와는 무관. 근데 도메인 자체를 옮기는 컷오버 같은 작업에선 빈틈이다.
빈틈 4. DB 마이그레이션
코드는 v1 → v2로 바뀌는데 DB 스키마가 같이 바뀐다? 이건 무중단의 가장 큰 적 중 하나다.
- v1 코드가 옛 스키마를 기대하고 있는데 새 스키마로 바뀌면 에러
- v2 코드가 새 스키마를 기대하고 있는데 옛 스키마로 들어가면 에러
그래서 운영에선 보통 하위호환 가능한 마이그레이션 패턴을 쓴다. (Expand-Contract 패턴 같은 거.)
1단계: 컬럼 추가 (v1, v2 둘 다 동작) 2단계: 코드 v2로 배포 (새 컬럼 사용) 3단계: 옛 컬럼 제거 (v2만 남았을 때)
이걸 안 지키면 배포 중에 일부 요청이 무조건 깨진다.
빈틈 5. Cache, CDN
새 버전 배포했는데 CDN/Front Door 캐시에 옛 버전이 박혀있으면 사용자 일부는 옛 버전을 본다. 정적 자산은 보통 hash 붙여서 해결하지만, HTML이나 JSON 응답이 캐싱되어 있으면 purge가 필요하다.
이것도 “무중단”의 일부로 봐야 하는지는 살짝 애매한데, 사용자 입장에서 옛 버전을 보는 것도 일종의 불일치니까 넓게 보면 빈틈에 들어간다.
6. 그래서 결론 — “거의 무중단”이지 “0초”는 아니다
여기까지 정리해보면, 무중단 배포라는 건 사실:
“잘 셋업하면, 사용자 거의 대부분이 못 느낄 만큼 짧은 끊김으로 새 버전을 띄울 수 있다”
정도가 정확한 표현인 것 같다.
가용성 SLA를 보면 이게 더 명확해진다.
| SLA | 연간 허용 다운타임 |
|---|---|
| 99% | 약 3.65일 |
| 99.9% | 약 8.76시간 |
| 99.99% | 약 52.6분 |
| 99.999% (Five 9’s) | 약 5.26분 |
대부분 클라우드 서비스가 SLA로 보장하는 게 99.9% ~ 99.95% 수준이다. 즉 1년에 몇 시간은 끊긴다는 뜻이고, 그 안에 무중단 배포 중 발생하는 짧은 끊김도 다 들어가 있다고 보면 될 것 같다.
진짜 0초의 무중단이 가능하려면:
- 모든 요청이 idempotent 해야 하고
- graceful shutdown이 모든 layer에서 완벽해야 하고
- WebSocket 같은 long-lived 연결은 클라이언트에서 자동 재연결을 매끄럽게 하고
- DB 마이그레이션은 하위호환 패턴을 지키고
- 캐시는 적절히 purge하고
이 모든 게 다 맞아떨어져야 한다. 그리고 사실, 이 모든 게 다 맞아떨어진 시스템은 진짜 드물다.
마치며
사실 그동안 “무중단 배포 = 진짜 안 끊김” 이라고 막연하게 생각했었는데, 실상은 아니라는거다.
오히려 정확한 표현은 — “끊김이 사용자가 못 느낄 만큼 짧고, 끊겨도 자동 복구가 되도록 설계된 배포” 가 무중단 배포인 것 같다.
물론 뭐, 동영상도 사실은 사진의 연속이니까… 우리 눈이 느끼지만 못할 뿐..
암튼 그냥 끄적여봤다.
마침.