본 글은 데이터 중심 어플리케이션(마틴 클레프만)으로 스터디하며 해당 책의 내용을 요약 정리한 내용입니다.
https://github.com/ddia-study/ddia-study
키워드만 간단보기 : https://johngrib.github.io/wiki/d-i-a-05-replication/
우연히 찾은 저자의 유튜브 : https://www.youtube.com/watch?v=uNxl3BFcKSA (quorum 관련 이야기)
복제란 네트워크로 연결된 여러 장비에 동일한 데이터의 복사본을 유지한다는 의미다.
데이터 복제가 필요한 여러 이유
복제에서 모든 어려움은 복제된 데이터의 변경 처리에 있다.
복제 서버 중 하나를 leader(master, primary)로 지정한다.
클라이언트가 DB에 쓰기를 할 때 클라이언트는 요청을 리더에게 보내야 한다.
다른 복제 서버는 follwer(read replica, slave, secondary, hot standby)라고 한다.
리더가 로컬 저장소에 새로운 데이터를 기록할 때마다 데이터 변경을 replication log나 change stream의 일부로 팔로워에게 전송한다.
각 팔로워가 리더로부터 로그를 받으면 리더가 처리한 것과 동일한 순서로 모든 쓰기를 적용해 갱신한다.
동기식 복제의 장점
동기식 복제의 단점
모든 팔로워가 동기식인 상황은 비현실적이다
현실적으로, DB에서 동기식 복제를 사용하려면 보통 팔로워 하나는 동기식으로 하고 그 밖에는 비동기식으로 하는 것을 의미한다.
semi-synchronous : 적어도 두 노드(리더 & 하나의 동기 팔로워)에 데이터의 최신 복사본이 있는 것을 보장한다.
보통 리더 기반 복제는 완전히 비동기식으로 구성한다.
이런 경우 리더가 잘못되고 복구할 수 없으면 팔로워에 아직 복제되지 않은 모든 쓰기는 유실된다.
하지만 완전 비동기식 설정은 모든 팔로워가 잘못되더라도 리더가 쓰기 처리를 계속할 수 있다는 장점이 있다.
새로운 팔로워가 리더의 데이터 복제본을 정확히 가지고 있는지 어떻게 보장할까?
데이터베이스를 잠가서 디스크의 파일을 일관성 있게 만들 수 있지만 고가용성 목표에 부합하지 않는다.
리더 기반 복제에서 고가용성은 어떻게 달성할 수 있을까?
로그를 이용해 발생한 데이터 변경을 통해 리더를 다 따라잡을 수 있다.
팔로워 중 하나를 새로운 리더로 승격해야 하고 클라이언트는 새로운 리더로 쓰기를 전송하기 위해 재설정이 필요하며 다른 팔로워는 새로운 리더로부터 데이터 변경을 소비하기 시작해야 한다.
이 과정을 failover라 한다.
자동 장애 복구 과정
장애 복구 과정에서 잘못될 수 있는 것들
리더는 모든 쓰기 요청(statement)을 기록하고 쓰기를 실행한 다음 구문 로그를 팔로워에게 전송한다.
이 접근법은 합리적인 것 같지만 복제가 깨질 수 있다.
모든 쓰기는 로그에 기록한다.
완전히 동일한 로그를 사용해 다른 노드에서 복제 서버를 구축할 수 있다.
팔로워가 이 로그를 처리하면 리더에서 있는 것과 정확히 동일한 데이터 구조의 복제본이 만들어진다. (Postgresql, Oracle)
가장 큰 단점은 로그가 제일 저수준의 데이터를 기술한다는 점이다. 어떤 디스크 블록에서 어떤 바이트를 변경했는지와 같은 상세 정보를 포함하고 있기 때문에, 복제가 저장소 엔진과 밀접하게 엮인다.
복제 로그를 저장소 엔진 내부와 분리하기 위한 방안 중 하나는, 복제와 저장소 엔진을 위해 다른 로그 형식을 사용하는 것이다.
이런 종류의 복제 로그를 logical log라고 한다.
RDB용 논리적 로그는 대개 로우 단위로 데이터베이스 테이블에 쓰기를 기술한 레코드 열이다.
장점
트리거는 사용자 정의 애플리케이션 코드를 등록할 수 있게 한다.
이 애플리케이션 코드는 DBS에서 데이터가 변경되면 자동으로 실행된다.
트리거는 데이터 변경을 분리된 테이블에 로깅할 수 있는 기회를 가진다.
이 테이블로부터 데이터 변경을 외부 프로세스가 읽을 수 있다.
트리거 기반 복제에는 다른 복제 방식보다 많은 오버헤드가 있다.
리더 기반 복제는 모든 쓰기가 단일 노드를 거쳐야 하지만 읽기 전용 질의는 어떤 복제 서버에서도 가능하다.
read-scaling 아키텍처에서는 간단히 팔로워를 더 추가함으로써 읽기 전용 요청을 처리하기 위한 용량을 늘릴 수 있다.
애플리케이션이 비동기 팔로워에서 데이터를 읽을 때 팔로워가 뒤쳐진다면 지난 정보를 볼 수도 있다. 하지만 이런 불일치는 일시적인 상태에 불과한다.
데이터베이스에 쓰기를 멈추고 잠시 동안 기다리면 팔로우는 따라잡게 된다. 이런 효과를 최종적 일관성이라 한다.
새로운 데이터가 제출되면 리더에게 전송해야 하지만 사용자가 데이터를 볼 때는 팔로워에서 읽을 수 있다.
이것은 데이터를 자주 읽지만 가끔 쓰는 경우에 특히 적합하다.
쓰기 후 읽기 일관성(자신의 쓰기 읽기 일관성)이 필요하다.
리더 기반 복제 시스템에서 쓰기 후 읽기 일관성을 어떻게 구현할까?
사용자가 시간이 거꾸로 흐르는 현상을 목격할 수 있다.
사용자가 각기 다른 복제 서버에서 여러 읽기를 수행할 때, 비동기적 쓰기 이슈로 인해 발생
monotonic read는 이런 종류의 이상 현상이 발생하지 않음을 보장한다.
강한 일관성보다는 덜한 보장이지만 최종적 일관성보다는 더 강한 보장이다.
즉, 이전에 새로운 데이터를 읽은 후에는 예전 데이터를 읽지 않는다.
각 사용자의 읽기가 항상 동일한 복제 서버에서 수행되게끔 하는 것이다.
인과성 위반이 일어나는 경우를 방지하기 위해 Consistent Prefix Read 같은 보장이 필요하다.
일관된 순서로 읽기는 일련의 쓰기가 특정 순서로 발생한다면 이 쓰기를 읽는 모든 사용자는 같은 순서로 쓰여진 내용을 보게 됨을 보장한다.
애플리케이션 개발자가 이런 미묘한 복제 문제를 걱정하지 않고 "올바른 작업 수행"을 위해 항상 데이터베이스를 신뢰할 수 있다면 훨씬 좋다. 이것이 트랜잭션이 있는 이유다.
리더가 하나만 존재하고 모든 쓰기는 해당 리더를 거쳐야 한다.
어떤 이유로 리더에 연결할 수 없다면 쓰기를 할 수 없다.
따라서 쓰기를 허용하는 노드를 하나 이상 두는 것으로 자연스럽게 확장된다.
이를 다중 리더 설정이라고 한다.(master master, active/active)
- 다중 데이터 센터
Auto Increment Key, Trigger, 무결성 등 문제가 될 가능성이 높아서 다중 리더 복제는 가능하면 피해야 하는 영역으로 간주되기도 한다.
- 오프라인 작업을 하는 클라이언트
- 협업 편집
단일 리더 DB : 첫 번째 쓰기 완료까지 두 번째 쓰기 차단 or 두 번째 쓰기 중단 후 재시도
다중 리더에서는 동기식으로 하면 이점을 잃음
특정 레코드의 모든 쓰기가 동일한 리더를 거치도록 애플리케이션이 보장한다면 충돌이 발생하지 않는다.
다중 리더 설정에서는 쓰기 순서가 정해지지 않아 최종 값이 무엇인지 명확하지 않다.
단순하게 각 복제 서버가 쓰기를 본 순서대로 적용한다면 DB는 결국 일관성 없는 상태가 된다.
따라서 DB는 수렴(convergent) 방식으로 충돌을 해소해야 한다.
이는 모든 변경이 복제돼 모든 복제 서버에 동일한 최종 값이 전달되게 해야 한다는 의미다.
자동 충돌 해소
Geek News : https://news.hada.io/topic?id=2962
채널톡 : https://channel.io/ko/blog/crdt_vs_ot
복제 토폴로지는 쓰기를 한 노드에서 다른 노드로 전달하는 통신 경로를 설명한다.
리더 없는 복제는 아마존이 내부 Dynamo 시스템에서 사용한 후 다시 데이터베이스용 아키텍처로 유행했다. (Dynamo Style)
Dynamo Style : https://en.wikipedia.org/wiki/Dynamo_(storage_system)
리더 없는 설정에서는 장애 복구가 필요하지 않다.
다운된 노드가 온라인 상태가 되면 클라이언트가 해당 노드에서 데이터를 읽을 때 응답으로 outdated 값을 얻을 수 있다.
이 문제를 해결하기 위해 클라이언트가 DB에서 읽을 때 하나의 복제 서버로 요청을 보내지 않고 읽기 요청을 병렬로 여러 노드에 전송한다.
이렇게 전송 받은 데이터 중 버전 숫자를 사용해 어떤 값이 최신 내용인지 결정한다.
사용 불가능한 노드가 온라인 상태가 된 후 누락된 쓰기를 어떻게 따라잡아야 할까?
n개의 복제 서버가 있을 때 모든 쓰기는 w개의 노드에서 성공해야 쓰기가 확정되고 모든 읽기는 최소한 r개의 노드에 질의해야 한다.
w + r > n이면 읽을 때 최신 값을 얻을 것으로 기대한다.
최소한 r개의 노드 중 하나에서 최신 값을 읽을 수 있기 때문이다.
이런 r과 w를 따르는 읽기와 쓰기를 정족수 읽기와 쓰기라고 부르며, 유효한 읽기와 쓰기를 위해 필요한 최소 투표수를 r과 w로 생각할 수 있다.
다이나모 스타일 데이터베이스에서 n, w, r 파라미터는 대개 설정 가능하다.
n은 홀수, w = r = (n + 1) / 2 (반올림)
w + r > n이면 다음과 같이 사용 불가능한 노드를 용인한다.
정족수가 읽기 시 최근에 쓴 값을 반환하게끔 보장하지만 실제로는 그렇게 간단하지 않다.
매개변수 w와 r로 오래된 값을 읽는 확률을 조정할 수 있지만 이를 절대적으로 보장할 수는 없다.
적절히 설정된 정족수가 있는 데이터베이스는 장애 복구 없이 개별 노드 장애를 용인한다.
하지만 지금까지 설명한 대로 정족수는 내결함성이 없다. 네트워크 중단으로 다수의 데이터베이스 노드와 클라이언트는 쉽게 연결이 끊어질 수 있다.
노드가 n개 이상인 대규모 클러스터에서 클라이언트는 네트워크 장애 상황에서 일부 데이터베이스 노드(특정 값을 위한 정족수 구성에 들어가지 않는 노드)에 연결될 가능성이 있다.
쓰기와 읽기는 여전히 w와 r의 성공 응답이 필요하지만 값을 위해 지정된 n개의 "홈" 노드에 없는 노드가 포함될 수 있다.
암시된 핸드오프 : 네트워크 장애 상황이 해제되면 한 노드가 다른 노드를 위해 일시적으로 수용한 모든 쓰기를 해당 "홈" 노드로 전송한다.
느슨한 정족수는 쓰기 가용성을 높이는 데 특히 유용하다
리더 없는 복제도 동시 쓰기 충돌, 네트워크 중단, 지연 시간 급증을 허용하기 때문에 다중 데이터센터 운영에 적합하다.
여러 클라이언트가 동시에 같은 키에 쓰는 것을 허용하기 때문에 충돌이 발생한다.
최종적 일관성을 달성하기 위해 복제본들은 동일한 값이 돼야 한다.
각 복제본이 가진 "예전" 값을 버리고 가장 "최신" 값으로 덮어쓰는 방법이 있다.
쓰기에 타임스탬프를 붙여 가장 "최신"이라는 의미로 제일 큰 타임스탬프를 선택하고 예전 타임스탬프를 가진 쓰기는 무시한다. (LWW)
손실 데이터를 허용하지 않는다면 LWW가 충돌 해소에 적합하지 않다.
작업 B가 작업 A에 대해서 알거나 A에 의존적이거나 어떤 방식으로든 A를 기반으로 한다면 작업 A는 작업 B의 happens-before이다.
작업이 다른 작업보다 먼저 발생하지 않으면 동시 작업이라고 말한다.
작업이 동시에 발생한 것을 어떻게 알 수 있을까?
쓰기가 이전 읽기의 버전 번호를 포함하면 쓰기가 수행되지 이전 상태를 알 수 있다.
버전 번호를 포함하지 않은 쓰기는 다른 쓰기와 동시에 수행된 것이므로 아무것도 덮어쓰지 않는다.
여러 작업이 동시에 발생하면 클라이언트는 동시에 쓴 값을 합쳐 정리해야 한다.
형제 값 병합은 다중 리더 복제에서 충돌을 해소하는 문제와 본질적으로 같다.
형제를 병합할 때 삭제한 아이템이 합집합에서 재등장할 수 있기 때문에, 해당 버전 번호에 표시를 남겨둔다. (툼스톤)
애플리케이션 코드에서 형제 병합은 복잡하고 오류가 발생하기 쉽다.
자동 병합을 수행할 수 있게 데이터 구조를 설계하려는 노력이 있다. (CRDT)
다중 복제본의 동시 쓰기를 받아들이기 위해 복제본당 버전 번호도 사용한다.
모든 복제본의 버전 번호 모음을 version vector라고 부른다.
버전 벡터는 값을 읽을 때 데이터베이스 복제본에서 클라이언트로 보내고, 값이 기록될 때 데이터베이스로 다시 전송한다.
이 버전 벡터를 통해 데이터베이스는 덮어쓰기와 동시 쓰기를 구분할 수 있다.
리악에서 사용하는 dotted version vector : https://github.com/ricardobcl/Dotted-Version-Vectors
Chapter 8. 분산 시스템의 골칫거리 - Part 2 (0) | 2022.06.10 |
---|---|
Chapter 8. 분산 시스템의 골칫거리 - Part 1 (0) | 2022.06.10 |
Chapter 6. Partitioning(파티셔닝) (0) | 2022.06.10 |
Chapter 3. Storage and Search (0) | 2022.06.10 |
Chapter 1. Reliability, Scalability, Maintainability (0) | 2022.06.10 |
댓글 영역