diff --git "a/ch06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\234\244\354\234\240\355\203\201.md" "b/ch06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\234\244\354\234\240\355\203\201.md" new file mode 100644 index 0000000..7ea0cdf --- /dev/null +++ "b/ch06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\234\244\354\234\240\355\203\201.md" @@ -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로 쓰고 그 노드가 바로 죽으면 데이터가 사라짐. "일관성 조정 가능"이 "유실 허용"까지 포함하는 해석인가? + +