6. 키-값 저장소 설계
들어가며
key-value database
non relational database
e.g. Amazon dynamo, memcached, Redis...
문제 이해 및 설계 범위 확장
키-값 쌍의 크기는 10KB 이하이다.
어느 정도일까?
평균적으로 1-5KB
큰 데이터를 저장할 수 있어야 한다
e.g. 수십 TB ~ PB
높은 가용성을 제공해야 한다
따라서 시스템은 설사 장애가 있더라도 빨리 응답해야 한다
e.g. 99.9%
높은 규모 확장성을 제공해야 한다
따라서 트래픽 양에 따라 자동적으로 서버 증설/삭제가 이루어져야 한다
데이터 일관성 수준은 조정이 가능해야 한다
C(consistency), A(Availability) Trade off 조절
응답 지연시간(latency) 이 짧아야 한다
단일 서버
한 대의 서버만 이용
키-값 쌍 전부를 해시 테이블에 저장하는 것
빠르지만, 모든 데이터를 메모리에 두는 것은 불가능함.
이를 위해 data compression, 자주 사용하는 데이터만 메모리에 올려두고, 나머지는 디스크에 저장하는 방법
그래도 한계가 올 것
분산 키-값 저장소
CAP
Consistency
모든 클라이언트는 어떤 노드에 접속했느냐에 관계없이 언제나 같은 데이터를 보게 되어야한다.
Availability
클라이언트는 일부 노트에 장애가 발생했더라도, 항상 응답을 받을 수 있어야한다.
Partition tolerance
두 노드 사이에 통신 장애가 발생했더라도, 시스템은 계속 동작해야한다.
![[Pasted image 20250817151259.png]]
3가지를 모두 달성하는 시스템은 존재 불가
CP -> 데이터 일관성, 파티션 감내 지원, 가용성 희생
AP -> 가용성, 파티션 감내 지원, 일관성 희생
CA -> 일관성, 가용성 지원, 파티션 감내 희생
통상적으로 네트워크 장애는 피할 수 없다.
그러므로 파티션 문제는 감내할 수 있도록 설게되어야함
그러므로 CA만 지원하는 시스템은 실세계에![[Pasted image 20250817151259.png]] 없음.
![[Pasted image 20250817151831.png]]
n3 node에서 장애, n1, n2 노드와는 통신할 수 없는 상태
즉 n1, n2 에서 기록한 데이터가 n3에 전달되지 않고,
n3에 기록되었으나, n1, n2 로 전달되지 않는 데이터도 있을 수 있음.
이 상황에서 일관성을 선택한다면?
n1, n2 에서 쓰기 연산을 중단시켜야한다.
이럴 경우 가용성은 깨진다.
e.g. 은행권 시스템
가용성을 선택한다면?
일단 읽기 연산을 허용한다.
n1, n2 에 쓰기 연산을 허용하고,
파티션 문제가 해결되면 n1, n2 의 새로운 데이터가 n3에 전송될 것
시스템 컴포넌트
데이터 파티션
데이터 다중화(replication)
일관성 (consistency)
일관성 불일치 해소(inconsistency resolution)
장애 처리
시스템 아키텍처 다이어그램
쓰기 경로(write path)
읽기 경로(read path)
데이터 파티션
파티션을 나눌 때 가장 중요한 것 2가지
데이터를 여러 서버에 고르게 분산할 수 있는가?
노드가 추가되거나, 삭제 될때 데이터의 이동을 최소화할 수 있는가?
Consistency Hashing
Virtual nodes를 통해 균등한 부하 분산 추가할 수 있음.
[물리 노드만 사용]
Ring: A(10°)--B(20°)--C(300°)
└─작은구간─┘ └─큰구간─┘
→ C가 전체의 80% 처리!
[Virtual Nodes 사용]
Ring에 각 노드를 여러 개 배치:
A-1, B-1, C-1, A-2, B-2, C-2, A-3, B-3, C-3...
→ 각 노드가 링 전체에 고르게 분포
→ 표준편차 3% 이하로 균등 분산
노드 추가 제거시 최소한 데이터 이동 평균(평균 K/N개의 키만 재배치, K=전체 키, N=노드 수)
[해시 링 구조]
0°/360°
|
NodeA(45°)
|
Key1(60°) → NodeB로
|
NodeB(120°)
|
Key2(150°) → NodeC로
|
NodeC(240°)
|
Key3(300°) → NodeA로 (순환)
[노드 추가 시]
NodeD를 180°에 추가
→ Key2만 NodeC에서 NodeD로 이동
→ 나머지는 그대로!
노드를 추가하면 그 구간의 데이터만 이동하면 되니까, 평균적으로 전체 데이터의 1/N만 이동
데이터 다중화
높은 가용성, 안정성을 확보하기 위해서는 데이터를 N개 서버에 비동적으로 다중화(replication) 할 필요 있음.
N은 튜닝 가능한 값
![[Pasted image 20250817153456.png]]
N = 3 이라면, 시계방향으로 돌면서 만난 3가지 노드 s1, s2, s3 에 저장된다.
가상 노드를 사용하면 실제 물리서버의 갯수는 N보다 작아질 수 있으므로, 노드를 선택할 때 같은 물리서버를 중복으로 선택하지 않도록 해야함
데이터 일관성
복제 전략: Quorum 기반 복제
N-W-R 모델 적용:
N = 3 (복제본 수)
W = 2 (쓰기 정족수, 2개 노드에 쓰기 성공해야 완료)
R = 2 (읽기 정족수, 2개 노드에서 읽어야 완료)
Strong Consistency
이유:
W + R > N (2+2 > 3)이므로 강한 일관성 보장
필요시 R=1로 조정하여 가용성 우선 모드 가능
1개 노드 장애 시에도 읽기/쓰기 가능
이 W+R > N 전략의 핵심은 '쓰기 집합과 읽기 집합이 반드시 겹친다'
[초기 상태 - 모든 노드가 v1 가지고 있음]
Node A: v1
Node B: v1
Node C: v1
[쓰기 발생 - v2로 업데이트, W=2]
Client → Write v2
↓
Node A: v2 ✓ (성공)
Node B: v2 ✓ (성공)
Node C: v1 (아직 전파 안됨)
→ 2개 노드 쓰기 성공, 응답 반환
[이후 읽기 시나리오들, R=2]
Case 1: A,B에서 읽기
Node A: v2 ┐
Node B: v2 ┘→ 둘 다 v2 → v2 반환 ✓
Case 2: A,C에서 읽기
Node A: v2 ┐
Node C: v1 ┘→ v2 > v1 → v2 반환 ✓
Case 3: B,C에서 읽기
Node B: v2 ┐
Node C: v1 ┘→ v2 > v1 → v2 반환 ✓
모든 경우에 최신 값 v2를 읽게 됨.
정리해보면
[W+R > N인 경우: W=2, R=2, N=3]
쓰기 집합: {A, B}
가능한 읽기 집합: {A,B}, {A,C}, {B,C}
→ 모든 읽기 집합이 쓰기 집합과 최소 1개 겹침!
→ 항상 최신 데이터를 가진 노드를 포함
[W+R ≤ N인 경우: W=1, R=1, N=3]
쓰기 집합: {A}
가능한 읽기 집합: {A}, {B}, {C}
→ B나 C에서 읽으면 구버전 읽을 수 있음!
→ 일관성 보장 안됨
Strong Consistency (강한 일관성)
W=2, R=2, N=3 (W+KaR=4 > N=3)
장점: 강한 일관성
단점: 2개 노드 응답 필요 → 느림
Weak Consistency (약한 일관성)
Eventual Consistency (최종 일관성)
W=1, R=1, N=3 (W+R=2 < N=3)
장점: 빠른 응답
단점: 일시적 불일관성 가능
사용 예시
금융 데이터: W=2, R=2 (일관성 우선)
로그 데이터: W=1, R=1 (성능 우선)
일반 서비스: W=2, R=1 (쓰기는 안전하게, 읽기는 빠르게)
비 일관성 해소 기법 : 데이터 버저닝
버저닝 (versioning)
데이터를 변경할 때 마다 해당 데이터의 새로운 버전을 만드는 것.
따라서 각 버전의 데이터는 변경 불가능 (immutable)
벡터 시계 (vector clock)
'서버, 버전' 의 순서쌍을 데이터에 매단 것
[초기 상태]
Value: "v0"
VClock: {A:0, B:0, C:0}
[Client1이 Node A에 쓰기]
Value: "v1"
VClock: {A:1, B:0, C:0}
[Client2가 Node B에서 읽고 수정]
Read: "v1" {A:1, B:0, C:0}
Write: "v2" {A:1, B:1, C:0}
[Client3가 Node A에서 v1 읽고 동시 수정]
Read: "v1" {A:1, B:0, C:0}
Write: "v3" {A:2, B:0, C:0}
[충돌 감지]
v2: {A:1, B:1, C:0}
v3: {A:2, B:0, C:0}
→ v2가 v3보다 크지도, 작지도 않음
→ 동시 쓰기(Sibling) 발생!
LWW (Last Write Win)
[각 쓰기에 타임스탬프 추가]
Write 1: {
value: "홍길동",
timestamp: 1000
}
Write 2: {
value: "김철수",
timestamp: 1001
}
→ 1001 > 1000 이므로 "김철수" 선택
장점
구현 간단
자동 충돌 해결
저장 공간 적음
단점
시계 동기화 이슈
데이터 손실 가능성
진짜 마지막이 아닐 수 있음
장애 감지
gossip protocool
decentralized failure detection, 중앙 서버 없이 각 노드가 독립적으로 장애를 감지하는 방법
A 노드 -> B 노드 (랜덤), A 노드가 가지고 있는 heartbeat counter list를 보냄
B 노드는 list를 기반으로 최신 값으로 갱신해야함.
if 갱신하지 않으면? B node는 장애 상태이다!
[각 노드가 관리하는 정보]
Node A의 Membership List:
{
NodeA: {heartbeat: 100, timestamp: t1},
NodeB: {heartbeat: 95, timestamp: t2},
NodeC: {heartbeat: 97, timestamp: t3},
NodeD: {heartbeat: 50, timestamp: t4} ← 오래됨!
}
매 초마다:
1. 자신의 heartbeat 증가
2. 랜덤 노드 선택해서 리스트 교환
3. 더 최신 정보로 업데이트
4. Timeout(15초) 지난 노드 → Suspected (e.g. Node D, 마지막 업데이트 시간이 15초나 되었네?)
5. 30초 지나면 → Dead
[감염 모델로 분석]
Round 0: Node D 죽음
Round 1: 1개 노드가 감지
Round 2: 2개 노드가 알게됨
Round 3: 4개 노드가 알게됨
...
Round log(N): 모든 노드가 알게됨
1000개 노드 → 10 라운드면 전파 완료!
일시적 장애 처리
strict quorum : 장애가 발생한 순산 읽기 쓰기 연산 금지
sloppy quorum : 가용성을 높이는 방향.
즉 쓰기 연산 수행할 W 개의 건강한 노드
읽기 연산을 수행할 R개의 건강한 노드를 고르는 방향.
선택에서 장애 노드는 무시
장애노드로 가는 요청들은 다른 서버가 처리.
hinted handoff 기법.
임시로 쓰기 연산을 처리한 서버가 그에 대한 힌트를 남겨서, 장애 회복 노드가 추후 인지할 수 있도록 하는 방법
Hinted Handoff
[기본 아이디어]
"너한테 줄 편지인데 네가 없어서
내가 들고 있다가 돌아오면 줄게"
정상 상황:
Client → Node B (Primary): 데이터 저장
Node B 장애 시:
Client → Node D (임시): 데이터 + "B한테 줄 것" 메모
↓
B 복구 시
↓
Node D → Node B: "네가 없을 때 받은 데이터야"
프로세스
[정상 상황 - Preference List 사용]
Key: "user:123"
Preference List: [B, C, D] (B가 primary)
쓰기 요청:
Client → B: "user:123" = "홍길동" ✓
→ C: 복제본 ✓
→ D: 복제본 ✓
[Node B 장애 시]
Preference List: [B(죽음), C, D, E]
쓰기 요청:
Client → C: "user:123" = "김철수" ✓
→ D: 복제본 ✓
→ E: 복제본 + Hint ✓
E가 저장하는 내용:
{
hint: {
target_node: "B",
original_key: "user:123"
},
data: {
key: "user:123",
value: "김철수",
timestamp: 1234567890,
version: 2
}
}
[Handoff 시나리오]
Timeline:
T0: Node B 다운
T1: 쓰기 요청 → E가 hint와 함께 저장
T2: 추가 쓰기들 → E에 계속 쌓임
T3: Node B 복구
T4: E가 B 복구 감지
T5: E → B: Hint된 데이터 전송 시작
T6: 전송 완료, Hint 삭제
E의 Hint Storage:
Before: [(B, key1), (B, key2), (B, key3)]
After: [] (모두 전달 완료)
장점
고가용성 달성
99.9% → 99.99% 가용성
Hinted는 1개의 노드만 살아있어도 됨.
일시적 장애 투명성
클라이언트는 장애를 모르고,
자동 복구되며,
개발자 (운영자) 의 개입이 최소화 된다.
단점
Hint 폭증
대량 장애 시, 스토리지가 부족해질 수 있음.
읽기 일관성
복구 부하
Hint 만료
영구 장애 처리
anti-entropy
사본들을 동기화.
사본들을 비교하여 최신 버전으로 갱신
즉 주기적으로 데이터를 비교하고 동기화 하는 과정
Merkle Tree 기반 동기화
구조
[데이터를 트리로 해싱]
Root Hash
/ \
Hash01 Hash23
/ \ / \
Hash0 Hash1 Hash2 Hash3
| | | |
Data0 Data1 Data2 Data3
각 노드: 자식들의 해시를 다시 해시
Root: 전체 데이터셋의 지문(fingerprint)
비교 과정
[Node A와 Node B 동기화]
Step 1: Root 비교
Node A Root: 0xABCD
Node B Root: 0xABCD
→ 같으면 종료 (데이터 일치!)
Step 2: Root가 다르면 자식 비교
Node A: [Hash01: 0x1234, Hash23: 0x5678]
Node B: [Hash01: 0x1234, Hash23: 0x9999]
→ Hash23 영역만 불일치!
Step 3: 불일치 영역만 깊이 탐색
Hash23 하위 비교...
→ Data3만 다름 발견
Step 4: 최소 데이터만 전송
Node A → Node B: Data3만 전송
Last updated
Was this helpful?