키-값 저장소(key-value store)는 고유 식별자를 키로 갖는 값들이 저장된 비 관계형 데이터베이스이다. 이런 연결 관계를 “키-값” 쌍이라고 지칭한다.
이번 장에서는 put/get 연산을 지원하는 키-값 저장소를 설계해볼 것이다.
설계 목표
설계 범위
단일 서버로 키-값 저장소를 설계하는 쉬운 방법은 키-값 쌍 전부를 메모리에 해시 테이블로 저장하는 것이다. 하지만 이 방식은 모든 데이터를 메모리 안에 두는 것이 불가능 할 수도 있다는 약점을 갖는다. 개선책은 다음과 같다.
하지만 위의 방법도 한계가 명확하여 분산 키-값 저장소를 만들 필요가 있다.
분산 키-값 저장소는 키-값 쌍을 여러 서버에 분산시킨다. 분산 시스템을 설계할 땐느 CAP 정리 (Consistency, Availablity, Partition tolerence theorem)를 이해해야 한다.
CAP 정리는 데이터 일관성(consistency), 가용성(availability), 파티션 감내(partition tolerance)라는 세 가지 요구사항을 동시에 만족하는 분산 시스템을 설계하는 것은 불가능하다는 정리이다.
세 가지 요구사항 중 어떤 두 가지를 충족하려면 나머지 하나는 반드시 희생되어야 한다. 두 가지를 만족하는 것을 분류하면 다음과 같다.
실세계의 분산 시스템
분산 시스템에서는 파티션 문제를 피할 수 없다. 따라서 파티션 문제가 발생하면 일관성과 가용성 중 하나는 택해야한다. 아래 그림에서는 n3 노드에 장애가 발생하여 n1 및 n2 노드와 통신할 수 없는 상황이다.
키-값 저장소 구현에 사용될 핵심 컴포넌트들 및 기술들을 살펴보자.
데이터 파티션
대규모 애플리케이션의 경우, 데이터를 작은 파티션들로 분할하고 여러 대의 서버에 저장하는 것이 좋다. 이때 다음 두가지 문제를 중요하게 따져봐야 한다.
안정 해시(consistent hash)는 이런 문제를 풀기에 적합하다. 키는 시계방향으로 가장 가까운 서버에 저장된다.
안정 해시를 사용한 데이터 파티셔닝의 장점
데이터 다중화
높은 가용성과 안정성을 위해 데이터를 N개의 서버에 비동기적으로 다중화(replication)할 필요가 있다.
같은 데이터 센터에 속한 노드는 정전, 네트워크 이슈, 자연재해 등 문제를 동시에 겪을 가능성이 있다. 따라서 안정성을 담보하기 위해 데이터의 사본은 다른 센터의 서버에 보관하고, 센터들은 고속 네트워크로 연결한다.
데이터 일관성
다중화된 데이터에 대해 일관성을 보장하기 위해서는 데이터간의 적절한 동기화가 필요하다. 정족수 합의(Quorum Consensus) 프로토콜을 사용하면 읽기/쓰기 연산 모두에 일관성을 보장할 수 있다. 관련된 정의를 살펴보자.
N, W, R의 값을 정하는 것은 응답 지연과 데이터 일관성 사이의 타협점을 찾는 과정이다.
예를 들어 W=1 또는 R=1 인 경우, 중재자는 한 서버로부터 응답만 받으면 되니 응답 속도가 빠르다.
반면 W나 R이 1보다 크면 일관성은 커지지만, 응답속도는 가장 느린 서버로부터의 응답을 기다려야 하므로 느리다.
요구되는 일관성 수준에 따라 적절히 N, W, R의 값을 조정하면 된다.
일관성 모델은 데이터 일관성의 수준을 결정한다.
비 일관성 해소 기법: 데이터 버저닝
문제 상황: 동시에 한 데이터에 대해 쓰기 연산이 발생
벡터 시계는 [서버, 버전]
의 순서쌍을 데이터에 매달아(associate) 어떤 버전이 먼저인지 판단하고, 다른 버전과 충돌이 있는지 판단할 때 사용되는 기술이다.
[S1,v1]
, [S2,v2]
, … [Sn,vn]
)[Si,vi]
가 있으면 vi를 증가시킴 (버전 있으면 v 증가)[Si,1]
생성 (새 버전)D1을 Sx 서버에서 처리 → 새 버전 → 벡터시계: D1([Sx,1]
)
D2을 Sx 서버에서 처리 → 버전 있으므로 v 증가 → 벡터시계: D2([Sx,2]
)
D3을 Sy 서버에서 처리 → 새 버전 → 벡터시계: D3([Sx,2]
, [Sy,1]
)
D4을 Sz 서버에서 처리 → 새 버전 → 벡터시계: D4([Sx,2]
, [Sz,1]
)
D5을 Sx 서버에서 처리 → 버전 있으므로 v 증가→ 벡터시계: D5([Sx,3]
, [Sy,1]
,[Sz,1]
)
X ≤ Y:
벡터 X의 모든 항목이 벡터 Y와 같거나 작다면, X는 Y의 이전 상태이다.
[Sx,2]
) 는 D3([Sx,2]
, [Sy,1]
)에 대해 모든 항목이 작거나 같으므로 (포함하므로) 이전 상태X ≠ Y
and X ⊀ Y
and Y ⊀ X
: 서로 비교할 수 없는 경우, 두 상태는 충돌로 간주된다.
[Sx,2]
, [Sy,1]
)과 D4([Sx,2]
, [Sz,1]
)는 우위에 있는 쪽이 없다. Sy에 대해선 D3가 우위고, Sz에 대해선 D4가 우위이다.[S0,1]
, [S1,1]
)와 D([S0,2]
, [S1,0]
) 도 마찬가지로, 어느 한 쪽도 우위에 있지 않으므로 충돌클라이언트 충돌 해소 전략
- 자동 충돌 해소
- LWW(last write wins): 동일 데이터에 대한 여러 버전이 존재할 때, 마지막 쓰기 연산을 채택하는 방식
- 타임스탬프나 버전 번호를 비교하여 가장 최신의 데이터를 선택할 수 있음
- 구현이 쉽고, 클라이언트단에서 복잡한 충돌 해소를 구현하지 않아도 됨
- OT(Operational Transformation)
- 입력한 순서에 따라 중앙 서버가 이를 적절히 변형하여 전달하는 방식
- 충돌이 날 수 있는 부분에 대해서 먼저 발생된 사건 순으로 정렬
- 서버에서 이를 순차적으로 처리하여 보정
- 위의 예에서 만약 User1의 연산이 먼저 수행되었다고 보면, C를 인덱스 0에 추가한 후, User2의 연산을 수행. 이때 인덱스 0을 삭제하는 것이 한 칸씩 밀리면서 인덱스 1을 삭제하는 것으로 보정됨
- 장점: 직관적인 방법
- 단점: 변경사항을 보정해줄 중앙 서버가 필요하고, 트래픽이 몰렸을 때 과부하가 올 수 있음
- 현재는 잘 사용되지 않는 방식
- CRDT(Conflict-free Replicated Data Type)
- 전략: 어떤 변경사항을 받으면, 순서와 상관없이 변경사항만 같으면 같은 상태
- 시간이나 우선순위에 상관없이, 수정된 각 개체를 유니크한 값으로 봄
- OT 방식과 달리 유니크한 인덱스 0.5가 부여되므로 보정 작업이 필요 없음
- 클라이언트간의 변경사항만 잘 공유하면 됨
- 장점: 중앙 제어 서버가 필요하지 않고, 데이터를 수정하는 유저들끼리만 데이터를 교환하면 되므로 빠름
- 단점: 데이터가 섞여버리는 문제가 발생할 수 있음
- 수동 충돌 해소
- 사용자가 직접 해결: 병합 도구를 제공하거나 충돌된 부분을 마킹해서 보여줌으로 사용자가 직접 해결할 수 있도록 하는 방법
- Git merge conflict 해결 도구 등
- 참고: https://velog.io/@heelieben/%EC%8B%A4%EC%8B%9C%EA%B0%84-%EB%8F%99%EC%8B%9C-%ED%8E%B8%EC%A7%91-OT-%EC%99%80-CRDT
벡터 시계는 충돌을 감지하고 해소하는 좋은 방법이지만, 두 가지 단점이 존재한다.
[서버, 버전]
순서쌍장애 처리
대규모 시스템에서 장애는 아주 흔하게 벌어지는 사건이며, 장애를 어떻게 처리할 것이냐 하는 것은 매우 중요한 문제이다. 장애 감지 기법을 먼저 살펴보고, 이후 장애 해소(처리) 전략들에 대해 알아볼 것이다.
장애 감지
분산 시스템에서는 보통 두 대 이상의 서버가 똑같은 장애를 보고하면 장애로 간주한다.
모든 노드사이에 멀티캐스팅 채널을 구축하여 장애를 감지할 수 있지만, 서버가 많아질수록 비효율적인 방법이다. 분산형 장애 감지로는 가십 프로토콜이 훨씬 효율적이다.
가십 프로토콜(gossip protocol)
일시적 장애처리
영구 장애처리
데이터 센터 장애 처리
시스템 아키텍처 다이어그램
get(key)
, put(key, val)
로 통신한다.쓰기 경로
1. 쓰기 요청이 커밋 로그 파일에 기록됨
2. 데이터가 메모리 캐시에 기록됨
3. 메모리 캐시가 가득 차거나 사전에 정의된 어떤 임계치에 도달하면 데이터는 SSTable에 기록됨
읽기 경로
데이터가 메모리 캐시에 있는지 검사 (있으면 그대로 반환)
데이터가 캐시에 없으면 블룸 필터를 검사
블룸 필터를 통해 어떤 SSTable에 키가 보관되어 있는지 확인
SSTable에서 데이터 가져옴
결과 데이터를 클라이언트에 반환