정리
- 데이터 복제가 필요한 이유
- 지리적으로 가까운 곳에 데이터를 유지시켜서 지연 시간을 줄임 ⇒ CDN
- 시스템의 일부 장애에도 지속적으로 동작할 수 있게 가용성 증가 ⇒ HA(High Available)
- 읽기를 위한 장비의 수를 늘림 ⇒ ReadOnly Node
- 데이터 복제의 어려움 ⇒ 데이터 변경을 어떻게 처리할지?
- Single-leader 복제
- Multi-leader 복제
- Leaderless 복제
### 리더와 팔로워
- 리더(a.k.a 마스터, 프라이머리)에 쓰기 요청
- 팔로워(a.k.a 슬레이브, 세컨더리 등)가 복제로그나 변경 스트림의 일부를 받아 수정
![image](https://github.com/DevSprout/data-oriented-architecture/assets/3251003/a2846637-f9f6-4685-94a9-11c10483514d)
- 동기식 복제
- 복제가 제대로 이루어졌는지 확인하는 방식
- 장점) 일관성 있는 최신 데이터 복사본 유지
- 단점) 복제가 완료될 때까지 대기하는 비용 발생
- 비동기식 복제
- 장점) 빠른 응답으로 고가용성 확보
- 단점) 동기식에 비해 데이터 일관성이 떨어짐
- 반동기식(semi-synchronous) 복제
- 두 방식을 혼합하여 몇개의 레플리카만 동기식 복제를 지원함. 나머지 레플리카는 비동기식
- **Why?)** 리더가 중단되었을 때, 데이터가 완전히 유실되는 경우가 발생하기 때문에
### 새로운 팔로워 설정
- 새로운 팔로워가 필요한 이유
- 복제 서버 수를 늘려야할 때
- 장애 노드를 대체해야할 때
- DB 중단 없이 새로운 팔로워를 추가하는 방법
1. 리더의 특정 시점 스냅샷을 토대로 팔로워 노드에 복사함
2. 팔로워는 복사한 시점 이후의 변경사항을 리더로 요청
3. 데이터 변경의 미처리분(backlog)을 모두 적용하고 나면 리더에게 따라잡았다고 알림
4. 이후 리더는 발생하는 데이터 변화를 이어서 처리할 수 있음
### 노드 중단 처리
- 장애가 발생했거나, 장비 리부팅이 필요한 경우 노드가 중단될 수 있음
- 팔로워 장애
- 리더로부터 수신한 변경로그들을 로컬 파일로 저장해둠
- 이후 다시 복구할 때, 로컬 파일과 리더의 Backlog를 요청해서 복구 가능
- 리더 장애
- 팔로워 중 하나를 리더로 승격
- 클라이언트는 새로운 리더로 쓰기를 시작
- 다른 팔로워들은 새로운 리더로부터 변경사항을 받기 시작
- 이 과정은 수동/자동 모두 지원함
### 리더 노드 장애 복구 과정
- 리더 노드 장애 감지
- 노드 간에 주고받는 메시지 타임아웃을 통해 노드가 죽었는지를 판단함
- 새로운 리더 선택하는 방법
1. 복제 노드들이 새로운 리더를 선출
2. 제어 노드(Controller Node)가 새로운 리더를 선출
3. 가장 최신 데이터를 가지고 있는 팔로워 노드가 새로운 리더로 가장 적합함
- 새로운 리더 사용을 위해서 시스템 재설정
- 클라이언트에 리더 노드 변경을 알림
- 팔로워 데이터 변경 미처리분(Backlog) 초기화
- 중단됐던 리더 노드가 다시 복구되면 팔로워 노드로서 동작함
### 자동 장애 복구 위험성
- 내구성을 보장하지 않음
- 승격된 팔로워가 이전 리더의 최신 변경사항 중 일부를 받지 못한다면
- 이후 이전 리더가 다시 복구된다면 이 변경사항은 어떻게 할 것인가?
- ⇒ 일반적으로 폐기하게됨
- 이렇기 때문에 클라이언트 입장에서 내구성을 신뢰할 수 없음
- 데이터 불일치 발생
- 리더로 승격되는 과정에 승격된 팔로워가 최신 데이터를 보장하지 않음
- 새로운 리더에는 없지만, 이전 리더에만 존재하는 데이터가 있었지만 유실됨
- 스플릿 브레인(Split Brain)
- 두 노드가 자신이 리더라고 인식되는 경우가 발생
- 두 리더가 각자 쓰기 요청을 처리하기 때문에 충돌
- 장애 감지를 위한 적절한 타임아웃 시간을 찾기 어려움
- 타임아웃이 길면, 복구에 너무 오랜 시간이 소요됨
- 타임아웃이 짧으면, 불필요한 장애복구가 발생함
### 복제 로그 구현
- 구문 기반 복제
- 요청받은 구문을 기록하고 쓰기를 실행한 다음 구문을 팔로워에게 전송함
- SELECT, INSERT, UPDATE 같은 쿼리 자체를 전송
- 문제) 비결정적인 요인에 의해 복제가 깨질 수 있음
- NOW(), RAND() 등 서버마다 다른 값을 생성할 가능성
- Auto-increase 컬럼이나 기존 데이터에 의존하는 경우가 있음
- 순서가 다르면 구문의 효과가 다를 수 있음 ⇒ 여러 트랜잭션이 수행되는 것을 제한함
- 대안) 리더가 구문 기록 시 서버별로 다를 수 있는 값은 고정 값을 반환하도록 대체함
- 에지 케이스 대응이 어려움
- MySQL에서는 비결정성 요인이 있으면 Row 기반 복제방식으로 사용
- 쓰기 전 로그(WAL, Write-Ahead Log) 배송
- DB에 쓰기 전에 쓰기로그를 WAL에 저장함
- 리더가 팔로워에게 WAL을 전송하고 팔로워가 이를 처리함
- 로그에는 제일 저수준의 데이터를 기술함
- 디스크 블록에서 어떤 데이터를 변경했는지?
- 저장소 엔진과 밀접하게 연관됨
- 리더와 팔로워가 동일한 버전을 사용해야함이 전제조건
- 소프트웨어 업그레이드 시 중단시간이 필요함
- 논리적(Row 기반) 로그 복제
- 로그를 저장소 엔진과 분리하기 위한 대안으로 복제와 저장소 엔진에 각기 다른 로그 형식을 사용한다.
- **논리적 로그(logical log)**
- 이와 같이 복제에서 사용하는 로그를 저장소 엔진의 물리적 데이터 표현과 구별하여 부른다.
- RDBMS에서 논리적 로그는 대개 Row 단위이고, 테이블에 쓰기를 기술한 레코드 열이다.
- 삽입 : 모든 컬럼의 새로운 값을 포함
- 삭제 : Row를 식별하기 위한 정보(보통 기본 키)를 포함
- 갱신 : Row를 식별하기 위한 정보 + 모든 컬럼의 새로운 값 또는 변경된 컬럼의 새로운 값
- 여러 Row를 수정하는 트랜잭션은 여러 로그 레코드를 생성한 다음 트랜잭션 커밋을 레코드에 표시
- 논리적 로그와 저장소 엔진 내부를 분리
- 하위 호환성을 유지
- 리더와 팔로워가 각기 다른 버전의 소프트웨어에서 실행 가능
- 심지어 저장소 엔진이 다를 수도 있음
- 논리적 로그는 외부 애플리케이션이 파싱하기 쉽다.
- 데이터 웨어하우스와 같은 외부 시스템에 데이터베이스 내용 전송 시 유용하다.
- **변경 데이터 캡쳐(CDC, change data capture)**
- 트리거 기반 복제
- 트리거, 스토어드 프로시저 제공
- 트리거(Trigger)
- 사용자 정의 애플리케이션 코드를 등록할 수 있다.
- 데이터 변경 시 자동으로 실행
- 트리거를 통해 데이터 변경을 분리된 테이블에 로깅
- 이 테이블에 기록된 데이터 변경을 외부 프로세스가 읽고 처리한다.
- 필요한 애플리케이션 로직 적용 후 다른 시스템에 데이터 변경을 복제한다.
- 단점
- 트리거 기반 복제는 다른 복제 방식보다 많은 오버헤드가 있다.
- 그리고 데이터베이스에 내장된 복제보다 버그나 제한 사항이 더 많이 발생한다.
- 장점
- 유연성이 좋아서 매우 유용함
## 복제 지연 문제
- 정상적인 경우 복제 지연으로 인한 데이터 불일치는 순간적으로 발생하고 복구되므로 크게 문제되지 않음
- ⇒ 순간적인 일관성을 깨지지만, **최종적 일관성은 지킬 수 있음**
- 네트워크 문제가 발생하거나 복제 지연이 발생하면 불일치가 문제 됨
### 자신이 쓴 내용 읽기
- 복제 지연으로 인해서 사용자가 자신이 삽입한 레코드를 읽기 못할 수 있음
- 쓰기 후 읽기(read-after-write) 일관성
- 사용자가 페이지를 리로딩하면 자신이 제출한 모든 갱신을 볼 수 있음을 보장함
- 대신, 다른 사용자가 제출한 것은 보장하지 않음
- 리더 기반 복제 시스템에서 쓰기 후 읽기 일관성 구현
- 사용자가 수정한 내용을 읽을 때는 리더에서 읽고, 나머지는 팔로워에서 읽음
- SNS에서 사용자 정보는 소유자 자신만 편집이 가능 **⇒ 자신의 정보는 리더에서 조회**
- 다른 사용자의 정보는 팔로워에서 조회
- 시간 기준으로 판단
- 애플리케이션 내 대부분의 내용에 대해 사용자 편집이 가능하다면 적합하지 않음
- 레코드의 마지막 갱신 시각을 기준으로 리더 읽기 여부를 구분함
- 1분 이내에 갱신된 레코드는 리더에서 읽기
- 팔로워에서 복제 지연을 모니터링해서 이 시간을 조절함
- 클라이언트가 기억하는 가장 최근 쓰기의 타임스탬프 활용
- 각자 노드가 마지막 쓰기가 이뤄진 시간 타임스탬프를 관리하도록함
- 복제 서버에 아직 반영되지 않았다면 다른 서버로 요청을 패스하거나 갱신이 될 때까지 대기함
- 복제 서버가 여러 데이터센터에 분산되었다면
- 리더가 제공하는 요청은 전부 리더가 포함된 IDC로 라우팅되어야함
- 여러 디바이스 간 쓰기 후 읽기 일관성
- 사용자의 마지막 갱신 타임스탬프는 다른 디바이스에서 알 수 없음
- 메타데이터를 관리하는 중앙서버가 필요함
- 복제 서버가 여러 데이터센터 간에 분산된 경우
- 사용자 디바이스의 요청을 동일한 IDC로 라우팅하는 것을 보장해야함
### 단조 읽기
- 팔로워 간에도 동일한 쓰기에 대해 갱신 시점 차이가 존재함
- 동일한 읽기 요청을 여러번 보내면 각기 다른 팔로워에 전달될 수 있음
- 특정 팔로워는 알 수 없는 정보가 있을 수 있어서 시간이 거꾸로 흐르는 현상을 겪을 수 있음
- 단조 읽기(monotonic read)는 이런 이상현상이 발생하지 않음을 보장함
- 단조읽기 방법
- 각 사용자의 읽기가 항상 동일한 복제 서버에서 수행되도록 함
- 사용자 ID의 해시를 기반으로 복제 서버를 선택함
- 복제 서버가 고장나면 사용자 질의를 다른 서버로 재라우팅할 필요가 있음
### 일관된 순서로 읽기
- 파티션 간의 복제 시점에 차이가 있으면 나중에 삽입된 레코드를 먼저 확인할 가능성이 있음
![image](https://github.com/DevSprout/data-oriented-architecture/assets/3251003/d138c31b-d130-4d22-ab81-6ed602df5026)
- 일관된 순서로 읽기(Consistence Prefix Read) **⇒ 위 이상 현상을 방지하기 위함**
- 일련의 쓰기가 특정 순서로 발생한 경우 다른 사용자에게도 쓰기에 대해 쓰여진 순서로 읽는 것을 보장
- **인과성의 위반**
- 파티셔닝(or 샤딩)된 DB에서 발생하는 특징적인 문제
- 많은 분산 DB에서 파티션은 서로 독립적으로 동작함
- 인과성이 있는 쓰기는 동일한 파티션에 기록되도록 하는 방법
- 인과성을 명시하기 위한 알고리즘을 도입
### 복제 지연을 위한 해결책
- 쓰기 후 읽기와 같은 강한 보장을 제공하도록 설계
- 비동기식 복제를 사용하지만 동기식 방식으로 동작하는 것처럼 보임
- 애플리케이션이 DB보다 더 강력한 보장을 제공하도록 설계
- 특정 종류의 리더에서 읽기를 수행한다던지..
- 애플리케이션에 다루기 복잡해서 잘못될 가능성이 높음
- 트랜잭션
- 애플리케이션이 단순해지기 위해 DB가 더 강력한 보장을 제공함
- 애플리케이션 개발자가 직접 복제 문제를 걱정하지 않아도 됨
- 항상 DB를 신뢰할 수 있음
- 분산 시스템에서의 트랜잭션
- 분산 DB에서 트랜잭션은 가용성이나 성능 측면에서 너무 비쌈
- 확장 가능한 시스템에서 최종적 일관성을 선택하는 것은 불가피하다는 주장도 있음
## 다중 리더 복제
- 쓰기 처리를 하는 각 노드는 데이터 변경을 모든 노드에 전달하는데 이를 다중 리더로 설정
- 각 리더는 리더 역할과 동시에 다른 리더의 팔로워 역할도 함
- 둘 중 하나의 리더가 죽더라도 쓰기가 가능해짐
### 단일 리더 vs 다중 리더
- 성능
- 단일 리더 : 리더가 있는 IDC로 이동해야해서 쓰기 지연 발생
- 다중 리더 : 로컬 IDC에서 처리 후 비동기 방식으로 다른 IDC에 복제
- IDC 장애 시
- 단일 리더 : 리더가 있는 IDC에 장애가 나면 다른 IDC 팔로워가 리더로 승진
- 다중 리더 : IDC에 독립적으로 동작하기 때문에 고장난 IDC가 온라인으로 돌아왔을 때 복제
- 네트워크 문제 내성
- 단일 리더 : IDC 내 쓰기는 동기식으로 연결 문제에 민감함
- 다중 리더 : 비동기 복제를 사용해 네트워크 문제에 보다 잘 견딤, 일시적 네트워크 중단에도 쓰기 처리는 진행되기 때문
## 쓰기 충돌 다루기
- 오프라인 작업을 하는 클라이언트
- 인터넷이 끊어진 동안 앱이 동작해야하는 경우
- 협업 편집 : 동시에 여러 사람이 편집할 수 있는 애플리케이션
- 각 사용자가 동시에 편집 후 로컬 리더에 저장하였으나 변경을 비동기로 복제 시 쓰기 충돌 발생
- 동기식으로 충돌 감지를 하면 다중 리더 복제의 장점을 잃어버림
### 충돌 회피
- 특정 레코드의 모든 쓰기를 동일한 IDC의 리더에서 처리함
- 특정 사용자의 요청을 동일한 IDC로 라우팅하지 않으면 충돌회피가 실패함
### 일관된 상태 수렴
- 모든 복제 서버가 동일해야 함이 원칙
- 수렴(convergent) : 모든 변경이 복제돼 모든 복제 서버에 동일한 최종 값이 전달되게 해야함
- 수렴 충돌 해소 방법들
1. 각 쓰기에 고유 ID를 부여해 가장 높은 ID를 가진 쓰기를 선택
- 대중적이지만 데이터 유실 위험이 있음
2. 각 복제 서버에 고유 ID를 부여하고 높은 숫자의 서버에서 생긴 쓰기가 낮은 숫자의 서버에서 생긴 쓰기보다 항상 우선적으로 적용
- 데이터 유실 가능성이 존재함
3. 어떻게든 값을 병합
- 어떤 기준을 세워서 값을 병합하도록 함
4. 명시적 데이터 구조에 충돌을 기록해 모든 정보를 보존
- 나중에 사용자가 직접 충돌을 처리하도록 모든 정보를 보존함
### 사용자 정의 충돌 해소 로직
- 충돌 해소의 가장 적합한 방법은 애플리케이션에 따라 다름
- 쓰기 수행 중
- 복제된 변경 로그에서 DB 시스템 충돌이 감지되면 핸들러 호출
- 백그라운드에서 실행
- 읽기 수행 중
- 충돌 감지 시 모든 충돌 쓰기 저장
- 다음 번 읽기 시 여러 데이터 반환. 애플리케이션은 사용자에게 충돌 내용을 보여주거나 자동으로 충돌 해소해서 결과를 DB에 기록함
- 카우치 DB가 동작하는 방식
### 자동 충돌 해소
- 충돌 없는 복제 데이터 타입을 사용ㅎ
- Set, Map, 정렬 목록, 카운터 등을 위한 데이터 구조의 집합
- 병합 가능한 영속 데이터 구조
- Git처럼 명시적으로 히스토리를 추적하고 3-way merge를 사용함
- 구글 닥스 같은 협업 편집 애플리케이션의 충돌 해소 알고리즘
## 다중 리더 복제 토폴로지
- 복제 토폴로지는 쓰기를 한 노드에서 다른 노드로 전달하는 통신 경로
- 리더가 둘 이상이라면 다양한 토폴로지가 가능
![image](https://github.com/DevSprout/data-oriented-architecture/assets/3251003/57cf64b7-6b36-466e-bd36-99de0302ecaa)
### 원형 토폴로지
- 각 노드가 하나의 노드로부터 쓰기를 받고, 다른 노드에 전달 (MySQL의 기본 제공)
- 노드 장애 시 노드 간 복제 메시지 흐름에 방해를 줌
### 별 모양 토폴로지
- 지정된 루트 노드 하나가 다른 모든 노드에 쓰기 전달
- 트리로 일반화 가능
- 노드 장애 시 노드 간 복제 메시지 흐름에 방해를 줌
### 전체 연결 토폴로지
- 모든 리더가 각자의 쓰기를 다른 모든 리더에 전송
- 가장 일반적인 토폴로지
- 내결함성이 상대적으로 좋음
- 문제점
- 네트워크 연결 속도 차이로 복제 메시지 추월 발생
- 삽입 이전에 갱신을 처리하게 됨
- 올바른 이벤트 정렬을 위한 버전 벡터 기법으로 해결 가능
## 리더 없는 복제
- 일부 DB 시스템은 리더의 개념을 보리고 모든 복제 서버가 클라이언트로부터 쓰기를 직접하는 방식을 사용하기도 함
- Dynamo-style DB로 리악, 카산드라, 볼드모트 등
- 일부 리더 없는 복제 구현에서는 클라이언트가 복제 서버에 쓰기를 직접 전송하는 반면 코디네이터 노드가 클라이언트를 대싱해서 수행하기도 함
### 노드가 다운됐을 때 DB에 쓰기
- 다운된 노드에서 쓰기가 누락되어 오래된(유효하지 않은) 값을 읽게 됨
- 읽기 요청을 병렬로 여러 노드에 전송해 최신 값을 읽어와 해결 가능
- 버전 숫자를 통해 읽어온 값 중 최신 값을 결정함
### 읽기 복구와 안티 엔트로피 복제 계획
- 최종적으로 모든 데이터가 모든 복제 서버에 복사된 것을 보장해야 함
- 읽기 복구
- 클라이언트가 여러 노드에서 병렬로 읽기 수행하면 오래된 응답 감지 가능
- 복제 서버의 오래된 값을 새로운 값으로 기록
- 값을 자주 읽는 상황에 적합
- 안티 엔트로피 처리
- 백그라운드 프로세스와 복제 서버 간 데이터 차이를 찾아 누락된 데이터를 복사
- 특정 순서로 쓰기를 복사하기 때문에 지연이 있을 수 있음
### 정족수(Quorum)
- 합의체가 의사(議事)를 진행시키거나 의결을 하는 데 필요한 최소한도의 인원수
- 유효한 읽기와 쓰기를 위한 복제서버 수, 쓰기 성공 노드 수, 질의 노드 수
- 복제서버(n), 쓰기 노드(w), 읽기 노드(r) 설정 가능
- 일반적으로 n은 홀수, w = r = (n+1) / 2 (반올림) 설정
- 일반적으로 읽기와 쓰기는 항상 모든 n개의 복제 서버에 병렬 전송함
- w, r은 기다릴 노드를 결정함
- 읽기, 쓰기 성공 여부는 읽기, 쓰기가 성공한 노드의 개수로 확인함
- w, r 보다 사용가능한 노드 수가 적다면 에러를 반환함
### 정족수 일관성의 한계
- w + r > n 으로 설정하면 읽은 노드 중 최신 값을 가진 노드가 하나 이상이어야 함
- 그러나 모든 과정이 올바르게 동작해도 시점 문제로 오래된 값을 반환할 수 있음
- 즉, 정족수를 아무리 잘 설정해도 오래된 값을 읽을 가능성이 있음
### 느슨한 정족수와 암시된 핸드오프
- 정족수 불충족
- 네트워크 중단으로 DB 노드와 클라이언트 연결 유실
- 응답 가능한 노드가 w, r보다 적을 수 있음
- 느슨한 정족수
- 정족수 불충족 상황에서 보통 저장하는 노드가 아닌 연결이 가능한 다른 노드에 쓰기를 하는 경우
- 암시된 핸드오프
- 네트워크 장애가 해제되면 일시적으로 수용한 모든 쓰기를 해당 홈 노드로 전송
### 동시 쓰기 감지
- 여러 클라이언트가 동시에 같은 키에 쓰는 것을 허용해 엄격한 정족수를 사용해도 충돌이 발생할 수 있음
- 문제는 네트워크 지연 등으로 이벤트가 다른 노드에 다른 순서로 도착할 수 있음
- 최종 쓰기 승리 (동시 쓰기 버리기)
- 복제본을 가장 최신 값으로 덮어 쓰는 방법
- 쓰기에 타임스탬프를 붙여서 최신 값을 선택하는 방법 (LWW)
- 손신 데이터를 허용하지 않는다면 LWW가 부적합
- 카산드라에서 유일하게 제공하는 충돌 해소 방법, 리악에서는 선택적 기능
- 키를 한번만 쓰고 이후에 불변값으로 만들어 동시에 같은 키를 갱신하는 상황을 방지해야 함
- 카산드라 사용 시 키로 UUID를 사용해 모든 쓰기 작업에 고유한 키를 부여하는 것을 추천함
- 버전 벡터
- 모든 복제본의 버전 번호 모음을 나타냄
- 값을 읽을 때 데이터베이스 복제본에서 클라이언트로, 값이 기록될 때 데이터베이스로 전송
- 쓰기 시 버전 번호 증가, 다른 복제본의 번호도 추적해 덮어쓸 값, 형제 값을 구분함
- 데이터베이스는 덮어쓰기와 동시 쓰기를 구분할 수 있음
Leader가 로컬 저장소에 새로운 데이터를 기록할 때마다 데이터 변경을 replication log 나 change stream의 일부로 팔로워에게 전송한다
동기식 복제 vs 비동기식 복제
동기식 복제의 장점은 팔로워가 리더와 일관성 있게 최신 데이터 복사본을 가지는 것을 보장한다
동기식 복제는 임의 한 노드의 장애가 전체 시스템을 멈추게 한다. 이에 대한 대안은 반동기식 복제(적어도 follower 한 노드에 데이터의 최신 복사복이 있는 것을 보장)
새로운 팔로워 설정
일정 시점의 데이터베이스 Snapshot을 가져오고 복사
Snapshot 이후의 모든 데이터 변경을 팔로워에 업데이트
미처리를 다 하게 되면 팔로워로서 역할을 할 수 있음
노드 중단 처리
팔로워 장애 시 마지막 트랜잭션 이후에 리더로부터 받지 못한 데이터 변경을 모두 요청해서 sync
리더 장애 시
리더 장애인지 판단한다
새로운 리더 선출
새로운 리더 사용을 시스템을 재설정한다 - 새로운 쓰기 요청을 새로운 리더로 보내야 함
Github issue: out-of-date 된 mysql follower가 승격되어 장애가 발생한 사례가 있음. 새로운 로우의 기본키를 할당하기 위해 자동 증가 카운터를 사용했는데, 새로운 리더의 카운터는 이전 리더보다 뒤처져 있었기 때문에 이전 리더가 예전에 할당한 기본키를 재사용했다. 이 기본키는 레디스 저장에도 사용했기 때문에 기본키의 재사용은 Mysql과 레디스 간 불일치를 일으켰다. 결국 일부 개인 데이터가 사용자에게 공개되었다.
데이터가 유실된 것도 문젠데, 레디스 데이터 불일치로 인해 개인 데이터가 공개된 것이 더 문제인듯. 캐시키랑 DB키랑은 다르게 가져가는 게 더 나은 방법이 아닐까
복제 로그 구현
구문 기반 복제
쿼리에 NOW()나 RAND()가 있는 경우 복제 서버마다 다른 값을 생성할 가능성이 존재
쿼리가 정확히 같은 순서로 실행되어야 한다
쓰기 전 로그 배송
논리적(로우 기반) 로그 복제: Mysql에서 사용하는 방식
트리거 기반 복제
복제 지연 문제
Eventual Consistency: 시간이 지나면 모든 복제본이 같은 값을 가짐을 보장
쓰기 후 읽기 일관성: 모든 사용자가 제출한 모든 갱신을 볼 수 있음을 시스템이 보장해야 한다.
동기식을 쓰면 쉽게 해결될 수 있지만, 위의 기술한 문제로 인해 보통 비동기식을 사용하며 읽기 일관성을 지키려고 한다
해결 방법은 여러 가지가 될 수 있다. 해당 사용자가 수정된 내용을 읽는 경우에는 리더에서 읽거나, 좀 더 smart하게는 마지막 갱신 시간을 기준으로 1분 이상 늦은 팔로워에는 질의를 금지할 수도 있다.
Monotionic read(단조 읽기): 단조 읽기는 아래와 같은 이상 현상이 발생하지 않음을 보장한다. 사용자 ID의 Hash 기반으로 복제 서버를 선택해서, 같은 사용자일 경우 동일한 복제 서버에서 수행되도록 하는 방법으로도 구현이 가능하다.
첫번째 질의에서는 업데이트가 반영된 팔로워의 데이터를 보지만, 두번째 질의에서는 업데이트가 반영되지 않은 팔로워의 데이터를 보는 경우
일관된 순서로 읽기
다중 리더 복제
단일 리더일 경우 리더가 문제가 생기면, failover하는 동안 쓰기를 할 수 없다. 그래서 리더 기반 복제 모델에서는 리더를 하나 이상 두는 것이 자연스럽다.
다중 리더의 경우 성능/데이터센터 중단/네트워크 문제 내성에 대해서도 더 잘 견딘다.
단점
쓰기 충돌 - 동일한 데이터를 다른 두 개의 데이터 센터에서 동시에 변경하는 경우
해결책
충돌 감지 - 단일 리더 일 경우 이러한 케이스가 발생하지 않지만, 다중 리더일 경우 로컬 리더에 변경을 적용하고 비동기로 다른 리더에 전달할 때까지 기다린다.
이렇게 하면 다중 리더 복제의 주요 장점(각 복제 서버가 독립적으로 쓰기 허용)을 잃음
충돌 회피 - 특정 레코드의 모든 쓰기가 동일한 리더를 거치도록 애플리케이션이 보장
일관된 상태 수렴 - 데이터베이스는 결과적으로 수렴 방식으로 충돌을 해소하여 일관된 값을 보장해야 한다
수렴 충돌 해소 방법 - LWW(Last Write wins), 높은 복제 서버 쓰기를 무조건 택하는 방식 등등
사용자 정의 충돌 해소 로직
쓰기 수행 중 - 복제된 변경사항 로그에서 데이터베이스 시스템이 충돌 감지하자마자 충돌 핸들러를 호출한다
읽기 수행 중 - 충돌을 감지하면 모든 충돌 쓰기를 저장한다. 그리고 그 버전들 중에 하나를 택하는 식으로 충돌을 해소하고 다시 저장.
다중 리더 복제 토폴로지
복제 토폴로지는 쓰기를 한 노드에서 다른 노드로 전달하는 통신 경로를 설명한다.
가장 일반적인 토폴로지는 전체 연결이다. Mysql는 기본적으로 원형 토폴로지만 제공
무한 루프를 방지하기 위해, 각 노드에는 고유 식별자가 있고 복제 로그에서 각 쓰기는 거치는 모든 노드의 식별자가 태깅된다.
일반적으로 전체 연결 토폴로지가 원형 토폴로지나 별 모양 토폴로지보다 내결함성이 훨씬 좋다. 다만, “일관된 순서로 읽기”가 깨질 여지가 있다. “버전 벡터”라는 기법이 해결책이 될 수는 있다.
하지만 많은 다중 리더 복제 시스템에서 충돌 감지 기법은 제대로 구현되지 않았다.
리더 없는 복제
일부 리더 없는 복제 구현에서는 클라이언트가 여러 복제 서버에 쓰기를 직접 전송하는 반면 코디네이터 노드가 클라이언트를 대신해 이를 수행하기도 한다.
리더 없는 복제에서는 정족수만 넘으면 사용 불가능한 노드를 용인하고, 읽기와 쓰기를 수행한다.
3대 중 2대의 최신 데이터를 쓸 수 없었더라도, 모든 노드로부터 데이터를 읽고 최신 데이터를 택하는 방식
w + r > n (w: 쓰기 성공 노드 수, r: 읽기 노드 수, n: 모든 복제 서버 수)
가장 많이 사용하는 리더(프라이머리)를 기반으로 한 복제는 일반적으로 가장 많이 쓰이는 방식이라고 한다. ( 주로 RDB에서 많이 쓰는 방식인 듯 )
카프카나 래빗MQ에서도 리더 기반 복제를 사용한다고 함
일반적으로 리더 기반 복제에선 비동기 복제를 사용한다고 함 ( 합리적인 것 같음 )
새로운 노드가 추가될 경우의 복제 방식은 다음과 같다.
일단 현재 DB의 데이터베이스 스냅샷을 가져온다.
해당 스냅숏을 새로운 팔로워 노드에 복사한다.
이후 변경된 데이터들을 요청해서 가져온다.
해당 위치의 명칭은 DB마다 다른데, 포스트그레는 log sqeuence요, MySQL은 binlog coordinate라고 한다. ( 신기하다!! )
궁금한 점은, 이걸 애플리케이션으로 구현한다면 어떤 접근으로 해볼 수 있을까?
리더 기반 복제에서의 노드 중단 처리는 다음과 같다.
팔로워는 로컬 디스크에 저장하니까 죽으면, 유실된 시점부터 마스터로 다시 재요청
리더는 팔로워 중 하나를 리더로 승격시킴
리더의 경우 팔로워보단 조금 더 고려할 사항이 많음
리더 기반 복제의 경우 구문 기반 복제 또는 쓰기 전 로그 배송, 논리적(로우 기반) 로그 복제, 트리거 기반 로그 복제가 있다.
복제 지연 문제 😢 😢 😢
복제 지연은 일반적으로 아주 짧은 순간이지만, 시스템이 가용량 근처에서 동작하거나 네트워크 문제가 있으면 수 초, 수 분으로 증가할 수 있다고 한다.
이 현상들의 세 가지 상황과 해법을 간략히 정리한다.
자신이 쓴 내용 읽기라는 방법이 있는데 SNS에서 자신의 프로필 정보는 Master에서 읽고 남의 프로필 정보는 Slave에서 읽는 방식이다.
단조 읽기는 비동기식 팔로워에서 발생할 수 있는 시간이 거꾸로 흐르는 현상인데, 이를 막기 위해 이전에 새로운 데이터를 읽은 후에는 예전 데이터를 읽기 장ㄶ는 방식이다. 이를 달성하기 위해서 사용자의 읽기는 항상 같은 복제 서버에서 수행되게끔 하는 것이다. ( 해싱같은걸 써서 )
일관된 순서로 읽기는, 순서가 뒤섞여서 제대로 보이지 않는 문제인데 일반적으로 파티셔닝된 DB에서 주로 발생한다.
해결책으로는, 서로 인과성이 있는 쓰기의 경우 동일한 파티션에 기록하게 하는 방법이 있다.
저자는 복제 지연 해결을 위한 해결책으로 "복제가 비동기지만 동기식으로 동작하는 척"하게 하는 것이 해결책이라고 얘기한다.
트랜잭션이 성능과 가용성 측면에서 너무 비싸고 확장 가능 시스템에서는 어쩔 수 없이 Eventual Consistency를 채택할 수 밖에 없다는 의견에 대해서도 구체적으로 설명할 예정이라고 한다.
개인적으로 이 트랜잭션에 대한 내용이 크게 공감이 갔다.
서비스 개발자 입장에선 Eventual Consistency가 가능한 서비스가 있고 아닌 서비스가 있는게 아닐까 싶기도 하다. ( 기획으로 풀거나..? 하지만 기획자들은 그런걸 싫어하는 듯 하다. )
다중 데이터 센터 운영 방식은, 다른 데이터센터에 복사본이 있고, 각 데이터 센터 내에서는 리더 팔로워 복제를 쓰고, 데이터 센터 간에는 각 데이터 센터의 리더가 다른 데이터 센터 리데에게 변경 사항을 복제하는 방식이다.
쓰기 충돌이 발생하는 경우는 두 리더가 모두 라이트를 하는 상황일 경우에 발생할 수 있다.
복제 토폴로지는 원형, 별, 전체 연결이 있는데 MySQL은 원형 토폴뢰만 지원한다.
리더 없는 복제는 복사본 전달을 위한 코디네이터가 존재한다.
다이나모 스타일에서는 n개의 복제 서버 중 w개의 노드에서 성공해야 쓰기가 확정되고 모든 읽기는 최소한 r개의 노드에 질의한다는 공식이 존재한다.
따라서 이는 w+ r > n이면 최신 값을 얻을 것으로 기대한다.
근데 어디까지나 기대여서 확실하진 않은 것 같고, 결국 데이터마다 버저닝을 해서 최신 데이터의 유무를 확인하는 수 밖엔 없을 것 같다.
끄적끄적
정리
- 데이터 복제가 필요한 이유 - 지리적으로 가까운 곳에 데이터를 유지시켜서 지연 시간을 줄임 ⇒ CDN - 시스템의 일부 장애에도 지속적으로 동작할 수 있게 가용성 증가 ⇒ HA(High Available) - 읽기를 위한 장비의 수를 늘림 ⇒ ReadOnly Node - 데이터 복제의 어려움 ⇒ 데이터 변경을 어떻게 처리할지? - Single-leader 복제 - Multi-leader 복제 - Leaderless 복제 ### 리더와 팔로워 - 리더(a.k.a 마스터, 프라이머리)에 쓰기 요청 - 팔로워(a.k.a 슬레이브, 세컨더리 등)가 복제로그나 변경 스트림의 일부를 받아 수정 ![image](https://github.com/DevSprout/data-oriented-architecture/assets/3251003/a2846637-f9f6-4685-94a9-11c10483514d) - 동기식 복제 - 복제가 제대로 이루어졌는지 확인하는 방식 - 장점) 일관성 있는 최신 데이터 복사본 유지 - 단점) 복제가 완료될 때까지 대기하는 비용 발생 - 비동기식 복제 - 장점) 빠른 응답으로 고가용성 확보 - 단점) 동기식에 비해 데이터 일관성이 떨어짐 - 반동기식(semi-synchronous) 복제 - 두 방식을 혼합하여 몇개의 레플리카만 동기식 복제를 지원함. 나머지 레플리카는 비동기식 - **Why?)** 리더가 중단되었을 때, 데이터가 완전히 유실되는 경우가 발생하기 때문에 ### 새로운 팔로워 설정 - 새로운 팔로워가 필요한 이유 - 복제 서버 수를 늘려야할 때 - 장애 노드를 대체해야할 때 - DB 중단 없이 새로운 팔로워를 추가하는 방법 1. 리더의 특정 시점 스냅샷을 토대로 팔로워 노드에 복사함 2. 팔로워는 복사한 시점 이후의 변경사항을 리더로 요청 3. 데이터 변경의 미처리분(backlog)을 모두 적용하고 나면 리더에게 따라잡았다고 알림 4. 이후 리더는 발생하는 데이터 변화를 이어서 처리할 수 있음 ### 노드 중단 처리 - 장애가 발생했거나, 장비 리부팅이 필요한 경우 노드가 중단될 수 있음 - 팔로워 장애 - 리더로부터 수신한 변경로그들을 로컬 파일로 저장해둠 - 이후 다시 복구할 때, 로컬 파일과 리더의 Backlog를 요청해서 복구 가능 - 리더 장애 - 팔로워 중 하나를 리더로 승격 - 클라이언트는 새로운 리더로 쓰기를 시작 - 다른 팔로워들은 새로운 리더로부터 변경사항을 받기 시작 - 이 과정은 수동/자동 모두 지원함 ### 리더 노드 장애 복구 과정 - 리더 노드 장애 감지 - 노드 간에 주고받는 메시지 타임아웃을 통해 노드가 죽었는지를 판단함 - 새로운 리더 선택하는 방법 1. 복제 노드들이 새로운 리더를 선출 2. 제어 노드(Controller Node)가 새로운 리더를 선출 3. 가장 최신 데이터를 가지고 있는 팔로워 노드가 새로운 리더로 가장 적합함 - 새로운 리더 사용을 위해서 시스템 재설정 - 클라이언트에 리더 노드 변경을 알림 - 팔로워 데이터 변경 미처리분(Backlog) 초기화 - 중단됐던 리더 노드가 다시 복구되면 팔로워 노드로서 동작함 ### 자동 장애 복구 위험성 - 내구성을 보장하지 않음 - 승격된 팔로워가 이전 리더의 최신 변경사항 중 일부를 받지 못한다면 - 이후 이전 리더가 다시 복구된다면 이 변경사항은 어떻게 할 것인가? - ⇒ 일반적으로 폐기하게됨 - 이렇기 때문에 클라이언트 입장에서 내구성을 신뢰할 수 없음 - 데이터 불일치 발생 - 리더로 승격되는 과정에 승격된 팔로워가 최신 데이터를 보장하지 않음 - 새로운 리더에는 없지만, 이전 리더에만 존재하는 데이터가 있었지만 유실됨 - 스플릿 브레인(Split Brain) - 두 노드가 자신이 리더라고 인식되는 경우가 발생 - 두 리더가 각자 쓰기 요청을 처리하기 때문에 충돌 - 장애 감지를 위한 적절한 타임아웃 시간을 찾기 어려움 - 타임아웃이 길면, 복구에 너무 오랜 시간이 소요됨 - 타임아웃이 짧으면, 불필요한 장애복구가 발생함 ### 복제 로그 구현 - 구문 기반 복제 - 요청받은 구문을 기록하고 쓰기를 실행한 다음 구문을 팔로워에게 전송함 - SELECT, INSERT, UPDATE 같은 쿼리 자체를 전송 - 문제) 비결정적인 요인에 의해 복제가 깨질 수 있음 - NOW(), RAND() 등 서버마다 다른 값을 생성할 가능성 - Auto-increase 컬럼이나 기존 데이터에 의존하는 경우가 있음 - 순서가 다르면 구문의 효과가 다를 수 있음 ⇒ 여러 트랜잭션이 수행되는 것을 제한함 - 대안) 리더가 구문 기록 시 서버별로 다를 수 있는 값은 고정 값을 반환하도록 대체함 - 에지 케이스 대응이 어려움 - MySQL에서는 비결정성 요인이 있으면 Row 기반 복제방식으로 사용 - 쓰기 전 로그(WAL, Write-Ahead Log) 배송 - DB에 쓰기 전에 쓰기로그를 WAL에 저장함 - 리더가 팔로워에게 WAL을 전송하고 팔로워가 이를 처리함 - 로그에는 제일 저수준의 데이터를 기술함 - 디스크 블록에서 어떤 데이터를 변경했는지? - 저장소 엔진과 밀접하게 연관됨 - 리더와 팔로워가 동일한 버전을 사용해야함이 전제조건 - 소프트웨어 업그레이드 시 중단시간이 필요함 - 논리적(Row 기반) 로그 복제 - 로그를 저장소 엔진과 분리하기 위한 대안으로 복제와 저장소 엔진에 각기 다른 로그 형식을 사용한다. - **논리적 로그(logical log)** - 이와 같이 복제에서 사용하는 로그를 저장소 엔진의 물리적 데이터 표현과 구별하여 부른다. - RDBMS에서 논리적 로그는 대개 Row 단위이고, 테이블에 쓰기를 기술한 레코드 열이다. - 삽입 : 모든 컬럼의 새로운 값을 포함 - 삭제 : Row를 식별하기 위한 정보(보통 기본 키)를 포함 - 갱신 : Row를 식별하기 위한 정보 + 모든 컬럼의 새로운 값 또는 변경된 컬럼의 새로운 값 - 여러 Row를 수정하는 트랜잭션은 여러 로그 레코드를 생성한 다음 트랜잭션 커밋을 레코드에 표시 - 논리적 로그와 저장소 엔진 내부를 분리 - 하위 호환성을 유지 - 리더와 팔로워가 각기 다른 버전의 소프트웨어에서 실행 가능 - 심지어 저장소 엔진이 다를 수도 있음 - 논리적 로그는 외부 애플리케이션이 파싱하기 쉽다. - 데이터 웨어하우스와 같은 외부 시스템에 데이터베이스 내용 전송 시 유용하다. - **변경 데이터 캡쳐(CDC, change data capture)** - 트리거 기반 복제 - 트리거, 스토어드 프로시저 제공 - 트리거(Trigger) - 사용자 정의 애플리케이션 코드를 등록할 수 있다. - 데이터 변경 시 자동으로 실행 - 트리거를 통해 데이터 변경을 분리된 테이블에 로깅 - 이 테이블에 기록된 데이터 변경을 외부 프로세스가 읽고 처리한다. - 필요한 애플리케이션 로직 적용 후 다른 시스템에 데이터 변경을 복제한다. - 단점 - 트리거 기반 복제는 다른 복제 방식보다 많은 오버헤드가 있다. - 그리고 데이터베이스에 내장된 복제보다 버그나 제한 사항이 더 많이 발생한다. - 장점 - 유연성이 좋아서 매우 유용함 ## 복제 지연 문제 - 정상적인 경우 복제 지연으로 인한 데이터 불일치는 순간적으로 발생하고 복구되므로 크게 문제되지 않음 - ⇒ 순간적인 일관성을 깨지지만, **최종적 일관성은 지킬 수 있음** - 네트워크 문제가 발생하거나 복제 지연이 발생하면 불일치가 문제 됨 ### 자신이 쓴 내용 읽기 - 복제 지연으로 인해서 사용자가 자신이 삽입한 레코드를 읽기 못할 수 있음 - 쓰기 후 읽기(read-after-write) 일관성 - 사용자가 페이지를 리로딩하면 자신이 제출한 모든 갱신을 볼 수 있음을 보장함 - 대신, 다른 사용자가 제출한 것은 보장하지 않음 - 리더 기반 복제 시스템에서 쓰기 후 읽기 일관성 구현 - 사용자가 수정한 내용을 읽을 때는 리더에서 읽고, 나머지는 팔로워에서 읽음 - SNS에서 사용자 정보는 소유자 자신만 편집이 가능 **⇒ 자신의 정보는 리더에서 조회** - 다른 사용자의 정보는 팔로워에서 조회 - 시간 기준으로 판단 - 애플리케이션 내 대부분의 내용에 대해 사용자 편집이 가능하다면 적합하지 않음 - 레코드의 마지막 갱신 시각을 기준으로 리더 읽기 여부를 구분함 - 1분 이내에 갱신된 레코드는 리더에서 읽기 - 팔로워에서 복제 지연을 모니터링해서 이 시간을 조절함 - 클라이언트가 기억하는 가장 최근 쓰기의 타임스탬프 활용 - 각자 노드가 마지막 쓰기가 이뤄진 시간 타임스탬프를 관리하도록함 - 복제 서버에 아직 반영되지 않았다면 다른 서버로 요청을 패스하거나 갱신이 될 때까지 대기함 - 복제 서버가 여러 데이터센터에 분산되었다면 - 리더가 제공하는 요청은 전부 리더가 포함된 IDC로 라우팅되어야함 - 여러 디바이스 간 쓰기 후 읽기 일관성 - 사용자의 마지막 갱신 타임스탬프는 다른 디바이스에서 알 수 없음 - 메타데이터를 관리하는 중앙서버가 필요함 - 복제 서버가 여러 데이터센터 간에 분산된 경우 - 사용자 디바이스의 요청을 동일한 IDC로 라우팅하는 것을 보장해야함 ### 단조 읽기 - 팔로워 간에도 동일한 쓰기에 대해 갱신 시점 차이가 존재함 - 동일한 읽기 요청을 여러번 보내면 각기 다른 팔로워에 전달될 수 있음 - 특정 팔로워는 알 수 없는 정보가 있을 수 있어서 시간이 거꾸로 흐르는 현상을 겪을 수 있음 - 단조 읽기(monotonic read)는 이런 이상현상이 발생하지 않음을 보장함 - 단조읽기 방법 - 각 사용자의 읽기가 항상 동일한 복제 서버에서 수행되도록 함 - 사용자 ID의 해시를 기반으로 복제 서버를 선택함 - 복제 서버가 고장나면 사용자 질의를 다른 서버로 재라우팅할 필요가 있음 ### 일관된 순서로 읽기 - 파티션 간의 복제 시점에 차이가 있으면 나중에 삽입된 레코드를 먼저 확인할 가능성이 있음 ![image](https://github.com/DevSprout/data-oriented-architecture/assets/3251003/d138c31b-d130-4d22-ab81-6ed602df5026) - 일관된 순서로 읽기(Consistence Prefix Read) **⇒ 위 이상 현상을 방지하기 위함** - 일련의 쓰기가 특정 순서로 발생한 경우 다른 사용자에게도 쓰기에 대해 쓰여진 순서로 읽는 것을 보장 - **인과성의 위반** - 파티셔닝(or 샤딩)된 DB에서 발생하는 특징적인 문제 - 많은 분산 DB에서 파티션은 서로 독립적으로 동작함 - 인과성이 있는 쓰기는 동일한 파티션에 기록되도록 하는 방법 - 인과성을 명시하기 위한 알고리즘을 도입 ### 복제 지연을 위한 해결책 - 쓰기 후 읽기와 같은 강한 보장을 제공하도록 설계 - 비동기식 복제를 사용하지만 동기식 방식으로 동작하는 것처럼 보임 - 애플리케이션이 DB보다 더 강력한 보장을 제공하도록 설계 - 특정 종류의 리더에서 읽기를 수행한다던지.. - 애플리케이션에 다루기 복잡해서 잘못될 가능성이 높음 - 트랜잭션 - 애플리케이션이 단순해지기 위해 DB가 더 강력한 보장을 제공함 - 애플리케이션 개발자가 직접 복제 문제를 걱정하지 않아도 됨 - 항상 DB를 신뢰할 수 있음 - 분산 시스템에서의 트랜잭션 - 분산 DB에서 트랜잭션은 가용성이나 성능 측면에서 너무 비쌈 - 확장 가능한 시스템에서 최종적 일관성을 선택하는 것은 불가피하다는 주장도 있음 ## 다중 리더 복제 - 쓰기 처리를 하는 각 노드는 데이터 변경을 모든 노드에 전달하는데 이를 다중 리더로 설정 - 각 리더는 리더 역할과 동시에 다른 리더의 팔로워 역할도 함 - 둘 중 하나의 리더가 죽더라도 쓰기가 가능해짐 ### 단일 리더 vs 다중 리더 - 성능 - 단일 리더 : 리더가 있는 IDC로 이동해야해서 쓰기 지연 발생 - 다중 리더 : 로컬 IDC에서 처리 후 비동기 방식으로 다른 IDC에 복제 - IDC 장애 시 - 단일 리더 : 리더가 있는 IDC에 장애가 나면 다른 IDC 팔로워가 리더로 승진 - 다중 리더 : IDC에 독립적으로 동작하기 때문에 고장난 IDC가 온라인으로 돌아왔을 때 복제 - 네트워크 문제 내성 - 단일 리더 : IDC 내 쓰기는 동기식으로 연결 문제에 민감함 - 다중 리더 : 비동기 복제를 사용해 네트워크 문제에 보다 잘 견딤, 일시적 네트워크 중단에도 쓰기 처리는 진행되기 때문 ## 쓰기 충돌 다루기 - 오프라인 작업을 하는 클라이언트 - 인터넷이 끊어진 동안 앱이 동작해야하는 경우 - 협업 편집 : 동시에 여러 사람이 편집할 수 있는 애플리케이션 - 각 사용자가 동시에 편집 후 로컬 리더에 저장하였으나 변경을 비동기로 복제 시 쓰기 충돌 발생 - 동기식으로 충돌 감지를 하면 다중 리더 복제의 장점을 잃어버림 ### 충돌 회피 - 특정 레코드의 모든 쓰기를 동일한 IDC의 리더에서 처리함 - 특정 사용자의 요청을 동일한 IDC로 라우팅하지 않으면 충돌회피가 실패함 ### 일관된 상태 수렴 - 모든 복제 서버가 동일해야 함이 원칙 - 수렴(convergent) : 모든 변경이 복제돼 모든 복제 서버에 동일한 최종 값이 전달되게 해야함 - 수렴 충돌 해소 방법들 1. 각 쓰기에 고유 ID를 부여해 가장 높은 ID를 가진 쓰기를 선택 - 대중적이지만 데이터 유실 위험이 있음 2. 각 복제 서버에 고유 ID를 부여하고 높은 숫자의 서버에서 생긴 쓰기가 낮은 숫자의 서버에서 생긴 쓰기보다 항상 우선적으로 적용 - 데이터 유실 가능성이 존재함 3. 어떻게든 값을 병합 - 어떤 기준을 세워서 값을 병합하도록 함 4. 명시적 데이터 구조에 충돌을 기록해 모든 정보를 보존 - 나중에 사용자가 직접 충돌을 처리하도록 모든 정보를 보존함 ### 사용자 정의 충돌 해소 로직 - 충돌 해소의 가장 적합한 방법은 애플리케이션에 따라 다름 - 쓰기 수행 중 - 복제된 변경 로그에서 DB 시스템 충돌이 감지되면 핸들러 호출 - 백그라운드에서 실행 - 읽기 수행 중 - 충돌 감지 시 모든 충돌 쓰기 저장 - 다음 번 읽기 시 여러 데이터 반환. 애플리케이션은 사용자에게 충돌 내용을 보여주거나 자동으로 충돌 해소해서 결과를 DB에 기록함 - 카우치 DB가 동작하는 방식 ### 자동 충돌 해소 - 충돌 없는 복제 데이터 타입을 사용ㅎ - Set, Map, 정렬 목록, 카운터 등을 위한 데이터 구조의 집합 - 병합 가능한 영속 데이터 구조 - Git처럼 명시적으로 히스토리를 추적하고 3-way merge를 사용함 - 구글 닥스 같은 협업 편집 애플리케이션의 충돌 해소 알고리즘 ## 다중 리더 복제 토폴로지 - 복제 토폴로지는 쓰기를 한 노드에서 다른 노드로 전달하는 통신 경로 - 리더가 둘 이상이라면 다양한 토폴로지가 가능 ![image](https://github.com/DevSprout/data-oriented-architecture/assets/3251003/57cf64b7-6b36-466e-bd36-99de0302ecaa) ### 원형 토폴로지 - 각 노드가 하나의 노드로부터 쓰기를 받고, 다른 노드에 전달 (MySQL의 기본 제공) - 노드 장애 시 노드 간 복제 메시지 흐름에 방해를 줌 ### 별 모양 토폴로지 - 지정된 루트 노드 하나가 다른 모든 노드에 쓰기 전달 - 트리로 일반화 가능 - 노드 장애 시 노드 간 복제 메시지 흐름에 방해를 줌 ### 전체 연결 토폴로지 - 모든 리더가 각자의 쓰기를 다른 모든 리더에 전송 - 가장 일반적인 토폴로지 - 내결함성이 상대적으로 좋음 - 문제점 - 네트워크 연결 속도 차이로 복제 메시지 추월 발생 - 삽입 이전에 갱신을 처리하게 됨 - 올바른 이벤트 정렬을 위한 버전 벡터 기법으로 해결 가능 ## 리더 없는 복제 - 일부 DB 시스템은 리더의 개념을 보리고 모든 복제 서버가 클라이언트로부터 쓰기를 직접하는 방식을 사용하기도 함 - Dynamo-style DB로 리악, 카산드라, 볼드모트 등 - 일부 리더 없는 복제 구현에서는 클라이언트가 복제 서버에 쓰기를 직접 전송하는 반면 코디네이터 노드가 클라이언트를 대싱해서 수행하기도 함 ### 노드가 다운됐을 때 DB에 쓰기 - 다운된 노드에서 쓰기가 누락되어 오래된(유효하지 않은) 값을 읽게 됨 - 읽기 요청을 병렬로 여러 노드에 전송해 최신 값을 읽어와 해결 가능 - 버전 숫자를 통해 읽어온 값 중 최신 값을 결정함 ### 읽기 복구와 안티 엔트로피 복제 계획 - 최종적으로 모든 데이터가 모든 복제 서버에 복사된 것을 보장해야 함 - 읽기 복구 - 클라이언트가 여러 노드에서 병렬로 읽기 수행하면 오래된 응답 감지 가능 - 복제 서버의 오래된 값을 새로운 값으로 기록 - 값을 자주 읽는 상황에 적합 - 안티 엔트로피 처리 - 백그라운드 프로세스와 복제 서버 간 데이터 차이를 찾아 누락된 데이터를 복사 - 특정 순서로 쓰기를 복사하기 때문에 지연이 있을 수 있음 ### 정족수(Quorum) - 합의체가 의사(議事)를 진행시키거나 의결을 하는 데 필요한 최소한도의 인원수 - 유효한 읽기와 쓰기를 위한 복제서버 수, 쓰기 성공 노드 수, 질의 노드 수 - 복제서버(n), 쓰기 노드(w), 읽기 노드(r) 설정 가능 - 일반적으로 n은 홀수, w = r = (n+1) / 2 (반올림) 설정 - 일반적으로 읽기와 쓰기는 항상 모든 n개의 복제 서버에 병렬 전송함 - w, r은 기다릴 노드를 결정함 - 읽기, 쓰기 성공 여부는 읽기, 쓰기가 성공한 노드의 개수로 확인함 - w, r 보다 사용가능한 노드 수가 적다면 에러를 반환함 ### 정족수 일관성의 한계 - w + r > n 으로 설정하면 읽은 노드 중 최신 값을 가진 노드가 하나 이상이어야 함 - 그러나 모든 과정이 올바르게 동작해도 시점 문제로 오래된 값을 반환할 수 있음 - 즉, 정족수를 아무리 잘 설정해도 오래된 값을 읽을 가능성이 있음 ### 느슨한 정족수와 암시된 핸드오프 - 정족수 불충족 - 네트워크 중단으로 DB 노드와 클라이언트 연결 유실 - 응답 가능한 노드가 w, r보다 적을 수 있음 - 느슨한 정족수 - 정족수 불충족 상황에서 보통 저장하는 노드가 아닌 연결이 가능한 다른 노드에 쓰기를 하는 경우 - 암시된 핸드오프 - 네트워크 장애가 해제되면 일시적으로 수용한 모든 쓰기를 해당 홈 노드로 전송 ### 동시 쓰기 감지 - 여러 클라이언트가 동시에 같은 키에 쓰는 것을 허용해 엄격한 정족수를 사용해도 충돌이 발생할 수 있음 - 문제는 네트워크 지연 등으로 이벤트가 다른 노드에 다른 순서로 도착할 수 있음 - 최종 쓰기 승리 (동시 쓰기 버리기) - 복제본을 가장 최신 값으로 덮어 쓰는 방법 - 쓰기에 타임스탬프를 붙여서 최신 값을 선택하는 방법 (LWW) - 손신 데이터를 허용하지 않는다면 LWW가 부적합 - 카산드라에서 유일하게 제공하는 충돌 해소 방법, 리악에서는 선택적 기능 - 키를 한번만 쓰고 이후에 불변값으로 만들어 동시에 같은 키를 갱신하는 상황을 방지해야 함 - 카산드라 사용 시 키로 UUID를 사용해 모든 쓰기 작업에 고유한 키를 부여하는 것을 추천함 - 버전 벡터 - 모든 복제본의 버전 번호 모음을 나타냄 - 값을 읽을 때 데이터베이스 복제본에서 클라이언트로, 값이 기록될 때 데이터베이스로 전송 - 쓰기 시 버전 번호 증가, 다른 복제본의 번호도 추적해 덮어쓸 값, 형제 값을 구분함 - 데이터베이스는 덮어쓰기와 동시 쓰기를 구분할 수 있음