백엔드 컨테이너 하나를 새 버전으로 교체하면, 이전 컨테이너가 내려간 시점과 새 컨테이너가 요청을 받을 수 있는 시점 사이에 공백이 생긴다. 이 공백을 없애려면 순서를 바꿔야 한다. 이전 버전이 요청을 받는 동안 새 버전을 준비하고, 연결을 넘긴 뒤에도 이미 받은 요청과 작업을 마칠 때까지 이전 버전을 살려 둔다. 얼쑤는 기존 Docker Compose와 Nginx에 이 순서를 적용했다.

같은 호스트에 백엔드 슬롯 두 개를 뒀다

슬롯은 백엔드 한 인스턴스를 실행하는 자리다. Compose에 블루와 그린을 정의하고, 두 슬롯이 같은 런타임 설정을 쓰도록 했다. 배포할 때 비활성 슬롯에 새 버전의 JAR을 붙인다. 두 슬롯은 같은 MySQL을 사용한다. 사용자는 평소와 같은 HTTPS 주소로 접속하고, Nginx가 활성 슬롯으로 요청을 보낸다.

사용자 ── HTTPS ──> Nginx
                     ├──> 블루 백엔드  ──┐
                     └──> 그린 백엔드  ──┴──> 공유 MySQL

평상시에는 한 슬롯만 실행한다. 블루가 활성이라면 다음 배포는 그린에서 준비한다. 그린이 활성화되면 그다음 배포는 블루에서 준비한다. 기존 Compose에 슬롯을 추가한 이유는 정기 배포에서 교체할 대상이 백엔드 한 서비스였기 때문이다. 두 JVM이 겹치는 구간에는 CPU·메모리와 DB 연결 여유가 필요하다.

준비, 전환, 종료를 각각 확인한다

Jenkins는 develop의 변경을 감지해 체크아웃한 커밋을 빌드한다. 같은 커밋의 산출물을 배포 스크립트에 넘기면, 스크립트가 활성 슬롯을 읽고 반대쪽에서 새 버전을 준비한다.

단계확인하는 것실패하거나 작업이 남았을 때
준비대기 슬롯의 준비 상태와 운영 DB 연결 대상기존 슬롯으로 계속 응답
전환Nginx 설정 검사와 리로드, 실제로 로드된 슬롯, HTTPS 준비 상태와 API 응답살아 있는 이전 슬롯으로 경로 복구
종료이전 워커 종료, 진행 중인 업무 처리 완료, 새 슬롯 준비 상태이전 슬롯을 유지하고 다시 확인

Nginx 리로드가 성공하면 새 워커가 새 연결을 받고, 이전 워커는 이미 맡은 연결을 처리한 뒤 종료한다. 이전 워커가 블루에 요청을 전달할 수 있으므로, 그린에서 정상 응답을 받았어도 블루를 내리기 전에 이전 워커의 종료를 확인한다. 배포 스크립트는 전환 전 상태를 기록한다. 실행이 중단되면 다음 실행이 그 기록을 읽어 복구부터 시작한다. Nginx 설정 리로드 문서

HTTP 응답 뒤에도 작업이 계속된다

얼쑤에서 악보를 올리면 HTTP 요청이 끝난 뒤에도 인식 작업이 큐에 남는다. 작업 상태는 공유 DB에 있지만, 임시 원본과 읽기 권한은 접수한 슬롯에 연결돼 있다. 그린이 블루의 작업을 가져가면 같은 DB 행을 읽어도 원본을 처리할 수 없다.

그래서 임시 원본 키에 접수 슬롯을 표시했다. 각 슬롯은 자기 작업만 가져오고, 시작할 때도 자기 슬롯의 미완료 작업만 복구한다. 인식 서비스가 원본을 읽을 때도 접수한 슬롯으로 연결한다. 이전 슬롯을 내리기 전에는 Nginx 워커뿐 아니라 그 슬롯의 대기·진행 중인 악보 변환과 실행 중인 음원 작업도 확인한다. 정해진 대기 시간 안에 끝나지 않으면 종료를 미룬다.

공유 DB의 스키마도 배포 순서에 영향을 준다. 얼쑤는 필요한 Flyway 변경을 전환 전에 적용하므로, 변경된 스키마를 기존 버전과 새 버전이 함께 사용할 수 있어야 한다. 트래픽을 이전 슬롯으로 돌리는 절차와 DB 스키마 복구 절차는 각각 관리한다.

무엇으로 동작을 확인했나

격리된 Docker 환경에서 실제 Nginx와 시험용 백엔드 두 개를 띄웠다. 두 방향으로 전환하는 동안 연속 HTTPS 요청 104건이 모두 응답했다. 전환 직전에 블루가 받은 업로드는 그린이 응답하기 시작한 뒤에도 블루에서 마무리됐다. 응답의 SHA-256을 보낸 본문과 비교해 파일 내용도 일치하는 것을 확인했다.

별도의 임시 MySQL 시험에서는 그린이 시작한 뒤에도 블루의 진행 중인 변환이 보존됐고, 각 슬롯이 자기 작업만 가져갔다. 운영에서는 Jenkins가 의도한 커밋을 포함하는 버전을 자동 배포한 것을 확인한 뒤, 배포 작업의 성공 표시와 별도로 실제 주소에 요청을 보냈다.

최초 설치와 이후 전환

처음에는 라우팅 설정과 두 슬롯을 추가하느라 일부 컨테이너를 재생성했다. 이후에는 같은 구성을 유지하면서 준비된 슬롯으로 연결을 넘겼다.

전환 전후에 보낸 운영 요청

운영 주소에 준비 상태 확인과 약관 읽기를 전환 전부터 반복해 응답을 기록했다.

작업운영에서 본 결과
블루그린 구성의 최초 적용API 요청 실패가 약 36초 동안 관측됐다.
블루에서 그린으로 전환준비 상태와 약관 읽기 요청 241건이 모두 HTTP 200이었다.
그린에서 블루로 전환같은 종류의 요청 168건이 모두 HTTP 200이었다. Nginx와 MySQL 컨테이너 ID도 유지됐다.

최초 적용에는 컨테이너 재생성이 포함돼 정기 전환과 작업 조건이 달랐다. 241건과 168건은 두 종류의 읽기 요청을 관측한 결과다. 업로드 연속성은 앞서 설명한 격리 시험에서 확인했다. 운영에서 로그인, 녹음 업로드, 긴 채점 작업까지 이어졌는지 판단하려면 각 흐름을 전환 전부터 실행해 결과를 따로 기록해야 한다.