데이터 엔지니어링

[데이터 중심 애플리케이션 설계] 5장 복제

*이 글은 [데이터 중심 애플리케이션 설계]를 공부하며 기록을 남긴 것입니다.

노드 간 변경을 복제하기 위한 세 가지 알고리즘

복제 : 네트워크로 연결된 여러 장비에 동일한 데이터의 복사본을 유지하는 것

복제 서버(replica) : 데이터베이스의 복사본을 저장하는 각 노드

 

  • 단일 리더(single-leader)
  • 다중 리더(multi-leader)
  • 리더 없는(leaderless)

리더 기반 복제(master-slave 복제)

리더 기반 복제가 작동하는 방식

  1. 클라이언트가 쓰기를 할 때 리더에게 요청을 보냄
  2. 리더는 로컬 저장소에 새로운 데이터를 기록함
  3. 리더가 새롭게 데이터를 기록할 때마다, 데이터 변경을 복제 로그(replication log)나 변경 스트림(change stream)의 형태로 슬레이브에게 보냄
  4. 슬레이브는 로그에 따라 순서대로 쓰기를 진행하며 복사본을 갱신함

1) 동기식 vs 비동기식

  • 동기식 : 슬레이브에서 쓰기를 수신했는지 확인하고, 확인이 끝나면 사용자에게 성공을 보고한다
    • 장점 : 리더와 슬레이브가 최신 데이터의 복사본을 가지는 것을 보장하고, 리더가 작동하지 않아도 슬레이브를 통해 데이터 사용 가능
    • 단점 : 동기 슬레이브가 응답하지 않으면 쓰기가 처리될 수 없다. 리더는 모든 쓰기를 차단하고 기다려야 함. 한 노드의 장애는 전체 시스템의 장애를 유발함
  • 비동기식 : 슬레이브의 응답을 기다리지 않는다. 보통 리더 기반 복제는 완전한 비동기식으로 구현함
    • 장점 : 모든 슬레이브가 응답하지 않아도 리더를 통해 쓰기 처리 가능
    • 단점 : 리더가 잘못되어 슬레이브에 복제되지 않은 데이터는 유실됨
  • 반동기식 : 슬레이브 하나는 동기식으로, 나머지는 비동기식으로 하는 방법. 현실적으로 이 방법을 사용
    • 장점 : 적어도 두 노드에 최신 복사본이 있음을 보장

2) 새로운 슬레이브 설정

새로운 슬레이브가 리더의 데이터를 정확히 가지고 있는지 보장하는 방법

  1. 리더의 데이터베이스 스냅숏을 일정 시점에 가져옴
  2. 스냅숏을 새로운 슬레이브에 복사
  3. 슬레이브는 리더에게 스냅숏 이후 발생한 데이터 변경을 요청
  4. 슬레이브가 스냅숏 이후 백로그를 모두 처리하면, 모든 변경을 따라잡았다고 함

3) 노드 중단 처리

슬레이브 장애 : 로그에서 결함이 발생하기 전에 처리한 마지막 트랜잭션을 알아냄 -> 장애가 일어난 동안 발생한 데이터 변경을 리더에게 요청

리더 장애(장애 복구)

  • 리더가 장애인지 판단한다 : 보통 타임아웃을 사용
  • 새로운 리더를 선택한다 : 선출 과정(9장-합의 문제 참고)을 거치거나 미리 선택된 제어 노드 사용
  • 새로운 리더 사용을 위해 시스템을 재설정

장애 복구에서 유의해야 할 것

  • 비동기식 복제라면 이전 리더가 실패하기 전에 발생한 일부 쓰기를 수신하지 못할 수도 있음
  • 두 노드가 모두 자신이 리더라고 믿을 수 있다(스플릿 브레인 split brain이라고 한다) 두 노드 사이에 쓰기 충돌 해소 과정이 필요
  • 리더가 죽었다고 판단하기 적절한 타임아웃의 기준 : 부하 급증이나 네트워크 지연 등의 문제라면 불필요한 장애 복구가 상황을 악화시킬 수 있음

4-1) 복제 로그 구현: 리더 기반 복제가 내부적으로 동작하는 방법

1. 구문 기반 복제

  • 리더는 모든 쓰기 요청(구문)을 기록하고 쓰기를 실행한 다음 쓰기 로그를 슬레이브에게 전송
  • 구문을 그대로 실행하기 때문에 NOW(), RAND() 같은 비결정적 함수들은 다른 값을 생성할 가능성이 있음. 같은 값을 반환하도록 대체할 수 있지만 여러 케이스가 있기 때문에, 다른 방법을 선호

2. 쓰기 전 로그(Write-Ahead Log) 배송

  • 모든 쓰기는 로그에 저장되고, 로그를 사용하여 동일한 복제 서버를 구축할 수 있음
  • 개별 디스크 블록에 덮어쓰는 B 트리(3장 참고)는 모든 변경을 WAL에 쓰기 때문에, 고장 이후 색인을 쉽게 복원할 수 있음
    • 단, 어떤 블록에서 어떤 바이트를 변경했는지와 같은 저수준의 데이터를 포함하기 때문에, 복제가 저장소 엔진과 밀접하게 연관됨

3. 논리적(로우 기반) 로그 복제

  • 복제 로그를 저장소 엔진과 분리하기 위해 다른 로그 형식(논리적 로그)을 사용
  • 논리적 로그 : 로우 단위로 데이터베이스 테이블에 쓰기를 기술한 코드 열
  • 장점
    • 분리함으로써 하위 호환성을 쉽게 유지
    • 리더와 슬레이브에서 다른 소프트웨어나 저장소 엔진 사용 가능
    • 외부 애플리케이션이 파싱하기 쉬움 -> 오프라인 분석이나 변경 데이터 캡처(데이터 웨어하우스 같은 외부 시스템에 데이터를 전송)에 유용

4. 트리거 기반 복제

  • 사용자 정의 애플리케이션 코드를 등록하여, 데이터베이스에서 데이터가 변경되면(쓰기 트랜젝션) 자동으로 실행됨
  • 데이터의 서브셋만 복제하거나 충돌 해소 로직이 필요한 경우 등 유연성이 필요할 때 사용함
  • 반면, 버그나 제한 사항이 더 많음

4-2) 복제 지연 문제

복제 지연 : 리더의 쓰기와 슬레이브의 반영 사이의 지연

리더 기반 복제 : 어떤 replica에서도 읽기 요청 처리 가능

  • 읽기 요청이 많은 작업의 경우, 많은 슬레이브를 만들어 읽기 요청 분산
    • 리더의 부하를 줄이고, 사용자와 가까운 슬레이브에서 처리 가능
    • but, 비동기 복제에서만 동작함
      • 단일 노드 장애나 네트워크 고장 등으로 전체 쓰기 작업 중단, 노드 다운 가능성 ↑
    • 비동기 슬레이브에서 리더에 뒤쳐진 데이터 읽을 가능성있음
      • 쓰기를 멈추고 슬레이브 복구 -> 최종적 일관성

4-3) 복제 지연 사례

1. 자신이 쓴 내용 읽기

  • 비동기식 복제에서, 사용자가 쓰기를 수행한 직후 데이터를 보면 반영이 안 되어있을 수도 있음
  • 쓰기 후 읽기 일관성 : 사용자가 페이지를 재 로딩했을 때 항상 자신이 제출한 모든 갱신을 볼 수 있도록 보장하는 것
    • 구현 방법
      • 사용자가 수정한 내용을 읽을 때는 리더, 이외에는 슬레이브에서 읽는다
      • 클라이언트에서 가장 최근 쓰기의 타임스탬프를 기억한다
      • 분산 데이터베이스의 경우, 모든 요청은 리더가 포함된 데이터센터로 라우팅 되어야 함
  • 디바이스 간 쓰기 후 읽기 일관성 : 한 사용자가 여러 디바이스에서 접근할 경우
    • 타임스탬프 등의 메타데이터는 중앙집중식으로 관리
    • 분산 데이터베이스의 경우, 사용자 디바이스의 요청을 동일한 데이터센터로 라우팅 해야 함

2. 단조 읽기

  • 비동기식 복제에서, 사용자가 최신 슬레이브와 예전 슬레이브에서 각각 데이터를 읽을 경우 최신 데이터가 보였다가 안 보일 수 있는 현상(시간 역전)
  • 단조 읽기 : 새로운 데이터를 읽은 후에는 예전 데이터를 보지 않도록 보장하는 것
    • 구현 방법
      • 각 사용자의 읽기가 동일한 슬레이브에서 수행되게끔 하는 것
      • ex) 사용자 ID의 해시를 기반으로 슬레이브 할당, 문제시 재 라우팅

3. 일관된 순서로 읽기

  • 일련의 쓰기가 특정 순서로 발생하면, 이 쓰기를 읽는 모든 사용자는 같은 순서로 데이터를 읽도록 보장하는 것
  • 파티셔닝 된 데이터베이스에서 발생 -> 6장 참고
    • 구현 방법
      • 인과성 있는 쓰기가 동일한 파티션에 기록되도록 함
      • 인과성을 명시적으로 유지하기 위한 알고리즘 적용

4-4) 복제 지연을 위한 해결책

  • 사실은 비동기식으로 동작하지만 동기식으로 동작하는 척하기!...
  • 트랜잭션
    • 애플리케이션에서 기본 데이터베이스보다 강력한 보장을 제공하지만, 잘못되기 쉬움
    • 트랜잭션은 애플리케이션을 더 쉽고 신뢰할 수 있게 해 줌
  • 단일 노드 트랜잭션은 오래전부터 존재
  • 분산 데이터베이스로 전환되면서, 트랜잭션이 성능과 가용성 측면에서 비싸기 때문에 최종적 일관성을 사용해야 한다고 주장하기도 함 -> 7장, 9장 트랜잭션 참고

다중 리더 복제

쓰기를 허용하는 노드를 하나 이상 두는 것

1) 다중 리더 복제의 사용 사례

1. 다중 데이터 센터 운영(단일 리더와 비교)

  1. 성능
    • 단일 리더 : 모든 요청을 리더에게 전송 -> 지연시간 증가
    • 다중 리더 : 로컬에서 처리 후 비동기 방식으로 다른 데이터센터에 복제 -> 데이터센터 간 지연이 숨겨져 사용자 체감 성능 향상
  2. 데이터센터 중단 내성
    • 단일 리더 : 다른 데이터센터에서 리더 재설정
    • 다중 리더 : 모든 데이터센터는 독립적으로 동작, 고장 후 복구되면 다시 따라잡음
  3. 네트워크 문제
    • 단일 리더 : 데이터센터 내 연결은 동기식이므로 네트워크에 민감
    • 다중 리더 : 네트워크 중단에도 쓰기 처리 가능
  •  

하지만, 동일한 데이터를 여러 개의 데이터센터에서 동시에 변경할 가능성 -> 충돌 해소 필요

이 외에도 오프라인 동작 애플리케이션, 실시간 협업 편집 등의 사례에서 쓰기 충돌이 일어날 수 있다.

2) 쓰기 충돌

다중 리더 설정에서 발생할 수 있는 충돌 다루기

1. 동기 대 비동기 충돌 감지

  • 단일 리더 : 첫 번째 쓰기가 완료될 때까지, 두 번째 쓰기나 트랜잭션을 중단
  • 다중 리더 : 모든 쓰기는 성공, 이후 특정 시점에서 비동기식으로 충돌 감지
  • 이론적으로 충돌 감지는 동기식으로 구현하지만, 다중 리더 복제의 장점(각 복제 서버가 독립적으로 쓰기 허용)을 잃음

2. 충돌 회피

  • 특정 레코드의 모든 쓰기가 동일한 리더를 거치도록 애플리케이션이 보장하면 충돌 x
  • ex) 특정 사용자의 요청을 동일한 데이터센터로 항상 라우팅

3. 일관된 상태 수렴

  • 단일 리더: 순차적으로 쓰기 진행
  • 다중 리더: 순서가 명확하지 않음 -> 수렴을 보장해야함(모든 변경에 복제되어 모든 복제 서버에 최종적으로 동일한 값이 전달되어야 한다는 것)
    • 순서를 정하는 방법: 각 쓰기에 고유 ID(타임스탬프, UUID 등) 적용, 어떻게든 값을 병합..., 충돌을 기록하여 모든 정보를 보존하고 사용자가 직접 해소하도록 함

3) 다중 리더 복제 토폴로지

복제 토폴로지 : 쓰기를 한 노드에서 다른 노드로 전달하는 통신 경로

원형 토폴로지 별 모양 토폴로지 전체 연결 토폴로지

쓰기가 모든 복제 서버에 도달하기까지 여러 노드를 거쳐야 함(고유 식별자를 사용해 무한 복제 방지)

but, 한 노드에 장애가 발생하면 다른 노드 간 복제에도 영향

내결함성이 보다 좋음

네트워크 연결이 빠른 부분에서 데이터의 추월이 일어날 수 있음, 순서 충돌

-> 이벤트 정렬을 위해 버전 벡터 사용

리더 없는 복제

앞서 살펴본 단일/다중 리더 복제는 리더가 쓰기를 처리하는 순서를 정하고 팔로워가 동일한 순서로 쓰기를 처리한다. 반면 리더 없는 복제는 모든 복제 서버가 클라이언트로부터 직접 쓰기를 받을 수 있다.

1) 장애 복구

리더 없는 복제에서는 장애 복구가 필요하지 않다. 예를 들어, 세 개의 복제 서버가 있을 때 두 개의 서버에서 성공하면 충분하다고 가정하면, 나머지 서버의 실패 여부는 무시되고 쓰기는 성공했다고 간주된다. 이후 장애 서버가 다시 온라인 상태가 되면 다른 복제 서버와 버전을 비교하여 실패한 쓰기를 복구한다.

이전 발생 관계와 동시성

이전 발생 관계: 작업 B가 작업 A에 대해 의존적이거나, A를 아는 등 A를 기반으로 하는 작업이라면 B는 A보다 이전에 발생했다고 한다.

동시 작업: 이전 발생 관계가 없는 두 작업에 대해 동시에 수행되었다고 한다. 여기서 물리적인 시각은 관계가 없다는 것이 포인트.

버전 번호를 통한 동시성 파악

하나의 복제본을 가진 서버가 있을 때, 버전 번호를 이용한 동시성 관리는 다음과 같이 이루어진다.

  1. 서버는 모든 키에 대한 버전 번호를 가지고, 키를 기록할 때마다(쓰기) 버전 번호를 증가시킨다.
  2. 클라이언트가 키를 읽을 때, 서버는 최신 버전뿐만 아니라 덮어쓰지 않은 모든 값을 반환한다.
  3. 키를 읽은 후 클라이언트가 쓰기를 수행할 때, 이전 읽기에서 받은 모든 값까지 합치고 이전 읽기의 버전 번호를 포함하여 전달한다.
  4. 서버가 특정 버전 번호를 가진 데이터를 받으면, 해당 버전 이하의 값들을 모두 덮어쓸 수 있다(이미 클라이언트에 의해 합쳐진 값이므로). 하지만 이보다 높은 버전의 값들은 유지한다.

이를 통해 데이터 유실이 없음을 보장할 수 있지만, 클라이언트에서 추가 작업을 해야 한다는 단점이 있다.

3번의 합치는 과정에서 합집합(데이터 단순 추가만 있을 경우 사용 가능) 또는 툼스톤(아이템이 삭제되는 경우, 실제 데이터베이스에서 삭제하지 않고 해당 버전 번호에 아이템을 제거했다는 표시를 같이 남기는 방법) 방법을 사용할 수 있다.

버전 벡터

복제본이 하나가 아니라 여러 개인 경우, 키당 버전 번호 뿐 아니라 복제본당 버전 번호도 필요하다.

이처럼 모든 복제본의 버전 번호를 모아둔 것을 버전 벡터라고 한다.

2) 읽기/쓰기를 위한 정족수(quorum)

정족수

위에서 가정한 것처럼, 장애를 허용하고 최신 값을 보장하기 위해 필요한 조건(복제 서버의 개수)은 다음과 같다.

n: 총 복제 서버의 개수
w: 쓰기를 성공한 복제 서버의 개수
r: 읽기를 수행하기 위해 질의해야 하는 복제 서버의 개수일 때,

w+r> n 이면, 모든 읽기에서 최신 값을 얻는다고 기대할 수 있다.
일반적으로 n을 홀수(3 or 5), w=r=(n+1)/2로 지정한다.

정족수의 한계

정족수가 최신 값을 보장하도록 설계되긴 했지만 다양한 상황에 의해 절대적인 보장은 어렵다(낮은 내결함성). 트랜잭션이나 합의 등의 방식으로 최종적 일관성을 보장하도록 최적화해야 한다.

 

  • 두 개의 쓰기가 동시에 발생한 경우, 순서가 불분명하다. -> 쓰기 충돌 다루기, 동시 쓰기 감지 참고
  • 쓰기와 읽기가 동시에 발생하면, 쓰기는 일부 복제 서버에만 반영될 수 있다.
  • 일부 복제 서버에서 쓰기를 실패하여 성공한 서버가 w보다 작다면, 이후의 읽기에서 최신 값을 보장할 수 없다.

느슨한 정족수(sloppy quorum)와 암시된 핸드오프

  1. 네트워크 장애가 발생한 상황에, 클라이언트는 일부(정족수 이외의 노드) 노드로 연결될 가능성이 있다. 이 경우 n개에 포함되지 않는 새로운 노드(느슨한 정족수)가 일단 쓰기를 받아둔다.
  2. 장애가 해결되면, 쓰기를 받아두었던 노드가 복구된 홈(n개에 포함되는) 노드로 쓰기를 전송한다(암시된 핸드오프).

느슨한 정족수를 사용하면 쓰기 가용성을 높일 수 있다. 장애 상황에서도 데이터가 w 노드 어딘가에는 저장됨을 보장한다. 하지만 암시된 핸드오프가 완료될 때 까지는 r 노드의 읽기가 최신 값을 읽는다는 보장은 없다.

이처럼, 리더 없는 복제는 동시 쓰기 충돌, 네트워크 중단, 지연 시간 급증 등을 허용하므로 다중 리더 복제에서 살펴본 것과 같이 다중 데이터센터 운영에 적합하다.