키-값 저장소는 키-값 데이터베이스라고도 부리는 비 관계형 데이터베이스다. 이 저장소에 저장되는 값은 고유 식별자를 키로 가져야 한다. 키와 값 사이의 연결 관계를 키-값 쌍이라고 지칭한다.
키는 유일해야 하며, 텍스트일 수도 있고 해시 값일 수도 있다. 성능 상의 이유로 키는 짧을수록 좋다.
이런 키 값 저장소로는 AWS의 DynamoDB, memcahced, redis 등이 있다. 해당 6장은 NoSQL 중 파티션을 사용하는 데이터베이스의 설계를 어떻게 진행할 것인가에 대한 장이다.
문제 이해 및 설계 범위 확정
데이터의 일관성과 가용성 사이의 타협적 결정을 내린 설계를 만드는 정도가 타협할 수 있는 수준일 것이다.
- 키-값 쌍의 크기는 10kb 이하다.
- 큰 데이터를 저장할 수 있어야 한다.
- 높은 가용성을 제공해야 한다.
- 높은 규모 확장성을 제공해야 한다.
- 데이터 일관성 수준은 조정이 가능해야 한다.
- 응답 지연시간(Latency)이 짧아야 한다.
단일 서버 키-값 저장소
단일 서버의 키-값 저장소를 설계하는 것은 쉽다. 키-값 전부를 메모리에 해시 테이블로 저장하는 것이다. 그러나, 이 접근 법은 빠른 속도를 보장하지만 모든 데이터를 메모리 안에 두는 것이 불가능 할 수도 있다. 물론 해결책은 있지만, 제한적이다.
- 데이터 압축(compression)
- 자주 쓰이는 데이터만 메모리에 두고 나머지는 디스크에 저장
제한적인 한계를 극복할 수 없을 때의 대안이 분산 키-값 저장소이다.
분산 키-값 저장소
분산 키-값 저장소는 분산 해시 테이블이라고도 불린다. 키-값 쌍을 여러 서버에 분산시키는 것이다. 분산 시스템을 설계할 때는 CAP 정리를 이해하고 있어야 한다.
- CAP(Consistency, Availability, Partition Tolerance theorem) 정리
- 데이터 일관성 : 분산 시스템에 접속하는 클라이언트는 어떤 노드에 접속했느냐에 관계 없이 언제나 같은 데이터를 보게되야 한다.
- 가용성 : 분산 시스템에 접속하는 클라이언트는 일부 노드에 장애가 발생하더라도 항상 응답을 받을 수 있어야 한다.
- 파티션 감내 : 파티션은 두 노드 사이에 통신 장애가 발생하였음을 의미한다. 파티션 감내는 네트워크에 파티션이 생기더라도 시스템은 계속 동작해야 한다는 것을 의미한다.
- CP 시스템 : 일관성과 파티션 감내를 지원하는 키-값 저장소, 가용성을 희생한다.
- AP 시스템 : 가용성과 파티션 감내를 지원하는 키-값 저장소, 데이터 일관성을 희생한다.
- CA 시스템 : 일관성과 가용성을 지원하는 키-값 저장소, 파티션 감내는 지원하지 않는다. 그러나 통상 네트워크 장애는 피할 수 없는 일로 여겨지므로 분산 시스템은 반드시 파티션 문제를 감내할 수 있도록 설계되야 한다. 그러므로 실 세계에 CA 시스템은 존재하지 않는다.
CAP 정리는 데이터 일관성(Consistency), 가용성(Availability, 파티션 감내(Partition tolerance)라는 세 가지 요구사항을 동시에 만족하는 분산 시스템을 설계하는 것은 불가능하다는 정리다.
키-값 저장소는 앞서 제시한 세 가지 요구사항 중 두 가지를 만족하냐에 따라 다음과 같이 분류된다.
이 정도 정의만으로는 이해하기 어렵기 때문에 책에서는 구체적인 사례를 들고 있다. 분산 시스템에서 데이터는 보통 여러 노드에 복제되어 보관된다.
세 대의 복제 노드(replica)에서 노드 n1,n2,n3에 데이터를 복제하여 보관하는 상황을 가정하면,
이상적 환경
이상적 환경이라면, 네트워크가 파티션되는 상황은 절대로 일어나지 않을 것이다. n1의 데이터는 자동적으로 n2와 n3에 복제된다. 데이터 일관성과 가용성도 만족한다.
실세계의 분산 시스템
분산 시스템은 파티션 문제를 피할 수 없다. 그리고 파티션 문제(네트워크나 노드가 끊어져 클라이언트가 데이터에 정상 접근할 수 없는 현상)가 발생하면 일관성과 가용성 중 하나를 선택해야 한다.
위 처럼 n3가 끊기면 n1 또는 n2에 기록한 데이터는 n3에 전달되지 않는다. n3에 기록됐으나 아직 n1이나 n2로 전달되지 않은 데이터가 있다면 n1과 n2는 오래된 사본을 갖고 있을 것이다.
만약, 가용성 대신 일관성을 선택한다면 데이터 일관성이 깨지는 것을 막기 위해 n1과 n2에 쓰기 연산을 중단시켜야 하는데 이러면 가용성이 깨진다. 은행권의 경우 일관성을 선택하는 경향이 있다. 항상 최신의 정보를 출력해야 하기 때문이다.
일관성 대신 가용성을 선택하는 경우, 오래된 데이터를 보내주는 한이 있더라도 읽기 연산을 허용해야 한다. n1과 n2는 쓰기 연산 마저 허용할 것이고, 파티션 문제가 해결된 뒤 동기화 문제를 다시 해결할 것이다.
- 시스템 컴포넌트
- 데이터 파티션
- 데이터를 여러 서버에 고르게 분산할 수 있는가?
- 노드가 추가되거나 삭제될 때 데이터의 이동을 최소화할 수 있는가?
- 데이터 다중화
- 데이터 일관성
- N = 사본 개수
- W = 쓰기 연산에 대한 정족수, 쓰기 연산이 성공하려면 적어도 W개의 서버로부터 쓰기 연산이 성공했다는 응답을 받아야 한다.
- R = 읽기 연산에 대한 정족수, 읽기 연산이 성공한 것으로 간주되려면 적어도 R개의 서버로부터 응답을 받아야 한다.
- R = 1, W = N : 빠른 읽기 연산에 최적화된 시스템
- W = 1, R = N : 빠른 쓰기 연산에 최적화된 시스템
- W + R > N : 강한 일관성이 보장됨
- W + R ≤ N : 강한 일관성이 보장되지 않음
- 일관성 모델
- 일관성 불일치 해소
- 비 일관성 모델: 데이터 버저닝
- 데이터 버저닝 : 버저닝은 데이터를 변경할 때마다 새로운 버전을 만드는 경우를 말한다. 각 버전의 데이터는 변경 불가능하다.
- 벡터 시계 : 벡터 시계는 [서버, 버전]의 순서 쌍을 데이터에 달아놓은 것이다.
- 클라이언트가 데이터 D1을 시스템에 기록한다. 이 쓰기 연산을 처리한 서버는 Sx이다. 따라서 벡터 시계는 D1[(Sx, 1)]이다.
- 다른 클라이언트가 D1을 읽고 D2로 업데이트 한 뒤 기록한다. D2는 D1에 대한 변경이기 때문에 D1을 덮어쓴다. 쓰기 연산은 같은 서버 Sx가 처리한다고 가정하면, 벡터 시계가 D2[(Sx, 2)]로 바뀐다.
- 다른 클라이언트가 D2를 읽어 D3로 갱신한 다음 기록한다. 이 쓰기 연산은 Sy가 한다. 벡터 시계 상태는 D3[(Sx, 2), (Sy, 1)]이다.
- 또 다른 클라이언트가 D2를 읽어 D4로 갱신한 다음 기록한다. 쓰기 연산은 Sz가 처리한다. 벡터 시계는 D4([Sx, 2], [Sz,1])일 것이다.
- 이 후 클라이언트가 D3와 D4를 읽으면 데이터 충돌이 있다는 것을 알게 된다. 이 요청에서 충돌이 해결되고, 서버에 기록하는데 이 쓰기 연산은 Sx가 한다고 치면 벡터 시계는 D5([Sx,3], [Sy,1], [Sz,1])이 된다.
- 장애 처리
- 장애 처리
- 장애 감지
- 모든 노드 사이의 멀티 캐스팅 구축 : 손쉽지만, 서버가 많다면 비효율적
- 가십 프로토콜 같은 분산형 장애 감지 : 보다 더 효율적
- 일시적 장애 처리
- 영구적 장애 처리
- 데이터 센터 장애 처리
- 시스템 아키텍처 다이어그램
- 클라이언트는 키-값 저장소가 제공하는 두 가지 단순한 API, 즉 get put과 통신한다.
- 중재자는 클라이언트에게 키-값 저장소에 대한 proxy역할을 하는 노드다.
- 노드는 안정 해시의 해시 링위에 분포한다.
- 노드를 자동적으로 추가 또는 삭제할 수 있도록, 시스템은 완전히 분산된다.
- 데이터는 여러 노드에 다중화된다.
- 모든 노드는 같은 책임을 지므로, SPOF는 존재하지 않는다.
위 키-값 저장소를 구현하기 위한 핵심 컴포넌트 및 기술은 다음과 같다.
대규모 데이터를 하나의 데이터베이스에 저장하는 것은 성능 면에서 더 불리하다. 확장성도 낮고 가용성 마저 떨어진다. 따라서, 데이터를 작은 파티션으로 분리하는 것은 키-값 저장소에서 불가피한 전략이다.
라는 문제는 떠오르지만 5장에서 다룬 안정해시 전략을 취함으로서 문제를 해결할 수 있다.
5장에서 다룬 안정 해시와 가상 노드 전략을 취하면 데이터 파티션 문제를 잘 수행할 수 있다.
확장성과, 각 서버의 용량에 맞는 다양성을 챙길 수 있기 때문에 하나의 데이터베이스보다 더 효율적이다.
높은 가용성과 안정성을 확보하기 위해, 데이터를 N개 서버에 비동기적으로 다중화(replication)할 필요가 있다. 여기서 N은 튜닝 가능한 값이다. 가상 노드를 사용할 때는 같은 물리 서버를 중복 선택하지 않도록 조심해야 한다.
만약 N을 3이라고 친다면, 위 예제에서 key0은 s1,s2,s3에 저장된다.
여러 노드에 다중화된 데이터는 적절히 동기화되야 한다. 정족수 합의(Quorum Consensus) 프로토콜을 사용하면 읽기/쓰기 연산 모두 일관성을 보장할 수 있다.
W가 1이라는 것은, 한 대의 서버에만 기록하겠다는 뜻은 아니며 중재자가 최소 한 대의 서버로부터 쓰기 응답을 받아야 한다는 뜻이며 1대를 받으면 나머지는 받지 않아도 된다. 중재자는 클라이언트와 노드 사이에서 proxy역할을 한다.
W,R,N에 대한 구성은 다음과 같이 정의될 수 있다.
키-값 저장소를 설계할 시 일관성의 수준을 결정해야 한다.
- 강한 일관성 : 모든 읽기 연산은 가장 최근에 갱신된 결과를 반환해야 한다. 강한 일관성을 달성하는 방법은 모든 사본에 현재 쓰기 연산 결과가 반영될 때 까지 읽기/쓰기 연산을 금지하는 것이지만 가용성이 떨어진다.
- 약한 일관성 : 읽기 연산은 가장 최근에 갱신된 결과를 반환하지 못할 수도 있다.
- 결과적 일관성 : 갱신 결과가 결국 모든 사본에 반영되는 모델이다.
다이나모는 결과적 일관성 모델을 택하고 있다. 결과적 일관성 모델의 경우, 쓰기 연산이 병렬로 실행되면 시스템에 저장된 값이 깨질 수 있는데 클라이언트 측에서 데이터 버전 정보를 활용해 일관성이 깨진 데이터를 읽지 않도록 하는 데이터 버저닝 기법을 사용해야 한다.
강한 일관성 모델이 아니라면, 사본 간 데이터 일관성이 깨질 수 있다.버저닝과 벡터 시계는 그 문제를 해소하기 위한 기술이다.
위와 같은 형태의 저장소에서 만약
쓰기 연산을 통해 데이터를 동시에 변경한다고 하면, conflic가 일어나는데 이 충돌을 해결하기 위해 자동으로 해결해 낼 버저닝 시스템이 필요하며, 벡터 시계는 이 시스템을 기반으로 문제를 해결하기 위해 보편적으로 사용되는 기술이다.
벡터 시계를 사용하는 방법은 위 그림을 통해 설명할 수 있다.
이제 이 벡터시계를 가지고 클라이언트는 자체 내부 로직을 통해 충돌 감지 및 해소를 실행한다. 클라이언트에 충돌 해소 로직이 들어가고 구현이 복잡해지는 단점과 서버:버전의 순서 쌍이 빠르게 늘어날 경우 길이가 길어지면 벡터 쌍을 제거하는 설정이 필요하다는 것이다.
장애 감지 기법을 통해, 장애 처리 기법을 살펴볼 수 있다.
가십 프로토콜로 장애를 감지한 시스템은 가용성을 보장하기 위해 필요한 조치를 해야 한다. 예를들면, 장애 서버로 가는 요청은 다른 서버가 잠시 맡아서 처리하고 이에 대한 단서를 남겨둔다.
반 엔트로피 - 프로토콜을 통해 사본들을 비교하며 최신 버전으로 갱신하는 과정을 포함하여, 사본간의 일관성이 망가진 상태를 탐지하고 전송 데이터의 양을 줄이기 위해 머클 트리를 사용한다.
머클 트리란 해시 트리라고도 불리며 각 노드에 그 자식 노드들에 보관된 값의 해시, 또는 자식 노드들의 레이블로부터 계산된 해시 값을 레이블로 붙여두든 트리이다.
이 머클 트리를 사용하면 동기화해야 하는 데이터의 양은 실제로 존재하는 차이의 크기에 비례할 뿐, 두 서버에 보관된 데이터의 총량과는 무관해진다. 하지만 실제로 쓰이는 시스템의 경우 버킷 하나의 크기가 꽤 크다는 것은 알아두어야 한다.
데이터 센터 장애에 대응할 수 있는 시스템을 만들려면 데이터를 여러 데이터 센터에 다중화하는 것이 중요하다.
키-값 저장소로 다이어 그램의 주 기능은 다음과 같다.
해당 절차를 모두 잘 따라왔다면, 분산 키-값 저장소는 다음과 같은 기능 전부를 지원해야 한다.
DynamoDB로 살펴보는 실 사례
- 데이터 파티션
다이나모 디비는 파티션 키값을 파라미터로 계산된 Hash Value를 기준으로 DynamoDB Table 내부의 파티션이 결정되어 데이터는 적재된다.
테이블을 생성할 때 지정하는 Primary Key 또한 파티션 키이며, GSI, LSI 또한 파티션 키를 가진다. 따라서 인덱스 설계에 따라 파티션을 나눌 수 있다.
- 데이터 다중화 및 일관성
각 테이블의 파티션은 테이블 인덱스 설계단계에서 한다고 하더라도 데이터의 가용성 해당 가용성을 챙기기 위해 테이블의 replica를 생성하면서 일관성을 지키기란 어렵다.
다이나모디비는 해당 문제를 해결하기 위해 글로벌 테이블을 지원한다.
글로벌 테이블은 여러 복제본 테이블로 구성된다. 각 복제본 테이블은 서로 다른 리전에 있지만, 모든 복제본은 같은 이름과 키를 가진다. 데이터가 복제본 테이블에 기록되면, 다이나모디비는 해당 데이터를 글로벌 테이블의 다른 모든 복제본 테이블에 자동으로 복제한다.
글로벌 테이블을 통해 하나의 리전의 네트워크나 사용성에 문제가 생겨도 가장 가까운 리전의 글로벌 테이블을 참조하고 있는 다른 레플리카 테이블에서 데이터를 가져와 사용할 수 있다.
다이나모디비는 가용성을 99.999%를 제공하기 때문에, 이를 통해 가용성을 해결하고 만약 6장에서 제시된 충돌을 해결하기 위해서 다이나모 디비 글로벌 테이블은 동시 업데이트 간 마지막 작성자의 채택을 조정하는 식의 방식 충돌 해결과정에서 복제본이 모든 테이블의 최신 업데이트를 일치시키고, 동일한 데이터를 보유하도록 만듬으로서 데이터의 일관성을 지키려고 노력합니다.
- 쓰기 경로
- Client가 쓰기 요청을 보낸다.
- DynamoDB는 쓰기 요청을 받고 해당 테이블의 클라이언트가 지정한 파티션 키를 사용하여 어느 파티션에 저장되야 하는지를 결정한다.
- 해당 파티션에 복제본을 관리하는 글로벌 테이블 서비스가 쓰기 요청을 받는다.
- 복제본을 관리하는 서비스는 쓰기 요청을 모든 복제본에 전달한다.
- 복제본은 쓰기를 수행하고, 클라이언트에게 응답한다.
- 읽기 경로
- 클라이언트는 읽기 요청을 보낸다.
- 다이나모 DB는 읽기 요청을 받고, 클라이언트가 지정한 파티션 키를 사용하여 어느 파티션에 저장됐는지 확인한다.
- 해당 파티션에 대해 복제본을 관리하는 서비스가 읽기 요청을 받는다.
- 복제본을 관리하는 서비스는 읽기 요청에 대한 응답을 반환한다.
요약
분산 키-값 저장소가 가져야 하는 기능과 그 기능 구현에 이용되는 기술 정리
- 대규모 데이터 저장 ⇒ 안정 해시를 사용해 서버들에 부하 분산
- 읽기 연산에 대한 높은 가용성 보장 ⇒ 데이터를 여러 데이터센터에 다중화
- 쓰기 연산에 대한 높은 가용성 보장 ⇒ 버저닝 및 벡터 시계를 사용한 충돌 해소
- 데이터 파티션 ⇒ 안정 해시
- 점진적 규모 확장성 ⇒ 안정 해시
- 다양성 ⇒ 안정 해시
- 조절 가능한 데이터 일관성 ⇒ 정족수 합의
- 일시적 장애 처리 ⇒ 느슨한 정족수 프로토콜과 단서 후 임시 위탁
- 영구적 장애 처리 ⇒ 머클 트리
- 데이터 센터 장애 대응 ⇒ 여러 데이터 센터에 걸친 데이터 다중화
Share article