무중단 배포에서 Connection과 Session을 고려해야 하는 이유
웹서비스를 운영하다 보면 새로운 기능을 반영하거나 버그를 수정하기 위해 애플리케이션을 배포해야 한다.
서비스 규모가 작을 때는 프로세스를 종료하고 새로운 버전을 실행하는 방식도 가능하다. 하지만 실제 운영 환경에서는 몇 초의 연결 끊김도 사용자 오류로 이어질 수 있다.
그래서 많이 사용하는 방식이 무중단 배포다.
Blue-Green Deployment, Rolling Deployment, Canary Deployment처럼 여러 방식이 있지만 단순히 새로운 서버를 실행한 뒤 트래픽을 넘긴다고 해서 완전한 무중단 배포가 되는 것은 아니다.
실제 운영 환경에서 더 중요한 것은 기존 서버가 가지고 있던 Connection과 Session을 어떻게 처리하느냐다.
배포 과정에서 이 두 가지를 제대로 고려하지 않으면 서버는 정상적으로 교체됐는데 사용자는 갑자기 로그아웃되거나, 다운로드가 중단되거나, API 요청이 실패하는 문제가 발생할 수 있다.
이번 글에서는 무중단 배포 과정에서 Connection과 Session이 왜 중요한지 서버 내부 동작을 기준으로 살펴본다.
서버가 교체돼도 기존 Connection은 남아 있다
무중단 배포 환경을 단순화하면 다음과 같은 구조를 생각할 수 있다.
Client
↓
Load Balancer / Nginx
↓
Application Server A
Application Server B
현재 대부분의 요청이 A 서버에서 처리되고 있다고 가정해보자.
새로운 버전을 배포하기 위해 B 서버를 실행한 뒤 Load Balancer에서 신규 요청을 B로 전달하면 겉보기에는 자연스럽게 서버가 교체된다.
하지만 문제는 이미 A 서버와 연결돼 있는 사용자다.
TCP Connection은 요청 하나가 끝났다고 반드시 바로 종료되는 것이 아니다.
HTTP Keep-Alive가 활성화돼 있다면 하나의 TCP Connection을 이용해 여러 HTTP 요청을 처리할 수 있다.
예를 들어 사용자가 다음과 같은 Connection을 유지하고 있을 수 있다.
Client
│
│ established connection
▼
Server A
이 상황에서 Load Balancer가 신규 Connection을 모두 Server B로 전달한다고 해도 기존 Connection이 자동으로 B 서버로 이동하는 것은 아니다.
이미 생성된 TCP Connection은 Client와 Server A 사이에 존재한다.
따라서 Server A 프로세스를 즉시 종료하면 해당 Connection도 함께 끊어진다.
사용자 입장에서는 무중단 배포를 했다고 하지만 실제로는 요청 실패를 경험할 수 있다.
무중단 배포에서 Connection이 문제가 되는 이유
TCP Connection은 다음과 같은 정보에 의해 식별된다.
Source IP
Source Port
Destination IP
Destination Port
Protocol
일단 Connection이 생성되면 해당 연결 상태는 운영체제의 TCP Stack에서 관리된다.
다음 명령으로 현재 연결 상태를 확인할 수 있다.
ss -ant
예를 들어 다음과 같은 결과가 있을 수 있다.
ESTAB 0 0 10.0.0.10:443 203.0.113.10:52341
ESTAB 0 0 10.0.0.10:443 203.0.113.11:52342
ESTAB는 현재 TCP Connection이 정상적으로 연결된 상태라는 의미다.
배포 과정에서 이 Connection이 존재하는데 애플리케이션이나 서버 프로세스를 강제로 종료하면 연결이 중간에 끊길 수 있다.
특히 다음과 같은 서비스에서는 영향이 더 크게 나타난다.
대용량 파일 다운로드
파일 업로드
Streaming
WebSocket
SSE(Server-Sent Events)
Long Polling
긴 API 요청
일반적인 웹페이지 요청은 수십~수백 ms 안에 끝날 수 있지만 WebSocket처럼 수시간 동안 Connection을 유지하는 서비스도 있다.
이 때문에 무중단 배포는 단순히 서버 프로세스를 교체하는 문제가 아니다.
기존 Connection이 안전하게 종료될 시간을 확보하는 과정이 필요하다.
Graceful Shutdown이 필요한 이유
이 문제를 해결하기 위해 사용하는 대표적인 방식이 Graceful Shutdown이다.
Graceful Shutdown은 서버 프로세스를 즉시 종료하지 않고 기존 요청을 처리한 뒤 종료하는 방식이다.
일반적인 배포 흐름은 다음과 같다.
기존 서버 A 실행
↓
새 서버 B 실행
↓
B Health Check 성공
↓
신규 트래픽을 B로 전달
↓
A로 신규 요청 전달 중지
↓
A의 기존 요청 처리 완료
↓
A 종료
여기서 중요한 단계가 바로 A 서버로 새로운 요청이 들어오지 않도록 만드는 것이다.
이를 흔히 Drain 또는 Connection Draining이라고 부른다.
예를 들어 Load Balancer가 Server A를 Backend Pool에서 제거하더라도 기존 Connection은 일정 시간 유지하도록 설정할 수 있다.
이 시간을 충분히 확보하면 기존 사용자의 요청을 처리하면서 자연스럽게 Server B로 트래픽을 이동시킬 수 있다.
반대로 서버를 Backend에서 제거하자마자 프로세스를 종료하면 무중단 배포 효과가 크게 떨어진다.
Session은 Connection과 다른 문제를 만든다
Connection 문제가 해결됐다고 해서 무중단 배포가 완성되는 것은 아니다.
다음으로 고려해야 할 것이 Session이다.
웹 애플리케이션에서 로그인 상태나 사용자 정보를 Session에 저장하는 경우가 많다.
예를 들어 Server A 메모리에 다음과 같은 Session이 있다고 가정해보자.
Session ID : ABC123
User ID : user01
Login : true
Cart : product01
사용자가 Server A를 이용하는 동안에는 문제가 없다.
하지만 배포 후 요청이 Server B로 전달되면 문제가 발생할 수 있다.
Server B에는 해당 Session 데이터가 없기 때문이다.
구조를 단순화하면 다음과 같다.
Client
│
│ Session ID : ABC123
▼
Server B
Session ABC123 없음
결과적으로 사용자는 갑자기 로그아웃되거나 장바구니 정보가 사라지는 현상을 경험할 수 있다.
서버 입장에서는 정상적으로 새로운 버전으로 전환됐지만 사용자 경험은 무중단이 아닌 것이다.
Sticky Session만으로 해결하기 어려운 이유
Session 문제를 해결하기 위해 Sticky Session을 사용하는 환경도 있다.
Sticky Session은 특정 사용자의 요청을 가능한 한 동일한 서버로 보내는 방식이다.
User A → Server A
User B → Server B
User C → Server A
이를 이용하면 Server Local Session을 사용하는 환경에서도 일정 부분 Session 문제를 줄일 수 있다.
하지만 무중단 배포에서는 한계가 있다.
Server A를 제거해야 하는 순간이 결국 오기 때문이다.
Server A가 종료되면 A 메모리에 저장된 Session도 함께 사라진다.
따라서 Sticky Session은 서버 교체 문제의 근본적인 해결책이라기보다는 트래픽 분배 정책에 가깝다.
서버를 자유롭게 추가하거나 제거해야 하는 환경에서는 Session을 특정 애플리케이션 서버에 종속시키지 않는 구조가 훨씬 안정적이다.
외부 Session Store가 필요한 이유
대규모 서비스에서는 Session을 애플리케이션 서버 내부가 아니라 외부 저장소에서 관리하는 경우가 많다.
대표적인 구조는 다음과 같다.
┌→ Application A
Client → LB ──┤
└→ Application B
│
▼
Redis
Application A와 Application B 모두 같은 Redis에 접근한다.
Session 데이터가 Redis에 저장돼 있다면 사용자의 요청이 어떤 서버로 전달되더라도 동일한 Session을 사용할 수 있다.
예를 들어 다음과 같다.
Session ID : ABC123
│
▼
Redis
│
┌───┴───┐
▼ ▼
Server A Server B
이 구조에서는 Server A가 배포 때문에 종료되더라도 Session 자체는 유지된다.
따라서 새로운 요청이 Server B로 이동해도 로그인 상태를 유지할 수 있다.
Redis 외에도 Database나 별도의 Distributed Cache를 Session Store로 사용할 수 있다.
핵심은 Session의 생명주기를 애플리케이션 서버 프로세스와 분리하는 것이다.
실제 배포 중 나타날 수 있는 증상
Connection이나 Session 처리가 제대로 되지 않은 환경에서는 배포 시간에만 간헐적인 장애가 발생하는 경우가 많다.
대표적으로 다음과 같은 현상이 나타난다.
502 Bad Gateway
503 Service Unavailable
Connection Reset
Socket Closed
Request Timeout
로그인 상태 해제
장바구니 정보 소실
WebSocket 재연결
파일 다운로드 중단
특징적인 점은 장애가 오래 지속되지 않는다는 것이다.
배포가 끝나면 대부분 정상 상태로 돌아오기 때문에 원인을 찾기 어려울 수 있다.
특히 여러 대의 서버 중 일부만 교체된다면 모든 사용자에게 문제가 발생하지 않는다.
특정 시점에 특정 서버와 연결돼 있던 사용자만 오류를 경험하게 된다.
이 때문에 서버 로그만 보면 정상 배포로 판단하기 쉽다.
배포 과정에서 어떤 부분을 분석해야 할까
무중단 배포 장애를 분석할 때는 애플리케이션 로그만 보는 것으로 충분하지 않다.
먼저 Load Balancer 또는 Reverse Proxy의 트래픽 전환 시점을 확인해야 한다.
Nginx라면 Access Log와 Error Log에서 배포 시간대의 상태 코드를 확인한다.
grep " 502 " /var/log/nginx/access.log
grep " 503 " /var/log/nginx/access.log
502나 503이 배포 시간과 정확히 겹친다면 Backend 서버 교체 과정에서 요청이 실패했을 가능성을 살펴봐야 한다.
TCP Connection 상태도 확인할 수 있다.
ss -antp
특정 애플리케이션 프로세스가 종료되기 직전까지 많은 ESTABLISHED Connection을 가지고 있었다면 너무 빠르게 서버를 종료했을 가능성이 있다.
애플리케이션 측에서는 종료 로그도 확인해야 한다.
예를 들어 종료 신호를 받은 이후 기존 요청을 처리했는지 아니면 프로세스가 즉시 종료됐는지를 확인한다.
Session 문제가 의심된다면 배포 이후 로그인 관련 오류가 증가했는지 확인한다.
서버별 Session 저장 방식을 확인하는 것도 중요하다.
Local Memory Session
Redis Session
Database Session
JWT
어떤 방식을 사용하는지에 따라 원인 분석 방향이 크게 달라진다.
안정적인 무중단 배포 구조를 만드는 방법
안정적인 배포를 위해서는 먼저 새로운 서버가 완전히 준비된 이후에 트래픽을 전달해야 한다.
단순히 프로세스가 실행됐다고 Ready 상태로 판단하면 안 된다.
Database 연결, Cache 연결, 주요 초기화 작업까지 완료된 상태인지 확인해야 한다.
이를 위해 Health Check와 Readiness Check를 사용할 수 있다.
그다음 기존 서버에 신규 요청이 전달되지 않도록 한다.
Server A
New Request 차단
Server B
New Request 처리
Server A는 기존 요청을 처리하는 역할만 수행한다.
기존 Connection과 Request가 충분히 종료되면 그때 Server A를 종료한다.
또한 Session을 서버 내부 메모리에 저장하고 있다면 Redis 같은 외부 Session Store로 분리하는 것을 고려할 수 있다.
WebSocket처럼 장시간 Connection을 유지하는 서비스라면 별도의 종료 정책도 필요하다.
무조건 Connection이 없어질 때까지 기다릴 수는 없기 때문에 일정 시간 이후 Client가 재접속하도록 만드는 구조를 사용하는 경우가 많다.
무중단 배포에서 중요한 것은 서버가 아니라 사용자 요청이다
무중단 배포를 처음 구성하면 서버의 상태에 집중하기 쉽다.
Server A 종료
Server B 실행
하지만 실제 서비스 관점에서는 서버보다 사용자 요청의 상태가 더 중요하다.
서버가 정상적으로 교체됐더라도 사용자가 다음과 같은 경험을 했다면 완전한 무중단 배포라고 보기 어렵다.
요청 실패
다운로드 중단
로그아웃
WebSocket 종료
API Timeout
따라서 배포 시스템을 설계할 때는 서버 프로세스의 시작과 종료만 관리해서는 안 된다.
TCP Connection의 생명주기와 HTTP Keep-Alive, 애플리케이션의 Graceful Shutdown, Load Balancer의 Connection Draining, Session 저장 위치까지 함께 고려해야 한다.
정리
무중단 배포의 핵심은 새로운 버전의 서버를 실행하는 것이 아니라 기존 요청을 안전하게 새로운 환경으로 전환하는 데 있다.
Load Balancer에서 신규 트래픽을 다른 서버로 전달하더라도 기존 TCP Connection이 자동으로 이동하는 것은 아니다.
따라서 기존 서버를 바로 종료하면 처리 중이던 요청이나 장시간 유지되던 Connection이 끊길 수 있다.
이를 방지하기 위해서는 Connection Draining과 Graceful Shutdown을 이용해 기존 요청을 충분히 처리한 뒤 서버를 종료해야 한다.
Session 역시 중요한 요소다.
Session을 각 애플리케이션 서버의 메모리에 저장하면 배포 과정에서 사용자의 요청이 다른 서버로 이동할 때 로그인 상태나 사용자 정보가 사라질 수 있다.
Redis 같은 외부 Session Store를 사용해 Session을 애플리케이션 프로세스와 분리하면 서버를 교체하더라도 사용자 상태를 유지하기 쉬워진다.
결국 안정적인 무중단 배포는 단순한 서버 교체 기술이 아니다.
Connection, Session, Load Balancer, 애플리케이션 종료 방식이 하나의 흐름으로 설계돼야 실제 사용자 관점에서도 서비스 중단 없이 배포를 수행할 수 있다.




댓글 0
첫 댓글을 남겨보세요.