키-값 저장소는 키-값 데이터베이스라고도 불리는 비 관계형 데이터베이스이다.
다음 연산을 지원하는 키-값 저장소를 설계해보자.
put(key, value): 키-값 쌍을 저장소에 저장한다.get(key): 인자로 주어진 키에 메달린 값을 꺼낸다.읽기, 쓰기 그리고 메모리 사용량 사이에 어떤 균형을 찾고, 데이터의 일관서와 가용성 사이에서 타협적 결정을 내려야 한다.
이 글에서는 아래와 같은 특성을 가정한다.
가장 직관적인 방법으로는 키-값 쌍 전부를 메모리에 해시 테이블로 저장하는 것이다.
빠른 속도를 보장하긴 하지만 모든 데이터를 한 메모리 안에 두는 것이 불가능할 수도 있다.
개선책으로는
이 있지만 많은 데이터를 저장하려면 분산 키-값 저장소를 만들 필요가 있다.
분산 해시 테이블이라고도 불리며, 여러 서버에 키-값 쌍을 분산시킨다.
분산 시스템을 설계할 때는 CAP 정리가 이해하고 있어야 한다.
CAP 정리는 데이터 일관성, 가용성, 파티션 감내라는 세 가지 요구사항을 도이에 만족하는 분산 시스템을 설계하는 것은 불가능하다는 정리다.
CAP정리는 이들 중 어떤 2가지를 충족하려면 나머지 하나는 반드시 희생되어야 한다는 것을 의미한다.

키-값 저장소는 앞서 언급한 세가지 요구사항 가운데 어는 두가지를 만족하느냐에 따라 분류할 수 있다.
하지만 통상적으로 네트워크 장애는 피할 수 없는 일로 여겨지므로, 분산 시스템은 반드시 파티션 문제를 감내하도록 설계되어야 한다.
따라서 실세계에서 CA시스템은 존재하지 않는다.
키-값 저장소 구현에 사용될 핵심 컴포넌트들 및 기술들을 살펴볼 것이다.
대규모의 애플리케이션의 경우, 전체 데이터를 작은 파티션으로 분할한 다음 여러 대의 서버에 저장해야 한다.
데이터를 파티션 단위로 나눌 때 다음 문제를 해결해야한다.
이에 대한 해결책으로는 앞 장에서 설명한 안정해시가 적합하다.
안정해시를 사용하면 아래와 같은 장점이 있다.
고가용성과 안정성을 확보하려면 비동기적인 다중화가 필요하다.
해시 링 위에 저장할 키 값을 배치하고 만나는 N개의 서버에 데이터 사본을 보관하는 것이다.

위 의 그림에서 N = 3으로 설정했을 때 key0은 s1, s2, s3에 저장된다.
가사노드를 사용한다면 같이 선택한 N개의 노드가 대응될 실제 물리 서버의 개수가 N보다 작아질 수 있다.
이러한 문제를 피하려면 같은 물리서버를 중복 선택하지 않도록 해야 한다.
여러 노드에 다중화된 데이터는 적절히 동기화가 되어야 한다.
W, R, N의 값을 정하는 것은 응답 지연과 데이터 일관성 사이의 타협점을 찾는 전형적인 과정이다.
W, R이 작은 경우 응답속도는 빠를 것이다. W,R이 큰 경우는 데이터 일관성 수준은 향상되지만 응답은 느려질 것이다.
N, W, R값이 가능한 몇가지 구성이 있다.
일관성 모델은 데이터의 일관성의 수준을 결정하는데, 종류가 다양하다.
데이터를 다중화하면 가용성은 높아지지만 사본 간 일관성이 깨질 가능성은 높아진다.
이를 해결하기 위해 버저닝(versioning)과 벡터 시계(vector clock)을 이용한다.
버저닝은 데이터를 변경할 때마다 데이터의 새로운 버전을 만드는 것을 의미한다. 각 버전의 데이터는 불변하다.
그리고 서로 다른 버전의 충돌을 해결하기 위해 벡터 시계가 보편적으로 이용된다.
벡터 시계는 [서버, 버전]의 순서쌍을 데이터에 메단 것이다. 어떤 버전이 선행 버전인지, 후행 버전인지, 아니면 다른 버전과 충돌이 있는지 판별하는데 쓰인다.
클라이언트가 데이터 D1을 시스템에 기록한다. 이 쓰기 연산을 처리한 서버는 Sx이다. 따라서 벡터 시계는 D1([Sx, 1])으로 변한다.
다른 클라이언트가 데이터 D1을 읽고 D2로 업데이트한 다음 기록한다. D2는 D1에 대한 변경이므로 D1을 덮어쓴다. 이 때 쓰기 연산은 같은 서버 Sx가 처리한다고 가정하자. 벡터 시계는 D2([Sx, 2])로 바뀔 것이다.
다른 클라이언트가 D2를 읽어 D3로 갱신한 다음 기록한다. 이 쓰기 요청은 Sy가 처리한다고 가정하면 벡터시계는 D3([Sx, 2], [Sy, 1])로 바뀐다.
Sy가 요청을 처리하기 전에 또 다른 클라이언트가 D2를 읽고 D4로 갱신한 다음 기록한다. 이때 쓰기연산은 서버 Sz가 처리한다고 가정하자. 그러면 벡터시계는 D4([Sx, 2], [Sz, 1])이 된다.
어떤 클라이언트가 D3와 D4를 읽으면 데이터 간 충돌이 있다는 것을 알게된다.
D3([Sx, 2], [Sy, 1]), D4([Sx, 2], [Sz, 1])
이런 경우, 클라이언트가 해소한 후에 서버에 기록한다. 이 쓰기 연산을 처리한 서버가 Sx였다고 하면, 벡터시계는 D5([Sx, 3], [Sy, 1], [Sz, 1])로 바뀐다.
대규모 시스템에서 장애는 그저 불가피하기만 한 것이 아니라 아주 흔하게 벌어지는 사건이다.
보통 두 대 이상의 서버가 똑같이 한 서버의 장애를 보고해야 해당 서버에 실제로 장애가 발생했다고 간주한다.
모든 노드 사이에 멀티캐스팅 채널을 구축하는 것이 서버 장애를 감지하는 가장 손쉬운 방법이지만 서버의 수가 많아진다면 비효율적일 것이다.
엄격한 정족수 접근법을 쓴다면 앞서 "데이터 일관성" 절에서 설명한 대로, 읽기와 쓰기 연산을 금지해야 할것이다.
느슨한 정족수 접근법은 이 조건을 완화하여 가용성을 높인다. 정족수 요구사항을 강제하는 대신, 쓰기 연산을 수행할 W개의 건강한 서버와 읽기 연산을 수행할 R개의 건강한 서버를 해시 링에서 고른다. 이때 장애 상태의 서버는 무시한다.
장애가 발생한 서버가 복구되었을 때 일괄 반영하여 데이터 일관성을 보존한다. 이를 위해 임시로 쓰기 연산을 처리한 서버에는 그에 관한 단서(hint)를 남겨두는 단서 후 임시 위착(hinted handoff) 기법을 사용한다.
앞에서 얘기한 단서 후 임시 위탁 기법은 "임시적" 장애를 처리하기 위함이다.
우리 노드의 장애가 영구적이 될 수 있는데, 이럴 때는 반-엔트로피 프로토콜을 구현해서 사본들을 동기화 할 것이다.
머클 트리를 이용하여 사본 간의 일관성이 망가진 상태를 탐지하고 전송 데이터 양을 줄인다.
키 공간이 1부터 12까지일 때 머클 트리를 만드는 예제를 한번 살펴보자.

완성된 두 머클 트리에서 루트 노드의 해시값을 비교하며 아래쪽으로 탐색해 나가다 보면 다른 데이터를 갖는 버킷을 찾을 수 있으므로, 그 버킷들만 동기화한다.
데잍터 센터 장애는 정전, 네트워크 장애, 자연재해 등 다양한 이유로 발생할 수 있다.
그러므로, 데이터를 여러 데이터 센터에 다중화하는 것이 중요하다.

SSTable은 Sorted-String Table의 약어로, <키, 값>의 순서쌍을 정렬된 리스트 형태로 관리하는 테이블이다.
읽기 요청을 받은 노드는 데이터가 메모리 캐시에 있는지부터 살핀다. 있는 경우에는 해당 데이터를 클라이언트에게 반환한다.
데이터가 메모리에 없는 경우에는 디스크에서 가져와야한다. 어느 SSTable에 키가 있는지 알아낼 방법으로 블룸 필터가 흔히 사용된다.
출처
가상 면접 사례로 배우는 대규모 시스템 설계 기초(알렉스 쉬 지음 | 이병준 옮김)