Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions ch06-키값-저장소/윤유탁.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# [Ch.06] 키-값 저장소 설계 — 윤유탁

> 1주차 1차 설계안 (책 해설을 읽기 전, 문제와 요구사항만 보고 설계)

## 요구 사항

1. put/get만 지원
2. 키-값 쌍이 10kb이하
3. 큰 데이터 저장 가능
4. 높은 가용성
5. 트래픽에 따라서 증설/삭제
6. 일관성 수준 조정 가능
7. 짧은 응답 지연 시간

---

### 요구사항 상세 정리

1. put/get만 지원
- 연산이 단순하다?
2. 키-값 쌍이 10kb이하
- 값이 작다. 메모리로 커버가 가능하다
3. 큰 데이터 저장 가능
- 쌍 하나는 작은데 전체가 크다. 샤딩 해야할듯?
4. 높은 가용성
- 노드가 죽어도 응답가능해야 한다.
5. 트래픽에 따라서 증설/삭제
- 노드 수가 수시로 변한다
6. 일관성 수준 조정 가능
- 데이터 복제 구조가 고정되어있지 않은..?
7. 짧은 응답 지연 시간
- put/get이 아주 빨라야 한다.

3번 큰 데이터 저장 가능, 7번 낮은 지연 -> 두 개가 충돌. 메모리만으로는 못 담는데 디스크는 느리다

4번 고가용성, 6번 강한 일관성 -> 두 개가 충돌. 장애 중 최신 보장 하려면 응답을 못한다

5번 트래픽따라 증설/삭제, 7번 낮은 지연 -> 두 개가 충돌. 재배치 트래픽이 서비스 트래픽과 경합한다.

---

## 의사결정 및 고민할 것들

### 데이터를 어떻게 나눌 것인가? (샤딩)
선택지
1. 모듈러 기반 해싱
2. 범위 기반 (ex. key 사전순 분할)
3. 안정 해시

기본적으로 샤딩을 하기때문에 3번 요구사항(큰 데이터 저장가능)을 만족함.

모듈러 기반 해싱은 구현이 단순하다. 균등하게 분포가 가능하다. 그런데 노드 수가 바뀌면 키 재배치가 필요. 자동 증설/삭제를 만족 못함

범위 조회에 유리한데, 범위 조회가 필요가 없어보임. 키 분포가 쏠릴 수 있다.

안정해시는 노드 추가/삭제시 키 이동이 최소화 됨. 구현 복잡도는 올라간다.

안정해시가 제일 좋아보임

---

### 노드 한 대 안에서는 데이터를 어떻게 저장? (저장 엔진)
선택지
1. 순수 인메모리 해시 테이블
2. 디스크 기반 (B-tree 같은 거)
3. 인메모리 해시 + 디스크에 쓰기 로그 남기기

순수 인메모리는 제일 빠른데 노드가 죽으면 데이터가 전부 날아감. 가용성 위반.

디스크 기반은 안 죽는데 랜덤 I/O라 느림. 그리고 put/get만 있는데 B-tree의 정렬 기능은 쓸데 없는 비용임.

3번은 put이 오면 디스크에 로그를 순차로 한 줄 적고 메모리 갱신. 순차 쓰기라 빠르고, 죽어도 로그 다시 읽으면 복구 가능. 읽기는 메모리에서 바로.

근데 3번도 메모리보다 큰 데이터는 못 담음. 자주 안 읽는 데이터는 디스크로 내리는 식으로 보완해야 할 듯. 대신 콜드 데이터 읽기는 느려지는 거 감수.

3번이 제일 좋아보임. 로그가 무한히 자라니까 주기적으로 스냅샷 떠서 로그 정리하는 것도 필요함.

---

### 복제는 어떻게?
선택지
1. 리더 1대 + 동기 복제
2. 리더 1대 + 비동기 복제
3. 리더 없이 N대에 복제. 쓸 때 몇 대(W) 기다릴지, 읽을 때 몇 대(R) 읽을지 조절

1번은 항상 최신을 읽을 수 있는데, 팔로워 하나만 느려도 쓰기 전체가 느려짐. 그리고 일관성이 "항상 강함"으로 고정됨. 6번 요구사항(일관성 조정 가능) 위반.

2번은 빠른데 리더 죽으면 전파 안 된 쓰기가 유실됨. 일관성이 "항상 약함"으로 고정. 역시 6번 위반.

3번은 W랑 R 값이 그대로 일관성 다이얼이 됨. W+R > N이면 최신 보장, W=1 R=1이면 빠르고 느슨하게. 리더가 없으니까 어느 노드가 죽어도 나머지로 계속 진행 가능. 가용성에도 좋음.

대신 같은 키에 동시에 쓰면 충돌이 남. 이건 따로 해결해야 함.

6번 요구사항이 사실상 3번을 강제하는 것 같음. 리더 방식은 일관성이 구조에 고정되는데, 리더리스는 일관성이 파라미터가 됨.

---

### 일관성 조정은 어떤 단위로 해주지
선택지
1. 시스템 전역 설정
2. 요청 단위 옵션

전역 설정이면 "조정 가능"이라기엔 좀 부족함. 한 시스템 안에 강한 일관성 필요한 데이터랑 아닌 데이터가 같이 있을 수 있는데 못 받아줌.

요청 단위가 맞는 것 같음. 대신 아무 생각 없이 쓰는 사용자를 위해 기본값은 안전한 쪽(W+R > N)으로.

---

### 키의 담당 노드는 누가 찾아주나? (라우팅)
선택지
1. 중앙 라우터/프록시
2. 아무 노드나 요청 받고, 받은 노드가 담당 노드로 전달
3. 클라이언트가 링 정보를 캐시해서 직접 담당 노드로

중앙 라우터는 그 자체가 단일 장애 지점이 됨. 모든 요청에 홉도 하나 추가됨.

2번은 단일 장애 지점이 없음. 클라이언트는 노드 아무거나 하나만 알면 됨. 대신 담당 아닌 노드에 닿으면 홉 하나 추가.

3번은 홉이 없어서 제일 빠른데 클라이언트가 무거워지고, 링이 바뀌었을 때 캐시 갱신 문제가 생김.

기본은 2번으로 하고, 성능 필요한 클라이언트는 3번 쓰게 하면 될 듯. 둘이 공존 가능함.

---

### 같은 키에 동시에 쓰면? (충돌 해소)
리더리스를 골랐으니 피할 수 없는 문제.

선택지
1. 타임스탬프 찍어서 최신 게 이김
2. 버전 정보 들고 다니다가 충돌나면 양쪽 다 보관, 읽을 때 클라이언트가 해소
3. 키마다 직렬화 담당 노드를 정함

1번은 단순하고 알아서 수렴함. 근데 서버 간 시계가 어긋나 있으면 나중에 쓴 게 조용히 사라질 수 있음. 유실이 눈에 안 보이는 게 무서움.

2번은 유실이 없는데 클라이언트가 복잡해짐. "값 하나"라는 단순한 모델이 깨짐.

3번은 충돌 자체가 없는데, 그게 사실상 리더를 다시 만드는 거임. 복제에서 버린 단점이 그대로 돌아옴.

일단 1번으로 감. put/get만 있는 단순한 모델에서 2번은 과한 것 같음. 근데 이 판단이 맞는지는 자신 없음. 토론하고 싶은 부분.

---

### 노드 목록은 누가 관리하나? (장애 감지)
선택지
1. 중앙 코디네이터가 노드 목록이랑 링을 관리
2. 노드끼리 heartbeat 주고받으면서 알아서 전파

1번은 단일 진실 원천이라 단순함. 대신 코디네이터가 죽으면 증설/절체가 안 되니까 코디네이터 자체를 3~5대로 복제해야 함.

2번은 단일 장애 지점이 없는데, 전파되는 동안 노드마다 링이 다르게 보일 수 있음. 그 상태를 머리로 추론하기가 어려움.

1차 설계에서는 1번으로. 단순한 게 좋음. 코디네이터는 작은 합의 클러스터로 복제.

코디네이터가 heartbeat 받아서 노드 죽음 판정하고, 트래픽 지표 보고 증설/삭제 결정하고, 링 갱신해서 뿌리는 역할까지 맡김.

---

요구사항이랑 맞춰보면

1. put/get만 → 해시 기반 저장으로 단순하게. 정렬/범위 기능 비용 자체를 없앰
2. 10kb 이하 → 요청 받는 입구에서 검증하고 거절. 값이 작다는 전제 덕에 메모리 중심 설계가 가능했음
3. 큰 데이터 → 안정 해시 샤딩 + 노드별로 콜드 데이터 디스크 강등
4. 고가용성 → 3벌 복제, 리더 없어서 절체 대기도 없음
5. 자동 증설/삭제 → 코디네이터가 지표 보고 노드 추가, 안정 해시라 이동량 최소
6. 일관성 조정 → 요청 단위로 W/R 선택. W=2 R=2면 최신 보장, W=1 R=1이면 빠르게
7. 짧은 지연 → 읽기는 메모리, 쓰기는 순차 로그 한 번, 홉 최소화

---

## 병목 / 장애 지점 / 고민거리

병목
1. 핫 키. 안정 해시는 키 개수는 고르게 펴주는데 트래픽은 못 펴줌. 특정 키 폭주하면 그 복제본 3대만 두들겨 맞음. 읽기는 복제본으로 분산되는데 쓰기 핫 키는 이 설계로 해결 못 함
2. 재배치 트래픽. 증설할 때 키 이동이 서비스 트래픽이랑 경합함. 이동 속도 제한 걸면 되는데 그러면 스케일아웃 효과가 늦게 나타남
3. 쓰기 로그. 내구성 챙기려면 fsync 해야 하는데 이게 쓰기 지연의 바닥일듯. 여러 요청 묶어서 한 번에 fsync 하면 완화되는데 또 지연 vs 내구성 트레이드오프

장애 시나리오
4. W개 응답 받고 성공 처리했는데 나머지 복제본에 전파되기 전에 노드가 죽으면? 복제본 간 불일치가 영구화될 수 있음. 복구된 노드를 따라잡게 하는 백그라운드 동기화가 필요할 것 같은데 구체적으로는 못 정함
5. 재배치 중인 키에 요청이 오면? 일단 이동 끝나기 전까진 옛 노드가 정본이고 끝나면 링 갱신하는 걸로 생각했는데, 그 사이에 들어온 쓰기를 어떻게 할지 빈틈 있음
6. 코디네이터랑 데이터 노드 사이 네트워크만 끊기면? 멀쩡한 노드를 죽었다고 오판해서 불필요한 재배치가 일어날 수 있음. 죽음 판정 기준을 얼마나 보수적으로 잡을지 고민

토론하고 싶은 것
7. 시계 의존. 이 단순함이 운영에서 용납 가능한 수준인가?
8. delete가 없음. put(key, null)로 지운다 치면, 죽었다 살아난 복제본이 옛 값을 다시 살려내는 좀비 데이터 문제가 생기지 않나?
9. W=1로 쓰고 그 노드가 바로 죽으면 데이터가 사라짐. "일관성 조정 가능"이 "유실 허용"까지 포함하는 해석인가?