2022년 10월 15일
판교 데이터센터의 A동 지하 3층 전기실 화재로 인해 서버 작동에 필요한 전원 공급이 끊겨 해당 IDC에 입주한 모든 서비스들이 다운되어 버린 사건이 발생합니다.
해당 화재로 인해 전국민이 사용하던 유명한 메신저, 해당 회사의 대다수 서비스 및 음원 서비스 등의 이용 불가로 많은 불편함이 있었습니다. 당시 정부까지 나서 방송통신재난대응상황실 등을 설치하며 복구 지원에 나설정도로 큰 서비스 장애였습니다.
이는 물론 데이터센터의 관리, 재해대책의 부족 등의 이유도 있겠지만, 사실 디지털서비스 제공에 필요한 핵심 기능인 데이터의 이중화가 되어 있지 않거나, 시스템 전체 관점에서의 이중화가 부족했기 때문입니다.
만일 한 데이터센터 전체에 문제가 생기더라도, 이중화 시스템이 정상 작동을 했다면, 다른 데이터센터로 이중화 전환이 이뤄져 빠르게 복구가 돼야 했지만, 그러지 못해 일일이 수동전환 대응을 진행을 했던 것입니다.
해당 문제 이후 해당 서비스는 장애 재발 방지를 위한 발표를 통해 전용 데이터센터를 이중화 하는 계획을 이야기 하였으며, 이를통해 24시간 무중단 운영을 위한 이중화 인프라 구축을 목표로 한다는 것을 발표하기도 했습니다.
이번 글에선 이러한 데이터의 이중화와 시스템 적인 이중화를 데이터베이스의 관점
에서 어떻게 처리할 수 있는지를 알아보려 합니다.
웹 애플리케이션을 구성한다고 가정 했을 때 가장 쉽게 생각해 볼 수 있는 애플리케이션의 구조는 single-server
의 구조로 웹 애플리케이션의 모든 컴포넌트가 단 한 대의 서버에서 실행되는 간단한 시스템이 있습니다.
팀 버너스리에 의해 탄생한 CERN, 1990년대 최고의 웹 디렉토리인 Yahoo! 모두 초기에는 단일서버를 사용했습니다.
특히나 1960대의 컴퓨터 자체는 매우 크고 비쌌기 때문에, 하나의 서버에 모든 컴포넌트를 구성하는 것이 효율적 이었습니다.
하지만 이런 단일서버는 데이터베이스의 관점에서 데이터가 증가함에 따른 서버 용량 부족, 트래픽 증가에 따른 서버 성능의 저하 등의 문제가 있을 수 있습니다. 물론 Scale-up등의 방식으로 어느정도 해결은 할 수 있지만 단일 장애점(SPOF), 높은 가용성 등의 부분에서의 아쉬움은 아직 남아있습니다.
이후 컴퓨터 기술의 발전으로 사용자 기반이 증가함에 따라 하나의 서버로는 충분하지 않는 상황이 생겼으며, 웹/모바일 트래픽용
서버와 데이터베이스용
서버를 분리하여 구성하는 방식이 탄생합니다.
이를 통해 데이터베이스의 관점에선 분리된 역할로 인한 성능, 확장성, 안정성, 가용성 향상 등을 위한 기반이 갖춰졌습니다.
단일 마스터 노드의 데이터베이스 구조는 가장 간단하고 비용적인 측면에서 저렴한 방법이지만, 가용성, 성능, 확장성 등의 측면에서 문제점을 가지고 있습니다.
위에서 설명한 내용과 같이 단일 장애점
과 데이터 손실 위험
, 노드 읽기/쓰기 작업이 하나의 마스터 노드를 통해 처리되기 때문에 성능 저하
등의 문제가 발생할 수 있습니다.
이로 인해 대다수의 서비스에선 데이터베이스의 복사본을 저장하는 복제 서버(replica)를 구성하고 있으며, 이는 DB 이중화 라고도 불립니다. 물론 이러한 이중화는 데이터베이스에만 해당하는 것은 아니며, 하드웨어의 파워
, Web Server
등에도 적용되는 내용입니다.
이러한 DB 이중화는 2대 이상의 데이터베이스를 통해 데이터를 저장하는 방식이며 권한(Write/Read)에 따라 Master(leader)/Slave(follower) 구조로 구축하는 방식을 말합니다.
우리가 흔히 말하는 동기식 과 비동기식은 데이터베이스의 복제에도 적용되는 이야기입니다. 서로 다른 두 서버의 통신으로써 어떠한 복제 방식을 사용하느냐에 따라 각각이 미치는 영향 또한 다릅니다.
두 가지 방식은 각각의 트레이드 오프가 존재하지만, 일반적으로 대부분의 리더 기반의 복제 방식에서는 비동기식 복제방식을 채택하고 있습니다.
그렇다면 동기식과 비동기식 복제 방식은 어떻게 동작을 하고, 어떠한 차이(장/단점)이 있는지 간단하게 살펴 보겠습니다.
동기식 방식의 경우 리더에 쓰기 혹은 변경의 요청이 있을때 리더가 팔로워(복제서버)가 쓰기를 수신했는지 확인해줄 때까지 기다리는 방식을 말합니다.
동기식 복제의 장점은 팔로워가 리더와 일관성 있게 최신 데이터 복사본을 가지는 것을 보장할 수 있으며, 리더(마스터 디비)가 갑자기 동작하지 않더라도 데이터는 팔로워에서 계속 사용할 수 있음을 확신할 수 있습니다.
다만, 팔로워가 죽거나 네트워크적 오류로 인해 동기 팔로워가 응답하지 않을 경우 쓰기에 대한처리가 이루어질 수 없다는 것입니다. 리더는 모든 쓰기를 차단하고 팔로워 서버가 다시 사용할 수 있을 때까지 기다려야 하는 문제가 있습니다. 이러한 이유로 모든(여러 팔로워가 있다면) 팔로워가 동기식으로 동작하는 것은 비현실적이기도 합니다. 따라서 현실적으로 구축하기 위해 일부는 동기 일부는 비동기로 동작하는 반동기식(semi-synchronous)
방식을 쓰기도 합니다.
비동기식 방식의 경우 마찬가지로 리더에 쓰기 혹은 변경의 요청이 있을 경우 팔로워(복제서버)가 해당 쓰기를 수신했는지 확인 응답을 줄때까지 기다리지 않고 클라이언트에게 바로 결과를 리턴합니다.
비동기식 복제의 경우 동기식과 다르게, 리더에만 쓰기가 완료되면 사용자에게 응답을 줄 수 있다는 점에서 빠른 응답이 가능하며, 팔로워에 문제가 생겼을 경우에도 리더에서는 쓰기 처리를 계속 할 수 있다는 장점이 있습니다.
반대로 리더가 잘못되고 복구할 수 없을 경우 팔로워에 아직 복제되지 않은 모든 쓰기는 유실될 수 있으며, 이는 클라이언트에게 쓰기가 확인된 경우에도 지속성을 보장하지 않는다는 의미이기도 합니다. 이러한 데이터 유실은 심각한 문제가 될 수 있기에, 이를 해결하기 위한 여러 방법이 있으며 합의를 통해 팔로워중 새로운 리더를 선출 하는 방식, 최종적 일관성 등이 그 예 입니다.
복제는 데이터베이스의 복사본을 하나 이상의 보조 서버에 만드는 프로세스로 리더서버
에서 데이터가 변경될 때마다 해당 변경 사항이 팔로워서버
에 복제됩니다.
복제 로그는 복제 프로세스의 핵심 구성 요소로서 복제 로그에는 리더서버에서 발생한 모든 데이터 변경 사항에 대한 정보가 포함됩니다. 이러한 정보를 사용하여 팔로워 서버를 리더서버와 동기화하고 데이터 손실을 방지할 수 있습니다.
데이터베이스 기준 복제 로그 구현에는 여러 가지 방법이 있지만 그 중 몇가지 방법을 알아보겠습니다.
구문 기반의 복제란 말 그대로 쓰기 요청을 기록하고 실행한 다음 해당 구문 로그를 팔로워에게 전달 합니다. 이때 NSERT
, UPDATE
, DELETE
구문을 팔로워에게 전달하며, 각 팔로워들은 클라이언트에서 직접 받은 것처럼 SQL 구문을 파싱하고 실행합니다.
이때 데이터를 변견한 문은 모두 바이너리 로그
에 기록되며 해당 로그를 통해 복제가 실행됩니다. MySQL의 경우 mysqlbinlog 덤프를 사용하여 구문 기반의 복제가 가능합니다.
이러한 구문 기반의 복제 작업은 복제로그(바이너리 로그)
에 기록되는 이벤트는 리더노드에서 발생한 실제 작업들로, 로그 파일은 필요한 최소한의 저장공간만 차지할 수 있다는 장점이 있습니다.
즉, 작업이 수천행에 영향을 주든 한 행에만 영향을 주든 상관없이 하나의 이벤트로 기록되는 것이죠. 또한 이러한 형식의 또 다른 큰 장점은 로그 파일에 작업을 그대로 기록하기 때문에 데이터베이스의 작업을 감사하는 데 사용할 수 있다는 점 또한 장점이기도 합니다.
반대로 구문 기반 복제 사용시 UUID()
, RAND()
, NOW()
와 같은 연산은 사용자가 제어할 수 없는 요인에 따라 값을 생성하게 됩니다. 이러한 연산이 복제본에서 동일하게 생성될 경우 리더와 다른 값이 생성될 위험이 있습니다.
또한 자동증가 컬럼을 사용하는 구문의 경우 각복제 서버에 정확히 같은 순서대로 실행돼야만 합니다. 그러지 않을 경우 기대하는 값이 달라질 수 있으며, 이런 방식은 동시에 여러 트랜잭션이 수행되는 것을 제한하게 됩니다.
물론 리더가 구문을 기록할시에, 모든 비결정적 함수(UUID(), RAND()등) 호출에 대해 고정값을 반환하게끔 대채할 순 있지만 여러 에지 케이스가 존재하는 것은 어쩔 수 없습니다.
MySQL6.X
이전의 버전에선 이러한 구문 기반 복제(SBR)가 사용되거나, 혼용하여 사용될 수 있었지만, 이후 버전부터는 이후에 설명할 RBR(행 기반의 복제) 방식을 기본으로 사용하고 있습니다.
일반적으로 데이터베이스의 모든 쓰기는 로그에 기록이됩니다. 이를 우리는 WAL(write-ahead log) 라고 합니다. 이러한 WAL은 데이터베이스에 대한 모든 변경 사항의 순차적 기록으로 트랜잭션이 데이터를 수정 및 입력 할때마다 실제 디스크에 기록되기 전에 WAL이라는 로그파일에 기록됩니다.
이를 통해 데이터베이스는 데이터베이스를 이러한 WAL을 사용하여 데이스의 장애로 인해 복구하기 위한 작업(REDO)등이 가능합니다.
이러한 특성을 활용하여 완전히 동일한 로그를 사용해 다른 노드에서 복제 서버를 구성할 수 있습니다. 리더는 디스크에 로그를 기록하는 일 외에도 팔로워에게 네트워크로 로그를 전송합니다.
postgresql에서는 이러한 WAL 세그먼트 파일을 전달하여 복제를 진행하는 방법이 있으며, WAL 세그먼트 파일을 대기 서버(복제서버)에서 사용할 수 있도록 가져오는 설정을 통해 복제가 가능합니다.
이러한 쓰기전 로그를 이용한 복제의 경우 WAL의 특성상 디스크 블록에서 어떤 바이트를 변경했는지와 같은 상세한 정보를 포함하고, 이는 결국 복제가 저장소 엔진과 강하게 결합되는 것을 의미합니다. 즉 이는 리더와 팔로워의 데이터베이스 소프트웨어 버전을 다르게 실행할 수 없는 것이 단점입니다.
행 기반의 복제 형식(row-based replication)에서는 리더노드가 작업 대신 개별 데이터 항목의 업데이트를 로그에 기록합니다. 따라서 로그 파일에 기록된 항목은 리더에서 데이터가 어떻게 추가 되었고 변경되었는지를 나타냅니다. 팔로워(복제서버)에서는 해당 로그를 읽어 변경사항을 적용하는 방식입니다.
단순 동작 방식만 봤을때는 구문 기반의 복제와 큰 차이가 없어보이지만, 아래의 차이를 보면 알 수 있습니다.
[구문 기반 복제에서 보이는 로그]
UPDATE tasks SET is_done = true WHERE user_id = 53;
[로우 기반 복제에서 보이는 로그]
tasks:121 is_done=true
tasks:142 is_done=true
tasks:643 is_done=true
tasks:713 is_done=true
tasks:862 is_done=true
실제 위의 쿼리문으로 영향받은 행이 5개라면 로우 기반의 복제 로그 파일에 기록된 이벤트에는 변경된 각 데이터 항목에 대해 하나씩 총 5개의 항목이 포함됩니다.
로우 기반의 복제 방식은 모든 변경 사항을 복제본에 기록하며, 계산된 값만 기록되기 때문에 구문 기반의 복제와 다르게 비결정적 연산에서도 안전하다는 특징이 있으며, 저장소 엔진 내부와 분리된 특성으로 리더와 팔로워에서 각각 다른 버전의 데이터베이스 소프트웨어 혹은 다른 데이터베이스를 사용하더라도 지원이 가능합니다.
다만, 구문 기반의 복제 로그
대비 변경된 로우 기반의 모든 데이터를 생성해야 하기 때문에, 변경 혹은 생성 쿼리문이 많은 행을 변경하는 경우 훨씬 더 많은 데이터를 바이너리 로그에 기록합니다. 또한 이는 롤백 되는 상황에서도 마찬가지이며, 이는 백업을 만들고 복원하는 데 더 많은 시간이 소요될 수 있다는 단점이 있습니다.
이러한 로우 기반의 형식은 외부 애플리케이션에서 파싱하기 더 쉽기에, 오프라인 분석 혹은 데이터 웨어하우스 같은 외부시스템에 데이터베이스 내용을 전송하고자 할 때 유용하며, 변경된 데이터(로우)에 대한 결과를 전달하는 변경데이터 캡쳐(change data captur -CDC) 방식을 통해서도 많이 사용됩니다.
변경 데이터 캡처(CDC)는 데이터 소스의 변경 사항을 추적하고 해당 변경 사항에 응답해야 하는 다른 시스템과 서비스에 알려주는 데이터 통합 패턴입니다. 이를 통해 데이터에 의존하는 모든 시스템에서 일관성을 유지시킬 수 있습니다.
예시로 Kafka의 경우 Debezium Connector를 통해 모든 row level의 변경을 changed event stream
에 기록, 복제서버는 이 스트림을 통해 변경 이벤트를 순서대로 읽어 DB 변경사항을 수집하고 이를 통해 논리적인 복제를 가능하게 합니다.
이러한 복제는 모든 쓰기가 리더 노드를 거쳐야 하지만 읽기 전용 질의는 어떤 복제 서버에서도 가능하기 때문에, 대부분이 읽기 요청인 서비스의 경우 읽기 요청을 분산하여 리더의 부하를 없앨 수 있습니다.
이러한 읽기 확장
아키텍처는 비동기식 복제로 동작하는 특성으로 인해 팔로워 서버가 뒤쳐진다면 이전 정보를 볼 수 도있으며, 이런 상황은 데이터베이스의 불일치
로 사용자에겐 이상한 경험이 될 수 있습니다.
그러나 이런 불일치는 일시적인 상태에 불과하며, 리더 데이터베이스에 쓰기를 멈추고 잠시 기다린다면 팔로워 서버는 리더의 상태를 따라잡게 되며, 최종적으로 일치하게 됩니다. 이런 효과를 최종적 일관성 이라고합니다.
정상적인 상태에서의 지연은 사실 눈에 띄지 않는 아주 짧은 순간으로 문제가 없을 수 있지만, 네트워크 장애등으로 인한 지연의 문제는 실제 문제로 이어질 수 있습니다.
이러한 복제 지연의 문제를 해결하기 위한 방법중 하나로 쓰기 후 읽기 일관성 방법이 있습니다. 이는 사용자가 삽입 또는 갱신에 대한 작업을 수행 했을때 모든 갱신을 볼 수 있음을 보장합니다.
다만 이는 자신의 데이터에 대한 일관성을 보장하는 것으로, 다른 사용자의 갱신에 대한 내용은 일정 시간이 지난 이후 최종적 일관성이 이루어지고 확인이 가능할 수 있습니다.
이러한 쓰기 후 읽기 일관성 방법은 사용자가 새로운 정보에 대해 업데이트할 경우 리더로부터 그정보를 다시 읽어들여 보장할 수 있습니다. 이런 경우 리더에는 사용자가 업데이트한 정보에 대한 처리가 완료되었지만, 비동기적으로 아직 팔로워에 복제가 되지 않은 상황에서도 사용자가 수정한 데이터에대해 일관성을 제공할 수 있습니다.
또 다른 방법으로는 타임스탬프를 이용하는 방법으로 사용자는 가장 최근 쓰기에 대한 타임스탬프를 기억할 수 있다는 특성을 활용하여 시스템은 해당 사용자에 대한 읽기를 제공하는 복제서버가 해당 타임스탬프까지 업데이트를 받도록 보장하는 방법입니다.
이를 통해 해당 사용자가 최근 갱신한 데이터에 대한 요청이 올 경우 팔로워에 해당 타임스탬프 까지의 데이터가 없다면(최신이 아닌경우) 해당 복제 서버에 대한 쿼리를 다른 복제 서버가 처리하거나 질의를 대기시키는 방법입니다.
다중 리더란 두개이상의 리더 노드를 가지는 형태의 시스템을 말합니다. 위에서 설명한 단일 리더의 복제의 경우 만일 리더에 문제가 생겼을 경우(자연재해, 클라이언트와 리더 간 네트워크 중단 등) 데이터베이스에 쓰기를 할 수 없다는 문제가 있습니다.
처음 글의 시작에서 이야기한 IDC화재의 원인도 마찬가지입니다. 데이터의 이중화는 되어있지만, 실제 해당 데이터베이스의 화재로 인해 리더노드가 동작을 하지 못하게 되고, 그로인해 서비스의 복구에 더 오랜시간이 걸려면서 큰 문제가 되었던 경우입니다.
이러한 문제들로 단일 리더 기반 복제 모델을 쓰는 일부 기업은 쓰기를 허용하는 리더 노드를 하나 이상 두는 것으로 자연스럽게 확장됩니다. 이때 쓰기 처리를 하는 각 노드는 데이터 변경을 다른 모든 노드에 전달해야하며, 이를 다중 리더(마스터/마스터 혹은 액티브/액티브 복제) 라고 하며, 이때 각 리더는 동시에 다른 리더의 팔로워 역할도 하게 됩니다.
가장 대표적으로 데이터 센터의 이중화 작업이며, 흔히 IDC의 전면 장애에 대한 대응(전체 데이터 센터의 내결함성)을 위해 DR(Disaster Recovery)시스템 혹은 사용자에게 지리적으로 가까이 위치해 지연시간을 줄이기 위한(성능) 목적 등이 있습니다.
그렇다면 이러한 DR 센터에서 데이터베이스는 어떠한 방법으로 데이터의 일관성을 유지할 수 있을지 알아보겠습니다.
보통의 재해복구 데이터센터(DR)을 구성하는 경우 Active-Standby의 형태로 구현하게 됩니다. Standby란 Active 서버의 장애가 발생했을 경우 빠르게 Active의 역할을 할 수 있도록 하는 것으로, Active 서버와의 데이터 일관성을 유지해야할 필요성이 있습니다.
그러나 DR 센터의 경우 기본 클러스터에서 온라인으로 복제하고 필요한 경우 리더노드의 역할을 매우 빠르게 인수할 수 있어야 합니다. 문제는 빠르게 인수하더라도 최신버전등을 유지하기 어려워 장애가 해결되지 않는 경우도 있습니다. 이러한 특징으로 비동기 복제 채널을 양방향으로 설정하는 경우가 많습니다.(Active-Active)
이러한 Active-Active 형태의 구조는 두개의 마스터노드에 데이터를 기록하며, 이를 동기화하여 서로가 동일한 데이터를 보게 하는 방식이 있을 수 있습니다. 이러한 상황에서의 가장 큰 문제는 쓰기 충돌이 발생할 수 있으며, 이는 이를 해소해야할 필요가 있다는 것을 의미합니다.
이를 해결하기 위해선 충돌이 발생했는지 감지 해야하며, 이러한 충돌을 해결해야합니다. 이론적으로 충돌 감지의 경우 동기식으로 해결이 가능합니다. 다만 위에서 설명한 내용과 같이 쓰기가 성공한 사실을 사용자에게 말하기 전에 모든 복제(다른 리더서버까지)가 완료될 때까지 기다려야하는데, 이는 다중 리더 복제의 주요 장점(각 복제 서버가 독립적으로 쓰기를 허용)을 잃게 됩니다.
충돌을 처리하는 제일 간단한 방법은 충돌 자체를 피하는 것입니다. 특정 레코드의 모든 쓰기가 동일한 리더를 거치도록 애플리케이션이 보장한다면 충돌은 발생하지 않을 수 있습니다. 예를 들어 특정 사용자의 요청을 동일한 데이터센터로 항상 라우팅하고 해당 데이터센터의 리더를 사용해 읽기와 쓰기를 보장한다면 단일 리더의 방식으로 동작이 가능합니다.
다만 이런 구성은 데이터센터의 문제로 트래픽을 다른 데이터센터로 옮겼을 경우 여전히 충돌이 발생할 수밖에 없습니다. 따라서 다른 리더에서 동시 기록 가능성에 대한 대처가 필요합니다.
각 데이터베이스 벤더사들은 이러한 Active-Active 형태의 복제 구성을 지원하고있으며, 그에 따라 충돌에 대한 해소방법을 각자 제공하고 있습니다.
예시로 MySQL
의 경우 이러한 충돌을 감지하고 해결하기위해, 논리적인 시계를 사용합니다. 쓰기가 이루어진 리더 노드에서 변경이 일어날 때마다 논리적인 시계를 이용해 해당 시점을 표시하여 언제 무슨 변경이 일어났는지를 감지할 수 있습니다.
만일 하나의 리더노드(primary로 표현) 에서 변경 사항이 적용되면 영향을 받는 행에 대해 충돌기간
이 발생하며, 해당 기간안에 다른 리더노드(secondary로 표현)에서 다른 변경사항이 적용될 경우 불일치가 발생합니다. 만일 secondary서버에서 primary서버의 변경이 정상적으로 반영이 되었다면, 변경된 내용과 함께 해당 시점의 시계 값을 primary서버의 팔로워(복제서버)에게 보내고 충돌기간을 닫게 됩니다.
하지만 사용자가 primary노드에 변경을 진행하고 만일 해당 내용이 primary노드에 반영되기 전에 secondary에서 동일한 데이터를 변경(최신의 데이터)한다면, secondary 노드는 해당 데이터를 primary노드에 보내며, 이때 primary노드는 충돌이 발생했는지 확인하기 위해 논리적인 시계의 값을 비교합니다. 이후 충돌이 감지되면 이를 해결하기 위한 작업을 수행합니다.
이전의 MySQL 버전의 경우 간단히 주 키를 플래그 처리하거나 충돌하는 행중 하나의 변경을 취소함으로 해결했지만, 특정 알고리즘을 사용하여, primary노드는 영향을 받는 행 및 같은 트랜잭션에서 업데이트 된 다른 행의 데이터를 secondary서버에 덮어 씌우게 됩니다. 또한 만일 secondary서버에서 충돌하는 행에 대한 후속 트랜잭션이 있다면 해당 트랜잭션의 모든 변경사항도 함께 취소하는 방식으로 충돌을 해결합니다.
토스에서의 다중 데이터 센터 운영(토스증권)
토스 증권에서는 이러한 데이터센터의 이중화를 Active-Active 형태로 유지하며 서비스하고있다고 합니다. 이때,Kafka Connect
를 사용하여 두 IDC 간의 데이터 싱크를 맞추는 작업하는 방식을 사용하고 있으며,Kafka의 Offset
을 통해 IDC 교체작업이 발생 하더라도, 기존의 IDC에서 소비하던 구간부터 유실 없이 재개가 가능하도록 유지하고 있다고 합니다.
사실 데이터의 이중화, 데이터센터의 이중화 모두 여러 방법이 있습니다. 모든 것을 담지는 못했지만, 기존에 학습했던 내용중 중요하다고 생각한 부분과 현재 실무에서 사용하고 있는 데이터베이스의 기준으로 정리를 해봤습니다.