OS EOS 가 끌고 온 RabbitMQ 무중단 마이그레이션
운영 RabbitMQ 가 떠있던 서버의 OS 가 EOS (End of Service) 시점에 다다랐다. NCP 정책상 지원 종료된 OS 의 인스턴스는 계속 동작하지만 유사 서버 생성 · Auto Scaling · 서비스 운영 및 기술 지원이 모두 막힌다 (NCP 서버 이미지 라이프사이클). 거기에 보안 패치까지 끊긴 상태로 메시지 브로커를 운영에 두는 건 리스크 대비 합리적이지 않아서, 신규 OS 에 RabbitMQ 를 새로 띄우고 producer / consumer 의 연결 정보를 바꾸기로 했다.
작업 자체는 단순했다. 새 서버에 RabbitMQ 설치, 4 개 서비스의 연결 정보 교체, 배포. 관건은 그 사이 메시지 유실 없이 라이브 동기 트래픽을 안 죽이는 것이었다.
영향 범위부터 정리
기존 MQ 에 붙어있던 서비스:
| 서비스 | 역할 |
|---|---|
| 메시지 발행 서버 | MQ 발행 |
| 앱 서버 | 모바일 앱 — 소비 + 일부 발행 |
| DLQ 처리기 | Dead letter 처리 |
| B2B 파트너 연동 서비스 | 외부망 파트너 API |
B2B 파트너 연동 서비스만 외부망이라 배포 파이프라인이 다르다. 단계적 promote 가 없고 바로 deploy 가 떨어진다. 이게 나중에 시나리오 분기 포인트가 된다.
발행은 발행 서버 한 곳에서만 일어난다
이 글의 모든 시퀀싱이 가능했던 가장 큰 구조적 전제는 다음 한 가지다. 어떤 서비스도 RabbitMQ 에 직접 발행하지 않는다. 앱 서버에서 발행할 일이 있어도 RabbitMQ 가 아니라 발행 서버를 HTTP 로 호출해서 위임한다.
발행 path 가 한 줄기로 모여있다는 게 이번 작업에서 직접적인 안전판이 됐다 — 발행 서버 한 대만 신규 MQ 로 옮기면 모든 발행이 자동으로 신규 쪽으로 흐른다. 만약 앱 서버나 다른 내부 서비스가 자기 RabbitTemplate 으로 직접 발행하고 있었다면, 발행 경로가 여러 갈래로 갈라지면서 시퀀싱이 훨씬 까다로워졌다. "구 MQ 큐 잔량이 0 인지" 확인하는 그 단순한 조건도 갈래 수만큼 늘어났을 거다.
신/구 설정이 같다는 걸 어떻게 보장할까
수동으로 큐 / 익스체인지 / 바인딩을 다시 정의하면 무조건 빠진다. RabbitMQ 관리 UI 의 Export definitions 로 구 서버 정의를 통째로 받아서 신규 서버에 import 했다.
검증은 양쪽 definitions 를 다시 export 해서 비교했다. 큐 / 익스체인지 / 바인딩 정의가 완전히 동일한 것까지 확인하고 넘어갔다. 이 단계를 건너뛰면 운영 중 "왜 이 큐 메시지가 안 빠지지" 같은 사고가 한참 뒤에 터진다.
로컬 통합 테스트
운영 배포 전, 로컬에서 4 개 서비스를 모두 띄워 신규 MQ 와 붙여봤다.
- 모바일 앱 계정으로 신규 서버 connections 성공
- 발행 서버 / DLQ 처리기 / B2B 소비자 모두 로컬에서 신규 주소로 정상 접속
- 보험사별 배정 / 철회 워크플로 end-to-end 통과, DLQ 경로도 확인
배포 순서
배포 순서는 한 줄이다. 메시지 발행 서버 먼저, 구 MQ 큐 잔량이 0 인 걸 확인한 뒤 소비 서버.
T+0 발행 서버 → 신규 MQ 로 발행 시작
→ 구 MQ 는 더 이상 in-flow 없음
T+? 구 MQ 큐 잔량이 0 이 될 때까지 대기
→ 5~10 분 사이 (실측치)
T+N 소비 서버 (앱 / DLQ 처리기 / B2B 파트너) 배포이 순서가 아니면 두 가지가 터진다.
- 소비 먼저 배포: 신규 MQ 에 메시지가 없으니 소비자가 멍 때림. 그 사이 구 MQ 에 쌓인 메시지는 영영 안 빠짐.
- 동시 배포: 일부 발행은 구 MQ, 일부 소비는 신규 MQ → 메시지 분실.
이 순서를 택한 대가는 발행 ~ 소비 사이 약 5~10분간 비동기 흐름이 멈춤. 동기 사용자 요청은 정상 동작하지만, 일부 파트너사 배정 / 철회 메시지는 큐에 쌓여있다가 늦게 처리되는 상태가 된다.
DLQ 복구
배포 후 신규 MQ 에서 DLQ 로 빠진 메시지를 직접 꺼내 수동 재발행했다. 최종 누락 메시지는 0건. 자동 무유실이 아니라 DLQ 안전망 + 사람의 수동 재발행 두 단계 덕분에 0건이 나왔다.
회고
다시 한다면 바꿀 것:
- B2B 파트너 연동 서비스의 다른 배포 파이프라인을 더 일찍 팀에 공유. 외부망 서비스라 단계적 promote 단계 없이 바로 배포된다는 점은 알고 있었지만, 시나리오 문서와 팀 공지에 늦게 반영했다. 처음부터 별도 트랙으로 공표했어야 작업 당일 분기 안내가 없었을 거다.
- 구 MQ 잔량 0 확인을 자동화. 사람이 admin UI 띄워놓고 새로고침하는 건 5~10분 동안 집중력 깎인다. 이번엔 서버에 직접 접근 권한이 없어서
rabbitmqctl list_queues messages같은 CLI 폴링은 못 했는데, 다음엔 RabbitMQ Management HTTP API (/api/queues) 를 polling 하는 짧은 스크립트로 풀고 싶다. admin UI 와 같은 권한으로 충분하다. - definitions diff 도 자동화. 이번엔 VSCode 확장 프로그램으로 JSON 정렬 후 파일 diff 비교해서 눈으로 확인했다. 다음엔 정렬부터 diff 까지 스크립트로 한번에 돌리는 게 낫겠다.
이번 작업이 가능했던 구조적 전제 — 발행 / 소비 분리
이번에 시퀀싱이 가능했던 건 시스템이 메시지 발행 서버와 메시지 소비 서버로 이미 분리되어 있었기 때문이다. 같은 서버에 둘이 같이 있었다면 한 번에 두 방향을 끊고 다시 잇는 작업이 됐을 거다.
- 하나씩 넘기는 시퀀싱. 발행을 먼저 옮기고 구 MQ 가 자연스럽게 비워질 시간을 번 다음, 소비 서버를 옮겼다. 한쪽씩 손대니까 어디서 문제가 터졌는지 즉시 알 수 있다.
- 리소스 / 배포 격리. 한쪽 서버를 재기동하거나 부하가 튀어도 다른 쪽으로 번지지 않는다. 마이그레이션처럼 한쪽만 손대는 작업에서 이 격리가 그대로 활용된다.
- 점진적 배포의 자유도. 외부망 / 내부망 서비스를 따로 움직일 수 있었던 것도 분리 덕분이다.
분리 자체가 무조건 정답은 아니다. 인스턴스 수, 모니터링 surface, 운영 복잡도가 늘어난다. 다만 무중단 이전 같은 작업이 들어왔을 때 이 비용은 회수된다.