3 minute read

리더 기반 복제는 내부적으로 어떻게 동작하는지 살펴보자.

명령문 기반 복제

  1. 리더는 자신이 실행하는 모든 쓰기 요청(statement)을 기록하고 해당 요청 로그를 팔로워에게 보낸다.
  2. 관계형 데이터베이스의 경우, 모든 INSERT, UPDATE 또는 DELETE 문이 팔로워에게 전달된다.
  3. 각 팔로워는 해당 SQL 문을 클라이언트에서 받은 것처럼 구문 분석하고 실행한다.

예상과달리, 이러한 복제 방식은 여러 가지 방식으로 고장날 수 있다:

비결정적 함수 사용시

다음과 같은 비결정적 함수를 호출하는 문(statement)은 각 복제본에서 다른 값을 생성할 가능성이 높다.

  • 현재 날짜 및 시간을 가져오는 NOW()
  • 난수를 가져오는 RAND()

명령문이 기존 데이터에 종속되는 경우

명령문이 자동 증가 열을 사용하거나, 데이터베이스의 기존 데이터에 종속되는 경우 예를 들어, UPDATE … WHERE <조건> 문장을 사용했을 때 그렇다. 각 복제본에서 정확히 동일한 순서로 실행되어야 하며 그렇지 않으면 다른 결과가 발생할 수 있다.

부수효과(side effect)가 있는 문일 경우

부수효과가 있는 문일 경우. 예를 들어, 트리거, 저장 프로시저나 사용자 정의 함수 등은 각 복제본에서 서로 다른 부작용이 발생할 수 있다.

미리 쓰기 로그(WAL: Write Ahead Log) 전송

일반적인 스토리지 엔진은 모든 쓰기시에 로그를 추가한다.

  • 로그 구조의 스토리지 엔진(SSTables 및 LSM-Tree)의 경우, 이 로그가 주요 저장 장소다. 로그 세그먼트는 백그라운드에서 압축되고 가비지 컬렉션이 수행된다..
  • 개별 디스크 블록을 덮어쓰는 B-트리의 경우, 모든 수정 사항은 먼저 미리 쓰기 로그에 기록되므로 충돌 후 인덱스를 일관된 상태로 복원할 수 있다.

두 경우 모두 로그는 데이터베이스에 대한 모든 쓰기를 포함하는 추가 전용(append only) 바이트 시퀀스다. 동일한 로그를 사용하여 다른 노드에 복제본을 구축할 수 있다. 리더는 로그를 디스크에 쓰는 것 외에도 네트워크를 통해 팔로워에게 로그를 보낸다.
팔로워는 이 로그를 처리할 때 리더에서 발견한 것과 정확히 동일한 데이터 구조의 복사본을 만든다. 미리 쓰기 로그 방식은 PostgreSQL과 Oracle 에서 사용된다.

미리 쓰기 로그의 단점: 복제본이 스토리지 엔진과 밀접하게 연결되어 있다.

가장 큰 단점은 로그가 데이터를 매우 낮은(low-level) 수준이라는 것이다. 스토리지 엔진에 종속 될 경우, 리더와 팔로워가 서로 다른 데이터베이스 소프트웨어를 실행할 수 없다. 왜냐하면, WAL에는 어떤 디스크 블록에서 어떤 바이트가 변경되었는지에 대한 세부 정보가 포함되어 있기 때문이다.

논리적(행 기반) 로그 복제

논리적 로그란 스토리지 엔진의 물리적인 데이터 표현과 구분된 로그다. 관계형 데이터베이스의 논리적 로그는 일반적으로 행 단위로 데이터베이스 테이블에 대한 쓰기를 설명하는 레코드 시퀀스다.

논리적 로그의 특징

  • 삽입된 행의 경우 로그에는 모든 열의 새 값이 포함된다.
  • 삭제된 행의 경우 로그에는 삭제된 행을 고유하게 식별할 수 있는 정보가 포함되어 있다. (일반적으로 이 정보는 기본 키가 되지만 테이블에 기본 키가 없는 경우에는 모든 열의 이전 값을 기록해야 한다.)
  • 업데이트된 행의 경우 로그에는 업데이트된 행과 모든 열의 새 값을 고유하게 식별할 수 있는 충분한 정보가 포함된다.

여러 행을 수정하는 트랜잭션은 이러한 로그 레코드를 여러 개 생성하고 그 뒤에 트랜잭션이 커밋되었음을 나타내는 레코드를 생성한다.
행 기반 복제를 사용하도록 구성된 경우 MySQL의 binlog는 이 접근 방식을 사용한다.

논리적 로그의 장점은 무엇일까?

  1. 스토리지 엔진 내부에서 분리되어 있기 때문에 이전 버전과의 호환성을 보다 쉽게 유지할 수 있다. 이는 리더와 팔로워가 서로 다른 버전의 데이터베이스 소프트웨어 또는 서로 다른 스토리지 엔진을 실행할 수 있도록 해준다.
  2. 논리적 로그 형식은 외부 애플리케이션이 구문 분석하기에도 더 쉽다. 오프라인 분석을 위해 데이터베이스의 내용을 데이터 웨어하우스와 같은 외부 시스템으로 보내거나 사용자 정의 인덱스 및 캐시를 구축하려는 경우에 유용하다. 이 기법을 변경 데이터 캡처라고 한다.

트리거 기반 복제

지금까지 설명한 복제 접근 방식은 애플리케이션 코드 없이 데이터베이스 시스템에서 구현된다. 대부분의 경우 이 방식이 가장 적합하지만 유연성이 필요한 상황도 있다.

예를 들어,

  • 데이터의 하위 집합만 복제하거나 한 종류의 데이터베이스에서 다른 종류의 데이터베이스로 복제하려는 경우
  • 충돌 해결 로직이 필요한 경우 복제를 애플리케이션 계층으로 이동해야 할 수 있다.

이를 구현하기 위해, 관계형 데이터베이스에서 사용할 수 있는 기능인 트리거저장 프로시저를 사용하는 것이 일반적이다. 트리거를 사용하면,

  • 데이터베이스 시스템에서 데이터 변경(쓰기 트랜잭션)이 발생할 때 자동으로 실행되는 사용자 지정 애플리케이션 코드를 등록할 수 있다.
  • 트리거는 이 변경 사항을 별도의 테이블에 기록하여 외부 프로세스에서 읽을 수 있다. 그러면 외부 프로세스는 필요한 애플리케이션 로직을 적용하고 데이터 변경 사항을 다른 시스템으로 복제할 수 있다.

Oracle Databus와 Postgres Bucardo는 이와 같은 방식으로 작동한다. 트리거 기반 복제는 일반적으로 다른 복제 방법보다 오버헤드가 크고 데이터베이스에 내장된 복제보다 버그와 제한이 발생하기 쉽다. 하지만 그럼에도 불구하고 유연성 때문에 유용할 수 있다.

TLDR;

  • 리더 기반 복제를 구현하느 방식은 크게 2가지로써, 명령문 기반 복제와 로그 기반 복제가 있다.
  • 미리 쓰기 로그 기반 복제는 구현이 쉽지만, 스토리지 엔진과 결합되어 있어서 리더와 팔로워간 소프트웨어 버전 문제가 발생한다.
  • 논리적 로그 방식 복제는 호환성을 쉽게 유지가능하며 구문 분석이 쉬우므로 가장 많이 사용된다.
  • 마지막으로, 트리거 기반 복제는 트리거와 저장 프로시저를 사용하며, 유연성이 필요한 상황에서만 가끔씩 사용된다.