데이터 중심 애플리케이션 설계 스터디 발표 정리
참고: Designing Data-Intensive Applications 6장
이번 장에서 다루는 내용은 크게 네 가지다.
파티셔닝의 목적은 데이터와 질의 부하를 노드 사이에 고르게 분산시키는 것이다.
특정 파티션에 부하가 몰리는 현상을 쏠림(skew), 그 파티션을 핫스팟이라고 부른다.
이를 피하기 위한 두 가지 전략이 있다.
각 파티션에 연속된 키 범위를 할당하는 방식. 종이 백과사전이랑 같은 구조다.
타임스탬프 기반 데이터가 대표적인 예다. 현재 시각 기준으로 쓰기가 몰리면 항상 마지막 파티션만 과부하를 받는다.
해시 함수로 키의 파티션을 결정하는 방식.
해시 기반 전략은 크게 두 가지로 나뉜다.
모듈러 샤딩 (hash % N)
Consistent Hashing
💡 우아한형제들 사례 (블로그 링크)
콘텐츠 유지 기간이 24시간으로 제한되는 서비스였다.
데이터가 생성되는 만큼 삭제되기 때문에 전체 데이터 총량이 일정하게 유지되고, DB 서버를 추가로 증설할 필요성이 낮았다.
이 상황에서는 "재배치가 어렵다"는 모듈러 샤딩의 단점보다 "균일하게 분산된다"는 장점이 더 크다고 판단해 모듈러 샤딩을 선택했다.
모든 데이터를 해싱하면 범위 질의가 불가능하고, 모두 정렬하면 핫스팟이 생긴다.
카산드라는 기본키를 두 부분으로 나눠서 이 문제를 동시에 해결한다.
| 구성 요소 | 역할 |
|---|---|
| 파티션 키 (Partition Key) | 첫 번째 칼럼. 해싱하여 어느 노드에 저장할지 결정 → 균일한 분산 |
| 클러스터링 칼럼 (Clustering Column) | 나머지 칼럼. 파티션 내부에서 정렬하여 저장 → 범위 질의 지원 |
샤드 키를 고를 때 아래 4가지 기준을 기준으로 트레이드오프를 따져야 한다.
| 기준 | 핵심 질문 | 주의사항 |
|---|---|---|
| 카디널리티 (Cardinality) | 데이터를 얼마나 잘게 쪼갤 수 있는가? | 낮으면 점보 청크(Jumbo Chunk) 위험 |
| 빈도 (Frequency) | 특정 값에 데이터가 쏠리지 않는가? | 카디널리티가 높아도 특정 값이 편중되면 핫스팟 발생 → 복합 샤드 키로 해결 |
| 단조 변화 (Monotonic Change) | 쓰기가 한 서버에만 몰리는가? | 타임스탬프 같은 단조 증가 키를 Range Sharding에 쓰면 쓰기 병목 |
| 쿼리 패턴 (Query Pattern) | 검색할 때 모든 서버를 다 뒤져야 하는가? | 샤드 키 없는 검색은 Broadcast Query → 팬아웃 문제 |
참고: MongoDB 샤드 키 선택 가이드 / AWS RDS Sharding / Azure Sharding Pattern
보조 색인은 레코드를 유일하게 식별하는 게 아니라 특정 값을 가진 항목을 검색하는 수단이다.
파티셔닝에 보조 색인이 얹어지면 복잡해지는데, 크게 두 가지 방식이 있다.
각 파티션이 자신이 보유한 데이터에 대한 보조 색인을 독립적으로 유지한다.
💡 트위터 Earlybird 사례
트위터는 검색 인덱스를 파티셔닝할 때 문서(Tweet ID) 기준으로 쪼개는 방식을 선택했다.
실시간성이 핵심인 서비스 특성 때문이다.
- 매초 수만 건의 트윗이 생성되는 환경에서 Local Index는 새 트윗이 들어올 때 단 하나의 파티션만 업데이트하면 된다.
- Global Index(용어 기준)는 트윗 하나에 포함된 단어 수만큼 여러 파티션을 업데이트해야 해서 쓰기 지연이 발생한다.
- 트위터 검색은 대개 최신순이기 때문에, Scatter-Gather 방식이더라도 각 파티션의 상위 결과만 모아 Merge Sort하면 충분히 효율적이다.
Scatter-Gather의 팬아웃 문제는 병렬 처리 + In-memory Index로 해결했다.
최신 인덱스를 전부 RAM에 올려놔서 디스크 I/O 없이 수 밀리초 내에 검색을 완료한다.
모든 파티션의 데이터를 담당하는 전역 색인을 만들고, 이 색인 자체도 파티셔닝한다.
카산드라의 보조 색인은 각 노드에 로컬 인덱스로 저장된다.
특정 조건으로 검색하면 모든 노드가 조회에 참여해야 한다.
부하가 증가하거나 노드에 장애가 생기면 데이터와 요청을 다른 노드로 옮겨야 한다.
| 전략 | 설명 | 비고 |
|---|---|---|
| mod N 연산 | 노드 수가 바뀌면 대부분의 키 이동 | ❌ 쓰지 마세요 |
| 파티션 개수 고정 | 파티션을 노드 수보다 많이 만들고 노드 추가 시 파티션 일부 이동 | 안정적 |
| 동적 파티셔닝 | 데이터 양에 따라 파티션을 쪼개거나 합침 | HBase, MongoDB |
| 노드 비례 파티셔닝 | 노드 수 증가 시 개별 파티션 크기가 작아지도록 유지 | Cassandra |
완전 자동 재균형화는 편리하지만 예측하기 어렵고, 연쇄 장애 위험이 있다.
운영 환경에서는 사람이 개입하는 방식이 더 안전할 수 있다.
💡 디스코드 사례 (원문)
채널 ID + 시간 버킷 조합으로 파티션을 나눠 메시지를 분산 저장했다.
대형 채널에서 메시지가 몰리면 특정 파티션에 읽기가 집중되는 Hot Partition 문제가 생겼고, 두 가지로 해결했다.1. Cassandra → ScyllaDB 마이그레이션
Cassandra는 JVM 기반이라 GC 지연이 잦다. ScyllaDB는 C++로 구현되어 같은 워크로드를 더 적은 노드로 처리할 수 있다.2. 일관된 해시 라우팅 도입
같은 채널에서 오는 요청이 항상 동일한 Data Service 인스턴스로 라우팅되도록 해서 캐시 히트율을 높였다.
Hot Partition으로 인한 부하를 서비스 계층에서 흡수해, DB에 직접 몰리는 요청 자체를 줄였다.결과: 노드 수 177 → 72, 지연 시간도 크게 개선됐다.
클라이언트 요청을 올바른 파티션 노드로 전달하는 문제 — 서비스 찾기(service discovery) 라고 한다.
접근 방식은 세 가지다.
1. 클라이언트 → 아무 노드 → 노드가 직접 전달
2. 클라이언트 → 라우팅 계층(파티션 인지 로드밸런서) → 해당 노드
3. 클라이언트가 파티션 할당 정보를 직접 파악 → 해당 노드로 바로 접속
많은 분산 시스템은 파티션 할당 정보를 추적하기 위해 ZooKeeper 같은 코디네이션 서비스를 사용한다.
코디네이터 없이 노드들이 서로 직접 소통하며 정보를 전달하는 방식은 가십 프로토콜(Gossip Protocol) 이라고 한다.
Redis, Elasticsearch 등이 이 방식을 사용하는데, 딱히 표준이 있는 건 아니고 "지들끼리 떠드는 것"에 가깝다. 지들끼리 못 떠들면 ZooKeeper 같은 코디네이터가 대신 전달해준다.
💡 Elasticsearch의 커스텀 라우팅
기본적으로 문서 ID를 해시하여 샤드에 분배한다.
사용자별 데이터나 우편번호처럼 특정 패턴이 있는 경우 커스텀 라우팅을 쓰면 관련 문서를 특정 샤드에 모아둘 수 있다.
검색할 때 모든 샤드를 뒤질 필요가 없어져 성능이 크게 향상된다.
참고 문헌