DevSprout / data-oriented-architecture

데이터 중심 애플리케이션 설계
4 stars 0 forks source link

05장: 복제 #5

Open MinJunKweon opened 6 months ago

MinJunKweon commented 6 months ago

끄적끄적

정리 - 데이터 복제가 필요한 이유 - 지리적으로 가까운 곳에 데이터를 유지시켜서 지연 시간을 줄임 ⇒ 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를 사용해 모든 쓰기 작업에 고유한 키를 부여하는 것을 추천함 - 버전 벡터 - 모든 복제본의 버전 번호 모음을 나타냄 - 값을 읽을 때 데이터베이스 복제본에서 클라이언트로, 값이 기록될 때 데이터베이스로 전송 - 쓰기 시 버전 번호 증가, 다른 복제본의 번호도 추적해 덮어쓸 값, 형제 값을 구분함 - 데이터베이스는 덮어쓰기와 동시 쓰기를 구분할 수 있음
LOG-INFO commented 6 months ago

끄적끄적

HaeUlNam commented 6 months ago

끄적끄적

minkukjo commented 6 months ago

끄적 끄적