본문 바로가기
TIL/데이터 엔지니어링

데이터 중심 어플리케이션 설계 - 5장. 복제

by yeon_zoo 2023. 5. 30.

1. 리더와 팔로워


모든 쓰기는 모든 레플리카에서 처리되어야 한다. → 리더 기반 복제 (= 능동/수동, 마스터-슬레이브 복제)

  • 리더 (= 마스터, 프라이머리)
  • 팔로워 (읽기 복제 서버(replica), 슬레이브, 2차(secondary), 핫 대기(hot standby))

리더가 로컬 저장소에 새로운 데이터 기록 시 데이터 변경을 복제 로그나 변경 스트림의 일부로 팔로워에게 전송한다.

쓰기는 리더에게만 요청하고 읽기는 리더나 임의의 팔로워에게 요청한다. 

→ 사용처 : 여러 관계형 데이터베이스 및 비관계형 데이터베이스, 고가용성 큐 같은 분산 메시지 브로커

a. 동기식 대 비동기식 복제

  • 동기식 : 팔로워가 쓰기를 수신했는지 여부의 응답을 받는다. ( 한 노드의 장애가 전체 시스템의 문제가 될 수 있으므로 비현실적인 방안)
  • 비동기식 : 응답 대기 x
  • 반동기식 : 1개의 팔로워만 동기식, 나머지는 비동기식 → 최소 2개의 노드에 최신 데이터가 존재

리더 기반 복제 : 완전 비동기식으로 구성 (내구성은 떨어져도 가용성이 올라감) ∵ 데이터 유식 가능성 o

b. 새로운 팔로워 설정

과정 

  1. 전체 DB 잠그지 않고, 리더의 데이터베이스 스냅샷을 일정 시점에 가져온다. 
  2. 스냅샷을 새로운 팔로워 노드에 복사한다. 
  3. 팔로워를 리더에 연결해 스냅샷 이후 발생한 모든 변경을 요청한다. (스냅샷이 리더의 복제 로그의 정확한 위치와 연결할 필요가 있다)
  4. 팔로워가 데이터 변경의 미처리분(backlog)를 모두 처리하면 따라잡은 것이다. 

c. 노드 중단 처리 

개별 노드의 장애에도 전체 시스템이 동작하도록 유지하고 노드 중단의 영향을 최소화 해야 한다. 

팔로워 장애 : 따라잡기 복구

각 팔로워는 리더로부터 수신한 데이터 변경로그를 로컬 디스크에 저장한다. 

팔로워 장애 후 복구 과정

  1. 보관된 로그에서 결함 발생 전 마지막 트랜잭션을 알아냄
  2. 팔로워가 리더에 요청해 연결이 끊어진 동안 발생한 데이터 변경을 수신
  3. 해당 변경들을 모두 적용

리더 장애 : 장애 복구

리더 장애 복구 과정

  1. 팔로워 중 하나를 새로운 리더로 승격
  2. 클라이언트는 새 리더로 쓰기 전송을 위한 재설정이 필요
  3. 다른 팔로워는 새 리더로부터 데이터 변경을 소비

자동 장애 복구 단계 

  1. 리더가 장애인지 판단 (by. 타임아웃)
  2. 새로운 리더 선택 (선출과정이 필요하거나 선출된 제어 노드가 선택)
    가장 적합한 후보는 이전 리더의 최신 데이터 변경 사항을 가진 replica
  3. 새로운 리더 사용을 위해 시스템 재설정
    새로운 리더로 클라이언트의 쓰기 요청, 이전 리더가 돌아오면 이전 리더는 팔로워가 되고 새로운 리더를 인식하도록 해야 한다. 

문제 발생 지점 :

  • 새로운 리더에는 없는 이전 리더의 일부 쓰기 존재 / 리더가 바뀐 후의 수신 쓰기로 충돌도 발생 가능.
    → 이전 리더의 복제되지 않은 쓰기를 단순 폐기하면 처리 가능 → 내구성 ↓
  • 단순 폐기의 문제 : ex) github : 이전 리더와 새로운 리더의 중복된 pk 자동 증가 카운터와 해당 값을 사용하고 있던 redis 문제로 개인 데이터가 다른 사용자에게 잘못 공개되는 문제 발생
  • 스플릿 브레인 : 두 노드가 동시에 자신이 리더라고 믿음
    → 두 리더 감지 시 노드 하나 종료 매커니즘 O. but 둘 다 종료 가능성이 있으니 주의가 필요.
  • 리더 장애 판단에 적합한 타임아웃 시간은? 너무 길면 장애 ~ 복구까지 시간 지연이 길어지고, 너무 짧으면 불필요한 장애 복구가 잦아질 수 있다. 

d. 복제 로그 구현

구문 기반 복제

실제 statement 로그를 팔로워에게 전달하는 방식이다. 

주의할 점 :

  • NOW(), RAND()와 같은 비결정적 함수 사용시 값이 달라질 수 있다. 
  • 자동 증가 칼럼 사용 시 각 복제 서버에서 정확히 같은 순서로 실행할 필요가 있다. 
  • 부수 효과가 있는 구문(트리거, 스토어드 프로시저, 사용자 정의 함수) → 부수 효과가 완전히 결정적이어야 한다. 

대안 해결책 : 리더가 구문 기록 시 비결정적 함수를 호출하여 고정 값을 반환하도록 대체한다. 
하지만 이런 경우에도 에지 케이스가 존재하므로 다른 복제 방법을 선호한다. 

쓰기 전 로그(WAL) 배송

로그 : DB의 모든 쓰기를 포함하는 append-only 바이트열

리더의 역할 : 디스크에 로그를 기록 + 팔로워에게 네트워크로 로그를 전송

단점 : 로그는 가장 저수준의 데이터를 기록한다. (WAL은 어떤 디스크 블록에서 어떤 바이트로 변경했는지의 상세 정보를 기술)
          ∴ 각 리더와 팔로워의 데이터베이스 소프트웨어 버전이 다르게 실행되면 안 된다. = 데이터베이스 업그레이드 시 순환식 X → 다운 타임이 발생.

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

WAL의 물리적 데이터 표현과는 다른 논리적 로그(logical log) : 로우 단위로 쓰기를 기술. 

  • 삽입된 로우의 로그는 모든 칼럼의 새로운 값을 포함
  • 삭제된 로우의 로그는 로우를 고유하게 식별하는 데 필요한 정보를 포함 (pk, pk가 없다면 모든 칼럼의 예전 값 로깅)
  • 갱신된 로우의 로그는 로우를 고유하게 식별하는 데 필요한 정보와 모든 칼럼의 새로운 값 포함

데이터 웨어하우스 같은 외부 시스템에 전송 시 유리하다. → 변경 데이터 캡쳐 (change data capture)

트리거 기반 복제

지금까지의 복제 방법과 다른 점 : 어플리케이션 코드의 사용

데이터의 subset만 복제 or 데이터베이스에서 다른 종류의 데이터베이스로 복제 or 충돌 해소 로직 필요시 복제를 어플리케이션 단으로 이동

트리거 : 사용자 정의 어플리케이션 코드를 등록 → 데이터베이스 시스템에서 변경 발생 시(= 쓰기 트랜잭션) 이 코드를 자동으로 실행 → 데이터 변경을 분리된 테이블에 로깅 → ㅇㅚ부 프로세스가 해당 테이블에서 데이터 변경을 읽을 수 있다. 

  • 오버헤드가 크고 버그나 제한 사항이 많지만 유연성이 높다. 

2. 복제 지연 문제


복제의 목적:

  • 확장성 : 단일 장비에서 감당하지 못하는 요청 처리
  • 지연 시간 : 사용자에게 지리적으로 더 가까운 replica

읽기 확장은 비동기식 복제에서만 동작한다. 

문제 : 비동기 팔로워가 뒤쳐진다면 과거 정보를 볼 수도 있다. 

하지만 일시적인 불일치로, 결국에는 '최종적 일관성'을 보장한다. 

복제 지연은 (정상 동작이라는 범주 하에서는) 아주 짧은 순간이긴 하지만 발생할 수 있는 다음 3가지 사례가 있다. 

a. 자신이 쓴 내용 읽기

쓰기 후 읽기 일관성 : 사용자가 페이지를 재로딩 → 항상 자신이 제출한 모든 갱신을 볼 수 있도록 보장해야 한다. 

보장 방법 

  • 사용자가 수정한 내용을 읽을 때는 리더에서. 이를 위해서는 실제로 질의하지 않고 무엇이 수정됐는지 알 수 있는 방법이 필요하다 (p.165)
  • 어플리케이션 내 대부분의 내용을 사용자가 편집할 가능성이 있다면 비효율적이다. 
    이런 경우 리더에서 읽기를 하는 기준 : 마지막 갱신 후 1분 동안은 리더에서 모든 읽기 수행.
    + 복제 지연 모니터링 : 리더보다 1분 이상 늦은 모든 팔로웨어 대한 질의 금지
  • 클라이언트는 가장 최신 쓰기의 타임스탬프를 기억. → 시스템에서 복제 서버가 최소한 해당 타임스탬프까지 갱신을 반영할 수 있도록 함. 
  • 여러 데이터 센터에 분산되면 복잡도가 높아진다. 리더가 제공해야 하는 모든 요청은 리더가 포함된 데이터 센터로 라우팅되어야 한다. 

동일한 사용자가 여러 디바이스로 서비스를 접근한다면 고려해야 할 지점 (디바이스 간(cross-device) 쓰기 후 읽기 일관성의 보장)

  • 사용자의 마지막 갱신 타임스탬프를 기억하는 방식으로는 어렵다. ( ∵ 한 기기에서 수행 중인 코드는 다른 디바이스에서 발생한 갱신을 알 수 없음) 이 메타데이터를 중앙집중식으로 관리해야 함. 
  • 리더에서 읽어야 할 필요가 있는 접근법이라면 먼저 같은 사용자의 디바이스 요청을 동일한 데이터 센터로 라우팅해야 한다. 

b. 단조 읽기

사용자가 시간이 거꾸로 흐르는 현상을 목격할 수 있다. 

 단조 읽기 (monotonic read) : 강한 일관성보다는 덜한 보장 / 최종적 일관성보다는 강한 보장. 한 사용자가 여러 번에 걸쳐 읽어도 시간이 되돌아가는 현상을 보지 않는다. 

달성 방법 : 각 사용자의 읽기가 항상 동일한 복제 서버에서 수행되게끔 하는 것. 임의 선택 보다는 사용자 ID의 해시를 기반으로 복제 서버를 선택한다. 복제 서버가 고장나면 사용자 질의를 다른 복제 서버로 재라우팅 필요.

c. 일관된 순서로 읽기

인과성의 위반 우려. 어떤 질문에 대한 대답은 인과성을 갖는다. 만약 해당 질문은 아주 긴 복제 지연이 생겨서 대답보다 늦게 복제 서버에 도달했다면 그 복제 서버에서 읽기를 시도한 사용자는 질문 없이 대답만 읽을 수 있게 된다. 

→ 일관된 순서로 읽기의 보장이 필요

파티셔닝(샤딩된) 데이터베이스에서 발생하는 문제. 데이터베이스가 항상 같은 순서로 쓰기를 적용한다면 해결할 수 있으나 서로 다른 파티션이 독립적으로 동작하면 쓰기의 전역 순서가 없다. 

해결책 = "이전 발생" 관계와 동시성에서..

d. 복제 지연 해결책

최종적 일관성 시스템인 경우 복제 지연이 길어지면 사용자 경험에 좋지 않음. 따라서 비동기식 복제가 동기식으로 동작하는 척 할 수 있음. → 어플리케이션이 기본 데이터베이스보다 더 강력한 보장을 제공하는 방법. (ex. 특정 종류의 리더에서 읽기 수행) but 복잡도 ↑

트랜잭션 : 단일 노드에서 자주 사용되어 왔으나 분산 데이터베이스로 전환되면서 가용성과 성능 측면에서 너무 비싸 최종적 일관성을 선택하게 됨. 

3. 다중 리더 복제


리더 기반 복제의 주요 단점 : 리더가 하나만 존재하고 모든 쓰기는 해당 리더를 거쳐야 한다. 리더에 연결할 수 없다면 데이터베이스에 쓰기를 할 수 없다. 

여러 리더를 두는 것으로 확장. (= 다중 리더 설정, 마스터 마스터, 액티브/액티브 복제)

a. 다중 리더 복제의 사용 사례

단일 데이터센터 내에서 다중 리더는 적합하지 않음. ( ∵ 복잡도가 크게 증가)

다중 데이터 센터 운영

각 데이터센터마다 리더가 존재. 데이터센터 내부에서는 보통의 리더 팔로워 복제를 사용하고 데이터 센터 간에는 각 리더가 다른 데이터센터의 리더에게 변경 사항을 복제한다. 

  • 성능
    • 쓰기는 로컬 데이터센터에서 처리한 다음 데이터센터 간 복제는 비동기식으로 진행 (단일 리더 설정에서 리더에 쓰기는 동기식으로 이루어지는 것보다 체감상 성능 더 좋을 수 있음)
  • 데이터센터 중단 내성
    • 각 데이터센터는 다른 데이터센터와 독립적으로 동작. 고장난 데이터센터가 온라인으로 돌아왔을 때 복제를 따라잡음. 네트워크 문제 내성 (p.171)
    • 데이터센터 간 트래픽은 공개 인터넷을 통해 처리. ∴ 데이터센터 내의 로컬 네트워크보다 안정성이 떨어짐. → 네트워크 문제를 보다 잘 견딤.. 일시적인 중단에도 쓰기 처리는 진행됨. 
  • 단점
    • 동일한 데이터를 두 개의 데이터센터에서 동시에 변경할 시 쓰기 충돌을 반드시 해소해야 함.
    • 미묘한 설정 상의 실수나 다른 데이터베이스 기능과의 뜻밖의 상호작용 등 문제의 소지가 많음. 

오프라인 작업을 하는 클라이언트

인터넷 연결이 끊어진 동안 어플리케이션이 계속 동작해야 하는 경우에 유용. 

디바이스 내의 로컬 데이터베이스에서 쓰기 요청 수신 → 디바이스에서 다중 리더 복제를 비동기 방식으로 수행(동기화) 

복제 지연은 사용자가 인터넷 접근이 가능해진 시점에 따라 몇 시간 ~ 며칠 이상도 걸릴 수 있음. 

즉, 각 디바이스는 데이터센터가 되고 디바이스 간 네트워크 연결은 극히 신뢰할 수 없음. → 카우치DB는 이런 동작 모드를 위해 설계됨. 

협업 편집

실시간 협업 편집 어플리케이션에서 이용. 

한 사용자가 문서를 편집할 때 변경 내용(웹 브라우저나 클라이언트 어플리케이션의 문서 상태)을 즉시 로컬 복제 서버에 적용하고 동일한 문서를 편집하는 다른 사용자와 서버에 비동기 방식으로 복제. 

편집 충돌이 없음을 보장하는 가장 쉬운 방법 : 문서의 잠금을 얻어 사용자의 변경이 커밋되고 잠금이 해제될 때까지 다른 사용자는 사용하지 못함. 

더 빠른 협업을 위해 변경 단위를 매우 작게 해서 잠금을 피할 수 있음 → 충돌 해소가 필요한 경우 등 다중 리더 복제에서 발생하는 문제에 대한 해결 필요

b. 쓰기 충돌 다루기

동기 대 비동기 충돌 감지

  • 단일 리더 데이터베이스에서는 첫 번째 쓰기가 완료될 대까지 두 번째 쓰기를 차단해 기다리게 하거나 두 번째 쓰기 트랜잭션을 중단해 사용자가 쓰기를 재시도하게 한다. 
  • 다중 리더의 경우는 두 쓰기 모두 성공 + 충돌은 이후 비동기로만 감지한다. 사용자에게 충돌 해소 요청하는 것이 너무 늦을 수 있다. 

동기식 충돌 감지(쓰기가 성공한 사실을 반환하기 전에 모든 레플리카가 쓰기를 복제하기를 기다림)는 다중 리더의 장점인 각 복제 서버가 독립적으로 쓰기를 허용한다는 점을 살릴 수 없음. 

충돌 회피

가장 간단한 전략. 특정 레코드의 모든 쓰기가 동일한 리더를 거치도록 보장. 

ex) 특정 사용자의 요청을 동일한 데이터센터로 항상 라우팅하고 데이터센터 내 리더를 사용해 읽기와 쓰기를 하게끔 보장. 

한 데이터센터가 고장 나서 트래픽을 다른 데이터센터로 다시 라우팅해야 하거나 사용자가 다른 지역으로 이동해서 다른 데이터센터가 가깝다면 지정된 리더를 변경하고 싶을 수 있다. 이 경우, 충돌 회피가 실패

일관된 상태 수렴

마지막 쓰기가 필드의 최종 값으로 결정된다. ∵ 최종적 일관성을 보장

수렴 충돌 해소 방법 :

  • 각 쓰기에 고유 ID (타임스탬프, UUID, 긴 임의숫자 등)을 부여하고 가장 높은 ID를 가진 쓰기를 선택. 타임스탬프를 사용하는 방식 = 최종 쓰기 승리(last write wins, LWW).
    가장 대중적이지만 데이터 유실 위험이 존재 
  • 각 복제 서버에 고유 ID를 부여하고 높은 숫자의 복제 서버에서 생긴 쓰기가 낮은 숫자의 복제 서버에서 생긴 쓰기보다 항상 우선 적용. 복제 서버 자체의 우선순위를 정한다는 뜻. 역시 데이터 유실 위험이 존재
  • 어떻게든 값을 병함. 예를 들면 사전 순으로 정렬한 후 연결
  • 명시적 데이터 구조에 충돌을 기록해 모든 정보를 보존. 사용자에게 나중에 보여줌으로써 충돌을 해소하는 어플리케이션 코드가 필요.

사용자 정의 충돌 해소 로직

충돌 해소에 적합한 방법은 어플리케이션에 따라 다르다. 따라서 어플리케이션 코드를 사용해 충돌 해소 로직을 작성하는 것이 일반적. 

  • 쓰기 수행 중 충돌 해소
    복제된 변경 사항 로그에서 DB 시스템이 충돌을 감지하자마자 충돌 핸들러를 호출. 사용자에게 충돌 내용을 표시하지 않고, 백그라운드 프로세스에서 빠르게 실행
  • 읽기 수행 중 충돌 해소 
    충돌 감지 시 모든 충돌 쓰기를 저장. 다음 읽기 시 여러 버전의 데이터가 반환됨. 사용자에게 충돌 내용을 보여주거나 자동으로 충돌을 해소. 충돌을 해소한 결과는 다시 데이터베이스에 기록. 

(?) 위키는.. 읽기 수행 중일까 쓰기 수행 중일까 (→ 사용자에 보여주는 걸 보면 읽기인데.. 저장 시에만 충돌 내용을 보여줌..?)

자동 충돌 해소 방법

  • 충돌 없는 복제 데이터타입(conflict-free replicated datatype, CRDT) : 데이터 구조의 집합으로 동시에 여러 사용자가 편집할 수 있고 합리적인 방법으로 충돌을 자동 해소. 
  • 병합 가능한 영속 데이터 구조 : 깃 버전 제어 시스템과 유사하게 명시적으로 히스토리를 추적. 삼중 병합 함수 이용 (CRDT는 이중 병합)
  • 운영 변환(operational transformation) : 협업 편집 애플리케이션의 충돌 해소 알고리즘. 문자 목록과 같은 정렬된 항목 목록의 동시 편집을 위해 설계

충돌은 무엇인가

명백하지 않은 충돌이 발생할 수도 있다. (ex. 회의실 예약 시스템) → 7장에서 좀 더 확인..

d. 다중 리더 복제 토폴로지

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

  • 전체 연결 : 모든 리더가 각자의 쓰기를 다른 모든 리더에 전송. 
  • 원형 토플로지 : 각 노드가 하나의 노드로부터 쓰기를 받고, 이 쓰기를 다른 한 노드에 전달
  • 별 모양 토플로지 : 지정된 루트 노드 하나가 다른 모든 노드에 쓰기를 전달. (트리로 일반화 가능)

원형과 별 모양 토폴로지

  • 쓰기가 모든 복제 서버에 도달하기 위해 여러 노드를 거쳐야 함. 
  • 무한 복제 루프 방지를 위해 각 노드의 고유 식별자가 로그에 태깅됨.
  • 하나의 노드에 장애가 발생하면 장애가 다른 노드 간 복제 메시지 흐름에 방해를 줌 → 장애 노드를 회피하게끔 토폴로지 재설정 가능. (보통 수동으로..)
  • 빽빽하면 내결함성이 훨씬 더 좋아짐. 

전체 연결 토폴로지

  • 일부 복제 메시지가 다른 메시지를 추월할 수 있음.  → 인과성의 문제 발생 가능
    타임스탬프로도 해결 불가능. 올바르게 정렬하기에 충분할 정도로 노드들의 시간이 동기화됐다고 신뢰할 수 없음. 
  • 버전 벡터 기법 사용. 
  • 실제로 믿을만한 보장을 제공하는지 각 데이터베이스를 철저하게 테스트할 필요가 있음. 

4. 리더 없는 복제


다이나모 스타일 : 리더없는 복제 종류의 데이터베이스

클라이언트가 여러 복제 서버에 쓰기를 직접 전송하지 않고 코디네이터 노드가 이를 대신해주기도 함. → 코디네이터 노드는 특정 순서로 쓰기를 수행하지 않음. 

a. 노드가 다운 됐을 때 데이터베이스에 쓰기 

리더 없는 설정에서는 장애 복구가 불필요. 

읽기 요청을 병렬로 여러 노드에 전송. 여러 노드에서 다른 응답(오래된(outdated) 값 포함)을 받을 수 있지만 버전 숫자를 사용해서 어떤 값이 최신 내용인지 결정

(?) 그럼 가용성이 높아진 것인지..? 오히려 1요청 : 3 읽기로 분산 되지 못한 것이 아닌지 + 심지어 쓰기는 전부 동기식 → 전체 노드에 쓰기를 할 때 사용자 체감 속도가 너무 높아지지 않을까?

읽기 복구와 안티 엔트로피

  • 읽기 복구
    오래된 응답을 감지하면 해당 복제 서버에 새로운 값을 다시 기록. 값을 자주 읽는 상황에 적합
  • 안티 엔트로피 처리
    백그라운드 프로세스를 두고 복제 서버간 데이터 차이를 지속적으로 찾아 누락된 데이터를 복사해옴. 특성 순서로 쓰기를 복사하기 때문에 데이터 복사까지 상당한 지연이 있을 수 있다. 

모든 시스템에서 위의 두 매커니즘을 구현한 건 아님. 

읽기와 쓰기를 위한 정족수

세 개 중 두 개에서만 쓰기를 처리해도 성공한 것으로 간주. 

n개의 복제 서버가 있을 때 모든 쓰기는 w개의 노드에서 성공해야 쓰기가 확정 + 모든 읽기는 최소한 r개의 노드에 질의

  • w + r > n 이면 읽을 때 최신 값을 얻을 것으로 기대. (r : 정족수 읽기, w : 정족수 쓰기)
  • 일반적으로 n은 홀수(3이나 5)로, w = r = (n + 1) / 2 (반올림) 
  • 쓰기가 적고 읽기가 많은 작업 부하는 w= n, r = 1로 커스텀할수도. 읽기는 빨라지지만 노드 하나가 고장나면 모든 DB 쓰기가 실패함. 
  • 클러스터에는 n개 이상의 노드가 있을 수 있지만 주어진 값은 n개 노드에만 저장된다. (데이터셋을 파티션한 경우)
  • 정족수 조건이 w+r > n 이면
    • w < n 이면 노드 하나를 사용할 수 없어도 여전히 쓰기를 처리할 수 있다. 
    • r < n 이면 노드 하나를 사용할 수 없어도 여전히 읽기를 처리할 수 있다. 

필요한 w나 r개 노드보다 사용 가능한 노드가 적다면 쓰기, 읽기는 에러를 반환.  

정족수 일관성의 한계

보통 r과 w의 값으로 노드의 과반수(n/2 초과)를 선택. ( ∵ n/2 노드 장애까지 허용해도 w + r > n이 보장되기 때문) 하지만 정족수가 다수일 필요는 업삳. → 유연성이 허용됨. 

w와 r이 작을 수록 오래된 값을 읽을 확률이 높다. r과 w를 다수 노드로 선택하면 낮은 지연 시간과 높은 가용성이 가능하다.

w + r > n 인 경우에도 오래된 값을 반환하는 에지 케이스 

  • 느슨한 정족수를 사용시 w개의 쓰기는 r개의 읽기와 다른 노드에서 수행될 수 있음. 
  • 두 개의 쓰기가 동시 발생 시 어떤 쓰기가 먼저 일어났는지 분명하지 않음. 최종 쓰기 승리로 결정되면 시계 스큐로 인해 쓰기가 유실될 수 있음
  • 쓰기와 읽기 동시 발생 시 쓰기는 일부 복제 서버에서만 반영될 수 있음.
  • 일부 복제 서버에서 쓰기 성공했으나 일부에서 실패한 경우, 전체에서 성공한 서버 개수가 w보다 적다면 성공한 복제 서버에서는 롤백하지 않는다.
  • 새 값을 전달하는 노드가 고장나면 예전 값을 가진 복제 서버에서 복원될 수 있고 새로운 값이 저장된 복제 서버의 개수가 w보다 적어져서 정족수 조건이 깨질 수 있음
  • 모든 과정이 올바르더라도 시점 문제로 에지 케이스 발생 가능

복제 지연 문제의 자신의 쓰기 읽기, 단조 읽기, 일관된 순서로 읽기를 보장 받을 수 없다. → 트랜잭션이나 합의가 필요

최신성 모니터링

데이터베이스가 최신 결과를 반환하는지 여부를 모니터링하는 일은 중요

  • 리더 기반 복제 : 리더의 현재 위치에서 팔로워의 현재 위치를 빼면 복제 지연량 측정 가능
  • 리더리스 복제 : 쓰기 순서 고정 불가 → 모니터링이 더 어려움. 
    최종적 일관성은 모호한 보장이지만 '최종적'에 대한 정량화는 필요

느슨한 정족수와 암시된 핸드 오프

정족수가 잘 설정되면 장애 복구 없이 개별 노드 장애를 용인. 개별 노드의 응답이 느려지는 것도 허용 가능. → 높은 가용성과 낮은 지연 시간

만약 오래된 값 읽기를 허용하는 사용 사례라면 적합할 수 있음. 

하지만 정족수는 내결함성이 없음. 정상 연결된 노드가 w나 r보다 적어지면 아예 정족수 충족 가능성이 없어짐. 

노드가 n개 이상인 대규모 클러스터에서 네트워크 장애 상황이라면 정족수 구성에 들어가지 않는 노드에 연결될 가능성이 있다. 이 경우의 트레이드 오프는 다음과 같다. 

  • w나 r 노드 정족수를 만족하지 않는 모든 요청에 오류 반환
  • 느슨한 정족수 : 일단 쓰기를 받아들이고 값이 보통 저장되는 n개 노드에 속하지는 않지만 연결할 수 있는 노드에 기록 (다른 파티션에 저장한다는 뜻)

느슨한 정족수 

  • 암시된 핸드오프 : 장애 상황이 해결되면 일시적으로 수용한 모든 쓰기를 해당 노드로 전송.
  • 쓰기 가용성을 높이는데 유용. 
  • w + r > n 인 경우에도 키의 최신 값을 읽는다고 보장 x ( ∵ 최신 값이 일시적으로 n 이외의 일부 노드에 기록될 수 있음)

다중 데이터센터 운영

리더리스 모델에 다중 데이터센터 지원을 구현.

n개의 복제 서버 수에는 모든 데이터센터이 노드가 포함되고 각 데이터센터마다 n개의 복제 서버 중 몇 개를 보유할지를 지정

각 쓰기는 데이터센터 상관없이 모든 복제 서버에 전송. 하지만 클라이언트는 로컬 데이터센터 안에서 정족수 노드의 확인 응답을 기다리기 때문에 데이터센터 간 연결의 지연과 중단에 영향을 받지 않음. 

b. 동시 쓰기 금지

엄격한 정족수를 사용해도 충돌이 발생할 수 있다. 이벤트가 다른 노드에 다른 순서로 도착할 수도 있다. 

최종 쓰기 승리 (동시 쓰기 버리기)

각 복제본이 가진 예전 값은 버리고 최신값으로 덮어쓰는 방법.

LWW

  • 동일한 키에 여러 번의 동시 쓰기가 있다면 클라이언트에는 전부 성공으로 보고됨. → 최종적 수렴 달성이 목표지만 지속성을 희생
  • 손실 데이터를 허용하지 않는다면 LWW는 적합하지 않음. 
  • 데이터베이스를 안전하게 사용하는 유일한 방법 : 키를 한번만 쓰고 이후에는 불변 값으로 다룸. (같은 키를 동시에 갱신하는 상황을 방지)

"이전 발생" 관계와 동시성

이전 발생(happens-before) : 작업2가 작업1에 대해서 알거나 1에 의존적이거나 어떤 방식으로든 1을 기반으로 한다면 작업1은 작업2의 이전 발생.

동시 작업 : 어느 작업이 다른 작업에 대해 알지 못하는 경우 (서로 무관한 경우)

두 개 작업에 대한 가능성

  • B 이전에 A가 발생
  • B가 A 이전에 발생
  • A와 B가 동시에 발생

이전 발생 관계 파악하기

두 작업이 동시에 발생했는지, 하나가 이전에 발생했는지 여부를 결정하는 알고리즘. 

단일 복제본 데이터베이스 동작

  • 서버가 모든 키에 대한 버전 번호를 유지. 키를 기록할 때마다 버전 번호를 증가. 기록한 값은 새로운 버전 번호를 가지고 저장.
  • 클라이언트가 키를 읽을 때는 최신 버전뿐만 아니라 덮어쓰지 않은 모든 값을 반환. 클라이언트는 쓰기 전에 키를 읽어야 함. 
  • 클라이언트가 키를 기록할 때는 이전 읽기의 버전 번호를 포함하고 이전 읽기에서 받은 모든 값을 함께 합쳐야 함. 
  • 서버가 특정 버전 번호를 가진 쓰기를 받을 대 해당 버전 이하 모든 값을 덮어쓸 수 있음. 하지만 더 높은 버전 번호의 모든 값은 유지해야 함. (유입된 쓰기와 동시에 발생했기 때문)

동시에 쓴 값 병함

이 알고리즘은 어떤 데이터도 자동으로 삭제되지 않음을 보장하지만 클라이언트의 추가적인 작업이 필요

여러 작업이 동시에 발생하면 클라이언트는 동시에 쓴 값을 합쳐 정리해야 함. → 형제 값

형제 값 병합

  • LWW : 데이터 손실 발생 가능
  • 합집합을 취함 : 추가는 가능하지만 제거도 하려면 올바른 결과가 아닐 수 잇음. 
    • 제거 시 툼스톤 표시
  • → CRDT 데이터 구조군 사용

형제 병합은 복잡하고 오류가 발생하기 쉬움. 

버전 벡터

리더리스 다중 복제본 동작

  • 키당 버전 번호 + 복제본당 버전 번호도 사용
  • 각 복제본은 쓰기 처리 시 자체 버전 번호를 증가시키고 각기 다른 복제본의 버전 번호도 계속 추적
  • 버전 벡터는 값을 읽을 때 데이터베이스 복제본에서 클라이언트로 보낸다. 
  • 값이 기록될 때 데이터베이스로 다시 전송한다. (리악 - 인과성 컨텍스트라는 문자열로 부호화) 

버전 벡터 : 모든 복제본의 버전 번호 모음. 변형 사례 : 도티드 버전 벡터

버전 벡터를 사용하면 덮어쓰기와 동시 쓰기를 구분할 수 있다. 

 

댓글