들어가며
안녕하세요. VOOM Server Unit에서 LINE VOOM 서비스를 개발하고 있는 서용준입니다. LINE VOOM 서비스는 메인 콘텐츠인 포스트를 저장하기 위해 MySQL과 Cassandra를 사용하고 있습니다. 앞서 신홍중 님께서 개발자가 손수 대규모 Cassandra를 신규 클러스터로 이전하기라는 글에서 서버 랙 공간 부족과 장비 노후화를 해결하기 위해 Cassandra를 이전한 내용을 소개해 주셨는데요. 이번 글에서는 같은 이유로 LINE VOOM의 MySQL을 이전한 내용을 소개하겠습니다.
이전 작업 순서
Cassandra는 신규 클러스터를 만든 뒤 기존 클러스터와 복제 구성을 하면 하나의 클러스터처럼 운용할 수 있어 쓰기를 중단할 필요가 없었지만, MySQL을 이전할 때에는 그와 같은 방법을 사용할 수가 없어서 대책이 필요했습니다. 읽기 전용 모드로 전환하는 것을 선택한 이유와 그 과정을 살펴보기 전에, 기존 시스템이 어떻게 구성돼 있었는지 소개하고 전체 이전 흐름을 시간순으로 짚어보겠습니다.
신규 IDC에 DB 설치 및 복제 구성
첫 시작은 신규 IDC에 새로운 환경을 구축하는 것으로, 게이트웨이와 API 서버를 구축하고 데이터베이스를 설치했습니다. 신규 IDC에 새로 설치한 데이터베이스는 기존 IDC의 데이터베이스와 복제 구성을 했습니다. 각 지역의 데이터베이스는 하나의 소스와 여러 개의 레플리카로 구성해 생성, 수정, 삭제 요청은 소스 데이터베이스에서, 읽기 요청은 레플리카 데이터베이스에서 처리하도록 설정했습니다. 참고로 아래 그림에서 LEGY(LINE Event Delivery Gateway)는 LINE 클라이언트의 요청이 처음 도착하는 API 게이트웨이입니다.
트래픽 신규 IDC로 옮기기
신규 IDC에 서비스 제공 환경을 구성한 후 LEGY를 통해 트래픽을 기존 IDC에서 신규 IDC로 옮겼습니다. 먼저 사용자의 요청의 0.01%를 신규 IDC로 옮겨서 테스트한 후 10%, 30%, 50%, 70%, 100% 트래픽을 단계적으로 이동하며 모니터링했습니다.
소스 DB 이전
모든 사용자의 요청이 신규 IDC를 향하도록 한 후 읽기 전용 모드를 설정하고 소스 데이터베이스를 기존 IDC에서 신규 IDC로 변경했습니다. 읽기 전용 모드일 때는 서비스의 일부 기능을 이용할 수 없으므로 영향 범위를 미리 조사하고 사전에 사용자에게 점검 공지를 안내했습니다.
이와 같은 순서대로 IDC를 이전했는데요. 그럼 이제 왜 읽기 전용 모드로 전환해서 진행했는지 살펴보겠습니다.
이전할 때 읽기 전용 모드로 전환한 이유
LINE VOOM은 기본적으로 무중단 서비스를 제공하는 것을 원칙으로 합니다. 따라서 쓰기 요청을 허용한 상태에서 소스 데이터베이스를 변경할 수 있다면 좋겠지만, 이를 위해서는 변경하려는 데이터베이스들이 모두 같은 데이터를 갖고 있어야 합니다. 하지만 현실적으로 데이터를 완벽하게 이중화하는 것은 불가능하기 때문에 부득이하게 일부 기능을 차단하고 소스 데이터베이스를 옮기는 것으로 방향을 정했는데요. 이때 생각해 볼 수 있는 몇 가지 시나리오 및 각 시나리오의 장단점을 살펴보고, 그중에서 읽기 전용 방식을 채택한 경위를 말씀드리겠습니다.
시나리오 1 - 양쪽 IDC에 쓰기 허용
먼저 기존과 신규 IDC 양쪽에 쓰기를 허용하는 경우를 생각해 봅시다. 이때는 기존 IDC의 데이터 베이스와 신규 IDC의 데이터베이스가 서로 복제되도록 구성합니다. 기존 IDC에서 신규 IDC로 트래픽을 옮기기 시작하면 한 사용자는 기존 IDC에 요청하고 다른 사용자는 신규 IDC에 요청하는 상황이 오는데요. 이때 두 사용자 모두 쓰기 요청을 했다고 가정하면 그들의 게시글 데이터는 기존 IDC의 데이터베이스와 신규 IDC의 데이터베이스에 각각 쓰입니다. 여기서 이 두 데이터베이스는 서로 복제 구성이 돼 있으므로 신규 IDC에 쓰인 데이터는 기존 IDC로 복제되고 반대의 경우도 똑같이 진행됩니다. 트래픽을 모두 신규 IDC로 옮긴 후엔 데이터베이스 간 복제 구성을 걷어냅니다.
이와 같이 설정해 트래픽을 이전하면 애플리케이션에서 로직을 크게 수정하지 않고 저장소의 설정만으로 이전 작업이 가능하다는 장점이 있습니다. 그러나 데이터베이스의 완벽한 이중화는 현실적으로 불가능하므로 데이터 정합성이 보장되지 않습니다.
A 사용자는 기존 IDC를, B 사용자는 신규 IDC를 이용하는 순간을 예로 들어보겠습니다. A 사용자가 게시글을 작성했다고 가정해 봅시다. 이 사용자의 요청은 기존 IDC를 통해 처리됩니다. 작성된 게시글 데이터는 복제 구성 때문에 신규 IDC의 데이터베이스에도 저장됩니다. 이후 A 사용자가 작성한 게시글을 수정했다고 가정해 보겠습니다. 이 변경이 기존 데이터베이스에는 적용됐지만 아직 신규 데이터베이스에는 반영되지 않은 상태에서 B 사용자는 A 사용자가 수정하기 전 게시글을 보게 되며, 만약 복제 지연이 발생한다면 이 시간은 더 길어질 것입니다.
IDC 이전 과정에서 문제가 생겼을 때 롤백해야 한다면 머리가 더 복잡해집니다. 본 시나리오에서 롤백하려 하면 우선 복제 구성을 끊고 기존 IDC와 신규 IDC 데이터베이스의 데이터 정합성을 수동으로 맞춰야 합니다. 이 과정은 시간과 인력이 많이 필요하며, 롤백 과정 자체도 복잡합니다. 이와 같은 점을 고려해 이 방법은 지양하기로 결정했습니다.
시나리오 2 - 신규 IDC에만 쓰기 허용
이번 시나리오는 신규 IDC에만 쓰기를 허용하는 경우입니다. 먼저 기존 IDC의 데이터베이스를 신규 IDC의 데이터베이스로 복제하는 작업을 완료한 후 이전 작업 중에는 기존 IDC 서버로의 쓰기 요청을 막아둡니다.
이 경우에도 이전 중에는 신규 IDC의 데이터베이스에만 새로운 데이터가 쌓이므로 데이터 정합성을 완전히 보장할 수 없습니다. 또한, 이전 과정에서 문제가 발생해 롤백하려면 그동안 신규 IDC의 데이터베이스에 새로 기록된 내용을 다시 기존 IDC의 데이터베이스로 옮겨와야 하며, 롤백 중에는 새로운 쓰기 요청을 막아야 합니다.
데이터 정합성 문제를 해결할 수 없고, 롤백 시나리오도 여전히 복잡하기 때문에 이 방법도 기각됐습니다.
시나리오 3 - 읽기 전용
트래픽을 옮기는 동안 쓰기 요청을 두 IDC에서 모두 차단하는 읽기 전용 방법을 살펴보겠습니다. 먼저 트래픽을 옮기기 전에 양쪽 IDC로의 쓰기 요청을 모두 막아둡니다. 기존 IDC의 데이터베이스에 있는 데이터가 모두 신규 IDC의 데이터베이스로 복제된 후에 복제 구성을 제거한 뒤, 서서히 트래픽을 신규 IDC로 넘깁니다. 모든 트래픽이 다 넘어가면 신규 IDC에서 쓰기 요청을 다시 허용합니다.
이렇게 쓰기 요청을 모두 막아두면 데이터베이스 동기화가 완료된 후 읽기 연산만 수행하므로 두 IDC 간의 데이터 정합성을 보장할 수 있습니다. 더불어 이전 과정에서 문제가 발생할 경우, 기존 IDC와 신규 IDC의 데이터베이스를 동기화한 후 변경된 데이터가 없기 때문에 비교적 쉽게 롤백할 수 있습니다.
물론 쓰기 요청을 막아두면 서비스의 일부 기능을 사용하지 못하기 때문에 사용자에게 사전 공지를 해야 하지만, 데이터 정합성을 확실히 보장할 수 있고 롤백 계획도 단순해진다는 장점을 고려해 최종적으로 이 방법을 선택했습니다.
다음은 세 시나리오를 비교한 표입니다.
양쪽 IDC에 쓰기 허용 | 신규 IDC에만 쓰기 허용 | 읽기 전용 | |
---|---|---|---|
애플리케이션 작업양 | 적음 | 많음 | 많음 |
서비스 무중단 여부 | 무중단 | 일부 사용자 쓰기 제한 | 모든 사용자 쓰기 제한 |
데이터 정합성 | 보장 안됨 | 보장 안됨 | 보장됨 |
롤백 시나리오 | 매우 복잡함 | 복잡함 | 단순함 |
읽기 전용 모드 이전을 위한 사전 애플리케이션 준비 작업
읽기 전용 모드로 데이터베이스를 이전하기 위해 데이터베이스 자체에서도 읽기 전용 설정을 사용했지만, 조금 더 안전하게 작업하기 위해 애플리케이션 수준에서도 쓰기 요청을 막는 작업을 진행했습니다.
LINE VOOM 서비스에서 포스트를 다루고 있는 포스트 프로젝트는 전형적인 MVC 패턴으로 구현돼 크게 Controller와 Service, Repository 계층으로 나뉘어 있었습니다. 애플리케이션 수준에서 쓰기 요청을 막으려면 Controller 계층에서 쓰기 요청을 막아두면 될 것이라고 생각할 수 있는데요. 저희의 경우 읽는 동작에서도 내부에 쓰기 연산을 하는 부분이 존재했습니다. 따라서 데이터베이스로 쓰기 요청이 가지 않도록 확실하게 막기 위해서는 Repository 계층에서 제한하는 게 적절하다고 판단해 Repository 계층에서 읽기를 제외한 연산을 하는 모든 메서드를 찾아 목록으로 만들었습니다.
쓰기 요청을 제한하는 코드를 구체적으로 구현하는 방법 중 하나로 요청이 데이터베이스로 가지 않도록 제한하는 메서드를 만들어 데이터베이스에 쓰기 요청을 하는 부분에서 해당 메서드를 호출하는 것을 생각해 볼 수 있습니다. 하지만 이 방법은 기존 코드를 변경해야 한다는 단점이 있어서 저희는 기존 코드를 건들지 않고 쓰기를 제한하기 위해 AOP를 적용했습니다. @MaintenanceReadonlyMode
라는 애너테이션을 만든 후 대상 메서드에 모두 표시했고, 이 애너테이션이 붙은 메서드는 데이터베이스로 요청을 보내지 않도록 했습니다.
@MaintenanceReadOnlyMode
예시
@MaintenanceReadonlyMode
public void insert(PostMeta postMeta) {
postMetaMasterMapper.insert(userService.getPartitionId(), postMeta);
}
이제 쓰기 제한을 언제 적용할지의 문제만 남았습니다. 제한 여부 및 제한 시간을 프로젝트 속성에 넣고 서버를 배포하는 방법도 있지만 이 방법은 이전하는 과정에서 무슨 일이 생겼을 때 즉각 대응하기에는 다소 느리다는 단점이 있습니다. 이에 제한 여부 및 시간을 동적으로 조작할 수 있도록 LINE의 오픈소스인 Central Dogma를 활용했습니다. Central Dogma를 이용하면 프로젝트가 Central Dogma의 설정값을 바라보다가 변경 사항이 생기 면 곧바로 이를 적용하므로 돌발 상황에도 빠르게 문제를 처리할 수 있습니다.
D-Day 풍경
2022년 말 결전의 날이 왔습니다. 데이터베이스 이전 작업은 새벽에 진행했고, 각 팀은 야간 조와 주간 조로 나뉘어 작업 및 모니터링을 진행했습니다. 돌발 상황이 발생했을 때 대응 요청할 부서를 목록으로 만들어 뒀고, 야간 조는 이전 중에 생길 수도 있는 돌발 상황에 더욱 빠르게 대처하기 위해 재택근무 중이지만 이날만큼은 모두 사무실에 모였습니다. 말똥말똥한 정신을 유지하기 위해 주전부리도 한가득 준비해 놓았습니다.
만반의 준비를 갖춘 뒤 드디어 작업을 시작했고, 모두가 긴장한 마음으로 각자 맡은 역할을 신중히 수행하며 상황을 모니터링했습니다. 다행히 큰 사고는 발생하지 않았고, 모든 게 계획대로 순조롭게 진행됐습니다. 이른 오전에 야간 조가 이전을 마치고 주간 조가 출근해 새벽 동안의 작업 상황을 공유 받고 교대했습니다. 이전 완료 후에도 문제가 없는지 하루 정도 모니터링했는데요. 아무 문제도 발생하지 않았습니다. 이렇게 이전 작업은 모두 무사히 종료됐습니다.
마치며
서비스를 중단하지 않고 이미 구축된 시스템에 맞춰 IDC를 이전하려면 생각보다 많은 요소를 고려해야 합니다. 이전 과정에서 데이터 정합성을 유지해야 하고, 서비스는 계속 사용할 수 있어야 하며, 이전 과정에서 문제 발생 시 적절히 대처할 수 있는 방법이나 롤백할 수 있는 방법 등도 사전에 제대로 준비해 둬야 합니다. 또한 모든 요구 사항을 완전히 만족하는 만능열쇠는 없으므로 여러 요구 사항의 우선순위를 정해 일부 요구 사항은 만족하지 못하더라도 가장 합리적인 최선의 방안을 찾아 적용해야 하는데요. 미리 여러 시나리오를 준비해 꼼꼼히 살펴보며 합리적인 방법을 찾은 덕에 무탈하게 작업을 완료할 수 있었습니다.
데이터 센터를 이전하는 작업은 쉽게 경험해 볼 수 없습니다. 많은 잠재적 위험 요소를 예측하고 철저하게 대비해 성공적으로 완수했던 경험이 앞으로 업무를 진행할 때에도 더욱 안정적으로 작업할 수 있는 자양분이 되었습니다.