확장의 방법
데이터 분산의 종류
5장에서는 복제를 다룬다.
데이터 변경 처리.
5장에서는 이 내용을 주로 다룬다.
복제 방법 | 장점 | 단점 |
---|---|---|
단일 리더 복제 | 충돌 해소 우려 없음 | 가용성이 낮음 |
다중 리더 복제 | 결함 노드, 네트워크 중단, 지연 시간 급증이 있는 상황에도 가용성이 놓음 | 일관성이 보장되기 어려움 |
리더 없는 복제 | (같은 이유로)가용성이 좋음 | 일관성이 보장되기 어려움 |
대개 비동기식 쓰는 걸로.
복제 지연과 관련된 개념을 살펴봄
쓰기의 동시성을 판별하는 방법, 충돌을 해결하는 알고리즘을 알아본다.
복제본이 저장된 노드를 replica(복제 서버) 라고 한다.
DB 에 대한 모든 쓰기는 replica 에 모두 반영되어야 한다.
가장 일반적인 해결책은 리더 기반 복제(leader-based replication) 이다.
데이터 쓰기
데이터 읽기
리더 기반 복제는 다음과 같은 여러 DB 서비스들에서 사용하고 있다.
동기식 복제
리더가 팔로워의 데이터 갱신을 기다리고 클라이언트에게 성공 응답을 보낸다. 위 그림의 Follower1.
비동기식 복제
리더가 팔로워의 데이터 갱신을 기다리지 않고 클라이언트에게 성공 응답을 보낸다. 위 그림의 Follower2.
동기식의 장점
동기식의 단점
동기식의 단점으로 인해, 모든 팔로워를 동기식으로 복제하는 것은 비현실적이다. 현실적으로 동기식 복제를 사용하는 방법은 반동기식(semi-synchronous) 복제 다.
보통, 리더 기반 복제는 모두 비동기식 팔로워를 사용한다.
비동기식의 단점
비동기식의 장점
replica 를 늘리거나 장애 노드의 대체하기 위해 새로운 팔로워를 추가할 수 있어야 한다.
새로운 팔로워는 리더의 데이터 복제본을 가지고 있음을 보장해야 한다.
기존의 노드들은 가용성을 위해 멈추지 않고 계속해서 데이터를 쓰고 있기 때문에, 특정 시점의 복사 작업은 새로운 팔로워의 데이터셋 일관성을 보장하지 못한다.
무중단으로 팔로워를 추가하는 방법
1. 리더의 DB 스냅샷을 일정 시점에 가져와서 팔로워 노드에 복사한다.
2. 팔로워는 리더에 연결하여 스냅숏 이후 발생한 모든 데이터 변경을 요청하여 처리한다.(catch-up)
개별 노드의 장애에 전체 시스템이 동작하게 하여서 가용성을 확보하는 방법에 대해 팔로워 및 리더에게 장애가 발생한 경우로 나누어 알아본다.
리더에게 장애가 발생한 경우, 아래와 같은 처리가 필요하여 까다로운 편이다.
위와 같은 리더 장애를 복구하는 과정을 failover 라고 한다.
failover 는 수동 혹은 자동으로 진행하는 방법이 있다.
자동 failover 는 보통 다음의 단계로 구성된다.
자동 failover 는 다음과 같은 기술적 어려움이 있다.
리더 기반 복제는 내부적으로 다양한 복제 방법을 사용한다. 이에 대해 간단히 살펴보자.
리더는 모든 쓰기 요청에 대해 구문(statement) 를 기록하고 쓰기를 실행한다. 그 후, 구문을 팔로워에게 전송한다. RDBMS 는 INSERT, UPDATE, DELETE 구문을 전달한다. 팔로워는 직접 요청을 받은 것처럼 SQL 구문을 파싱하고 처리한다.
구문 기반 복제는 다음의 경우 복제가 깨질 수 있다.
이런 한계는 모두 극복 가능하지만, 일반적으로 다른 복제 방법이 선호된다.
모든 쓰기에 대해 WAL 를 작성한다면, WAL 를 복제 로그로 사용할 수 있다.
복제 로그로 WAL 를 이용할 때, WAL 로그의 단점은 저장소 엔진 내부와 결합도가 매우 높은, 물리적 데이터 표현이라는 점이다. 가령, 어떤 디스크 블로에서 어떤 바이트를 변경했는지와 같은 상세 정보를 포함한다. 이는 리더와 팔로워의 데이터베이스 소프트웨어 버전이 다른 경우 복구를 실행할 수 없게 한다.
복제 로그를 저장소 엔진 내부와 분리하기 위한 방법 중 하나는 복제와 저장소 엔진을 위해 다른 로그 형식을 사용하는 것이다. 이런 종류의 복제 로그를 저장소 엔진의 물리적 데이터 표현과 구별하기 위해 논리적(Logical log) 라고 부른다.
RDBMS 의 논리적 로그는 대개 로우 단위로 DB 테이블 쓰기를 기술한 레코드열이다. 다음의 특성을 지닌다.
논리적 로그의 장점
좀 더 유연한 복제를 제공하는 방식이다.
예를 들어, 다음의 유즈케이스가 있을 수 있다.
많은 RDBMS 에서 트리거 나 스토어드 프로시저 를 사용한다.
트리거는 데이터 변경에 사용자 정의 애플리케이션 코드를 실행하는 복제 방식이다. 외부 프로세스가 테이블의 데이터 변경을 포착하면 자동으로 사용자 정의 코드를 실행한다.
트리거 기반 복제의 단점
트리거 기반 복제의 장점
리더에서 일어난 쓰기가 팔로워로 복제되기 까지 지연을 복제 지연(Replication Lag) 이라고 한다.
비동기식 복제로 인해, 동일한 읽기 쿼리도 복제 지연에 따라 다른 결과를 반환할 수 있다. 하지만 시간이 흐르면 결국 모든 팔로워는 모든 리더의 모든 쓰기를 복제하게 되어 일관성을 회복하게 되는데, 이를 최종적 일관성 이라고 한다.
이번 절에서는 복제 지연이 발생할 수 있는 세 가지 사례를 제시하고 해결 방법을 간략히 설명한다.
리더에게 쓰기 요청을 했으나, 복제 지연으로 인해 읽기 요청은 실패하거나 지난 값을 읽을 수 있다.
이런 상황에서는 쓰기 후 읽기 (read-after-write) 일관성이 필요하다. 이 일관성에서는 자신의 읽기는 자신의 모든 쓰기가 반영돼있음을 보장한다. 단, 다른 이의 쓰기에 대해서는 보장하지 않는다.
쓰기 후 읽기 일관성을 구현하는 방법
동일한 사용자가 여러 디바이스(데스크톱, 모바일)로 서비스를 접근하는 경우, 디바이스 간 쓰기 후 읽기 일관성(cross-device read-after-wrtie consitency) 가 필요하다.
이 경우, 추가적으로 다음의 상황들을 고려해야 한다.
같은 사용자의 동일한 읽기 요청이 각기 다른 팔로워에게 라우팅 되었을 때, 시간이 거꾸로 흐르는 현상을 목격할 수 있다.
즉, 앞선 읽기에선 보였던 값이, 뒤이은 읽기에선 보이지 않을 수 있는 것이다.
단조 읽기(monotonic read) 란, 이렇게 시간이 거꾸로 흐르는 이상 현상이 발생하지 않는 것을 보장하는 일관성을 의미한다.
단조 읽기는 강한 일관성 보다는 덜하지만 최종적 일관성보다는 더 강한 일관성 보장이다.
단조 읽기를 달성하는 방법
일관된 순서로 읽기(consistent prefix read) 는 일련의 쓰기의 순서가 모든 사용자의 읽기에서 동일함을 보장한다.
주로 파티셔닝된 DB 에서 일관된 순서로 읽기가 깨지는 문제가 발생하고 한다. 많은 분산 DB 에서 파티션은 각각 독립적으로 동작하므로 전역적인 쓰기 순서가 보장되지 않기 때문이다.
일관된 순서로 읽기를 달성하는 방법
지금 까지는 단일 리더를 사용한 복제 아키텍처만을 고려했음.
단일 리더 기반 복제의 주요한 단점
쓰기 처리를 여러 노드로 확장하는 것을 다중 리더(Multi-Leader) 방식이라고 부른다. 이 방식에서 리더는 리더인 동시의 팔로워이다.
다중 데이터센터 운영시, 아래와 같이 데이터센터마다 리더가 있는 아키텍처를 고려해볼 수 있다.
다중 데이터센터 운영시 단일 리더 대비 다중 리더의 장점
항목 | 단일 리더 | 다중 리더 |
---|---|---|
성능 | 리더가 존재하는 데이터 센터로 쓰기 요청이 전달되어야 하기 때문에, 쓰기 지연 발생 | 가까운 데이터 센터에서 쓰기 요청이 처리되기 때문에 성능이 더 좋음 |
데이터센터 중단 내성 | 리더가 있는 데이터센터가 고장 나면, 장애 복구를 위해 다른 데이터 센터의 팔로워를 리더로 승진시켜야 한다. | 각 데이터 센터는 독립적으로 동작하기에, 고장난 데이터 센터가 온라인으로 돌아왔을 때, 복제를 따라잡는다. |
네트워크 문제 내성 ? | 데이터센터 내에서의 쓰기는 동기식이기 때문에, 데이터 센터 내 연결 문제에 매우 민감하다. | 데이터 센터 내에서도 비동기식이기 때문에 네트워크 중단에도 쓰기 처리는 진행되어 네트워크 문제에 보다 잘 견디다. |
다중 리더 단점
인터넷 연결이 끊어진 동안 어플리케이션이 계속 동작해야 하는 경우
가령, 다양한 디바이스에서 접근 가능한 캘린더 앱의 오프라인에서도 읽기/ 쓰기 요청이 가능해야 한다.
오프라인 상태에서의 작업이 온라인 상태가 됐을 대 다른 디바이스와 동기화돼야 한다.
모든 디바이스 로컬에서 리더처럼 동작하는 로컬 DB 가 있다.
또한 다른 디바이스의 리더와 복제를 비동기 방식으로 수행하는 프로세스(동기화)가 있다. 복제 지연은 온라인 상태로부터 몇 시간에서 며칠 이상이 소요될 수도 있다.
이 유즈 케이스는 근본적으로 데이터센터 간 다중 리더 복제와 동일하다.
CouchDB 는 이런 종류의 동작 모드를 위해 설계되었다.
구글 독스와 같은 실시간 협업 편집 애플리케이션의 경우
한 사용자가 문서를 편집할 때 변경 내용이 즉시 로컬 복제 서버에 적용하고 나서, 동일한 문서를 편집하는 다른 사용자와 서버에 비동기 방식으로 복제한다.
충돌 문제가 발생할 수 있으며 이를 해결할 수 있어야 한다.
다중 리더 복제에서 비동기식 복제를 사용하는 경우 충돌이 발생할 수 있다.
충돌을 처리하는 방법을 살펴본다.
제일 간단한 전략.
특정 레코드에 대한 모든 쓰기가 동일한 리더를 거치도록 보장하면 된다. 충돌 처리는 어려운 것이기 때문에, 충돌 회피는 자주 권장되는 방법이다.
예를 들어, 특정 사용자의 데이터(가령, 특정 사용자의 구글 닥스 문서)를 편집할 수 있는 애플리케이션에서 다른 사용자의 쓰기 요청이 동일한 데이터센터 내의 동일한 리더로 라우팅되도록 한다면 보장할 수 있다.
이러한 전략은 데이터 센터가 고장 나서 트래픽을 다른 데이터 센터로 다시 라우팅 해야하는 경우, 충돌 회피에 실패하게 된다. 이런 경우, 다른 리더에서 충돌을 해소할 수 있어야 한다.
수렴(convergent) 방식으로 충돌을 해소하는 방법이다.
다른 리더에서 쓰기가 모두 성공했다고 할지라도, 최종적으로는 일관된 상태로 수렴해야 한다는 것이다.
수렴 충돌 해소를 달성하는 방법
애플리케이션마다 적합한 충돌 해소 로직을 개발자가 직접 작성하는 것이다.
충돌 해소 로직이 적용되는 시점은 크게 두 가지가 있다.
자동 충돌에 관한 연구
복제 토폴로지는 노드 간 쓰기 통신 경로를 설명한다.
전체 연결(all-to-all)
원형 토폴로지(circular topology)
별 모양 토폴로지
원형과 별모양은 하나의 쓰기가 여러 노드를 거쳐야 전체 서버에 전파된다. 무한 복제 루프를 방지하기 위해, 복제 로그에 쓰기를 거친 노드의 식별자가 저장된다.
원형과 별모양의 문제점은 하나의 노드에서 장애가 발생하면 전체 서버에 복제가 중단(single point of failure)될 수 있다. 장애 노드 발생시 해당 노드를 회피하도록 재설정이 필요하다. 이는 주로 수동 재설정으로 이루어진다. 연결이 더 빽빽할 수록, 토폴로지의 내결함성이 좋아진다.
전체 연결 토폴로지의 문제점은 일관된 순서로 읽기가 깨질 수 있다는 것이다. 즉, 리더 노드마다 네트워크 속도의 차이로 인해, 쓰기의 순서가 보장되지 않을 수 있다.
이를 해결하기 위해, 버전 벡터(version vector) 라는 기법을 사용하곤 한다. 그러나, PostgresSQL, MySQL 등 많은 다중 리더 복제 시스템에선 이런 충돌 감지 기법이 제대로 구현되어 있지 않다.
다중 리더 복제를 사용하는 경우, 스택 후보가 충돌 감지에 대한 믿을 만한 보장을 제공하는지 확인하는 것이 권장된다.
리더 없는 복제(leaderless Replication) 이란 일부 DB 시스템은 리더 없이 모든 복제 서버에 쓰기가 가능한 것을 말한다.
Dynamo 에서 사용하여 Dynamo 스타일이라고도 불린다. Riak, Cassandra 등에서 사용된다.
leaderless 복제 일부 구현에서는 클라이언트가 복제 서버로 직접 쓰기 호출을 하지 않고, 코디네이터 노드를 통해서 전달한다. 코디네이터 노드는 특정 순서로 쓰기를 수행하지 않는다. 이런 차이는 DB 사용 방식에 중대한 영향을 미친다.
리더가 있을 때, 리더가 다운된 경우 리더에 대한 장애 복구가 필요하다.
리더가 없는 경우가 장애 복구가 필요하지 않다.
클라이언트는 모든 복제 서버로 쓰기 요청을 보내고, 하나의 노드에서 실패하고 두 개의 노드에서 성공했다면, 쓰기가 성공한 것으로 판단한다. 성공했다고 판단하면 특정 노드에서 실패한 쓰기에 대해선 단순히 무시한다.
쓰기가 실패한 노드에선 오래된(outdated) 데이터가 읽어질 수 있다. 2개의 노드에 대해 읽기 요청을 보내고, 버전을 비교하여 최신값을 선택한다.
리더 없는 복제에서도 최종적 일관성이 달성되려면, 노드가 복구됐을 때 최신 데이터에 대한 복제가 이루어져야한다.(catch-up)
리더 없는 복제의 catch-up엔, 주로 두 가지 메커니즘이 사용된다.
위 두 개의 방법 중 읽기 복구만 이용하면, 읽기 복구를 실행하는 어플리케이션이 읽지 않는 데이터에 대해선 최신화를 보장할 순 없다.
n개의 노드가 있고, 모든 쓰기는 w개의 노드에서 성공해야 확정되고, 모든 읽기는 최소한 r개의 노드에 질의를 한다고 하자.
이 때, w + r > n 이면 읽기는 반드시 최신값을 얻을 것으로 기대한다. w + r > n 을 만족하면, n 개의 노드 중 적어도 한 개의 노드에 대해선 최신값 쓰기와 읽기가 모두 성공하기 때문이다. w + r > n 을 정족수 조건 이라고 부르겠다.
보통 n을 홀수로 하고, w = r = (n + 1)/2 로 설정한다.
읽기가 많고 쓰기가 적은 유즈 케이스에선, w = n, r = 1 로 설정할 수 있다. 하지만, 이 경우 한 개의 노드라도 고장나면 쓰기에 실패한다.
정족수 조건을 만족해도 오래된 값을 반환하는 에지 케이스가 있다.
결국, 최종적 일관성이 달성된 상태에선 정족수 조건이 항상 최신값을 읽는 것을 보장하지만, 최종적 일관성이 달성되지 않거나 위와 같은 에지 케이스에선 정족수 조건이 절대적으로 최신값 읽기를 보장하진 못한다.
특히, 비동기식 복제를 사용하는 경우 [복제 지연 문제](# 복제 지연 문제)로 인해 최신값이 읽히지 않는 이상 현상이 발생할 수 있다. 견고한 보장은 일반적으로 트랜잭션이나 합의가 필요하다. 이는 7장, 9장에서 살펴본다.
Sloppy Quorums and Hinted Handoff
n 을 초과하는 노드로 구성된 클러스터에서, 특정 키 처리를 담당하는 정족수 n 개의 노드가 있다고 하자. 이 n 개 중 일부 노드들이 다운되거나 네트워크 연결이 소실된 상황에선 정족수를 구성하는 노드들에 데이터를 쓸 수가 없다.
정족수를 구성하는 노드 중 일부가 장애 상황일 때, 정족수 구성 외의 노드에 쓰기 처리를 위임하는 것을 느슨한 정족수 라고 한다.
장애 상황의 노드들이 복구가 되었을 때, 쓰기를 이 노드들에 다시 복제하는데 이를 암시된 핸드오프(hinted handoff) 라고 한다.
느슨한 정족수는 쓰기 가용성을 높이는데 효과적이다.
하지만, 정족수 조건이 만족된다고 해도, 일시적으로 최신값이 n 이외의 노드에 기록될 수 있기 때문에 r 노드가 최신값을 읽는 것을 보장하지 않는다.
리더 없는 복제에선, 여러 클라이언트가 동일 키에 대해 동시에 쓰는 것을 허용하기 때문에 엄격한 정족수를 사용하더라도 충돌이 발생한다. 읽기 복구나 암시된 핸드오프 상황에서도 충돌이 발생할 수 있다.
충돌이 발생한 경우, 실제로 어떤 값이 최신값인지 판단하기 어렵다. 아래 그림에서 Client A, B 가 동시에 쓰기를 했으나, 각 노드까지 반영되는 것이 모두 다르기 때문에, A가 최신값이 되는 경우도 있고 그 반대의 경우도 존재할 수 있다.(양자 역학?)
리더 없는 복제에서 쓰기 충돌을 해소하는 방법에 대해 알아본다.
타임스탬프가 우세한 경우의 쓰기를 취한다. 단, loser 의 쓰기는 유실될 수 있다.
타임스탬프가 동일한 경우는 로직을 작성하기에 따라 동작을 결정할 수 있다. 가령, 노드ID 에 우선 순위를 주거나, 도착한 순서로 우선 순위를 추가할 수 있다.
두 작업이 동시에 수행됐다를 어떻게 판별하는가?
작업 B가 다른 작업 A를 알고 있고 기반으로 한다면, 작업 B는 작업 A 보다 이전에 발생(happens-before)했다고 한다. B는 A에 인과성이 있다(causally dependent)고 한다.
두 작업이 서로 인과성이 없는 경우, 동시에 발생했다고 한다.
아래 그림은 동시에 발생하지 않은 경우
아래 그림은 동시에 발생한 경우
쓰기를 요청한 시각이 동시성을 결정하진 않는다. 요청의 시각이 다르더라도 서로 인과관계만 없다면 동시에 쓴 것으로 본다.
capturing the happens-beofre relationship
클라이언트가 자신이 알고 있는 버전을 쓰기 요청과 함께 보낸다.
알고있는 버전까지는 인과관계가 존재하는 것이며, 그 이후의 최신 버전과는 동시 쓰기가 아닌 것이다.
서버 데이터와 클라라이언트가 가지고 있는 데이터가 서로 다를 수 있으나, 손실된 쓰기는 없다.
구체적으로 다음과 같이 알고리즘이 동작한다.
이 알고리즘은 클라이언트단에서 동시값(형제(sibling)값)을 병합해야하는 추가적인 작업이 필요하다.
형제값을 병합하는 것은 버전을 기반으로 한 LWW 방식이므로 데이터 유실이 발생할 수 있다는 단점이 있다.
클라이언트가 형제값을 병합하는 합리적인 방법은 합집합을 구하는 것이다.
하지만, 값을 삭제하는 경우, 형제값에선 여전히 해당값이 존재할 수 있다.
이를 방지하기 위해선 단순히 병합한 집합에서 해당 값을 삭제하여 set 하는 것이 아니라, 해당 값을 삭제하겠다는 표시를 남겨야한다. 이를 툼스톤 이라고 한다.
복잡하고 오류가 발생 쉽다. Riak 에선 형제 병합을 지원하는 CRDT 라는 데이터 타입을 지원한다.
아래 그림에선, 단일 복제본을 사용했다.
리더가 없는 다중 노드 아키텍처에서의 알고리즘은 키당 버전뿐만 아니라 노드당 버전 번호도 사용해야 한다.
모든 노드의 버전 모음을 버전 벡터(version vector) 라고 한다.
클라이언트가 값을 읽을 때, 서버는 버전 벡터를 함께 보낸다.
클라이언트는 받은 버전 벡터를 이후 쓰기에 함께 보낸다.(인과 관계)