데이터베이스 확장
데이터베이스 확장¶
개요¶
이 문서에서는 데이터베이스 확장 전략을 다룹니다. 파티셔닝과 샤딩의 차이, 다양한 샤딩 전략(Range, Hash, Directory), 샤딩 키 선택, 핫스팟 방지, 그리고 리밸런싱을 학습합니다.
난이도: ⭐⭐⭐ 예상 학습 시간: 2-3시간 선수 지식: 07_Distributed_Cache_Systems.md, PostgreSQL 폴더
목차¶
1. 데이터베이스 확장의 필요성¶
1.1 단일 데이터베이스의 한계¶
┌─────────────────────────────────────────────────────────────────┐
│ 단일 데이터베이스 한계 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 트래픽/데이터 증가 시 문제: │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 저장 용량 한계 │ │
│ │ • 단일 디스크/서버의 물리적 한계 │ │
│ │ • 수십 TB 이상 데이터 관리 어려움 │ │
│ │ │ │
│ │ 2. 처리량 한계 │ │
│ │ • 단일 서버 CPU/메모리 한계 │ │
│ │ • 동시 연결 수 제한 │ │
│ │ │ │
│ │ 3. 응답 시간 증가 │ │
│ │ • 큰 테이블 → 느린 쿼리 │ │
│ │ • 인덱스 크기 증가 │ │
│ │ │ │
│ │ 4. 단일 장애점 (SPOF) │ │
│ │ • 서버 장애 = 전체 서비스 장애 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 해결 방법: │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 수직 확장 (Scale Up) │ │
│ │ → 더 강력한 하드웨어 (한계 있음) │ │
│ │ │ │
│ │ 수평 확장 (Scale Out) │ │
│ │ → 여러 서버로 분산 (파티셔닝/샤딩) │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 확장 전략 개요¶
┌─────────────────────────────────────────────────────────────────┐
│ 데이터베이스 확장 전략 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 읽기 복제 (Read Replica) │ │
│ │ • 읽기 부하 분산 │ │
│ │ • 다음 레슨에서 자세히 다룸 │ │
│ │ │ │
│ │ 2. 파티셔닝 (Partitioning) │ │
│ │ • 단일 DB 내에서 테이블 분할 │ │
│ │ • PostgreSQL 네이티브 파티셔닝 │ │
│ │ │ │
│ │ 3. 샤딩 (Sharding) │ │
│ │ • 여러 DB 서버로 데이터 분산 │ │
│ │ • 수평 확장의 핵심 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ [단일 DB] │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ [복제] [파티셔닝] [샤딩] │
│ 읽기 확장 단일DB 분할 다중DB 분산 │
│ │
└─────────────────────────────────────────────────────────────────┘
2. 파티셔닝 vs 샤딩¶
2.1 파티셔닝 (Partitioning)¶
┌─────────────────────────────────────────────────────────────────┐
│ 파티셔닝 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "하나의 DB 서버 내에서 테이블을 여러 파티션으로 분할" │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ Database Server │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────────────┐ │ │ │
│ │ │ │ orders (논리 테이블) │ │ │ │
│ │ │ └─────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ┌──────────────┼──────────────┐ │ │ │
│ │ │ ▼ ▼ ▼ │ │ │
│ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │
│ │ │ │orders_2022│ │orders_2023│ │orders_2024│ │ │ │
│ │ │ │ (파티션1) │ │ (파티션2) │ │ (파티션3) │ │ │ │
│ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │
│ │ │ │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ PostgreSQL 예시: │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ -- 파티션 테이블 생성 │ │
│ │ CREATE TABLE orders ( │ │
│ │ id SERIAL, │ │
│ │ order_date DATE, │ │
│ │ amount DECIMAL │ │
│ │ ) PARTITION BY RANGE (order_date); │ │
│ │ │ │
│ │ -- 파티션 생성 │ │
│ │ CREATE TABLE orders_2024 PARTITION OF orders │ │
│ │ FOR VALUES FROM ('2024-01-01') TO ('2025-01-01'); │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ • 쿼리 성능 향상 (파티션 프루닝) │
│ • 데이터 관리 용이 (파티션별 삭제/백업) │
│ • 인덱스 크기 감소 │
│ │
│ 한계: │
│ • 여전히 단일 서버 (용량, 처리량 한계) │
│ │
└─────────────────────────────────────────────────────────────────┘
2.2 샤딩 (Sharding)¶
┌─────────────────────────────────────────────────────────────────┐
│ 샤딩 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "여러 DB 서버로 데이터를 분산 저장" │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Application │ │ │
│ │ └──────────────────┬──────────────────────┘ │ │
│ │ │ │ │
│ │ ┌────────┴────────┐ │ │
│ │ │ Shard Router │ │ │
│ │ │ (샤드 결정) │ │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────────┼──────────────────┐ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Shard 1 │ │ Shard 2 │ │ Shard 3 │ │ │
│ │ │ (Server 1) │ │ (Server 2) │ │ (Server 3) │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ user_id │ │ user_id │ │ user_id │ │ │
│ │ │ 1-1000000 │ │ 1000001- │ │ 2000001- │ │ │
│ │ │ │ │ 2000000 │ │ 3000000 │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ • 이론적으로 무한 확장 가능 │
│ • 각 샤드의 부하 감소 │
│ • 장애 격리 │
│ │
│ 단점: │
│ • 복잡도 증가 │
│ • 크로스 샤드 쿼리 어려움 │
│ • 트랜잭션 제약 │
│ • 리밸런싱 어려움 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.3 비교¶
| 항목 | 파티셔닝 | 샤딩 |
|---|---|---|
| 위치 | 단일 서버 내 | 여러 서버 |
| 확장성 | 제한적 | 높음 |
| 복잡도 | 낮음 | 높음 |
| 트랜잭션 | 전체 가능 | 샤드 내만 |
| JOIN | 가능 | 크로스 샤드 어려움 |
| 관리 | DB가 자동 | 애플리케이션이 관리 |
3. 샤딩 전략¶
3.1 Range 기반 샤딩¶
┌─────────────────────────────────────────────────────────────────┐
│ Range 기반 샤딩 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "값의 범위에 따라 샤드 결정" │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ user_id 기반 Range 샤딩: │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Shard 1 │ │ Shard 2 │ │ Shard 3 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ user_id │ │ user_id │ │ user_id │ │ │
│ │ │ 1 ~ 1M │ │ 1M+1 ~ 2M │ │ 2M+1 ~ 3M │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ 날짜 기반 Range 샤딩: │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Shard 1 │ │ Shard 2 │ │ Shard 3 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ 2022년 │ │ 2023년 │ │ 2024년 │ │ │
│ │ │ 데이터 │ │ 데이터 │ │ 데이터 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 라우팅 로직: │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ def get_shard(user_id): │ │
│ │ if user_id <= 1_000_000: │ │
│ │ return "shard_1" │ │
│ │ elif user_id <= 2_000_000: │ │
│ │ return "shard_2" │ │
│ │ else: │ │
│ │ return "shard_3" │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ • 범위 쿼리 효율적 │
│ • 구현 단순 │
│ │
│ 단점: │
│ • 핫스팟 발생 가능 (신규 사용자가 마지막 샤드에 몰림) │
│ • 불균등 분배 가능 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 Hash 기반 샤딩¶
┌─────────────────────────────────────────────────────────────────┐
│ Hash 기반 샤딩 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "키의 해시값으로 샤드 결정" │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ shard = hash(user_id) % N │ │
│ │ │ │
│ │ user_id: 12345 │ │
│ │ hash(12345) = 67890 │ │
│ │ 67890 % 3 = 0 → Shard 0 │ │
│ │ │ │
│ │ user_id: 12346 │ │
│ │ hash(12346) = 12347 │ │
│ │ 12347 % 3 = 1 → Shard 1 │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Shard 0 │ │ Shard 1 │ │ Shard 2 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ user: 12345 │ │ user: 12346 │ │ user: 12347 │ │ │
│ │ │ user: 12348 │ │ user: 12349 │ │ user: 12350 │ │ │
│ │ │ ... │ │ ... │ │ ... │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 라우팅 로직: │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ def get_shard(user_id, num_shards=3): │ │
│ │ hash_value = hash(str(user_id)) │ │
│ │ shard_id = hash_value % num_shards │ │
│ │ return f"shard_{shard_id}" │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ • 균등 분배 │
│ • 핫스팟 방지 │
│ │
│ 단점: │
│ • 범위 쿼리 어려움 (모든 샤드 조회 필요) │
│ • 샤드 추가/제거 시 대규모 재분배 │
│ • → 일관성 해싱으로 해결 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.3 Directory 기반 샤딩¶
┌─────────────────────────────────────────────────────────────────┐
│ Directory 기반 샤딩 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "조회 테이블(디렉토리)로 샤드 결정" │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Application │ │
│ │ │ │ │
│ │ │ user_id: 12345 │ │
│ │ ▼ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ Directory Service│ │ │
│ │ │ (Lookup Table) │ │ │
│ │ │ │ │ │
│ │ │ user_id │ shard │ │ │
│ │ │ ────────┼─────── │ │ │
│ │ │ 12345 │ shard_2│ │ │
│ │ │ 12346 │ shard_1│ │ │
│ │ │ 12347 │ shard_3│ │ │
│ │ │ ... │ ... │ │ │
│ │ └─────────┬────────┘ │ │
│ │ │ │ │
│ │ │ shard_2 │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Shard 1 │ │ Shard 2 │ │ Shard 3 │ │ │
│ │ └─────────────┘ └──────┬──────┘ └─────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ user: 12345 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ • 유연한 데이터 이동 │
│ • 불균형 해소 용이 │
│ • 특정 사용자를 특정 샤드로 이동 가능 │
│ │
│ 단점: │
│ • 디렉토리 서비스가 SPOF │
│ • 추가 조회 오버헤드 │
│ • 디렉토리 크기 증가 │
│ │
│ 해결: │
│ • 디렉토리를 캐싱 (Redis) │
│ • 디렉토리 복제 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.4 전략 비교¶
| 전략 | 균등 분배 | 범위 쿼리 | 리밸런싱 | 복잡도 |
|---|---|---|---|---|
| Range | 낮음 | 좋음 | 어려움 | 낮음 |
| Hash | 좋음 | 어려움 | 어려움 | 낮음 |
| Directory | 좋음 | 가능 | 쉬움 | 높음 |
| Consistent Hash | 좋음 | 어려움 | 쉬움 | 중간 |
4. 샤딩 키 선택¶
4.1 좋은 샤딩 키의 조건¶
┌─────────────────────────────────────────────────────────────────┐
│ 좋은 샤딩 키 조건 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 높은 카디널리티 (High Cardinality) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 좋음: user_id (수백만 개의 고유 값) │ │
│ │ 나쁨: country (수십 개) │ │
│ │ 나쁨: status (몇 개) │ │
│ │ │ │
│ │ → 고유 값이 많아야 균등 분배 가능 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 2. 균등 분포 (Even Distribution) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 좋음: 해시된 user_id │ │
│ │ 나쁨: 생성일 (신규 데이터가 한 샤드에 집중) │ │
│ │ 나쁨: 인기 상품 ID (특정 상품에 쿼리 집중) │ │
│ │ │ │
│ │ → 데이터와 쿼리가 균등하게 분배되어야 함 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 3. 쿼리 패턴과 일치 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 대부분 쿼리: "WHERE user_id = ?" │ │
│ │ → user_id를 샤딩 키로! │ │
│ │ │ │
│ │ 대부분 쿼리: "WHERE order_date BETWEEN ... AND ..." │ │
│ │ → order_date를 샤딩 키로! │ │
│ │ │ │
│ │ → 자주 사용하는 쿼리가 단일 샤드에서 처리되도록 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 4. 불변성 (Immutability) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 좋음: user_id (변하지 않음) │ │
│ │ 나쁨: email (사용자가 변경할 수 있음) │ │
│ │ 나쁨: status (변경됨) │ │
│ │ │ │
│ │ → 샤딩 키 변경 = 데이터 이동 (비용 큼) │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 샤딩 키 예시¶
┌─────────────────────────────────────────────────────────────────┐
│ 도메인별 샤딩 키 예시 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 소셜 미디어 (사용자 중심): │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ • users: user_id │ │
│ │ • posts: user_id (작성자) │ │
│ │ • followers: user_id (팔로우 당하는 사람) │ │
│ │ • messages: conversation_id │ │
│ │ │ │
│ │ → 사용자별 데이터가 같은 샤드에! │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 이커머스 (테넌트 중심): │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ • products: merchant_id │ │
│ │ • orders: user_id 또는 order_id │ │
│ │ • reviews: product_id │ │
│ │ │ │
│ │ 주의: orders를 user_id로 하면 │ │
│ │ 판매자 주문 조회 시 모든 샤드 조회 필요 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ SaaS (테넌트 중심): │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ • 모든 테이블: tenant_id │ │
│ │ │ │
│ │ → 테넌트별로 완전 격리 │ │
│ │ → 크로스 테넌트 쿼리 불필요 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 로그/분석 (시계열): │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ • logs: timestamp (Range 샤딩) │ │
│ │ • metrics: (device_id, timestamp) │ │
│ │ │ │
│ │ → 범위 쿼리에 효율적 │ │
│ │ → 오래된 샤드 삭제 용이 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5. 핫스팟 방지¶
5.1 핫스팟 문제¶
┌─────────────────────────────────────────────────────────────────┐
│ 핫스팟 문제 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "특정 샤드에 트래픽/데이터가 집중되는 현상" │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 정상: │ │
│ │ ┌───────┐ ┌───────┐ ┌───────┐ │ │
│ │ │Shard 1│ │Shard 2│ │Shard 3│ │ │
│ │ │ 33% │ │ 33% │ │ 33% │ │ │
│ │ └───────┘ └───────┘ └───────┘ │ │
│ │ │ │
│ │ 핫스팟: │ │
│ │ ┌───────┐ ┌───────┐ ┌───────┐ │ │
│ │ │Shard 1│ │Shard 2│ │Shard 3│ │ │
│ │ │ 80% │ │ 10% │ │ 10% │ 💥 과부하! │ │
│ │ └───────┘ └───────┘ └───────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 발생 원인: │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. Range 샤딩 + 순차 키 │ │
│ │ → 신규 데이터가 마지막 샤드에 집중 │ │
│ │ │ │
│ │ 2. 인기 있는 엔티티 │ │
│ │ → 유명인 계정, 인기 상품 │ │
│ │ │ │
│ │ 3. 시간 기반 패턴 │ │
│ │ → 특정 시간대에 특정 샤드 집중 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.2 핫스팟 방지 전략¶
┌─────────────────────────────────────────────────────────────────┐
│ 핫스팟 방지 전략 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Hash 기반 샤딩 사용 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Range 대신 Hash로 균등 분배 │ │
│ │ user_id: 1, 2, 3 → 같은 샤드가 아니라 분산 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 2. 복합 샤딩 키 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 단일 키: user_id │ │
│ │ → 인기 사용자가 핫스팟 │ │
│ │ │ │
│ │ 복합 키: (user_id, post_id) │ │
│ │ → 같은 사용자의 포스트도 분산 │ │
│ │ │ │
│ │ shard = hash(user_id + "_" + post_id) % N │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 3. 솔팅 (Salting) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 원래 키: celebrity_123 │ │
│ │ 솔팅: celebrity_123_0 │ │
│ │ celebrity_123_1 │ │
│ │ celebrity_123_2 │ │
│ │ │ │
│ │ # 쓰기: 랜덤 솔트 추가 │ │
│ │ salt = random(0, 10) │ │
│ │ key = f"{user_id}_{salt}" │ │
│ │ │ │
│ │ # 읽기: 모든 솔트 조회 후 합치기 │ │
│ │ for salt in range(10): │ │
│ │ results += query(f"{user_id}_{salt}") │ │
│ │ │ │
│ │ → 쓰기 분산, 읽기 복잡도 증가 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 4. 핫 엔티티 전용 처리 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ • 인기 엔티티를 캐시 (Redis) │ │
│ │ • 별도의 전용 샤드/서버 │ │
│ │ • 비동기 처리 (카운터 등) │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6. 리밸런싱¶
6.1 리밸런싱이 필요한 경우¶
┌─────────────────────────────────────────────────────────────────┐
│ 리밸런싱 필요 상황 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 샤드 추가 (확장) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Before: [Shard 1] [Shard 2] [Shard 3] │ │
│ │ After: [Shard 1] [Shard 2] [Shard 3] [Shard 4] │ │
│ │ │ │
│ │ 일부 데이터를 새 샤드로 이동 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 2. 샤드 제거 (축소) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Before: [Shard 1] [Shard 2] [Shard 3] │ │
│ │ After: [Shard 1] [Shard 2] │ │
│ │ │ │
│ │ Shard 3 데이터를 다른 샤드로 이동 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 3. 불균형 해소 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Before: [Shard 1: 80%] [Shard 2: 10%] [Shard 3: 10%] │ │
│ │ After: [Shard 1: 33%] [Shard 2: 33%] [Shard 3: 33%] │ │
│ │ │ │
│ │ Shard 1에서 다른 샤드로 데이터 이동 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 리밸런싱 전략¶
┌─────────────────────────────────────────────────────────────────┐
│ 리밸런싱 전략 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 일관성 해싱 (Consistent Hashing) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 샤드 추가/제거 시 최소한의 데이터만 이동 │ │
│ │ 이전 레슨에서 학습 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 2. 이중 쓰기 (Dual Write) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1단계: 새 샤드 추가, 쓰기는 양쪽에 │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │Old S1│ │Old S2│ │New S3│ │ │
│ │ │ R/W │ │ R/W │ │ W │ │ │
│ │ └──────┘ └──────┘ └──────┘ │ │
│ │ │ │
│ │ 2단계: 기존 데이터 마이그레이션 (백그라운드) │ │
│ │ │ │
│ │ 3단계: 읽기도 새 샤드로 │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │Shard1│ │Shard2│ │Shard3│ │ │
│ │ │ R/W │ │ R/W │ │ R/W │ │ │
│ │ └──────┘ └──────┘ └──────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 3. 버전 기반 라우팅 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ shard_config_v1: │ │
│ │ range 1-1M → shard_1 │ │
│ │ range 1M-2M → shard_2 │ │
│ │ │ │
│ │ shard_config_v2: (마이그레이션 후) │ │
│ │ range 1-700K → shard_1 │ │
│ │ range 700K-1.4M → shard_2 │ │
│ │ range 1.4M-2M → shard_3 │ │
│ │ │ │
│ │ # 새 쓰기는 v2, 기존 읽기는 v1 │ │
│ │ # 마이그레이션 완료 후 v2로 전환 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
7. 연습 문제¶
문제 1: 샤딩 전략 선택¶
다음 서비스에 적합한 샤딩 전략을 선택하세요.
a) 채팅 앱 (대화방별 메시지) b) 로그 분석 시스템 c) 글로벌 사용자 서비스 d) SaaS 멀티 테넌트
문제 2: 샤딩 키 선택¶
이커머스 서비스에서 다음 테이블의 샤딩 키를 선택하세요.
- users (id, email, name)
- orders (id, user_id, status, created_at)
- order_items (id, order_id, product_id, quantity)
- products (id, merchant_id, name, price)
요구사항: - 사용자별 주문 조회 빈번 - 판매자별 상품 조회 필요 - 주문과 주문 항목은 함께 조회
문제 3: 핫스팟 해결¶
인기 게시글에 댓글이 집중되어 해당 샤드가 과부하입니다. 해결책을 제시하세요.
문제 4: 리밸런싱 계획¶
3개 샤드에서 5개 샤드로 확장하려고 합니다. 무중단 마이그레이션 계획을 세우세요.
정답¶
문제 1 정답¶
a) 채팅 앱: Hash(conversation_id)
- 같은 대화방 메시지가 같은 샤드
- 대화방 단위로 조회하므로 효율적
b) 로그 분석: Range(timestamp)
- 시간 범위 쿼리가 효율적
- 오래된 로그 샤드 삭제 용이
c) 글로벌 사용자: Hash(user_id)
- 균등 분배
- 사용자별 데이터 조회 효율적
d) SaaS 멀티 테넌트: tenant_id
- 테넌트별 완전 격리
- 크로스 테넌트 쿼리 불필요
문제 2 정답¶
users: Hash(user_id)
- 기본 키로 사용
- 균등 분배
orders: Hash(user_id)
- user_id가 주 조회 조건
- 사용자별 주문 조회 효율적
order_items: Hash(order_id)
- 또는 Hash(user_id) (orders와 같은 샤드)
- order_id를 사용하면 orders와 다른 샤드 가능
- → user_id 권장 (orders와 같은 샤드)
products: Hash(merchant_id)
- 판매자별 상품 조회
- 판매자 단위 격리
크로스 샤드 주의:
- 주문의 상품 정보 → 캐시 활용
- 판매자 주문 조회 → 별도 인덱스 테이블
문제 3 정답¶
해결책:
1. 핫 게시글 캐싱
- 댓글을 Redis에 캐싱
- 주기적으로 DB에 배치 저장
2. 솔팅 적용
post_123_0, post_123_1, ...
- 쓰기 시 랜덤 솔트
- 읽기 시 모든 솔트 조회 후 합치기
3. 댓글 카운터 분리
- 카운터만 Redis에서 관리
- 비동기로 DB 업데이트
4. 샤딩 키 변경
- (post_id, comment_id) 복합 키로
- 같은 게시글 댓글도 분산
5. 전용 처리
- 인기 게시글 감지
- 전용 캐시 레이어 추가
문제 4 정답¶
무중단 마이그레이션 계획:
1단계: 새 샤드 준비
- Shard 4, 5 서버 준비
- 스키마 생성
2단계: 이중 쓰기 시작
- 라우터 업데이트
- 새 쓰기 → 기존 샤드 + 새 샤드 (일관성 해싱 기준)
3단계: 기존 데이터 마이그레이션
- 백그라운드 복사
- 일관성 해싱으로 대상 결정 (약 40% 이동)
4단계: 검증
- 데이터 카운트 비교
- 샘플 데이터 검증
5단계: 읽기 전환
- 새 라우팅 규칙 활성화
- 점진적 전환 (10% → 50% → 100%)
6단계: 이중 쓰기 중단
- 기존 라우팅 제거
7단계: 정리
- 기존 샤드에서 이동된 데이터 삭제
8. 다음 단계¶
데이터베이스 확장을 이해했다면, 데이터베이스 복제를 학습하세요.
다음 레슨¶
관련 레슨¶
추천 실습¶
- PostgreSQL 파티셔닝 실습
- 샤딩 라우터 구현해보기
- 일관성 해싱 구현
9. 참고 자료¶
도서¶
- Designing Data-Intensive Applications - Ch. 6
데이터베이스¶
- Vitess - MySQL 샤딩
- Citus - PostgreSQL 샤딩
- MongoDB Sharding
기업 사례¶
문서 정보 - 최종 수정: 2024년 - 난이도: ⭐⭐⭐ - 예상 학습 시간: 2-3시간