05장. 복제
포스트
취소
Preview Image

05장. 복제

복제를 위한 데이터 변경 처리에 대한 어려움을 알아보자

잘못될 수 있는 것과 잘못될 수 없는 것 사이의 주된 차이점은 잘못될 수 없는 것이 잘못됐을 때는 잘못을 파악하거나 고치는 것은 거의 불가능하다는 점이다. - 더글라스 애덤스, 대체로 무해함(1992)

0. 복제란?

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

복제가 필요한 여러 이유들

  • 지리적으로 사용자와 가깝게 데이터를 유지해 지연 시간을 줄
  • 시스템에 일부 장애가 발생하더라도 지속적으로 동작할 수 있게 해 가용성을 높임
  • 읽기 질의에 제공하는 장비의 수를 확장해 읽기 처리량을 늘림

복제 알고리즘

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

복제 트레이드오프

  • 동기 식 vs 비동기식
  • 잘못된 복제본을 어떻게 처리할지
  • 데이터베이스의 설정 옵션으로 조절 가능
  • 세부 사항은 데이터베이스마다 다양하지만 일반적인 원리는 유사함

1. 리더와 팔로워

  • 데이터베이스 복사본을 저장하는 각 노드: 복제 서버(replica)
  • 데이터베이스의 모든 쓰기는 모든 복제 서버에서 처리되어야 함
  • 리더 기반 복제
    • 리더 = 마스터 = 프라이머리
    • 팔로워 = 슬레이브 = 세컨더리 = 읽기 복제 서버 = 핫 대기
  • 리더가 새로운 데이터를 기록할 때마다 데이터 변경을 복제 로그나 변경 스트림의 일부로 팔로워에게 전송
  • 팔로워는 리더가 처리한 것과 동일한 순서로 몯ㄴ 쓰기를 적용하여 복사본을 갱신
  • 클라이언트 입장
    • 읽기: 리더 or 팔로워
    • 쓰기: 리더만 가능

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

  • 동기식
    • 리더는 팔로워가 쓰기를 수신했는지 확인해줄 때까지 기다림
    • 장점
      • 팔로워가 일관성 있게 최신 데이터 복사본을 가지는 것을 보장함
      • 갑자기 리더가 작동하지 않아도 데이터가 팔로워에서 계속 사용할 수 있음을 확신할 수 있음
    • 단점
      • 팔로워가 응답하지 않는다면 쓰기가 처리될 수 없음
      • 리더는 모든 쓰기를 차단하고 동기 복제 서버가 다시 사용할 수 있을 때까지 기다려야함
  • 비동기식
    • 리더는 메시지를 전송하지만 팔로워의 응답을 기다리지 않음
    • 보통 리더 기반 복제는 완전히 비동기식으로 구성함
    • 리더가 잘못되고 복구할 수 없으면 팔로워에 아직 복제되지 않은 모든 쓰기는 유실됨
      • 쓰기가 클라이언트에게 확인된 경우에도 지속성을 보장하지 않는다는 의미
    • 모든 팔로워가 잘못되더라도 리더가 쓰기 처리를 계속할 수 있음
  • 반동기식 (semi-synchronous)
    • 동기식의 단점 때문에 모든 팔로워를 동기식으로 구성하는 것은 비현실적
    • 하나의 팔로워만 동기식으로 구성하고 나머지는 비동기식으로 구성
    • 동기식 팔로워가 사용할 수 없게 되거나 느려지면 비동기식 팔로워 중 하나가 동기식이 됨
    • 적어도 두 노드에 데이터의 최신 복사본이 있는 것을 보장

1.2. 새로운 팔로워 설정

  • 복제 서버 수를 늘리거나 장애 노드의 대체를 위해서는 새로운 팔로워 설정이 필요함
  • 무중단으로 정확한 복제본을 가지고있는 새로운 팔로워를 추가하는 방법
    1. 전체 데이터베이스를 잠그지 않고 리더의 데이터베이스 스냅샷을 일정 시점에 가져옴. 대부분의 데이터베이스는 백업이 필요하기 때문에 이 기능을 갖추고 있음
    2. 스냅샷을 새로운 팔로워 노드에 복사
    3. 팔로워는 리더에 연결해 스냅샷 이후 발생한 모든 데이터 변경을 요청. 스냅샷이 리더의 복제 로그의 정확한 위치와 연관되어야 함 (위치: 로그 일련번호, 이진로그 좌표)
    4. 팔로워가 스냅샷 이후 데이터 변경의 미처리분(backlog)을 모두 처리했을 때 따라잡았다고 알려주고 이제부터 리더에 발생하는 데이터 변화를 처리할 수 있음

1.3. 노드 중단 처리

  • 모든 노드는 장애 또는 계획된 유지보수(보안 패치) 등으로 중단될 수 있음
  • 중단시간 없이 개별 노드를 재부팅할 수 있다는 점은 운영과 유지보수에 큰 장점
  • 개별 노드의 장애에도 전체 시스템이 동작하게끔 유지하고 노드 중단의 영향을 최소화하는 것이 목표
  • 리더 기반 복제에서 고가용성을 달성하는 방법은?

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

  • 각 팔로워는 리더로부터 수신한 데이터 변경 로그를 로컬 디스크에 보관함
  • 팔로워가 죽어 재시작을 하거나 리더와 팔로워 사이의 네트워크가 일시적으로 중단된다면 팔로워는 매우 쉽게 복구할 수 있음
    1. 보관된 로그에서 결함이 발생하기 전에 처리한 마지막 트랜잭션 확인
    2. 팔로워는 리더에 연결해 팔로워 연결이 끊어진 동안 발생한 데이터 변경을 모두 요청할 수 있음
    3. 이 변경이 다 적용되면 리더를 다 따라잡게 되고 이전과 같이 데이터 변경의 스트림을 계속 받을 수 있음

1.3.2. 리더 장애: 장애 복구

  • 이건 까다로움
  • 팔로워 중 하나를 새로운 리더로 승격해야 함
  • 클라이언트는 새로운 리더로 쓰기를 전송하기 위해 재설정이 필요
  • 다른 팔로워는 새로운 리더로부터 데이터 변경을 소비하기 시작해야함
  • 이 과정을 장애 복구라고 함
  • 자동 장애 복구 과정
    1. 리더가 장애인지 판단
      • 고장, 정전, 네트워크 문제 등 다양한 문제
      • 확실한 방법이 없기 때문에 타임아웃 시간을 설정하여 초과하면 노드가 죽은 것으로 간주함
    2. 새로운 리더 선택
      • 선출 과정을 통해 이뤄지거나 이전에 선출된 제어 노드에 의해 새로운 노드 임명
      • 가장 적합한 후보는 보통 이전 리더의 최신 데이터 변경사항을 가진 복제 서버
    3. 새로운 리더 사용을 위해 시스템 재설정
      • 클라이언트는 이제 새로운 쓰기 요청을 새로운 리더에게 보내야 함
      • 이전 리더가 돌아오면 시스템은 이전 리더가 팔로워가 되고 새로운 리더를 인식할 수 있게끔 해야함

장애 복구 과정은 잘못될 수 있는 것 투성이다.

  • 비동기식 복제를 사용하면 새로운 리더는 실패하기 전에 이전 리더의 쓰기 일부를 수신하지 못할 수 있음
    • 새로운 리더가 충돌하는 쓰기를 수신했을지도 모름
    • 가장 일반적인 해결책은 이전 리더의 복제되지 않은 쓰기를 단순히 폐기하는 방법
  • 쓰기를 폐기하는 방법은 데이터베이스 외부의 다른 저장소 시스템이 데이터베이스 내용에 맞춰 조정돼야 한다면 특히 위험함
    • 깃허브에서 발생한 유효하지 않은 MySQL 팔로워가 리더로 승격한 사례가 있음
    • 데이터베이스는 새로운 Row의 기본키를 할당하기 위해 auto increment를 사용했지만 새로운 리더의 카운터는 이전 리더보다 뒤처져 있었기 때문에 이전 리더가 예전에 할당한 기본키를 재사용함
    • 이 기본키는 레디스 저장에도 사용했디 때문에 MySQL, 레디스 간 불일치를 일으킴
    • 일부 개인 데이터가 잘못된 사용자에게 공개됨
    • (GitHub availability this week)
  • 특정 결함 시나리오에서 두 노드가 모두 자신이 리더라고 믿을 수 있음 (스플릿 브레인)
    • 두 리더가 쓰기를 받으면서 충돌을 해소하는 과정을 거치지 않으면 데이터가 유실되거나 오염됨
    • 일부 시스템에는 안전 장치로 두 리더가 감지되면 한 노드를 종료하는 메커니즘이 있지만 이 메커니즘을 주의깊게 설계하지 않으면 두 개의 노드가 모두 종료될 수 있음
  • 리더가 분명히 죽었다고 판단 가능한 적절한 타임아웃은?
    • 긴 타임아웃은 리더가 작동하지 않을 때 부터 복구까지 오랜 시간이 소요된다는 의미
    • 너무 짧으면 불필요한 장애 복구가 있을 수 있음
  • 이런 이유로 일부 운영팀은 소프트웨어가 자동 장애 복구를 지원하더라도 수동으로 장애 복구를 수행하는 방식을 선호함

1.4. 복제 로그 구현

1.4.1. 구문 기반 복제

  • 리더는 모둔 스기 요청(구문: statement)을 기록하고 쓰기를 실행한 다음 구문 로그를 팔로워에게 전송
  • 각 팔로워는 클라이언트에서 직접 받은 것 처럼 SQL 구문을 파싱하고 실행
  • 복제가 깨질 수 있는 사례
    • NOW(), RAND() 등 비결정적 함수를 호출하는 구문
    • 정확히 같은 순서로 실행되어야하는 구문
      • 자동증가 컬럼을 사용하는 구문
      • 데이터베이스 데이터에 의존하는 구문 (update ... where <condtion>)
    • 부수 효과를 가진 구문 (트리거, 스토어드 프로시저, 사용자 정의 함수)
      • 부수 효과가 완벽하게 결정적이지 않으면 각 복제 서버에서 다른 부수 효과가 발생할 수 있음
  • 대안 해결책
    • 리더는 구문을 기록할 때 모든 비결정적 함수 호출을 고정 값을 반환하게끔 대체할 수 있음
    • 그러면 팔로워 모두 같은 값을 얻을 수 있음
    • 하지만 엣지 케이스가 존재하여 다른 복제 방법을 선호

1.4.2. 쓰기 전 로그 배송

  • 일반적으로 모든 쓰기는 로그에 기록
    • 로그 그조화 저장소 엔진(SS테이블, LSM트리)
      • 로그 자체가 저장소의 주요 부분
      • 로그 세그먼트는 작게 유지되고 백그라운드로 가비지 컬렉션을 함
    • B 트리
      • 개별 디스크 블록에 덮어 씀
      • 모든 변경은 쓰기전 로그(Write-ahead log: WAL)에 쓰기 때문에 고장 이후 일관성 있는 상태로 색인을 복원
  • 두 경우 모두 데이터베이스의 모든 쓰기를 포함하는 추가 전용 바이트열
  • 완전히 동일한 로그를 사용해 다른 노드에서 복제 서버를 구축할 수 있음
  • 리더는 디스크에 로그를 기록하는 일 외에도 팔로워에게 네트워크로 로그를 전송하기도 함
  • 팔로워가 이 로그를 처리하면 리더에서 있는 것과 정확히 동일한 데이터 구조의 복제본이 만들어짐
    • postgresql, oracle 등에서 사용
  • 가장 큰 단점은 로그가 제일 저수준의 데이터를 기술한다는 점
    • WAL은 어떤 디스크 블록에서 어떤 바이트를 변경했는지와 같은 상세 정보를 포함
    • 데이터베이스가 저장소 형식을 다른 버전으로 변경한다면 대개 리더와 팔로워의 데이터베이스 소프트웨어 버전을 다르게 실행할 수 없음
  • 팔로워가 리더보다 새로운 소프트웨어 버전을 사용하게끔 복제 프로토콜이 허용된다면 팔로워를 먼저 업그레이드함으로써 중단시간 없이 데이터베이스 소프트웨어 업그레이드 수행이 가능
  • 업그레이드된 노드 중 하나를 새로운 리더로 선정하기 위해 장애 복구를 수행할 수 있음
  • 복제 프로토콜이 버전의 불일치를 허용하지 않는다면 업그레이드할 때 중단 시간이 필요함

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

  • 복제 로그를 저장소 엔진 내부와 분리하기 위한 대안 하나는 복제와 저장소 엔진을 위해 다른 로그 형식을 사용하는 것
  • 이 같은 종류의 복제 로그를 저장소 엔진의 데이터 표현과 구별하기 위해 논리적 로그라고 부름
  • 관계형 데이터베이스용 노리적 로그는 대개 로우 단위로 데이터베이스 테이블에 쓰기를 기술한 레코드열
    • 삽입된 로우의 로그는 모든 컬럼의 새로운 값을 포함
    • 삭제된 로우의 로그는 로우를 고유하게 식별하는데 필요한 정보를 포함
      • 일반적으로 기본키, 없다면 모든 컬럼의 예전 값 로깅)
    • 갱신된 로우의 로그는 로우를 고유하게 식별하는데 필요한 정보와 모든 컬럼의 새로운 값을 포함
  • 여러 로우를 수정하는 트랜잭션은 여러 로그 레코드를 생성한 다음 트랜잭션이 커밋됐음을 레코드에 표시
  • MySQL 이진 로그는 이 접근 방식을 사용
  • 논리적 로그를 저장소 엔진 내부와 부리했기 때문에 하위 호환성르더 쉽게 유지할 수 있음
  • 팔로워에서 다른 버전의 데이터베이스 소프트웨어나 다른 저장소 엔진을 실행할 수 있음
  • 논리적 로그 형식은 외부 애플리케이션이 파싱하기 더 쉬움
    • 오프라인 분석, 사용자 정의 색인, 캐시 구축을 위해 데이터 웨어하우스 같은 외부 시스템에 데이터베이스의 내용을 전송하고자 할 때 유용
    • 변경 데이터 캡쳐 (change data capture: CDC)

1.4.4. 트리거 기반 복제

  • 애플리케이션 코드로 복제하는 방법
  • 트리거는 사용자 정의 애플리케이션 코드를 등록할 수 있게 함
  • 데이터베이스 시스템에서 데이터가 변경되면 자동으로 실행하는 애플리케이션 코드
  • 트리거는 데이터 변경을 분리된 테이블에 로깅할 수 있는 기회를 가짐
  • 이 테이블로부터 데이터 변경을 외부 프로세스가 읽을 수 있음
  • 외부 프로세스는 필요한 애플리케이션 로직을 적용해 다른 시스템으로 데이터 변경을 복제
  • 오라클용 데이터버스, 포스트그레스큐엘용 부카르도
  • 다른 복제 방식보다 오버헤드가 많음
  • 데이터베이스 내장 복제보다 버그나 제한 사항이 더 많이 발생
  • 그럼에도 유연성 때문에 매우 유용

2. 복제 지연 문제

  • 복제는 확장성과 지연 시간에 의한 문제가 발생할 수 있음
  • 리더 기반 복제는 모든 쓰기가 단일 노드를 거쳐야하지만 읽기 전용 질의는 어떤 복제 서버에서도 가능
  • 대부분이 읽기 요청이고 쓰기가 아주 작은 비율로 구성된 작업 부하면 많은 팔로워를 만들어 팔로워 간 읽기 요청을 분산하는 매력적인 옵션이 있음
  • 리더 부하를 없애고 근처 복제 서버에서 읽기 요청을 처리할 수 있게 해줌
  • 간단히 팔로워를 추가하여 읽기 전용 요청 처리 가용성을 늘릴 수 있음
  • 읽기 확장(read-scaling) 아키텍처
  • 비동기식 복제에서만 동작
    • 동기식으로 모든 팔로워에 복제를 시도한다면 단일 노드 장애나 네트워크 중단으로 전체 시스템의 쓰기 불가능
    • 노드가 많아지면 다운될 가능성도 커져 완전한 동기식 설정은 매우 불안
  • 비동기 팔로워가 뒤처지면 과거 데이터를 볼 수 있음
  • 데이터베이스에서 쓰기를 멈추고 기다리면 팔로워가 따라잡아 리더와 일치되는 효과: 최종적 일관성

2.1. 자신이 쓴 내용 읽기

  • 새로운 데이터를 쓸 때는 리더에게 전송하지만 데이터를 볼 때는 팔로워에서 읽을 수 있는데 이 때 쓰기를 수행한 직후 데이터를 본다면 복제 서버에 반영되어 있지 않을 수 있는 문제
  • 사용자 입장에서 데이터가 유실된 것처럼 보일 수 있음
  • 쓰기 후 일관성을 보장해야함 (자신의 쓰기 읽기 일관성)
    • 사용자가 수정한 내용을 읽을 때는 리더에서 읽고 나머지는 팔로워에서 읽음
    • 리더에서 읽을지 말지 결정하기 위한 다른 기준을 사용
      • 마지막 갱신 시각을 찾아서 마지막 갱신 후 1분 동안은 리더에서 모든 읽기 수행
      • 팔로워에서 지연을 모니터링해 리더보다 1분 이상 늦은 모든 팔로워에 대한 질의 금지
    • 클라이언트에서 가장 최근 쓰기 타임스탬프를 기억할 수 있음
      • 시스템은 사용자 읽기를 위한 복제 서버가 최소한 해당 타임스탬프까지 갱신을 반영하게 할 수 있음
    • 복제 서버가 여러 데이터센터에 분산되었다면 복잡도가 증가함
      • 리더가 제공해야하는 모든 요청은 리더가 포함된 데이터센터로 라우팅 되어야 함
  • 동일한 사용자가 여러 디바이스로 접근할 때 다른 문제가 발생
  • 디바이스 간 쓰기 후 일관성이 보장이 필요함
    • 사용자의 마지막 갱신 타임스탬프를 기억해야하는 접근 방식은 더욱 어려움
      • 다른 디바이스에서 발생한 갱신은 알 수 없으므로 중앙집중식으로 메타데이터를 관리해야함
    • 복제 서버가 여러 데이터센터로 분산되어 있다면 다른 디바이스의 연결이 동일한 데이터센터로 라우팅된다는 보장이 없음
    • 리더에서 읽어야 할 필요가 있는 접근법이라면 먼저 사용자 디바이스의 요청을 동일한 데이터센터로 라우팅해야함

2.2. 단조 읽기

  • 비동기식 팔로워에서 읽을 때 시간이 거꾸로 흐르는 현상
  • 팔로워간 지연 시간 차이가 크고 질의 때마다 다른 팔로워에 데이터를 바라볼 때 발생할 수 있음
  • 각 사용자의 읽기를 동일한 복제 서버에서 수행되게 하여 단조 읽기를 달성할 수 있음
  • 단조 읽기는 강한 일관성보다는 덜한 보장이지만 최종적 일관성보다는 더 강한 보장임

2.3. 일관된 순서로 읽기

  • 데이터 A -> B 간의 순서나 인과가 있을 때
    • 데이터 A: 지연이 긴 팔로워에게 전달
    • 데이터 B: 지연이 거의 없는 팔로워에게 전달
  • 위와 같은 상황이면 데이터 B가 데이터 A보다 먼저 읽게 될 수 있음
  • 이 현상을 방지하기 위해 일관된 순서로 읽기가 보장되어야 함
  • 데이터베이스가 항상 같은 순서로 쓰기를 적용한다면 읽기는 항상 일관된 순서를 보기 때문에 이런 현상은 일어지는 않지만 분산 데이터베이스에서 서로 다른 파티션은 독립적으로 동작하므로 쓰기의 전역 순서가 없음
  • 인과성이 있는 쓰기는 동일한 파티션에 기록되게끔 하는 방법
  • 일부 애플리케이션에서는 효율적이지 않음

2.4. 복제 지연을 위한 해결책

  • 최종된 일관성 시스템이라면 복제가 비동기식으로 동작하지만 동기식으로 동작하는 척하는 것이 문제 해결 방안 - 쓰기 후 읽기
  • 애플리케이션에서 해결하는 방식이 있지만 코드로 다루기에는 너무 복잡하여 버그 발생 가능성이 높음
  • 올바른 작업 수행을 위해 데이터베이스를 신뢰할 수 있는 이유 - 트랜잭션
  • 단일 노드 트랜재션은 존재했지만 분산 데이터베이스에서는 트랜잭션 성능과 가용성이 너무 비싸기 때문에 최종적 일관성을 사용해야한다는 주장이 존재함

3. 다중 리더 복제

  • 리더가 하나만 존재하는 리더 기반 복제의 경우 리더가 죽으면 데이터베이스 쓰기가 불가함
  • 쓰기 허용 노드를 하나 이상 두는 것으로 확장하는 것
  • 다중 리더 설정 (마스터 마스터, 액티브 액티브)
  • 각 리더는 동시에 다른 리더의 팔로워 역할도 함

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

  • 다중 데이터센터의 경우 각 데이터센터에 리더가 하나씩 있고 각 리더별 팔로워를 둠
  • 데이터센터 간 복제는 리더가 다른 데이터센터의 리더에게 변경 사항을 복제함
  • 데이터베이스에서 기본적으로 다중 리더 설정 제공하거나 외부 구현 도구를 사용하기도 함
  • 동일한 데이터를 다른 데이터센터에서 동시에 변경하여 쓰기 충돌이 발생할 수 있기 때문에 반드시 해소해야함

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

  • 인터넷이 끊어진 동안 애플리케이션이 동작해야하는 경우
  • 디바이스에 리더처럼 동작하는 로컬 데이터베이스가 존재
  • 디바이스에서 로컬 데이터베이스로 쓰기 요청
  • 디바이스에서 데이터센터의 각 리더에게 비동기 쓰기 복제 요청
  • 디바이스 상에서 복제 서버 간 다중 리더 복제를 비동기 방식으로 수행하는 프로세스 (동기화)

3.1.2. 협업 편집

  • 구글 독스
  • 편집 충돌 없음을 보장하려면 애플리케이션은 사용자가 편집하기 전에 문서의 잠금을 얻어야 함
    • 다른 사용자가 같은 문서를 편집하려면 첫번재 사용자의 변경이 커밋되고 잠금이 해제될 때 까지 기다려야함
    • 리더에서 트랜잭션을 사용하는 단일 리더 복제와 동일
  • 더 빠른 협업을 위해 변경 단위를 매우 작게하여 잠금을 피할 수 있음
    • 여러 사용자가 동시 편집이 가능하지만 충돌 해소가 필요한 경우를 포함해 다중 리더 복제에서 발생하는 모든 문제를 야기함

3.2. 쓰기 충돌 다루기

  • 다중 리더 복제에서 제일 큰 문제는 쓰기 충돌 발생

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

  • 단일 리더 데이터베이스에서 첫 번재 쓰기가 완료될 때까지 두 번째 쓰기를 차단하거나 두 번째 쓰기 트랜잭션을 중단하여 사용자가 재시도하게 할 수 있음
  • 다중 리더 설정에서는 두 쓰기 모두 성공하여 충돌은 이후 특정 시점에 비동기로 감지하게 되는데 이 때 사용자에게 해소 요청을 하는 것은 너무 늦음
  • 동기식 충돌 감지
    • 쓰기 성공 응답을 모든 복제 서버에 복제하기를 기다림
    • 다중 리더 복제의 주요 장점을 잃기 때문에 사실상 불가능

3.2.2. 충돌 회피

  • 가장 간단한 충돌 처리 방법
  • 특정 레코드의 모든 쓰기가 동일한 리더를 거치도록 애플리케이션이 보장하는 방법
  • 많은 다중 리더 복제 구현 사례에서 자주 권장되는 방법
  • A 데이터센터가 고장나서 트래픽을 B 데이터센터로 다시 라우팅을 하거나 사용자가 다른 지역으로 이동해 현재는 다른 데이터센터가 가깝다면 리더를 변경하고 싶을 수 있음
    • 충돌 회피 실패
    • 다른 리더에서 동시 기록 가능성을 대처해야함

3.2.3. 일관된 상태 수렴

  • 단일 리더 데이터베이스는 순차 쓰기 적용으로 마지막 갱신 값이 최종 값
  • 다중 리더 설정은 쓰기 순서가 존재하지 않아 최종 값이 명확하지 않음
  • 수렴(convergent) 방식으로 충돌 해소
  • 모든 변경이 복제되 모든 복제 서버에 동일한 최종 값이 전달되게 해야 한다는 의미
  • 각 쓰기 고유 ID를 부여하고 가장 높은 ID를 가진 쓰기를 고르고 다른 쓰기는 버림
    • 타임스탬프를 사용하는 경우 최종 쓰기 승리
    • 대중적이지만 데이터 유실 위험 존재
  • 각 복제 서버에 고유 ID를 부여하고 가장 높은 ID의 복제 서버에서 생긴 쓰기가 우선 적용
    • 데이터 유실 가능성 존재
  • 어떻게든 값을 병합
    • e.g. 사전 순으로 정렬한 후 연결
  • 명시적 데이터 구조에 충돌을 기록해 모든 정보를 보존하고 애플리케이션에서 코드로 충돌 해소

3.2.4. 사용자 정의 충돌 해소 로직

  • 애플리케이션에 따라 적합한 충돌 해소 방법이 다르기 때문에 대부분 다중 리더 복제 도구는 애플리케이션 코드를 사용해 충돌 해소 로직 작성
  • 쓰기 수행 중
    • 충돌을 감지하면 충돌 핸들러 호출
    • 사용자에게 충돌 내용을 표시하지 않고 백그라운드 프로세스에서 빠르게 실행
  • 읽기 수행 중
    • 충돌을 감지하면 모든 충돌 쓰기를 저장
    • 읽을 때 여러 버전의 데이터가 반환되어 애플리케이션에서 사용자에게 보여주거나 자동 해소

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

  • 복제 토폴로지는 쓰기를 한 노드에서 다른 노드로 전달하는 통신 경로를 설명
  • 두 리더가 있다면 가능한 토폴로지는 하나뿐이지만 리더가 둘 이상이라면 다양한 토폴로지가 가능

  • 전체 연결 토폴로지
    • 가장 일반적인 토폴로지
    • 모든 리더가 각자의 쓰기를 다른 모든 리더에게 전송
  • 원형 토폴로지
    • MySQL 기본 제공
    • 각 노드가 하나의 노드로 부터 쓰기를 받고 쓴 이후에 다른 한 노드에 전달
  • 별 모양 토폴로지
    • 지정된 루트 노드가 다른 모든 노드에 쓰기 전달
    • 트리로 일반화 가능
  • 원형 / 별 문제점
    • 쓰기는 모든 복제 서버에 도달하기 전에 여러 노드를 거쳐야함
      • 노드들은 다른 노드로부터 받은 데이터 변경 사항을 전달해야 함
      • 무한 복제 루프를 방지하기 위해 각 노드에는 고유 식별자가 있고 복제 로그에서 각 쓰기는 거치는 모든 노드의 식별자가 태깅
      • 데이터를 받았을 때 자신의 식별자 태깅 여부로 변경 사항 무시 가능
    • 한 노드에 장애 발생 시 다른 노드 간 복제 메시지 흐름에 방해를 줌
      • 장애 노드가 복구될 때까지 통신 불가
      • 토플로지는 장애 노드를 회피하게끔 재설정 가능
      • 이런 재설정은 대부분 수동으로 수행해야함
      • 메시지가 여러 경로를 따라 이동할 수 있으면 단일 장애점을 피할 수 있기 때문에 조금 더 빽빽하게 연결한 토폴로지의 내결함성이 훨씬 더 좋음
  • 전체 연결 문제점
    • 일부 네트워크가 다른 연결보다 빠르다면 메시지 간의 추월 현상이 있을 수 있음
    • 일관된 순서로 읽기에서 본 인과성의 문제
    • 모든 쓰기에 타임스탬프를 추가하는 방식으로는 충분하지 않음
    • 버전 벡터 기법으로 쓰기 이벤트를 올바르게 정렬
    • 많은 다중 리더 복제 시스템에서 충돌 방지 기법은 제대로 구현되지 않음
  • 다중 리더 복제 시스템을 사용할 때는 이런 문제를 인지하고 문서를 주의 깊게 읽은 다음 데이터베이스를 철저하게 테스트해 실제로 믿을 만한 보장을 제공하는지 확인하는 편이 좋음

4. 리더 없는 복제

  • 아마존의 다이나모 시스템
  • 다이나모에서 영감을 받은 리악, 카산드라, 볼드모트 - 다이나모 스타일
  • 직접 복제 서버에 쓰기를 하거나 코디네이터 노드가 이를 대신 수행하기도 함
  • 리더와 다르게 코디네이터 노드는 특정 순서로 쓰기를 수행하지 않음
  • 설계에서 이런 차이는 데이터베이스 사용 방식에 중대한 영향을 미침

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

  • 복제 서버 중 하나를 사용할 수 없을 때 장애 복구가 필요하지 않음
  • 클라이언트가 복제 서버에 병렬로 쓰기를 전송하면 장애 서버는 쓰기를 못하지만 쓰기가 성공한 것으로 간주
  • 장애 노드가 복구되면 데이터가 누락되었기 때문에 이 노드에서 데이터를 읽으면 오래된(outdated) 값을 얻을 수 있음
  • 읽기 요청을 병렬로 여러 노드에 전송하고 버전으로 최신 내용을 결정

4.1.1. 읽기 복구와 안티 엔트로피

  • 복구 게획은 최종적으로 모든 데이터가 모든 복제 서버에 복사된 것을 보장해야함
  • 장애 복구된 노드가 누락된 쓰기를 따라잡는 방법
  • 읽기 복구
    • 클라이언트가 여러 노드에서 병렬 읽기 수행 시 오래된 응답 감지 가능
    • 클라이언트는 특정 서버의 값이 오래된 값이라는 사실을 알고 해당 복제 서버에 새로운 값을 다시 기록
    • 값을 자주 읽는 상황에 적합
  • 안티 엔트로피 처리
    • 백그라운드 프로세스를 두고 복제 서버 간 데이터 차이를 지속적으로 찾아 누락된 데이터를 복사
    • 리더 기반 복제의 복제 로그와 달리 특성 순서로 쓰기를 복사하기 때문에 상당한 지연이 있을 수 있음

4.1.2. 읽기와 쓰기를 위한 정족수

  • n개의 복제 노드 개수
  • w개의 노드에서 쓰기 성공
  • r개의 노드에서 읽기 요청
  • 읽을 때 최신 값을 얻을 것으로 기대, 최소한 r개의 노드 중 하나에서 최신 값을 읽을 수 있음
    • w + r > n
  • 다이나모 스타일에서 커스텀 가능
    • n을 보통 홀수로
    • w = r = (n + 1) / 2
    • 쓰기가 적고 읽기가 많은 작업 부하는 w = n, r = 1
      • 읽기가 빠르지만 노드 고장 시 모든 쓰기 실패

4.2. 정족수 일관성의 한계

  • n개의 복제 서버가 있고 w + r > n이 되게끔 w, r을 선택한다면 일반적으로 모든 읽기는 최신 값을 반환할 것을 기대함 - 쓰기 노드와 읽기 노드 셋이 겹치기 때문
  • 보통 r, w 값으로는 노드의 과반수를 선택하여 n / 2 노드 장애까지 허용되도 w + r / n이 보장되기 때문
  • 정족수가 다수일 필요는 없고 읽기와 쓰기 동작에서 사용하는 노드 셋 중 적어도 하나의 노드만 겹치면 됨
  • 다른 정족수 할당이 가능하기 때문에 분산 알고리즘 설계에서 어느정도 유연성을 허용
  • w, r을 더 작은 수로 설정 w + r <= n
  • 읽기와 쓰기를 계속 n개의 노드에 전송하지만 성공 응답 수는 더 적음
  • w, r이 작을수록 오래된 값을 읽을 확률이 높음
  • w + r > n 오래된 값 반환 엣지 케이스
    • 느슨한 정족수 사용하여 쓰기와 읽기가 다른 노드에서 수행되는 경우 w, r 노드가 겹치지 않을 수 있음
    • 두개의 쓰기가 동시에 발생하면 우선순위가 분명하지 않음
      • 안전한 해결책은 동시 쓰기를 합치는 방법밖에 없음
      • 타임스탬프 기반으로 결정되면 clock skew로 인해 유실될 수 있음
    • 쓰기, 읽기 동시 발생 시 쓰기는 일부 복제 서버에만 반영될 수 있음
      • 읽기가 예전 값 or 최신 값 반환하였는지 여부가 분명하지 않음
    • 쓰기가 일부 복제 서버에서 실패하여 전체에서 성공한 서버가 w 복제서버보다 적다면 롤백하지 않음
    • 새 값을 전달하는 노드가 고장나면 예전 값을 가진 다른 복제 서버에서 해당 데이터가 복원되고 새로운 값을 저장한 복제 서버 수가 w보다 낮아져 정족수 조건이 깨짐
    • 모든 과정이 올바르게 동작해도 시점 문제로 엣지 발생 가능성 존재

4.2.1. 최신성 모니터링

  • 리더 없는 복제 시스템에서는 쓰기가 적용된 순서를 고정할 수 없어 모니터링이 어려움
  • 데이터베이스가 읽기 복구만 사용한다면 자주 읽히지 않는 값이 얼마나 오래된 것인지 알 수 없음
  • 복제 서버의 오래됨(staleness)을 측정하고 매개변수에 따라 오래된 값을 읽는 비율을 예측하는 연구 있음
  • 최종적 일관성은 의도적으로 모호한 보장이지만 운용성을 위해서는 최종적을 정량화 할 수 있어야 함

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

  • 적절히 설정된 정족수가 있는 데이터베이스는 장애복구 없이 개별 노드 장애 용인
  • 요청은 w, r개의 응답만 기다리고 n개 모든 노드의 응답을 기다릴 필요가 없어서 느려지는 것도 허용
  • 높은 가용성과 낮은 지연 시간이 필요
  • 가끔 오래된 값 읽기를 허용하는 사용 사례에는 리더 없는 복제 기능 매력적임

  • 하지만 정족수의 내결함성이 없기 때문에 생기는 트레이드 오프
    • w나 r 노드 정족수를 만족하지 않는 모든 요청에 오류를 반환하는 편이 좋을까?
    • 아니면 일단 쓰기를 받아들이고 값이 보통 저장되는 n개 노드에 속하지는 않지만 연결할 수 있는 노드에 기록할까? - 느슨한 정족수
  • 네트워크 장애 상황이 해제되면 한 노드가 다른 노드를 위해 일시적으로 수용한 모든 쓰기를 해당 홈 노드로 전송 - 암시된 핸드오프 (내 집 열쇠를 다시 찾으면 이웃이 정중하게 소파에서 일어나 집으로 돌아가라고 요청)

  • 느슨한 정족수는 쓰기 가용성을 높이는데 유용함
    • w개 노드를 사용하는 동안 쓰기를 받아들일 수 있음
    • w + r > n 인 경우에도 최신 값을 읽는다고 보장할 수 없음
    • 최신 값이 일시적으로 n 이외의 일부 노드에 기록될 수 있기 때문
  • 느슨한 정족수는 모든 일반적인 다이나모 구현에서 선택 사항
    • 리악 기본 활성화, 카산드라, 볼드모트 비활성화

4.4. 동시 쓰기 감지

  • 다이나모 스타일은 동시에 같은 키에 쓰는 것을 허용하기 때문에 엄격한 정족수를 사용하더라도 충돌 발생
    • 다중 리더 복제의 쓰기 충돌과 유사
  • 문제는 다양한 네트워크 지연과 장애로 이벤트가 다른 노드에 다른 순서로 도착할 수 있음
  • 최종적인 일관성을 달성하기 위해 복제본들은 동일한 값이 돼야 함

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

  • 각 복제본이 가진 예전 값을 버리고 가장 최신 값으로 덮어쓰는 방법
  • 클라이언트가 쓰기 요청을 데이터베이스 노드에 전송할 때 다른 클라이언트에 대해서는 아는 것이 없기 때문에 어떤 이벤트가 먼저 발생했는지 확실하지 않음
  • 이벤트의 순서가 정해지지 않았기 때문에 동시 쓰기라고 해야함
  • 타임스탬프로 임의 순서를 정하고 최종 쓰기 승리(LWW) 알고리즘을 제공할 수 있음
    • 카산드라: 유일하게 제공, 리악: 선택적 기능
  • 최종 쓰기 승리 알고리즘은 최종적 수렴 달성이 목표지만 지속성 희생
  • 동일한 키에 여러 번 동시 쓰기가 있다면 클라이언트에 모두 성공으로 보고될지라도 쓰기 중 하나만 남고 다른 쓰기는 조용히 무시됨
  • 동시 쓰기가 아니어도 쓰기가 삭제될 수 있음
  • 캐싱과 같이 손실된 쓰기를 허용하는 상황이 있음
  • 손실데이터를 허용하지 않으면 LWW는 적절하지 않음
  • LWW로 데이터베이스를 안전하게 사용하는 유일한 방법은 키를 한번만 쓰고 이후에는 불변 값으로 다루는 것
  • 같은 키를 동시에 갱신하는 상황을 방지
    • e.g. 카산드라 추천 방법: 키로 UUID를 사용함

4.4.2. “이전 발생” 관계와 동시성

  • 두 작업 동시 수행 여부 결정 방법은?
  • 작업 B가 작업 A에 대해서 알거나 A에 의존적이거나 어떤 방식으로든 A를 기반으로 한다면?
    • 작업 A는 작업 B의 이전 발생(happens-before)
    • 한 작업이 다른 작업 이전에 발생했는지가 동시성의 의미를 정의하는 핵심
    • 사실 작업이 다른 작업보다 발생하지 않으면 단순히 동시 작업이라 말함
  • 세 가지 가능성
    • B 이전에 A가 발생
    • B가 A 이전에 발생
    • A, B 동시 발생
  • 동시성인지 아닌지 알 수 있는 알고리즘이 필요
  • 한 작업이 다른 작업 전에 발생한다면 나중 작업은 이전 작업을 덮어쓸 수 있지만 작업이 동시에 발생하면 충돌 해소 필요

4.4.3. 이전 발생 관계 파악하기

  • 어떤 작업이 다른 작업 이전에 발생했는지와 나중 작업이 이전에 수행된 작업을 알거나 의존했다는 사실 알 수 있음
  • 서버가 모든 키에 대한 버전 번호를 유지하고 키를 기록할 때마다 버전 번호를 증가 시킴
    • 기록한 값은 새로운 버전 번호를 가지고 저장
  • 클라이언트가 키를 읽을 때는 서버는 최신 버전뿐만 아니라 덮어쓰지 않은 모든 값을 반환
    • 클라이언트는 쓰기 전에 키를 읽어야 함
  • 클라이언트가 키를 기록할 때는 이전 읽기의 버전 번호를 포함해야 하고 이전 읽기에서 받은 모든 값을 합쳐야 함
    • 쓰기 요청이 현재 모든 값을 반환하기 때문에 읽기 요청과 같을 수 있음
  • 서버가 특정 버전 번호를 가진 쓰기를 받을 때 해당 버전 이하 모든 값을 덮어쓸 수 있음
    • 이보다 높은 버전 번호의 모든 값은 유지해야 함
    • 유입된 쓰기와 동시에 발생했기 때문에
  • 쓰기가 이전 읽기의 버전 번호를 포함하면 쓰기가 수행되기 이전 상태를 알 수 있음
  • 버전 번호를 포함하지 않은 쓰기는 다른 쓰기와 동시에 수행된 것이므로 아무것도 덮어쓰지 않음

4.4.4. 동시에 쓴 값 병합

  • 여러 작업이 동시에 발생하면 클라이언트는 동시에 쓴 값을 합쳐 정리해야 함
    • 리악은 이런 동시 값을 형제(sibling) 값이라 부름
  • 형제 값의 병합은 다중 리더 복제 충돌 해소 문제와 본질적으로 같음
  • 간단한 접근 방식으로 버전 번호나 타임스탬프 기반으로 하나의 값을 선택하는 방법이 있지만 데이터 손실 가능성 있음 - 그래서 애플리케이션 코드 내에서 더욱 지능적으로 대처 필요
  • 장바구니 예제에서 형제를 병합하는 합리적인 접근 방식은 합집합
  • 하지만 상품 제거는 합집합으로는 올바른 결과를 얻을 수 없음
    • 두 형제 장바구니를 합치고 그중 하나만 제거하면 형제의 합집합에 제거된 상품이 다시 나타남
  • 데이터베이스에서 삭제하지 않고 제거했음을 나타내는 버전 표시
    • 툼스톰, 소프트 삭제

4.4.5. 버전 벡터

  • 모든 복제본의 버전 번호 모음
  • 키당 버전 번호뿐 아니라 복제본당 버전번호를 사용
  • 각 복제본은 쓰기를 처리할 때 자체 버전 번호를 증가시키고 각기 다른 복제본의 버전 번호도 계속 추적해야 함
  • 이 정보는 덮어쓸 값과 형제로 유지할 값을 나타냄
  • 버전 벡터는 값을 읽을 때 데이터베이스 복제본에서 클라이언트로 보냄
  • 이후에 값이 기록될 때 데이터베이스로 다시 전송해야 함
  • 이 버전 벡터를 사용하면 데이터베이스는 덮어쓰기와 동시 쓰기를 구분할 수 있음
  • 버전 벡터 구조는 하나의 복제본을 읽은 당므 이어 다른 복제본에 다시 쓰는 작업이 안전함을 보장함
  • 이러면 형제가 생성되어도 형제가 올바르게 병합되는 한 데이터 손실은 없음
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

04장. 부호화와 발전

06장. 파티셔닝