데이터 중심 애플리케이션 설계 | 05장. 복제
발표자 : 이호준, 조성직
Part 2. 분산 데이터
1부에서는 단일 장비에서 데이터를 저장할 때 적용하는 데이터 시스템 측면을 살펴보았다.
2부에서는 여러 장비가 관여하는 경우에 대해서 다룬다.
그런데 여러 장비가 관여해야 하는 이유는 무엇일까? 그 이유는 다음과 같다.
확장성
데이터 볼륨, 읽기/쓰기 부하가 단일 장비에서 다룰 수 있는 양보다 커지는 경우 부하를 여러 장비로 분산한다.
확장성(Scalability) : 시스템이 특정한 요구사항을(요구 처리량의 변화와 같은) 충족하기 위해 애플리케이션의 상당 부분을 변경하지 않고도 하드웨어 수준의 변경(추가/감소, 업그레이드/다운그레이드) 등을 통해 시스템을 확장 또는 축소할 수 있는 능력 또는 그 정도를 의미함
내결함성/고가용성
장비 하나가 죽더라도 다른 장비가 존재하면 애플리케이션이 계속해서 동작할 수 있다.
가용성(Availability) : 서버, 네트워크와 같은 시스템이 정상적으로 사용가능한 정도
고가용성(HA, High Availability) : 위 가용성이 굉장히 높은 경우(>= 99.9999..) 또는 이에 근접하다면 시스템이 고가용성을 지녔다고 표현한다.
내결함성(FT, Fault Tolerance) : 결함이 발생하더라도 시스템이 정상적으로 동작할 수 있는 경우 내결함성을 지녔다고 표현한다. 가용성의 관점에서 다운 타임이 0인 것을 의미.
지연 시간 전 세계에 사용자가 있는 경우 사용자와 지리적으로 가까운 곳의 데이터센터에서 서비스를 제공하기 위해 전 세계 다양한 곳에 서버를 둘 필요가 있다. 이를 통해 서비스 지연 시간을 단축시킨다.
수직 확장과 수평 확장
수직 확장(scale up)
- 더 강력한 장비를 사용하는 것
- 많은 CPU, 메모리 칩, 디스크를 하나의 운영체제로 결합한다.
- 빠른 상호 연결로 모든 CPU가 메모리나 디스크의 모든 부분에 접근한다.
- 공유 메모리 아키텍처
- 모든 구성 요소를 단일 장비로 다룬다.
- 비용 대비 성능 측면에서 가성비가 좋지 않다.
- 제한적인 내결함성 제공 : 하이엔드 장비는 핫 스왑이 가능한 구성요소가 있다.
- 공유 디스크 아키텍처
- 독립적인 CPU, RAM을 탑재한 여러 장비를 사용
- 그러나 디스크는 공유한다.
- 각 장비는 고속 네트워크로 연결된다.
- 일부 데이터 웨어하우스 작업 부하에 사용된다.
- 잠금 경합과 오버헤드 등으로 인해 확장성에 제한이 있다.
수평 확장(scale out)
- 노드
- 데이터베이스 소프트웨어를 수행하는 각 장비 혹은 가상 장비
- 각 노드는 CPU, RAM, 디스크를 독립적으로 사용
- 노드 간의 코디네이션은 네트워크를 사용하여 소프트웨어 수준에서 수행된다.
- 비공유 아키텍처
- 특별한 하드웨어를 필요로 하지 않음 → 가성비를 갖춘 시스템을 사용할 수 있다.
- 여러 지리적인 영역에 데이터를 분산한다.
- 사용자 지연 시간을 줄일 수 있다.
- 전체 데이터 센터의 손실을 줄일 수 있다.
- 주의할 점이 있다. → 이 장에서 다루는 이유
- 분산 시스템에서 발생하는 제약 조건과 트레이드오프가 존재
- 부가적인 애플리케이션 복잡도를 야기
복제
복제와 필요한 이유
복제란 네트워크로 연결된 여러 장비에 동일한 데이터의 복사본을 유지하는 것을 말한다.
복제가 필요한 이유는 다음과 같다.
- 지리적으로 사용자와 가깝게 데이터를 유지 → 지연시간을 줄인다.
- 시스템 일부에 장애 발생해도 지속적으로 동작 → 내결함성, 고가용성 달성
- 읽기 질의를 제공하는 장비의 수 확장 → 읽기 처리량 향상
Q. 복제는 왜 어려운가?
A. 데이터가 변경되기 때문. 복제에서 오는 어려움은 복제된 데이터의 변경 처리와 관련이 있다.
복제 알고리즘
- 단일 리더(single-leader)
- 다중 리더(multi-leader)
- 리더가 없음(leaderless)
- 각 알고리즘은 장단점이 있음
리더와 팔로워
복제 서버(replica) 데이터 베이스의 복사본을 저장하는 각 노드
Q. 모든 복제 서버에 모든 데이터가 있다는 사실을 어떻게 보장할까?
A. 일반적인 해결방법 : 리더 기반 복제(leader-based replication)
그림 5-1. 리더 기반(마스터 슬레이브) 복제
리더 기반 복제(leader-based replication)
- = 마스터 슬레이브 복제(master slave replication), 능동/수동 복제(active/passive replication)
- 리더(leader)
- = 마스터, 프라이머리(primary)
- 클라이언트의 쓰기, 읽기 요청을 처리
- 쓰기 기록 후 팔로워에게 이를 전달
- 팔로워(follower)
- = 읽기 복제 서버(read replica), 슬레이브, 2차(secondary), 핫 대기(hot standby)
- 리더가 보낸 데이터 변경 로그, 변경 스트림을 전달 받아 데이터 복제본을 갱신
- 클라이언트의 읽기 요청 만을 처리
- RDBMS, NoSQL, 메시지 브로커 등에 사용
동기식 대 비동기식 복제
복제가 동기식으로 발생하는지, 비동기식으로 발생하는 지는 복제 시스템에서 중요한 세부사항이다.
그림 5-2. 한 팔로워는 동기식, 다른 팔로워는 비동기식인 리던 기반 복제
동기식 복제
- (그림 5-2) 팔로워1의 경우
- 리더가 팔로워의 쓰기 수신에 대한 응답을 확인하기 위해 대기한다.
- 확인이 끝나면 사용자에게 성공을 보고 후 해당 쓰기를 보여준다.
이 방식의 장단점은 다음과 같다.
- 리더와 팔로워의 일관성(최신 데이터)을 보장한다.
- 팔로워가 (죽거나 네트워크 등의 문제로)응답하지 않을 시 쓰기가 처리될 수 없다.
- 리더는 모든 쓰기를 차단(block)하고 팔로워가 사용가능할 때 까지 기다려야 한다.
비동기식 복제
- (그림 5-2) 팔로워2의 경우
- 리더가 팔로워의 쓰기 수신에 대한 응답을 기다리지 않는다.
- 일관성은 떨어지지만, 사용자 응답 지연 시간이 적고 고가용성 제공
동기식, 비동기식을 바탕으로 한 복제 구성 방식에는 다음과 같은 방식이 있다.
반동기식(semi-synchronous)
- 모든 팔로워가 동기식 복제 방식을 사용할 수는 없다. 하나의 노드만 고장나도 전체 시스템이 마비.
- 팔로워 하나는 동기식, 나머지는 비동기 식으로 구성하는 것을 의미
- 동기식 팔로워가 사용 불가 시 → 다른 비동기 팔로워가 동기식으로 동작
- 적어도 두 노드(리더, 동기식 팔로워)에 데이터의 최신 복사본이 존재
완전한 비동기식 리더 기반 복제 방식에서 일반적으로 선택
이 방식의 장단점은 다음과 같다.
- 리더가 잘못되고 복구 불가능한 경우 팔로워에 복제되지 않은 쓰기는 유실
- 클라이언트가 어떤 쓰기를 확인했어도 해당 쓰기의 지속성을 보장할 수 없음
- 모든 팔로워가 잘못되어도 리더가 쓰기 처리를 계속할 수 있다.
- 내구성이 약하다는 단점에도 불구하고 다음과 같은 조건 하에 많이 사용된다.
- 많은 팔로워가 존재하는 경우
- 노드가 지리적으로 분산된 경우
새로운 팔로워 설정
새로운 팔로워 설정이 필요한 경우가 있다.
- 복제 서버의 수를 늘려야 하는 경우
- 장애 노드를 대체하는 경우
Q. 새로운 팔로워의 추가 설정은 어떻게 진행해야 할까?
A1. 데이터 파일을 복사한다. (X)
- 복사하는 와중에도 클라이언트의 쓰기 요청은 계속 발생한다. 파일의 복사본은 유효하지 않은(out-of-date) 데이터를 포함한다.
A2. 일관성을 보장하기 위해 데이터베이스를 잠가 잠시동안 쓰기를 막는다. (X)
- 고가용성 목표에 부합하지 않는다.
A3. 다음과 같이 중단 없이 팔로워 설정을 수행한다. (O)
- 데이터베이스를 잠그지 않고 리더의 데이터베이스 스냅샷을 가져온다.
- 스냅샷을 새로운 팔로워 노드에 복사한다.
- 이후 팔로워는 리더에 연결해 스냅샷 이후 발생한 모든 데이터 변경 내역을 요청한다.
- 요청한 데이터 변경 미처리분(backlog)을 모두 처리하면 팔로워가 리더를 따라잡았다고 말한다.
- 이제 팔로워는 리더의 데이터 변경을 처리할 수 있다.
노드 중단 처리
시스템의 모든 노드는 다음과 같은 사유로 중단될 수 있다.
- 장애
- 계획된 유지보수
- ex. 커널 보안 패치를 위한 장비 리부팅
단일 노드는 중단 되더라도 전체 시스템은 중단되지 않고 서비스되어야 한다.
개별 노드의 장애애도 전체 시스템이 잘 동작하고 노드 중단의 영향을 최소화하는 것이 목표다.
리더 기반 복제에서 고가용성은 어떻게 달성할 수 있을까?
팔로워 장애: 따라잡기 복구 팔로워에 장애가 나는 경우. → 팔로워가 리더를 따라잡음으로써 복구한다.
- 팔로워는 리더로부터 수신한 데이터 변경 로그를 로컬 디스크에 보관
- 팔로워가 죽거나, 네트워크 중단 등으로 복구하는 경우 로그를 이용하여 복구를 시작할 수 있다.
- 로그에서 마지막으로 처리된 트랜잭션을 찾는다.
- 해당 트랜잭션 이후의 데이터 변경 내역을 리더에 요청
- 요청한 변경 내역을 모두 적용하면 리더를 따라잡아 복구가 완료된다.
리더 장애: 장애 복구(failover) 장애 복구 과정은 다음과 같이 진행한다.
- 팔로워 중 하나를 새로운 리더로 승격
- 클라이언트는 새로운 리더로 쓰기를 전송해야 함. → 재설정이 필요하다.
- 다른 팔로워는 새로운 리더로부터 데이터 변경을 소비하기 시작
- 수동 또는 자동으로 진행한다.
수동으로 복구하는 경우 관리자가 리더의 장애 알림을 수신 후 새로운 리더를 만들기 위한 조치를 취한다.
자동으로 복구하는 경우는 이하에서 설명한다.
자동 장애 복구 과정
자동 장애 복구는 다음과 같이 진행한다.
- 리더가 장애인지 판단한다.
- 판단할 수 있는 확실한 방법은 없음 → 보통 타임아웃을 사용
- 노드 간 메시지를 주고 받고 일정시간 응답하지 않는 노드는 죽은 것으로 간주
- 예외도 존재, 리더가 계획된 유지 보수를 위해 의도적으로 중단되는 경우
- 새로운 리더를 선택한다.
- 복제 노드들이 새로운 리더를 선출
- 또는 제어 노드(controller node)가 새로운 리더를 임명
- 최신 데이터 변경사항을 가진 복제 서버가 새로운 리더의 가장 적합한 후보로 지목된다.
- 새로운 리더 사용을 위해 시스템을 재설정한다.
- 클라이언트의 쓰기 요청, 팔로워의 데이터 변경 로그 재설정
- 이전 리더가 복구되는 경우 이전 리더가 새로운 리더를 인식하고 자신은 팔로워가 된다.
자동 장애 복구 과정에서의 위험
다양한 위험이 존재한다. 그래서 수동 장애 복구 방식을 선호하는 운영팀도 존재한다.
- 내구성을 보장하지 않음
- 비동기식 복제 사용 시 새로운 리더는 이전 리더의 최신 쓰기 중 일부를 받지 못했을 수 있음
- 이전 리더가 다시 클러스터에 추가되면 해당 최신 쓰기 내역은 어떻게 해야 하는가?
- 새로운 리더가 이와 충돌하는 쓰기를 받았을 수 있음 → 해당 쓰기를 폐기하는 것이 일반적
- 클라이언트 입장에서 내구성을 신뢰할 수 없음.
- 쓰기를 폐기하는 방식의 경우 DB 외부의 다른 저장소 시스템에서 DB 내용에 맞춰 조정되는 경우 문제 발생
- Github 실제 사례, MySQL과 Redis 간 데이터 불일치
- MySQL의 팔로워가 리더로 승격. 이 팔로워가 이전 리더의 쓰기 내역을 완벽히 갱신하지 못함
- 새로운 리더에는 없고, 이전 리더에만 존재하는 기본키를 레디스에서는 사용하고 있었음.
- 새로운 팔로워는 이미 할당된 기본키를 재사용 → 레디스와 MySQL간의 데이터 불일치 발생
- 스플릿 브레인(Split Brain)
- 특정 결함 시나리오에서 두 노드가 자신이 리더라고 인식
- 두 리더가 각자 쓰기 요청을 처리하기 때문에 쓰기 충돌이 발생
- 쓰기 충돌을 해소하지 못하면 데이터가 유실 또는 오염된다.
- 둘 이상의 리더가 감지되면 하나를 종료하는 메커니즘도 있으나 잘못 설계 시 둘 모두가 종료될 수 있음
- 죽었다고 판단하기에 적절한 타임아웃 값을 정하기가 어렵다.
- 타임아웃이 길면 → 복구에 너무 오랜 시간이 소요
- 타임아웃이 짧으면 → 불필요한 장애복구 발생
- 노드의 응답시간 조차 일시적인 부하 급증, 네트워크 문제 등으로 일정할 수 없다.
- 시스템이 높은 부하, 네트워크 문제와 씨름 중인 경우 불필요한 장애 복구는 상황을 악화시킬 수 있음.
복제 로그 구현
리더 기반 복제는 내부적으로 어떻게 동작하는가? 다음과 같은 다양한 복제 방법을 사용한다.
구문(Statement) 기반 복제
- 요청받은 구문을 기록하고 쓰기를 실행한 다음 구문을 팔로워에게 전송
- RDB : INSERT, UPDATE, DELETE …
- 비결정적인 요인에 의해 복제가 깨질 수 있다.
- NOW(), RAND() 등은 복제 서버마다 다른 값을 생성할 가능성이 존재
- 자동증가 컬럼을 사용하거나, 기존 데이터에 의존하는 경우(WHERE) 정확히 같은 순서로 실행되어야 함
- 순서가 다르면 구문의 효과가 다를 수 있음 → 동시에 여러 트랜잭션이 수행되는 것을 제한한다.
- 부수효과를 가진 구문의 경우 부수효과가 완벽히 결정적이어야 모든 팔로워에서 그 효과도 동일하다.
- 트리거, 스토어드 프로시저, 사용자 정의 함수
- 대안은 리더가 구문 기록 시 비결정적 함수 호출을 고정 값을 반환하도록 대체하는 것
- 여러 에지 케이스가 있어 현재는 다른 방식을 선호
- MySQL은 비결정성 요인이 있으면 로우 기반 복제 방식으로 변경
쓰기 전 로그(WAL, write-ahead log) 배송
- 일반적으로 데이터베이스의 모든 쓰기는 로그에 기록이 된다.
- 리더가 로그를 팔로워에게 전송하고, 팔로워는 이 로그를 처리함으로써 복제한다.
- 로그는 제일 저수준의 데이터를 기술함
- 디스크 블록에서 어떤 데이터를 변경했는 지와 같은 상세 정보 포함
- 복제 프로세스가 저장소 엔진과 밀접하게 연관된다.
- 리더와 팔로워가 동일한 소프트웨어 버전에서 실행되어야 한다.
- 소프트웨어 업그레이드 시 중단 시간이 필요하다.
- PostgresQL, Oracle
논리적(로우 기반) 로그 복제
- 로그를 저장소 엔진과 분리하기 위한 대안으로 복제와 저장소 엔진에 각기 다른 로그 형식을 사용한다.
- 논리적 로그(logical log)
- 이와 같이 복제에서 사용하는 로그를 저장소 엔진의 물리적 데이터 표현과 구별하여 부른다.
- RDBMS에서 논리적 로그는 대개 로우 단위이고, 테이블에 쓰기를 기술한 레코드 열이다.
- 삽입된 로우 : 모든 컬럼의 새로운 값을 포함
- 삭제된 로우 : 로우를 식별하기 위한 정보(보통 기본 키)를 포함
- 갱신된 로우 : 로우를 식별하기 위한 정보 + 모든 컬럼의 새로운 값 또는 변경된 컬럼의 새로운 값
- 여러 로우를 수정하는 트랜잭션은 여러 로그 레코드를 생성한 다음 트랜잭션 커밋을 레코드에 표시
- 논리적 로그와 저장소 엔진 내부를 분리
- 하위 호환성을 유지
- 리더와 팔로워가 각기 다른 버전의 소프트웨어에서 실행 가능
- 심지어 저장소 엔진이 다를 수도 있음
- 논리적 로그는 외부 애플리케이션이 파싱하기 쉽다.
- 데이터 웨어하우스와 같은 외부 시스템에 데이터베이스 내용 전송 시 유용하다.
- 변경 데이터 캡쳐(CDC, change data capture)
트리거 기반 복제
지금까지 설명한 복제 방식은 애플리케이션의 관여 없이 DB 시스템에 의해 구현되었다.
때로는 복제 방식에 유연성이 요구되며 애플리케이션이 관여할 필요가 있다.
- Oracle의 GoldenGate : 데이터베이스 로그를 읽어 애플리케이션이 데이터를 변경할 수 있도록 함
- 많은 RDBMS : 트리거, 스토어드 프로시저 제공
- 트리거(trigger)
- 사용자 정의 애플리케이션 코드를 등록할 수 있다.
- 데이터 변경 시(쓰기 트랜잭션) 자동으로 실행된다.
- 트리거를 통해 데이터 변경을 분리된 테이블에 로깅한다.
- 이 테이블에 기록된 데이터 변경을 외부 프로세스가 읽고 처리한다.
- 필요한 애플리케이션 로직 적용 후 다른 시스템에 데이터 변경을 복제한다.
- ex. Oracle의 Databus, PostgresQL의 Bucardo
트리거 방식의 장단점은 다음과 같다.
- 트리거 기반 복제는 다른 복제 방식보다 많은 오버헤드가 있다.
- 그리고 데이터베이스에 내장된 복제보다 버그나 제한 사항이 더 많이 발생한다.
- 그럼에도 불구하고 유연성 때문에 매우 유용하다.
복제 지연 문제
읽기 확장(read-scaling) 아키텍처
- 하나의 리더와 여러 팔로워로 구성
- 리더는 읽기 + 쓰기, 팔로워는 읽기 요청 처리
이 아키텍처의 장점은 다음과 같다.
- 웹 서비스의 경우 읽기 요청이 대부분, 쓰기 요청은 작은 비율로 구성
- 리더의 부하를 경감하고 복제 서버에서 읽기 요청을 처리 → 웹 서비스에 적합
- 복제의 목표 중 확장성, 지연시간 단축 달성
단점은 다음과 같다.
- 비동기식 복제 방식에서만 동작한다.
- 동기식 복제는 단일 노드의 장애, 네트워크 중단이 전체 시스템의 쓰기를 마비시켜 적절하지 않음
- 노드가 많아질 수록 다운될 가능성이 커져 동기식 설정과 적절하지 않다.
일시적인 데이터 불일치와 최종적 일관성
- 비동기 팔로워에서 데이터를 읽을 경우 지난 데이터를 읽을 수 있음 → 데이터 불일치 발생
- 불일치는 일시적이다. 결국 팔로워는 리더를 따라잡게 된다. → 최종적 일관성
Tip
최종적 일관성 (Eventual Consistency)
분산 컴퓨팅 환경에서 사용되는 일관성 모델 중 하나. 일시적으로는 데이터의 일관성이 깨지는 것을 허용한다.
그러나 최종적으로는 (데이터의 변경사항이 없다면) 데이터 대한 모든 접근들에 대해 마지막으로 갱신된 값을 반환하는 것을 보장한다.
복제 지연
- 정상적인 경우 복제 지연으로 인한 데이터 불일치는 찰나의 순간이고 크게 문제가 되지 않음
- 시스템이 가용량 근처에서 동작하거나 네트워크 문제가 있다면 복제 지연으로 인한 불일치가 문제가 된다.
복제 지연으로 인해 발생 가능한 사례는 다음과 같은 것들이 있다.
자신이 쓴 내용 읽기
복제 지연으로 인해 사용자가 자신이 제출한 레코드를 읽지 못할 수 있다.
그림 5-3. 사용자 쓰기를 한 다음 새로운 내용이 반영되지 않은 복제 서버에서 데이터를 읽는다. 이런 이상 현상을 방지하려면 쓰기 후 읽기(read-after-write) 일괄성이 필요하다.
쓰기 후 읽기(read-after-write) 일관성
- 자신의 쓰기 읽기 일관성
- 사용자가 페이지를 리로딩하면 자신이 제출한 모든 갱신을 볼수 있음을 보장한다.
- 다른 사용자가 제출한 것에 대해서는 보장하지 않는다.
그러면 어떻게 이를 구현할까?
리더 기반 복제 시스템에서 쓰기 후 읽기 일관성 구현하기
- 사용자가 수정한 내용을 읽을 때에는 리더에서 읽는다. 이 외의 경우는 팔로워에서 읽는다.
- ex. SNS
- 사용자 프로필은 소유자 자신만 편집이 가능하다. → 자신의 프로필 조회는 리더에서 조회
- 다른 사용자의 프로필은 팔로워에서 조회
- ex. SNS
- 시간을 기준으로 판단하기
- 애플리케이션 내 대부분의 내용에 대해 사용자 편집이 가능하면 첫 번째 방식은 적합하지 않다.
- 다른 기준이 필요.
- 레코드의 마지막 갱신 시각을 기준으로(ex. 1분 이내) 리더 읽기 여부를 구분하기
- 팔로워에서 복제 지연을 모니터링 하여 1분 이상 늦은 팔로워에 대한 질의 금지
- 클라이언트가 기억하는 가장 최근 쓰기의 타임스탬프를 사용하기
- 시스템은 팔로워에게 타임스탬프까지 갱신을 반영하도록 할 수 있음
- 복제서버에 아직 갱신이 반영되지 않았다면?
- 다른 복제 서버가 읽기 요청을 처리
- 복제 서버에 갱신이 반영될 때 까지 질의를 대기
- 타임스탬프는 논리적 의미의 타임스탬프 또는 실제 시스템 시간일 수 있음
- 복제 서버가 여러 데이터센터에 분산된 경우
- 사용자에게 지리적 근접성, 가용성을 보장하기 위해 설계된 경우
- 리더가 제공해야하는 요청은 전부 리더가 포함된 데이터센터로 라우팅되어야 한다.
또 다른 문제 : 디바이스 간(cross-device) 쓰기 후 읽기 일관성
- 동일한 사용자가 여러 디바이스(데스크톱 웹 브라우저, 모바일 앱)로 서비스에 접근하는 경우
- 하나의 디바이스에서 방금 입력한 정보가 다른 디바이스에서도 조회되어야 한다.
- 디바이스 간 쓰기 후 읽기 일관성이 보장되어야 한다.
멀티 디바이스 사용자 환경에서는 다음과 같은 문제를 추가적으로 고려해야 한다.
- 사용자의 마지막 갱신 타임스탬프의 경우 다른 디바이스에서는 알 수가 없다.
- 이러한 메타데이터를 중앙집중식으로 관리해야 다른 디바이스에서도 확인이 가능하다.
- 복제 서버가 여러 데이터센터 간에 분산된 경우
- 다른 디바이스의 연결이 동일한 데이터센터로 라우팅된다는 보장이 없다.
- 데스크톱 → 홈 광대역 연결, 모바일 기기 → 셀룰러 데이터 네트워크
- 각 디바이스의 네트워크 라우팅은 완전히 다르다.
- 사용자 디바이스의 요청을 동일한 데이터센터로 라우팅해야 한다.
단조 읽기
그림 5-4. 사용자는 최신 복제 서버에서 먼저 읽고 그당므 예전 복제 서버에서 읽는다. 시간 역전 현상이 나타난다. 이런 이상 현상을 방지하려면 단조 읽기가 필요하다.
시간이 거꾸로 흐르는 현상
- 팔로워 간에도 동일한 쓰기에 대해 갱신 시점의 차이가 존재한다.
- 동일한 읽기 요청을 여러번 보낼 때 각기 다른 팔로워에게 전달이 될 수 있다.
- 어떤 팔로워는 쓰기를 알고 있으나, 어떤 팔로워는 모른다면 시간이 거꾸로 흐르는 현상을 겪을 수 있다.
단조 읽기(monotonic read)
- 위와 같은 종류의 이상 현상이 발생하지 않음을 보장한다.
- 강한 일관성 보다는 덜하고, 최종적 일관성 보다는 강한 보장이다.
- 한 사용자가 여러 번에 걸쳐 읽어도 시간이 되돌아가는 현상을 경험하지 않는다.
- 이전에 새로운 데이터를 읽은 후에는 예전 데이터를 읽지 않는다.
단조 읽기를 달성하는 방법은 다음과 같다.
- 각 사용자의 읽기가 항상 동일한 복제 서버에서 수행되도록 한다.
- 사용자 ID의 해시를 기반으로 복제 서버를 선택
- 복제 서버가 고장나면 사용자 질의를 다른 복제 서버로 재라우팅할 필요가 있음
일관된 순서로 읽기
인과성의 위반 예시용 대화를 하나 보자. 다음 대화에는 인과성이 있다.
푼스 씨 : 미래에 대해 얼마나 멀리 볼 수 있나요. 케이크 부인?
케이크 부인 : 보통 10초 정도요, 푼스 씨.
이제 팔로워를 통해 이 대화를 듣고 있는 제3자 관찰자가 있다고 상상해보자.
케이크 부인이 한 말은 거의 지연없이 팔로워에게 전달이 되었으나 푼스 씨가 한 말은 긴 복제 지연이 있었다.
이런 상황에서 관찰자는 대화를 다음과 같이 들을 것이다.
케이크 부인 : 보통 10초 정도요, 푼스 씨.
푼스 씨 : 미래에 대해 얼마나 멀리 볼 수 있나요. 케이크 부인?
이를 그림으로 표현하면 다음과 같다.
파티션 간의 복제 시점에 차이가 있다면 관찰자 입장에서 질문 보다 대답을 먼저 확인할 가능성이 있다.
그림 5-5. 일부 파티션이 다른 것보다 느리게 복제되는 경우 관찰자는 질문을 보기 전에 대답을 볼 수 있다.
일관된 순서로 읽기(consistence prefix read)
- 이러한 종류의 이상현상을 방지하기 위해 일관된 순서로 읽기와 같은 유형의 보장이 필요
- 일련의 쓰기가 특정 순서로 발생한 경우 다른 사용자에게도 쓰기에 대해 쓰여진 순서대로 읽는 것을 보장
- 인과성의 위반
- **파티셔닝(샤딩)**된 데이터베이스에서 발생하는 특징적인 문제
- 많은 분산 데이터베이스에서 파티션은 서로 독립적으로 동작 → 쓰기의 전역 순서가 없음
- 한 가지 해결책은 서로 인과성이 있는 쓰기에 대해 동일한 파티션에 기록되도록 하는 방법
- 그러나 일부 애플리케이션에서 효율적이지 않음
- 인과성을 명시하기 위한 알고리즘 → 이후(이전 발생 관계와 동시성)에 다룰 예정
복제 지연을 위한 해결책
해결 방안으로는 다음과 같은 것들이 있다.
- 쓰기 후 읽기와 같은 강한 보장을 제공하도록 시스템을 설계
- 비동기식 복제를 사용하지만 동기식 방식으로 동작하는 것 처럼 보인다.
- 애플리케이션이 데이터베이스보다 더 강력한 보장을 제공하는 방법도 있다.
- ex. 특정 종류의 리더에서 읽기를 수행
- 그러나 애플리케이션에서 다루기에는 복잡하여 잘못되기 쉽다.
트랜잭션
- 애플리케이션이 단순해지기 위해 데이터베이스가 더 강력한 보장을 제공하는 방법
- 애플리케이션 개발자가 미묘한 복제 문제를 걱정하지 않는다.
- 또한 올바른 작업 수행을 위해 항상 데이터베이스를 신뢰할 수 있다.
- 이것이 트랜잭션이 존재하는 이유
분산 시스템에서의 트랜잭션
- 단일 노드에 대한 트랜잭션을 오랫동안 존재했음
- 분산(복제, 파티셔닝 된) 데이터베이스 상에선 많은 시스템이 트랜잭션을 포기
- 트랜잭션은 가용성과 성능 측면에서 너무 비싸다.
- 확장 가능한 시스템에서는 최종적 일관성을 선택하는 것이 불가피하다는 주장도 존재
- 이러한 주장의 일부는 사실이지만 지나치게 단순화된 측면이 있다.
- 트랜잭션 및 대안 메커니즘에 대해서는 이후 챕터에서 다룰 것
다중 리더 복제
다중 리더
- 쓰기 처리를 하는 각 노드는 데이터 변경을 모든 노드에 전달하는데 이를 다중 리더 설정 (마스터 마스터, 액티브/액티브 복제라고도 함)
- 여기서 각 리더는 동시에 다른 리더의 팔로워 역할도함
- 모든 쓰기를 해당 리더를 거쳐야 하고, 리더 연결이 불가능한 경우 쓰기 불가능한 단점 보완
다중 리더 복제의 사용 사례
다중 데이터 센터 운영
그림 5-6. 다중 데이터센터 간 다중 리더 복제
다중 리더 설정
- 각 데이터 센터마다 리더
- 각 데이터 센터내 리더 팔로워 복제
- 각 데이터 센터 간 리더가 다른 데이터 센터 리더에게 변경 사항 복제
- 동일한 데이터를 다른 두 개의 데이터센터에서 동시에 변경 가능하므로 쓰기 충돌은 반드시 해소되어야함
- 일부 데이터베이스는 기본적으로 다중 리더 설정 제공
단일 리더, 다중 리더 설정 비교
성능
- 단일 리더 : 쓰기는 인터넷을 통해 리더가 있는 데이터 센터로 이동해야해서 쓰기 지연 발생
- 다중 리더 : 쓰기는 로컬 데이터센터 처리 후 비동기 방식으로 다른 데이터센터에 복제
데이터센터 중단 내성
- 단일 리더 : 리더가 있는 데이터센터가 고장 나면 다른 데이터센터의 팔로워를 리더로 승진
- 다중 리더 : 각 데이터센터 독립적으로 동작, 고장난 데이터센터가 온라인으로 돌아왔을때 복제
네트워크 문제 내성
- 단일 리더 : 데이터 센터 내 쓰기는 동기식이기에 데이터 센터 내 연결 문제에 민감
- 다중 리더 : 비동기 복제를 사용해 네트워크 문제에 보다 잘 견딤, 일시적 네트워크 중단에도 쓰기 처리는 진행되기 때문
사용 사례 - 오프라인 작업을 하는 클라이언트
- 인터넷 연결이 끊어진 동안 애플리케이션 동작해야 하는 경우
- 협업 편집 : 동시에 여러 사람이 문서를 편집할 수 있는 애플리케이션 ex) 이더패드, 구글 독스 등
쓰기 충돌 다루기
그림 5-7. 동일한 레코드를 두 리더가 동시에 갱신하면 쓰기 충돌이 발생한다.
- 다중 리더 복제에서 가장 큰 문제는 쓰기 충돌
- 각 사용자가 동시에 편집 후 로컬 리더에 저장하였으나 변경을 비동기로 복제 시 쓰기 충돌 발생
- 동기식으로 충돌 감지를 하면 다중 리더 복제의 장점을 잃어버림
충돌 회피
- 충돌 처리하는 가장 간단한 전략
- 충돌 처리가 어려워 충돌 피하는 것이 자주 권장됨
- 특정 레코드의 모든 쓰기를 동일한 리더에서 처리함
- ex) 특정 사용자의 요청을 동일한 데이터센터로 항상 라우팅하고 데이터센터 내 리더를 사용해 읽기와 쓰기를 하게끔 보장 → 동일한 데이터센터로 라우팅하는 것이 아니면 충돌 회피가 실패
일관된 상태 수렴
- 모든 복제 서버가 동일해야 함이 원칙
- 수렴(convergent) : 모든 변경이 복제돼 모든 복제 서버에 동일한 최종 값이 전달되게 해야 함
수렴 충돌 해소 방법들
- 각 쓰기에 고유 ID () 부여해 가장 높은 ID를 가진 쓰기를 선택
- 타임스탬프를 사용하는 경우를 최종 쓰기 승리라 한다.
- 대중적이지만 데이터 유실 위험이 있다.
- 각 복제 서버에 고유 ID를 부여하고 높은 숫자의 복제서버에서 생긴 쓰기가 낮은 숫자의 복제 서버에서 생긴 쓰기보다 항상 우선적으로 적용
- 데이터 유실 가능성있다.
- 어떻게든 값을 병합
- 예로 사전 순 정렬 후 연결
- 명시적 데이터 구조에 충돌을 기록해 모든 정보를 보존
- 나중에 (사용자에게 알려줌) 충돌을 해소하는 애플리케이션 코드를 작성
사용자 정의 충돌 해소 로직
- 충돌 해소의 가장 적합한 방법은 애플리케이션에 따라 다르다.
- 따라서 대부분 다중 리더 복제도구는 애플리케이션 코드를 사용해 충돌 해소 로직 작성
- 쓰기 수행 중 :
- 복제된 변경사항 로그에서 데이터베이스 시스템 충돌 감지되면 충돌 핸들러 호출
- 백그라운드에서 실행됨
- 읽기 수행 중 :
- 충돌 감지 시 모든 충돌 쓰기 저장
- 다음 번 읽기 시 여러 데이터 반환. 애플리케이션은 사용자에게 충돌 내용 보여주거나 자동으로 충돌 해소해 결과를 데이터베이스에 기록
- 카우치DB가 이렇게 동작
자동 충돌 해소
- 충돌 없는 복제 데이터 타입
- Set, Map, 정렬 목록, 카운터 등을 위한 데이터 구조의 집합
- 병합 가능한 영속 데이터 구조
- Git 처럼 명시적으로 히스토리 추적하고 삼중 병합 함수를 사용한다
- 운영 변환
- 이더패트, 구글 독스 같은 협업 편집 애플리케이션의 충돌 해소 알고리즘
다중 리더 복제 토폴로지
복제 토폴로지
- 복제 토폴로지는 쓰기를 한 노드에서 다른 노드로 전달하는 통신 경로
- 리더가 둘 이상이라면 다양한 토폴로지가 가능
그림 5-8. 다중 리더 복제를 설정하는 세가지 토폴로지 예제
- 원형 토폴로지
- 각 노드가 하나의 노드로부터 쓰기를 받고, 이 쓰기를 다른 노드에 전달
- MySQL 에서 기본적으로 제공
- 노드 장애 시 노드 간 복제 메시지 흐름에 방해를 줌
- 별 모양 토폴로지
- 지정된 루트 노드 하나가 다른 모든 노드에 쓰기 전달
- 트리로 일반화 가능
- 노드 장애 시 노드 간 복제 메시지 흐름에 방해를 줌
- 전체 연결 토폴로지
- 모든 리더가 각자의 쓰기를 다른 모든 리더에 전송
- 가장 일반적인 토폴로지
- 내결함성이 상대적으로 좋음
그림 5-9. 다중 리더 복제에게 일부 복제 서버에 쓰기가 잘못된 순서로 도착할 수 있다.
- 전체 연결 토폴로지의 문제점
- 네트워크 연결 속도 차이로 인한 복제 메시지 추월
- 리더2는 삽입 이전에 갱신을 처리하게 됨
- 올바른 이벤트 정렬을 위한 버전 벡터 기법으로 해결 가능
- 따라서 다중 리더 복제 시스템을 사용하려면 이런 문제를 인지하고 문서를 주의깊게 읽은 다음 데이터 베이스를 철저하게 테스트해봐야 함
리더 없는 복제
- 일부 데이터 저장소 시스템은 리더의 개념을 버리고 모든 복제 서버가 클라이언트로부터 쓰기를 직접 하는 방식을 사용하기도 함
- 다이나모 스타일 DB로 리악, 카산드라, 볼드모트 등 오픈소스 데이터스토어가 있음
- 일부 리더 없는 복제 구현에서는 클라이언트가 여러 복제 서버에 쓰기를 직접 전송하는 반면 코디네이터 노드가 클라이언트를 대신해 이를 수행하기도 함
노드가 다운됬을 때 데이터베이스에 쓰기
그림 5-10. 정족수(quorum) 쓰기, 정족 수 읽기와 노드 중단 후 읽기 복구
- 다운된 노드에서는 쓰기가 누락되어 오래된(outdated) 값을 읽게 됨
- 읽기 요청을 병렬로 여러 노드에 전송해 최신 값을 읽어와 해결 가능
- 버전 숫자를 통해 읽어온 값 중 최신 값을 결정함
읽기 복구와 안티 엔트로피 복제 계획은 최종적으로 모든 데이터가 모든 복제 서버에 복사된 것을 보장해야 함
- 읽기 복구
- 클라이언트가 여러 노드에서 병렬로 읽기 수행하면 오래된 응답 감지 가능
- 복제 서버의 오래된 값을 새로운 값으로 기록
- 값을 자주 읽는 상황에 적합
- 안티 엔트로피 처리
- 백그라운드 프로세스와 복제 서버 간 데이터 차이를 찾아 누락된 데이터를 복사
- 특정 순서로 쓰기를 복사하기 때문에 지연이 있을 수 있음
정족수
- 여러 사람의 합의로 운영되는 의사기관에서 의결을 하는데 필요한 최소한의 참석자 수 (사전적 정의)
- 유효한 읽기와 쓰기를 위한 복제서버 수, 쓰기 성공 노드 수, 질의 노드 수를 나타냄
- 다이나모 스타일 DB에서는 복제 서버(n), 쓰기 노드(w), 읽기 노드(r) 설정 가능
- 일반적으로 n 은 3 또는 5 등의 홀수, w = r = (n+1) / 2 (반올림) 설정
그림 5-11. w + r > n이면 읽는 r개의 복제 서버 중 최소한 하나는 가장 최근 성공한 쓰기를 알아야 한다.
- 일반적으로 읽기와 쓰기는 항상 모든 n개의 복제 서버에 병렬 전송한다.
- w, r 은 기다릴 노드를 결정한다.
- 읽기, 쓰기 성공 여부는 읽기, 쓰기가 성공한 노드의 갯수로 확인한다.
- w, r개 보다 사용가능한 노드 수가 적다면 에러를 반환한다.
정족수 일관성의 한계
- w + r > n 으로 설정하면 읽은 노드 중 최신 값을 가진 노드가 하나 이상이어야 함. (그림 5-11)
- 그러나 모든 과정이 올바르게 동작해도 시점 문제로 오래된 값을 반환할 수 있음 (느슨한 정족수 사용)
- 즉, 정족수를 아무리 잘 설정해도 오래된 값을 읽을 가능성이 있음
느슨한 정족수와 암시된 핸드오프
정족수 불충족
- 네트워크 중단으로 데이터베이스 노드와 클라이언트 연결 유실
- 응답 가능한 노드가 w, r보다 적을 수 있음
느슨한 정족수
- 정족수 불충족 상황에서 보통 저장하는 노드가 아닌 연결이 가능한 다른 노드에 쓰기를 하는 경우
암시된 핸드오프
- 네트워크 장애가 해제되면 일시적으로 수옹한 모든 쓰기를 해당 홈 노드로 전송
동시 쓰기 감지
- 다이나모 스타일 데이터베이스는 여러 클라이언트가 동시에 같은 키에 쓰는 것을 허용해 엄격한 정족수를 사용해도 충돌이 발생할 수 있음
- 문제는 네트워크 지연 등으로 이벤트가 다른 노드에 다른 순서로 도착할 수 있음
그림 5-12. 다이나모 스타일 데이터스토어에ㅔ 동시 쓰기 잘 정의된 순서가 없다.
get 요청에서 노드들마다 읽어오는 값이 달라 일관성이 깨짐
최종 쓰기 승리 (동시 쓰기 버리기)
- 복제본을 가장 최신 값으로 덮어 쓰는 방법
- 쓰기에 타임스탬프를 붙여 최신 값을 선택하는 방법 (LWW)
- 손실 데이터를 허용하지 않는다면 LWW가 부적합
- 카산드라에서 유일하게 제공하는 충돌 해소 방법, 리악에서는 선택적 기능
- 키를 한번만 쓰고 이후에 불변값으로 만들어 동시에 같은 키를 갱신하는 상황을 방지해야 함
- 카산드라 사용 시 키로 UUID를 사용해 모든 쓰기작업에 고유한 키를 부여하는 것을 추천
이전 발생
- 작업 B가 작업 A에 대해 알거나 A에 의존적이거나, 어떤 방식으로든 A를 기반으로 한다면 작업 A는 작업 B의 이전 발생이라 함
- 작업이 다른 작업보다 먼저 발생하지 않으면 (어느 작업도 다른 작업에 대해 알지 못하면) 동시 작업이라 일컫음
이전 발생 관계 파악하기
그림 5-13. 두 클라이언트가 동시에 장바구니를 수정하는 동안 인과성 파악하기
버전\클라이언트 | 클라이언트 1 | 클라이언트 2 |
---|---|---|
버전 1 | 우유 | |
버전 2 | 우유 | 달걀 |
버전 3 | 우유, 밀가루 | 달걀 |
버전 4 | 우유, 밀가루 | 달걀, 우유, 햄 |
버전 5 | 우유, 밀가루, 달걀, 베이컨 | 달걀, 우유, 햄 |
- client1 - 우유 추가, version1 = [(우유)]
- client2 - 달걀 추가, version2 = [(우유) , (달걀)]
- client1 - 밀가루 추가, version3 = [(우유, 밀가루), (달걀)]
- 1번 응답인 우유에 밀가루 추가, version2 값 달걀
- client2 - 햄 추가, version4 = [(달걀, 우유, 햄), (우유, 밀가루)]
- 2번 응답인 우유, 달걀에 햄 추가, version3의 우유, 밀가루
- client1 - 베이컨 추가, version5 = [(우유, 밀가루, 달걀, 베이컨), (달걀, 우유, 햄)]
- 3번 응답인 우유, 밀가루, 달걀에 베이컨 추가, version4의 달걀, 우유, 햄
그림 5-14. 그림 5-14의 인과성 도표
동시, 이전 발생 결정 알고리즘
- 06.서버가 모든 키에 대한 버전 번호 유지, 버전 번호 증가하며 키를 기록함
- 클라이언트가 키를 읽을 때는 최신 버전과 덮어쓰지 않은 모든 값 반환
- 클라이언트가 키를 기록할 때는 이전 읽기의 버전 번호를 포함해야 하고 이전 읽기에서 받은 모든 값을 함께 합침
- 서버가 특정 번호를 가진 쓰기를 받을 때는 해당 버전 이하 모든 값을 덮어쓸 수 있지만 높은 버전 번호의 값은 유지해야 함
동시에 쓴 값 병함
- 위 알고리즘은 데이터가 자동으로 삭제되지 않지만 클라이언트의 추가 작업이 필요
- 동시에 쓴 값을 합쳐 정리하는 걸 리악에서 형제(sibling) 라 부름
- 값을 제거할 때에는 제거했다고 버젼에 표시를 남겨야하는데 이를 툼스톤이라고 함
버전 벡터 (version vector)
- 모든 복제본의 버전 번호 모음을 나타냄
- 리악2.0 에서 도티드 버전 벡터로 버전 벡터를 변형해 사용함
- 값을 읽을 때 데이터베이스 복제본에서 클라이언트로, 값이 기록될 때 데이터베이스로 전송
- 쓰기 시 버전 번호 증가, 다른 복제본의 번호도 추적해 덮어쓸 값, 형제 값을 구분함
- 데이터베이스는 덮어쓰기와 동시 쓰기를 구분할 수 있음