배포 중 발생한 요청 유실 — 502 Bad Gateway

블루-그린 배포 과정에서 트래픽 전환 시점과 기존 서버 종료 시점이 겹치며 요청 유실이 발생했다.
핵심 문제는 요청 처리 중인 상태에서 WAS가 먼저 종료되는 타이밍 충돌이었다.
실제 로그를 보면 이 타이밍이 명확하게 드러난다.
WAS는 이미 종료 절차에 들어갔지만,
Nginx는 0.05초 뒤에도 해당 서버로 요청을 전달했다.
이때 해당 요청은 정상 응답을 받지 못하고
OS 레벨에서 RST 패킷으로 연결이 강제 종료되며 유실되었다.
문제의 본질
- Nginx는 기존 요청을 보호하며 정상적으로 reload를 수행 ⇒ L7 계층
- 하지만 WAS는 SIGTERM 수신 즉시 프로세스 종료 절차 수행 ⇒ L4 계층
그 결과 요청을 받을 수 없는 서버로 트래픽이 유입되는 구간이 발생한 것이다.
즉, L7과 L4 작업 프로세스가 서로 영향을 받는 상태였다.
Graceful Shutdown 설계 기준 조정
Graceful Shutdown의 핵심은
처리 중인 요청을 중단하지 않고 끝까지 완료시키는 것이다.
이를 위해 p99 응답 시간을 기준으로 종료 대기 시간을 설정했다.

- 상위 1% 요청도 3초 내외에서 완료
- SIGTERM 이후 남아있는 요청은 최대 약 3.1초 내 종료 가능
[요청 처리 보장 시간 의사결정]
- Application (timeout-per-shutdown-phase) : 12s ~ 15s
- Docker (stop_-grace_period) : 25s ~ 30s
p99(≈3.1s)에 네크워크 지연 및 추가적인 리소스 정리를 고려해 충분한 대기 시간을 확보하고,
애플리케이션(L7)과 컨테이너(L4)의 종료 타이밍을 분리하여 설계했다.
L7과 L4의 종료 타이밍 분리 검증
종료 순서가 반대로 구성될 경우, 처리 중인 요청이 DB 커넥션을 사용하는 시점에
커넥션이 반환되며
SQLException: Connection is closed가 발생할 수 있다.따라서, 트래픽 차단 → 요청 처리 완료 → 리소스 반납 순으로, 종료 단계를 분리했다.
[1단계] 0 ~ 12초 ⇒ 요청 처리 종료 (L7 → L4)
Graceful Shutdown 적용 이전 — RST 강제 종료

- 요청 처리 중 프로세스 강제 종료(SIGKILL) ⇒ 커널이 소켓을 즉시 정리하며 RST 반환
- HTTP 응답 없이(9번 패킷 Len=0) 소켓 강제 종료
Graceful Shutdown 적용 이후 — FIN 정상 종료

- 패킷 4번 : HTTP 요청(POST /api/test/payment/ready)을 전송 및 SIGTERM 요청
- 패킷 6번 : 서버의 응답 데이터 전송(PSH, ACK)
- 패킷 8번 : 서버 → 클라이언트 HTTP/ 1.1 200 OK 보냄
- 패킷 9번 : 클라이언트 → 서버 ACK 보냄

- 패킷 10번 : 클라이언트 → 서버 Grafceful Shutdown 시작(FIN, ACK)
- 패킷 11번 : 클라이언트의 종료 동의(FIN, ACK)
- 패킷 12번 : 4-way-handshake 완료 (ACK)
TCP 레벨에서 FIN 기반 종료를 보장하며,
15초 동안 HTTP 요청 처리 + TCP 연결 종료까지 충분히 완료되는 구간임을 검증하였다.
[2단계] 12 ~ 25초 ⇒ 애플리케이션 내부 정리 (L4)
1) HTTP 요청 종료 대기

SIGTERM 이후 Spring은 즉시 ApplicationContext를 종료하지 않고,
WebServerStartStopLifecycle 단계에서 대기한다.이 구간에서 신규 요청은 차단되고, 진행 중인 요청이 정상적으로 완료될 때까지 대기한다.

모든 HTTP 요청 처리가 끝나면, Tomcat이 워커 스레드를 정리하고 더 이상 요청을 처리하지 않는 상태가 된다.
2) ApplicationContext 종료 및 커넥션 등 리소스 반납

이후 스프링은 빈(Bean) 소멸 단계에 진입하며, 다음 작업이 수행된다.
- 백그라운드 작업 종료
- HikariCP 커넥션 풀 반환
- GC 및 메모리/리소스 정리
이 과정은
@PreDestroy, DisposableBean 기반으로 의존성 역순으로 안전하게 실행된다. 핵심은 요청 완료 이후에만 DB 커넥션이 반납된다는 것이고,
이것이 보장되지 않는다면 트랜잭션 실패 및 데이터 유실이 발생할 수 있다.
완성된 무중단 배포 구조


유실 0건 동작 검증

- [14초] : 10명의 VU가 동시에 결제 요청 ⇒ 서버에서 처리 시작
- [15초] : SIGTERM 신호 수신 ⇒ 신규 요청 차단 & 기존 요청 유지

- [15~18초] : 종료 대기 구간
- 처리 중이던 10건의 요청 모두 정상 완료 (HTTP 200)
connection reset by peer미발생
- [18초 이후] : 종료 상태에서의 신규 요청 차단
- 신규 요청은 서버에서 처리되지 않고 차단
- 클라이언트에서는 timeout / Interrupted로 기록
SIGTERM 이후에도 이미 처리 중이던 요청은 모두 정상 완료되었으며, 종료 상태에서는 신규 요청이 안전하게 차단되었다.
또한 해당 시점에는 트래픽이 신규 WAS로 전환된 상태였기에, 진행 중이던 요청의 유실 없이 트래픽 전환과 서버 종료가 분리되어 동작함을 확인하였다.
배포 장애에 대비한 추가 안전 장치
1. 지수 백오프 기반 헬스 체크
초기에는 빠르게 상태를 확인하고, 실패가 반복될수록
대기 시간을 점진적으로 증가시켜 시스템이 안정화될 시간을 확보하였다.
2. Nginx 경유 헬스 체크 및 롤백 메커니즘
헬스 체크를 Nginx 경유로 수행해
실제 사용자 요청 경로 전체를 검증하도록 구성했다.

아래 조건이 모두 만족되어야 200이 반환되며,
하나라도 실패하면 즉시 이전 컨테이너로 롤백한다.
- Nginx 정상 실행 및 설정 반영 (reload 성공)
- upstream 라우팅 정상 동작
- 컨테이너 간 네트워크 연결
- WAS 애플리케이션 정상 응답





