샤딩 — 수평 분할로 대규모 데이터 처리하기
데이터가 단일 서버에 다 담기지 않을 만큼 커지면, 복제로는 해결할 수 없습니다. 데이터 자체를 여러 서버에 나눠야 한다면 어떻게 해야 할까요?
샤딩이란
샤딩(Sharding)은 하나의 데이터셋을 여러 데이터베이스 서버 에 분산 저장하는 수평 분할 기법입니다.
단일 서버:
┌──────────────────────┐
│ 모든 사용자 데이터 │ ← 한계에 도달
│ (10억 행) │
└──────────────────────┘
샤딩 후:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Shard 0 │ │ Shard 1 │ │ Shard 2 │ │ Shard 3 │
│ user 0- │ │ user 250M-│ │ user 500M-│ │ user 750M-│
│ 250M │ │ 500M │ │ 750M │ │ 1B │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
서버 A 서버 B 서버 C 서버 D
복제(Replication)는 같은 데이터를 복사하는 것이고, 샤딩은 다른 데이터를 나누는 것입니다.
| 구분 | 복제 | 샤딩 |
|---|---|---|
| 목적 | 읽기 분산, 고가용성 | 쓰기 분산, 저장 용량 확장 |
| 데이터 | 모든 서버가 동일 데이터 | 각 서버가 일부 데이터 |
| 확장 대상 | 읽기 처리량 | 읽기 + 쓰기 처리량 + 저장 용량 |
샤딩 전략
1. 해시 기반 샤딩 (Hash Sharding)
샤드 키의 해시값으로 데이터가 어느 샤드에 갈지 결정합니다.
shard_id = hash(user_id) % num_shards
user_id = 12345 → hash(12345) % 4 = 1 → Shard 1
user_id = 67890 → hash(67890) % 4 = 3 → Shard 3
**장점 **:
- 데이터가 균등하게 분배됩니다
- 구현이 단순합니다
** 단점 **:
- 범위 쿼리(
WHERE user_id BETWEEN 1000 AND 2000)를 효율적으로 처리할 수 없습니다 - 샤드 수를 변경하면 거의 모든 데이터의 재분배가 필요합니다
Consistent Hashing
일반 해시의 리샤딩 문제를 완화하는 방법입니다.
일반 해시: 샤드 4개 → 5개로 변경 시 ~80% 데이터 이동
Consistent Hashing: 샤드 추가 시 ~1/N의 데이터만 이동
해시 링(Hash Ring) 위에 샤드와 데이터를 배치하여, 샤드 추가/제거 시 최소한의 데이터만 이동합니다.
2. 범위 기반 샤딩 (Range Sharding)
연속된 값의 범위로 샤드를 나눕니다.
Shard 0: user_id 1 ~ 1,000,000
Shard 1: user_id 1,000,001 ~ 2,000,000
Shard 2: user_id 2,000,001 ~ 3,000,000
** 장점 **:
- 범위 쿼리가 효율적입니다 (해당 샤드만 조회)
- 데이터의 위치를 쉽게 예측할 수 있습니다
** 단점 **:
- 핫스팟 문제: 새 사용자가 항상 마지막 샤드에 집중됩니다
- 데이터 분포가 불균형해질 수 있습니다
3. 디렉토리 기반 샤딩 (Directory Sharding)
별도의 매핑 테이블(디렉토리)이 어떤 데이터가 어느 샤드에 있는지 관리합니다.
디렉토리 서비스 (Redis/별도 DB):
user_id 12345 → Shard 2
user_id 67890 → Shard 0
user_id 11111 → Shard 3
** 장점 **:
- 유연한 데이터 배치 (특정 사용자를 특정 샤드로 이동 가능)
- 리샤딩 시 디렉토리만 업데이트
** 단점 **:
- 디렉토리 자체가 단일 장애점(SPOF)이 될 수 있습니다
- 모든 쿼리가 디렉토리를 먼저 조회해야 합니다
샤드 키 선택
샤딩에서 가장 중요한 결정입니다.
좋은 샤드 키의 조건
- ** 높은 카디널리티 **: 값이 다양해야 균등 분배가 가능합니다
- ** 쿼리 패턴과 일치 **: 대부분의 쿼리에 샤드 키가 포함되어야 합니다
- ** 균등 분배 **: 특정 샤드에 데이터가 몰리지 않아야 합니다
좋은 샤드 키: user_id
→ 사용자별로 데이터가 분산
→ "내 주문 목록" 같은 쿼리가 단일 샤드에서 처리됨
나쁜 샤드 키: country
→ 한국, 미국 등 특정 국가에 데이터 집중
→ 핫스팟 발생
크로스 샤드 쿼리의 어려움
크로스 샤드 조인
-- 단일 DB에서는 간단
SELECT u.name, o.amount
FROM users u JOIN orders o ON u.id = o.user_id
WHERE o.amount > 10000;
-- 샤딩 후: users와 orders가 다른 샤드에 있을 수 있음
-- → 애플리케이션에서 두 샤드에 각각 쿼리 후 결과를 합침
해결 방법:
- ** 같은 샤드 키로 코로케이션 **: 같은 user_id의 users와 orders를 같은 샤드에 배치
- ** 글로벌 테이블 **: 변경이 드문 작은 테이블(국가 코드 등)은 모든 샤드에 복제
크로스 샤드 집계
-- 전체 매출 합계
SELECT SUM(amount) FROM orders;
-- → 모든 샤드에 쿼리 → 결과를 합산
분산 트랜잭션
여러 샤드에 걸친 트랜잭션은 2PC(Two-Phase Commit)가 필요합니다.
1단계 (Prepare): 모든 샤드에 "커밋할 준비 됐나?" 확인
2단계 (Commit): 모든 샤드가 OK 응답 시 커밋 실행
하나라도 실패하면 전체 롤백
2PC는 복잡하고 느리므로, 가능하면 ** 트랜잭션이 단일 샤드 내에서 완결 **되도록 설계합니다.
글로벌 유니크 ID
Auto Increment PK는 각 샤드에서 독립적으로 증가하므로 전체적으로 유니크하지 않습니다.
Shard 0: id = 1, 2, 3, ...
Shard 1: id = 1, 2, 3, ... ← 충돌!
해결 방법:
- UUID: 전역 유니크, 하지만 인덱스 성능 저하 (랜덤 분포)
- Snowflake ID: Twitter가 개발한 방식. 타임스탬프 + 워커ID + 시퀀스
- ** 범위 할당 **: Shard 0은 1
1000000, Shard 1은 10000012000000
Snowflake ID 구조 (64비트):
[1 bit 사인] [41 bit 타임스탬프] [10 bit 머신ID] [12 bit 시퀀스]
샤딩 미들웨어
Vitess
YouTube(Google)에서 개발한 MySQL 샤딩 플랫폼입니다.
애플리케이션 → VTGate (프록시) → VTTablet → MySQL
VTTablet → MySQL
VTTablet → MySQL
주요 기능:
- ** 자동 라우팅 **: 샤드 키를 분석하여 올바른 샤드로 전달
- ** 온라인 리샤딩 **: 서비스 중단 없이 샤드 분할/병합
- ** 스키마 관리 **: 모든 샤드에 DDL을 일관되게 적용
- ** 연결 풀링 **: MySQL 연결을 효율적으로 관리
ProxySQL
MySQL 프록시 레이어로, 읽기/쓰기 분리와 기본적인 샤딩 라우팅을 지원합니다.
애플리케이션 → ProxySQL → 쓰기: Source
→ 읽기: Replica (라운드 로빈)
쿼리 규칙을 설정하여 특정 패턴의 쿼리를 특정 서버로 라우팅할 수 있습니다.
리샤딩 전략
샤드 수를 변경하거나 데이터를 재분배하는 과정입니다. 가장 어려운 운영 작업 중 하나입니다.
1. 2배 확장 (Split)
기존 샤드를 2개로 분할합니다. 해시 기반에서 가장 단순합니다.
Before: Shard 0 (hash % 2 == 0), Shard 1 (hash % 2 == 1)
After: Shard 0 (hash % 4 == 0), Shard 2 (hash % 4 == 2) ← 기존 Shard 0에서 분리
Shard 1 (hash % 4 == 1), Shard 3 (hash % 4 == 3) ← 기존 Shard 1에서 분리
2. 온라인 리샤딩 과정
1. 새 샤드 서버 준비
2. 기존 샤드에서 새 샤드로 데이터 복제 시작 (Binlog 기반)
3. 복제가 따라잡으면 쓰기 일시 중단 (짧은 시간)
4. 라우팅 규칙을 새 구성으로 변경
5. 쓰기 재개
6. 기존 샤드에서 이동한 데이터 정리
Vitess는 이 과정을 자동화합니다.
3. 그림자 쓰기 (Shadow Writing)
1. 기존 샤드에 쓰기 계속
2. 동시에 새 샤드 구성으로도 데이터 이중 쓰기
3. 데이터 일관성 검증
4. 라우팅을 새 구성으로 전환
5. 기존 구성 제거
샤딩 도입 전 체크리스트
샤딩은 복잡도가 매우 높으므로, 먼저 다른 방법을 시도해야 합니다.
- ** 쿼리 최적화 **: 인덱스, 쿼리 튜닝으로 해결 가능한가?
- ** 수직 분할 **: 특정 컬럼을 별도 테이블로 분리하면 해결되는가?
- ** 읽기 복제 **: 읽기 부하가 문제라면 복제본으로 분산 가능한가?
- ** 캐싱 **: Redis 등으로 데이터베이스 부하를 줄일 수 있는가?
- ** 아카이빙 **: 오래된 데이터를 별도 저장소로 이동하면 되는가?
이 모든 방법으로도 해결이 안 될 때 샤딩을 도입합니다.
주의할 점
샤딩 도입 전에 다른 방법을 먼저 소진해야 한다
샤딩은 복잡도가 극적으로 증가합니다. 쿼리 최적화 → 인덱스 개선 → 읽기 복제 → 캐싱 → 아카이빙 순서로 먼저 시도하고, 이 모든 방법으로도 해결되지 않을 때만 샤딩을 도입합니다.
샤드 키를 잘못 선택하면 핫스팟이 발생한다
범위 기반에서 시간순 키를 사용하면 새 데이터가 항상 마지막 샤드에 집중됩니다. 해시 기반에서 카디널리티가 낮은 키(예: country)를 사용하면 특정 샤드에 데이터가 몰립니다.
크로스 샤드 JOIN과 트랜잭션은 극도로 어렵다
조인 대상 데이터가 서로 다른 서버에 있으면 단일 SQL로 처리할 수 없습니다. 분산 트랜잭션(2PC)은 복잡하고 느립니다. 가능하면 트랜잭션이 단일 샤드 내에서 완결되도록 설계해야 합니다.
정리
| 항목 | 설명 |
|---|---|
| 샤딩 vs 복제 | 복제는 같은 데이터 복사, 샤딩은 다른 데이터 분산 |
| 해시 기반 | 균등 분배, 범위 쿼리 비효율, 리샤딩 시 대량 이동 |
| 범위 기반 | 범위 쿼리 효율적, 핫스팟 위험 |
| 디렉토리 기반 | 유연한 배치, 디렉토리가 SPOF 위험 |
| 핵심 과제 | 크로스 샤드 조인, 분산 트랜잭션, 글로벌 유니크 ID |
| 미들웨어 | Vitess(YouTube/Google), ProxySQL |
| 대원칙 | 샤딩은 최후의 수단 |