복제를 위한 데이터 변경 처리에 대한 어려움을 알아보자
잘못될 수 있는 것과 잘못될 수 없는 것 사이의 주된 차이점은 잘못될 수 없는 것이 잘못됐을 때는 잘못을 파악하거나 고치는 것은 거의 불가능하다는 점이다. - 더글라스 애덤스, 대체로 무해함(1992)
0. 복제란?
- 네트워크로 연결된 여러 장비에 동일한 데이터의 복사본을 유지하는 것
복제가 필요한 여러 이유들
- 지리적으로 사용자와 가깝게 데이터를 유지해 지연 시간을 줄
- 시스템에 일부 장애가 발생하더라도 지속적으로 동작할 수 있게 해 가용성을 높임
- 읽기 질의에 제공하는 장비의 수를 확장해 읽기 처리량을 늘림
복제 알고리즘
- 단일 리더 (single-leader)
- 다중 리더 (multiple-leader)
- 리더 없는 (leaderless)
복제 트레이드오프
- 동기 식 vs 비동기식
- 잘못된 복제본을 어떻게 처리할지
- 데이터베이스의 설정 옵션으로 조절 가능
- 세부 사항은 데이터베이스마다 다양하지만 일반적인 원리는 유사함
1. 리더와 팔로워
- 데이터베이스 복사본을 저장하는 각 노드: 복제 서버(replica)
- 데이터베이스의 모든 쓰기는 모든 복제 서버에서 처리되어야 함
- 리더 기반 복제
- 리더 = 마스터 = 프라이머리
- 팔로워 = 슬레이브 = 세컨더리 = 읽기 복제 서버 = 핫 대기
- 리더가 새로운 데이터를 기록할 때마다 데이터 변경을 복제 로그나 변경 스트림의 일부로 팔로워에게 전송
- 팔로워는 리더가 처리한 것과 동일한 순서로 몯ㄴ 쓰기를 적용하여 복사본을 갱신
- 클라이언트 입장
- 읽기: 리더 or 팔로워
- 쓰기: 리더만 가능
1.1. 동기식 대 비동기식 복제
- 동기식
- 리더는 팔로워가 쓰기를 수신했는지 확인해줄 때까지 기다림
- 장점
- 팔로워가 일관성 있게 최신 데이터 복사본을 가지는 것을 보장함
- 갑자기 리더가 작동하지 않아도 데이터가 팔로워에서 계속 사용할 수 있음을 확신할 수 있음
- 단점
- 팔로워가 응답하지 않는다면 쓰기가 처리될 수 없음
- 리더는 모든 쓰기를 차단하고 동기 복제 서버가 다시 사용할 수 있을 때까지 기다려야함
- 비동기식
- 리더는 메시지를 전송하지만 팔로워의 응답을 기다리지 않음
- 보통 리더 기반 복제는 완전히 비동기식으로 구성함
- 리더가 잘못되고 복구할 수 없으면 팔로워에 아직 복제되지 않은 모든 쓰기는 유실됨
- 쓰기가 클라이언트에게 확인된 경우에도 지속성을 보장하지 않는다는 의미
- 모든 팔로워가 잘못되더라도 리더가 쓰기 처리를 계속할 수 있음
- 반동기식 (semi-synchronous)
- 동기식의 단점 때문에 모든 팔로워를 동기식으로 구성하는 것은 비현실적
- 하나의 팔로워만 동기식으로 구성하고 나머지는 비동기식으로 구성
- 동기식 팔로워가 사용할 수 없게 되거나 느려지면 비동기식 팔로워 중 하나가 동기식이 됨
- 적어도 두 노드에 데이터의 최신 복사본이 있는 것을 보장
1.2. 새로운 팔로워 설정
- 복제 서버 수를 늘리거나 장애 노드의 대체를 위해서는 새로운 팔로워 설정이 필요함
- 무중단으로 정확한 복제본을 가지고있는 새로운 팔로워를 추가하는 방법
- 전체 데이터베이스를 잠그지 않고 리더의 데이터베이스 스냅샷을 일정 시점에 가져옴. 대부분의 데이터베이스는 백업이 필요하기 때문에 이 기능을 갖추고 있음
- 스냅샷을 새로운 팔로워 노드에 복사
- 팔로워는 리더에 연결해 스냅샷 이후 발생한 모든 데이터 변경을 요청. 스냅샷이 리더의 복제 로그의 정확한 위치와 연관되어야 함 (위치: 로그 일련번호, 이진로그 좌표)
- 팔로워가 스냅샷 이후 데이터 변경의 미처리분(backlog)을 모두 처리했을 때 따라잡았다고 알려주고 이제부터 리더에 발생하는 데이터 변화를 처리할 수 있음
1.3. 노드 중단 처리
- 모든 노드는 장애 또는 계획된 유지보수(보안 패치) 등으로 중단될 수 있음
- 중단시간 없이 개별 노드를 재부팅할 수 있다는 점은 운영과 유지보수에 큰 장점
- 개별 노드의 장애에도 전체 시스템이 동작하게끔 유지하고 노드 중단의 영향을 최소화하는 것이 목표
- 리더 기반 복제에서 고가용성을 달성하는 방법은?
1.3.1. 팔로워 장애: 따라잡기 복구
- 각 팔로워는 리더로부터 수신한 데이터 변경 로그를 로컬 디스크에 보관함
- 팔로워가 죽어 재시작을 하거나 리더와 팔로워 사이의 네트워크가 일시적으로 중단된다면 팔로워는 매우 쉽게 복구할 수 있음
- 보관된 로그에서 결함이 발생하기 전에 처리한 마지막 트랜잭션 확인
- 팔로워는 리더에 연결해 팔로워 연결이 끊어진 동안 발생한 데이터 변경을 모두 요청할 수 있음
- 이 변경이 다 적용되면 리더를 다 따라잡게 되고 이전과 같이 데이터 변경의 스트림을 계속 받을 수 있음
1.3.2. 리더 장애: 장애 복구
- 이건 까다로움
- 팔로워 중 하나를 새로운 리더로 승격해야 함
- 클라이언트는 새로운 리더로 쓰기를 전송하기 위해 재설정이 필요
- 다른 팔로워는 새로운 리더로부터 데이터 변경을 소비하기 시작해야함
- 이 과정을 장애 복구라고 함
- 자동 장애 복구 과정
- 리더가 장애인지 판단
- 고장, 정전, 네트워크 문제 등 다양한 문제
- 확실한 방법이 없기 때문에 타임아웃 시간을 설정하여 초과하면 노드가 죽은 것으로 간주함
- 새로운 리더 선택
- 선출 과정을 통해 이뤄지거나 이전에 선출된 제어 노드에 의해 새로운 노드 임명
- 가장 적합한 후보는 보통 이전 리더의 최신 데이터 변경사항을 가진 복제 서버
- 새로운 리더 사용을 위해 시스템 재설정
- 클라이언트는 이제 새로운 쓰기 요청을 새로운 리더에게 보내야 함
- 이전 리더가 돌아오면 시스템은 이전 리더가 팔로워가 되고 새로운 리더를 인식할 수 있게끔 해야함
- 리더가 장애인지 판단
장애 복구 과정은 잘못될 수 있는 것 투성이다.
- 비동기식 복제를 사용하면 새로운 리더는 실패하기 전에 이전 리더의 쓰기 일부를 수신하지 못할 수 있음
- 새로운 리더가 충돌하는 쓰기를 수신했을지도 모름
- 가장 일반적인 해결책은 이전 리더의 복제되지 않은 쓰기를 단순히 폐기하는 방법
- 쓰기를 폐기하는 방법은 데이터베이스 외부의 다른 저장소 시스템이 데이터베이스 내용에 맞춰 조정돼야 한다면 특히 위험함
- 깃허브에서 발생한 유효하지 않은 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)에 쓰기 때문에 고장 이후 일관성 있는 상태로 색인을 복원
- 로그 그조화 저장소 엔진(SS테이블, LSM트리)
- 두 경우 모두 데이터베이스의 모든 쓰기를 포함하는 추가 전용 바이트열
- 완전히 동일한 로그를 사용해 다른 노드에서 복제 서버를 구축할 수 있음
- 리더는 디스크에 로그를 기록하는 일 외에도 팔로워에게 네트워크로 로그를 전송하기도 함
- 팔로워가 이 로그를 처리하면 리더에서 있는 것과 정확히 동일한 데이터 구조의 복제본이 만들어짐
- 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. 버전 벡터
- 모든 복제본의 버전 번호 모음
- 키당 버전 번호뿐 아니라 복제본당 버전번호를 사용
- 각 복제본은 쓰기를 처리할 때 자체 버전 번호를 증가시키고 각기 다른 복제본의 버전 번호도 계속 추적해야 함
- 이 정보는 덮어쓸 값과 형제로 유지할 값을 나타냄
- 버전 벡터는 값을 읽을 때 데이터베이스 복제본에서 클라이언트로 보냄
- 이후에 값이 기록될 때 데이터베이스로 다시 전송해야 함
- 이 버전 벡터를 사용하면 데이터베이스는 덮어쓰기와 동시 쓰기를 구분할 수 있음
- 버전 벡터 구조는 하나의 복제본을 읽은 당므 이어 다른 복제본에 다시 쓰는 작업이 안전함을 보장함
- 이러면 형제가 생성되어도 형제가 올바르게 병합되는 한 데이터 손실은 없음