5장 안정 해시 설계
수평적 규모 확장성을 위해는 요청 또는 데이터를 서버에 균등하게 나누는 것이 중요하다.
해시 키 재배치(Refresh) 문제
serverIndex = hash(key) % N(서버의 갯수)
| 키 |
해시 |
해시 % 4 (서버 인덱스) |
| key0 |
18358617 |
1 |
| key1 |
26143584 |
0 |
| key2 |
18131146 |
2 |
| key3 |
35863496 |
0 |
| key4 |
34085809 |
1 |
| key5 |
27581703 |
3 |
| key6 |
38164978 |
2 |
| key7 |
22530351 |
3 |
| 총 4대의 서버를 사용한다고 가정했을 때 다음과 같은 서버 인덱스로 분산하게 된다. |
|
|
|
|
|
| 이 방법은 서버 풀 크기가 고정되어 있을 때, 데이터 분포가 균등할 때 잘 동작한다. |
|
|
|
|
|
| 그러나 서버 풀의 크기가 4에서 3으로 변경된다고 했을 때는 키 분포가 달라진다. |
|
|
|
|
|
| 만약 server1이 죽으면 캐시 클라이언트가 데이터가 없는 엉뚱한 서버에 접속하게 된다. |
|
|
|
|
|
| 이는 대규모 캐시 미스가 발생하게 될 것이다. 안정 해시는 이 문제를 해결할 수 있다. |
|
|
안정 해시
안정 해시(Consistent hash)는 해시 테이블 크기가 조정될 때 평균적으로 k/n개의 키만 재배치
하는 해시기술이다. (k=키의 갯수, n=슬롯의 갯수)
반면, 전통적 해시 테이블은 슬롯의 수가 바뀌면 대부분의 키를 재배치한다.
s0~s3은 해시 서버이고, k0~k3은 해시 키다.
해시 함수는 "해시 키 재배치 문제"에 언급된 함수와 다르고, 나머지 연산을 차용하지 않는다고 가정한다.
캐시 키 또한 해시 링 어디에든 위치할 수 있다.
서버 조회는 시계 방향으로 이루어지는데, key0 경우, server0에 저장된다.
(key1->server1, key2->server2, key3->server3)
- k0과 s0 사이에 s4를 추가한다고 가정할 때, k0만 s4에 재배치 하게 된다.
- s1이 제거됐다고 가정했을 때, k1만 s2에 재배치 하게 된다.
기본 구현법의 두가지 문제
안정 해시 알고리즘은 서버와 키를 균등 분포 해시 함수를 사용하여 해시 링에 배치한다.
그리곤 키의 위치에서 시계 방향으로 탐색하여 만난 최초의 서버에 배치된다.
이는 '파티션 크기를 균등하게 유지하기 어렵다'는 문제점이 있다.
위의 그림에서 s1이 제거된다 했을때 s2의 파티션이 다른 파티션보다 매우 커지게 됨을 확인할 수 있다.
또한, '키의 균등 분포를 달성하기 어렵다.'는 문제점이 있다.
위의 그림에서 보면 s2에 키가 보관되는 것을 확인할 수 있다.
가상 노드, (replica)
가상 노드는 실제 노드 또는 서버를 가리키는 노드로, 하나의 서버는 여러 개의 가상 노드를 가질 수 있다.

가상 노드의 개수를 늘릴 수록 키의 분포는 균등해진다. 그러나 개수를 늘릴수록 데이터는 많아진다는 것을 명심하자.
재배치할 키 설정
server4 가 추가 되었을 때, s3 ~ s4 사이의 키들이 영향을 받게 된다.

마찬가지로 server 가 삭제됐을 때도, 해당 서버-1~해당 서버 (반시계 방향)의 키들이 영향을 받게 될 것이다.
요약
- 서버가 추가, 삭제될 때 재배치 되는 키의 수가 최소화 된다.
- 데이터가 균등하게 분포하므로 수평적 규모 확장성을 달성하기 쉽다.
- 핫스팟 키 문제를 줄인다.
6장 키-값 저장소 설계
key-value store는 비 관계형 데이터베이스이다.
저장되는 값은 고유 식별자를 키로 가져야 하며, 이런 연결관계를 key-value쌍이라고 지칭한다.
성능상의 이유로 키는 짧을 수록 좋다.
value는 리스트, 객체 등 무엇이 오든 상관하지 않는다.
다이나모, memached, 레디스 등이 있다.
단일 서버 키-값 저장소
가장 직관적인 방법은 해시 테이블로 저장한다. 해시 테이블은 빠른 속도를 보장하지만 모든 데이터를 메모리 안에
저장하는 것이 불가능할 수 있다.
해결책으로는
-> 데이터 압축(compression)
-> 자주 쓰는 데이터만 메모리에 두고 디스크에 저장
이 있다.
그러나 한 대의 서버만으로 부족한 때가 오기 때문에 분산 키-값 저장소가 필요하다.
분산 키-값 저장소(distributed key-value story)
분산 해시 테이블이라고 불린다. 분산 시스템을 설계할 때는 CAP 정리(Consistency, Availability, Partition Tolerance theorem)를 이해하고 있어야 한다.
CAP 정리
CAP 정리는 데이터 일관성(Consistency), 가용성(Availability), 파티션 감내(Partition tolerance)라는 세 가지 요구사항을 동시에 만족하지 못한다는 정리다.
- 데이터 일관성: 클라이언트는 어떤 노드에 접속하든 같은 데이터를 본다.
- 가용성: 일부 노드에 장애 발생하더라도 항상 응답 가능해야 한다.
- 파티션 감내: 파티션은 두 노드 사이에 통신 장애가 발생하였음을 의미한다.
파티션 감내는 네트워크 파티션이 생겨도 시스템은 동작해야 한다는 뜻이다.
CAP 정리는 두 가지 조건을 충족하려면 하나는 반드시 희생되어야 한다는 것을 의미한다.
- CP 시스템: 가용성을 희생한다.
- AP 시스템: 데이터 일관성을 희생한다.
CA 시스템: 존재하지 않는다. 분산 시스템은 반드시 파티션 문제를 감내할 수 있도록 설계!
- CP 시스템의 경우: 데이터 불일치를 피하기 위해 n1, n2에 대한 쓰기 연산을 중단해야 한다.
-> 가용성이 깨진다.
-> 은행권 시스템이 주로 차용
2. AP 시스템의 경우: 예전 데이터를 반환할 위험을 감수하고 계속 읽기 연산을 허용한다.
n1, n2 역시
쓰기 연산을 허용할 것이고 파티션 문제가 해결된 뒤에 새 데이터 n3을 전송한다.
시스템 컴포넌트
널리 사용되는 세 가지 키-값 저장소 (다이나모, 카산드라, 빅테이블) 의 사례를 참고한다.
데이터 파티션
대규모 애플리케이션의 경우 데이터를 저장하는 방식의 가장 단순한 해결책은 작은 파티션으로 분할하고 여러대의 서버에 저장하는 것이다.
데이터 파티션을 나눌 때는 다음의 두 문제를 따져야 한다.
- 데이터를 여러 서버테 고르게 분산했는가
- 노드가 추가되거나 삭제될 때 데이터의 이동을 최소화할 수 있는가
안정 해시는 두 문제를 해결할 수 있다.
또한, 안정 해시를 사용하여 데이터 파티션을 하게되면 다음과 같은 장점이 있다.
- 규모 확장 자동화(automatic scaling): 시스템 부하에 따라 서버가 자동으로 추가, 삭제 된다.
- 다양성(heterogeneity): 각 서버 용량에 맞게 가상 노드 수를 조정할 수 있다. 즉, 고성능 서버는 가상 노드를 더 많이 가질 수 있다.
데이터 다중화
가용성과 안정성을 확보하기 위해 데이터를 N(튜닝한 값)개 서버에 비동기적으로 다중화(replica)할 필요가 있다.
그런데 가상 노드를 사용하면 선택한 N개의 노드와 대응할 실제 물리 서버의 개수가 N보다 작아질 수 있다.
이 문제를 피하려면 물리 서버를 중복 선택하지 않도록 해야한다.
같은 데이터 센터에 속한 노드는 정전, 네트워크 이슈, 자연재해 등의 문제를 동시에 겪을 수 있다.
따라서 안정성을 담보하기 위해 데이터의 사본은 다른 센터의 서버에 보관하고 센터들은 고속 네트워크로 연결한다.
데이터 일관성
여러 노드에 다중화된 데이터는 적절히 동기화가 되어야 한다. 정족수 합의(Quorum Consensus) 프로토콜을 사용하면
읽기/쓰기 연산 모두에 일관성을 보장할 수 있다.
<정의>
N=사본 개수
W=쓰기 연산에 대한 정족수. 쓰기 연산이 성공한 것으로 간주되려면 적어도 W개의 서버로 부터 쓰기 연산이 성공했다는 응답을 받아야 함
R=읽기 연산에 대한 정족수. 읽기 연산이 성공한 것으로 간주되려면 적어도 R개의 서버로 부터 응답을 받아야 함

N=3인 경우에 대한 그림을 예로 설명하자면,
W=1은 쓰기 연산이 성공했다고 판단하기에 coordinator는 최소 한대 서버가 쓰기 성공 응답을 보냈다는 뜻이다.
coordinator는 클라이언트와 노드 사이에서 proxy 역할을 한다.
W, R, N을 정하는 것은 응답 지연과 데이터 일관성 사이의 타협점을 찾는 전형적인 과정이다.
W, R의 값이 1보다 클 경우 데이터 일관성 수준은 높아지나 응답 속도는 저하될 것이다.
- R=1, W=N: 빠른 읽기 연산에 최적화된 시스템
- W=1, R=N: 빠른 쓰기 연산에 최적화된 시스템
- W+R>N: 강한 일관성이 보장됨 (보통 N=3, W=R=2)
- W+R<=N: 강한 일관성이 보장되지 않음
일관성 모델
- strong consistency: 모든 읽기 연산은 가장 최근의 갱신 결과를 반환
- weak consistency: 읽기 연산은 가장 최근에 갱신된 결과를 반환하지 못할 수도 있다.
- eventual consistency: 약한 일관성의 한 형태로, 갱신 결과가 모든 사본에 동기화되는 모델이다.
강한 일관성을 위해 일반적으로는 모든 사본에 현재 쓰기 연산 결과가 반영될 때까지 R/W를 금지하는 것이다.
이 방법은 고가용성 시스템에는 적합하지 않는데 새로운 요청이 중단되기 때문이다.
다이나모나 카산드라 같은 저장소는 결과적 일관성 모델을 택한다.
이럴 경우 쓰기 연산이 병렬적으로 발생시 시스템에 저장도니 값의 일관성이 깨질 수 있다. 이는 클라이언트가 데이터 버전정보를 활용하여
해결할 수 있다.
비일관성 해소 기법: 데이터 버저닝
데이터 다중화시 가용성은 높아지나, 사본 간 일관성이 깨질 가능성도 높아진다.
버저닝과 벡터 시계를 통해 해결할 수 있다.
버저닝은 데이터를 변경할 때 새로운 버전을 만드는 것이다. 따라서 각 버전의 데이터는 변경 불가능(immutable) 하다.
- 서버1에서 "name"의 값 "john"->"jhonSanFran"으로 변경한다.
- 서버2에서 "name"의 값 "john"->"jhonNewYork"으로 변경한다.
- 두 연산은 동시에 이루어진다.
- 충돌 값을 V1, V2라고 한다.
변경이 이루어 지면 원래 값은 무시할 수 있다. 두 버전의 충돌을 해소하기 위해 버저닝 시스템이 필요하다.
벡터 시계는 두 버전의 충돌을 해소하기 위해 보편적으로 사용되는 해결 방법이다.
[서버, 버전]의 순서쌍을 데이터에 매달아서 사용된다.
백터 시계는 버전의 앞 뒤를 쉽게 판단할 수 있다.
어떤 버전 X와 Y사이 충돌이 있는지 알아보려면 Y 벡터 시계 구성 요소 가운데 X의 벡터 시계 동일 서버 구성 요소를 비교하면 된다.
그러나 벡터 시계는 두 가지 단점이 있다.
- 충돌 감지 및 해소 로직이 클라이언트에 들어가기 때문에 클라이언트 구현이 복잡해 진다.
- [서버:버전]의 순서쌍 개수가 빨리 늘어난다.
따라서 임계치(threshold)를 설정하고 오래된 순서쌍을 벡터 시계에서 제거해야 한다.
~~그러나, 버전 간의 선후 관계를 정확히 할수 없어 충돌 해소 과정의 효율성이 낮아진다는 단점이 있으나 실제 서비스에서 아마존에 의하면
그런 문제는 발생하지 않았다고 한다.~~
장애 감지
보통 두대 이상의 서버거 장애를 보고해야 실제로 장애가 발생했다고 간주한다.
멀티캐스팅 채널을 구축하여 서버 장애를 손쉽게 감지할 수 있다. 하지만 서버가 많을 땐 비효율적이다.
가십 프로토콜 같은 분산형 장애 감지(decentralized failure detection) 솔루션이 효율적이다.
- 각 노드는 멤버십 목록을 유지한다. (멤버 ID, 박동 카운터)의 목록이다.
- 각 노드는 주기적으로 박동 카운터를 증가시킨다.
- 각 노드는 무작위로 선정한 노드에게 주기적으로 자기 박동 카운터 목록을 보낸다.
- 받은 노드는 멤버십 목록을 최신 값으로 갱신한다.
- 어떤 멤버의 값이 갱신되지 않으면 해당 멤버는 offline(장애)상태인 것으로 간주한다.
일시적 장애 처리
가십 프로토콜로 장애를 감지한 시스템은 가용성을 보장하기 위한 조치를 취해야 한다.
- strict quorum 을 쓴다면 읽기와 쓰기 연산을 금지해야 한다.
- sloppy quorum 을 쓴다면 strict 보다 완화하여 가용성을 높인다.
sloppy quorum 은 쓰기 연산을 수행할 W개의 서버와 읽기 연산을 수행할 R개의 서버를 해시 링에서 고른다.
단, 장애 상태의 서버는 무시한다.
장애 서버의 경우 다른 서버가 임시로 맡아 처리한다. 변경 사항의 경우 장애 서버의 복구 시에 일괄 반영하며
데이터 일관성을 보존한다.
임시로 맡는 서버의 경우 쓰기 연산 처리에 대한 hint 를 남겨둔다.
이런 장애 처리 방안을 단서 후 임시 위탁(hinted handoff) 기법이라 부른다.
영구 장애 처리
영구적인 노드 장애 상태 처리 방안은 반-엔트로피(anti-entropy) 프로토콜을 구현하여 사본들을 동기화 해야한다.
반-엔트로피 프로토콜은 사본을 비교하여 최신으로 갱신하는 과정을 포함한다. 비교를 할 때는 상태를 탐지하고 데이터 양을 줄이기 위해 머클트리 를 사용한다.
머클트리를 이용한 영구 장애 처리의 예시(그림 6-13~6-16)를 보면, 루트 노드의 해시 값 비교로 시작한다.
머클 트리를 사용 시, 동기화 해야 하는 데이터의 양은 실제 차이 크기에 비례하며 두 서버의 보관한 데이터와는 무관하다.
하지만 실제 시스템의 경우 버킷 하나의 크기는 상당하는 것을 명심해야 한다.
데이터 센터 장애 처리
- 클라이언트는 get(key), put(key, value) 통신한다.
- coordinator는 클라이언트에게 키-값 저장소에 대한 프락시 역할을 한다.
- 노드는 안정 해시의 해시 링 위에 분포한다.
- 노드를 자동으로 추가, 삭제 할 수 있도록, 시스템은 완전히 decentralized.
- 데이터는 여러 노드에 다중화 된다.
- 모든 노드에게 책임이 있으므로 SPOF 는 존재하지 않는다.
쓰기 경로
카산드라에서 쓰기 요청이 특정 노드에게 전달되는 과정
- 쓰기 요청이 커밋 로그에 기록된다.
- 데이터가 메모리 캐시에 기록된다.
- 메모리 캐시가 가득 차거나, 임계치에 도다하면 데이터는 디스크의 SSTable에 기록된다.
읽기 경로
데이터가 메모리 캐시에 있는지 찾아본다.
있는 경우, 반환한다.
없는 경우, 디스크에서 가져와야 한다. 이 때, 어느 SSTable에 있는지 효율적으로 찾아보기 위해 블룸 필터를 사용한다.
- 데이터가 메모리에 있는지 조회한다.
- 없을 경우, 블룸 필터를 검사한다.
- 어떤 SSTable 에 키가 보관되어 있는지 알아낸다.
- SSTable 에서 데이터를 가져온다.
- 해당 데이터를 클라이언트에 반환한다.
참조
- Alex Xu, (2021). 가상 면접 사례로 배우는 대규모 시스템 설계 기초, 인사이트