- Published on
MongoDB Sharding 완벽 가이드: 샤드 키 설계부터 운영 자동화까지
- Authors
- Name
들어가며
단일 MongoDB 인스턴스가 감당할 수 없는 데이터량이나 처리량에 도달하면 Sharding이 필요합니다. Sharding은 데이터를 여러 서버(shard)에 수평 분산하여 읽기/쓰기 성능과 저장 용량을 확장하는 기술입니다.
이 글에서는 MongoDB 7.0+ 기준으로 Sharded Cluster를 구축하고 운영하는 과정을 다룹니다.
Sharded Cluster 아키텍처
MongoDB Sharded Cluster는 세 가지 컴포넌트로 구성됩니다:
- Shard: 실제 데이터를 저장하는 Replica Set
- Config Server: 메타데이터와 라우팅 정보를 저장하는 Replica Set
- mongos: 클라이언트 요청을 적절한 Shard로 라우팅하는 라우터
Docker Compose로 Sharded Cluster 구축
# docker-compose.yml
services:
# Config Server Replica Set
config-svr-1:
image: mongo:7.0
command: mongod --configsvr --replSet configRS --port 27019
volumes:
- config1-data:/data/db
config-svr-2:
image: mongo:7.0
command: mongod --configsvr --replSet configRS --port 27019
volumes:
- config2-data:/data/db
config-svr-3:
image: mongo:7.0
command: mongod --configsvr --replSet configRS --port 27019
volumes:
- config3-data:/data/db
# Shard 1 Replica Set
shard1-1:
image: mongo:7.0
command: mongod --shardsvr --replSet shard1RS --port 27018
volumes:
- shard1-1-data:/data/db
shard1-2:
image: mongo:7.0
command: mongod --shardsvr --replSet shard1RS --port 27018
volumes:
- shard1-2-data:/data/db
# Shard 2 Replica Set
shard2-1:
image: mongo:7.0
command: mongod --shardsvr --replSet shard2RS --port 27018
volumes:
- shard2-1-data:/data/db
shard2-2:
image: mongo:7.0
command: mongod --shardsvr --replSet shard2RS --port 27018
volumes:
- shard2-2-data:/data/db
# Mongos Router
mongos:
image: mongo:7.0
command: mongos --configdb configRS/config-svr-1:27019,config-svr-2:27019,config-svr-3:27019 --port 27017
ports:
- '27017:27017'
depends_on:
- config-svr-1
- config-svr-2
- config-svr-3
volumes:
config1-data:
config2-data:
config3-data:
shard1-1-data:
shard1-2-data:
shard2-1-data:
shard2-2-data:
초기화 스크립트
#!/bin/bash
# init-sharding.sh
# 1. Config Server Replica Set 초기화
docker exec -it config-svr-1 mongosh --port 27019 --eval '
rs.initiate({
_id: "configRS",
configsvr: true,
members: [
{ _id: 0, host: "config-svr-1:27019" },
{ _id: 1, host: "config-svr-2:27019" },
{ _id: 2, host: "config-svr-3:27019" }
]
})'
# 2. Shard 1 Replica Set 초기화
docker exec -it shard1-1 mongosh --port 27018 --eval '
rs.initiate({
_id: "shard1RS",
members: [
{ _id: 0, host: "shard1-1:27018" },
{ _id: 1, host: "shard1-2:27018" }
]
})'
# 3. Shard 2 Replica Set 초기화
docker exec -it shard2-1 mongosh --port 27018 --eval '
rs.initiate({
_id: "shard2RS",
members: [
{ _id: 0, host: "shard2-1:27018" },
{ _id: 1, host: "shard2-2:27018" }
]
})'
sleep 10
# 4. Shard를 클러스터에 추가
docker exec -it mongos mongosh --eval '
sh.addShard("shard1RS/shard1-1:27018,shard1-2:27018");
sh.addShard("shard2RS/shard2-1:27018,shard2-2:27018");
'
샤드 키 전략
샤드 키 선택은 Sharding 성능의 80%를 결정합니다.
1. Range Sharding
// 범위 기반 샤딩 - 연속 데이터 조회에 유리
use mydb;
sh.enableSharding("mydb");
// created_at을 샤드 키로 사용
db.orders.createIndex({ created_at: 1 });
sh.shardCollection("mydb.orders", { created_at: 1 });
// 장점: 날짜 범위 쿼리가 단일 샤드에서 처리
db.orders.find({
created_at: {
$gte: ISODate("2026-03-01"),
$lt: ISODate("2026-04-01")
}
});
// 단점: 최신 데이터가 하나의 샤드에 집중 (핫스팟)
2. Hashed Sharding
// 해시 기반 샤딩 - 균등 분산에 유리
db.users.createIndex({ user_id: 'hashed' })
sh.shardCollection('mydb.users', { user_id: 'hashed' })
// 장점: 쓰기가 모든 샤드에 균등 분산
// 단점: 범위 쿼리 시 모든 샤드를 스캔 (scatter-gather)
// 사전 청크 분할로 초기 불균형 방지
sh.shardCollection('mydb.events', { event_id: 'hashed' }, false, {
numInitialChunks: 64,
})
3. Compound Shard Key
// 복합 샤드 키 - 가장 권장되는 패턴
db.orders.createIndex({ customer_id: 1, order_date: 1 })
sh.shardCollection('mydb.orders', { customer_id: 1, order_date: 1 })
// customer_id로 카디널리티 확보
// order_date로 범위 쿼리 최적화
// 이 쿼리는 targeted (단일 샤드)
db.orders.find({
customer_id: 'C12345',
order_date: { $gte: ISODate('2026-01-01') },
})
4. Zone Sharding (지역 기반)
// Zone Sharding - 데이터 지역성 보장
sh.addShardToZone('shard1RS', 'APAC')
sh.addShardToZone('shard2RS', 'EU')
// Zone 범위 설정
sh.updateZoneKeyRange(
'mydb.users',
{ region: 'KR' },
{ region: 'KS' }, // KR의 다음 문자열
'APAC'
)
sh.updateZoneKeyRange('mydb.users', { region: 'DE' }, { region: 'DF' }, 'EU')
// 한국 사용자 데이터는 APAC 샤드에만 저장
// 독일 사용자 데이터는 EU 샤드에만 저장
샤드 키 선택 가이드
| 기준 | 좋은 샤드 키 | 나쁜 샤드 키 |
|---|---|---|
| 카디널리티 | 높음 (user_id) | 낮음 (status: active/inactive) |
| 분산도 | 균등 분산 | 편향 (특정 값에 집중) |
| 쿼리 패턴 | 자주 조건에 포함 | 거의 사용 안 함 |
| 단조 증가 | 피하거나 해시 | created_at (핫스팟) |
| 변경 가능 | MongoDB 5.0+ 가능 | 이전 버전은 변경 불가 |
샤드 키 변경 (MongoDB 5.0+)
// resharding - 샤드 키 변경
db.adminCommand({
reshardCollection: 'mydb.orders',
key: { customer_id: 1, order_date: 1 },
})
// 진행 상황 확인
db.getSiblingDB('admin').aggregate([
{ $currentOp: { allUsers: true, localOps: false } },
{ $match: { type: 'op', 'originatingCommand.reshardCollection': { $exists: true } } },
])
청크 관리
청크 분할과 마이그레이션
// 청크 상태 확인
use config;
db.chunks.find({ ns: "mydb.orders" }).sort({ min: 1 });
// 청크 수 확인
db.chunks.countDocuments({ ns: "mydb.orders" });
// 샤드별 청크 분포
db.chunks.aggregate([
{ $match: { ns: "mydb.orders" } },
{ $group: { _id: "$shard", count: { $sum: 1 } } }
]);
// 수동 청크 분할
sh.splitAt("mydb.orders", { customer_id: "C50000", order_date: ISODate("2026-01-01") });
// 수동 청크 이동
sh.moveChunk("mydb.orders",
{ customer_id: "C50000", order_date: ISODate("2026-01-01") },
"shard2RS"
);
밸런서 관리
// 밸런서 상태 확인
sh.getBalancerState()
sh.isBalancerRunning()
// 밸런서 시간 제한 (업무 시간 외에만 실행)
db.settings.updateOne(
{ _id: 'balancer' },
{
$set: {
activeWindow: { start: '02:00', stop: '06:00' },
},
},
{ upsert: true }
)
// 특정 컬렉션의 밸런싱 비활성화 (마이그레이션 중)
sh.disableBalancing('mydb.orders')
// 재활성화
sh.enableBalancing('mydb.orders')
쿼리 라우팅 이해
// Targeted Query - 단일 샤드에서 처리 (빠름)
// 샤드 키가 쿼리 조건에 포함
db.orders.find({ customer_id: 'C12345' }).explain('executionStats')
// "winningPlan": { "stage": "SINGLE_SHARD" }
// Scatter-Gather Query - 모든 샤드에서 처리 (느림)
// 샤드 키가 쿼리 조건에 미포함
db.orders.find({ product: 'iPhone' }).explain('executionStats')
// "winningPlan": { "stage": "SHARD_MERGE" }
// Broadcast Query - 모든 샤드에 전송
db.orders.aggregate([{ $group: { _id: '$status', total: { $sum: '$amount' } } }])
모니터링
// 샤딩 상태 전체 확인
sh.status();
sh.status(true); // 상세 정보 포함
// 청크 마이그레이션 히스토리
use config;
db.changelog.find({ what: "moveChunk.commit" }).sort({ time: -1 }).limit(10);
// 현재 진행 중인 마이그레이션
db.locks.find({ _id: "balancer" });
// 각 샤드의 데이터 크기
db.adminCommand({ listDatabases: 1, nameOnly: false });
mongosh 유틸리티
// 컬렉션 통계
db.orders.stats()
db.orders.stats().sharded // true면 샤딩됨
// 샤드별 문서 수
db.orders.getShardDistribution()
// Shard shard1RS: data 2.5GB, docs 5,000,000, chunks 32
// Shard shard2RS: data 2.3GB, docs 4,800,000, chunks 30
운영 주의사항
1. 샤드 추가
// 새 샤드 추가 (서비스 중단 없음)
sh.addShard('shard3RS/shard3-1:27018,shard3-2:27018')
// 밸런서가 자동으로 청크를 새 샤드로 이동
// 이동 완료까지 시간이 걸림
2. 샤드 제거
// 샤드 제거 (draining)
db.adminCommand({ removeShard: 'shard2RS' })
// 진행 상황 확인 (반복 실행)
db.adminCommand({ removeShard: 'shard2RS' })
// "state": "ongoing", "remaining": { "chunks": 15, "dbs": 0 }
// 완료될 때까지 반복 확인
3. 백업 전략
# mongodump로 전체 클러스터 백업 (mongos를 통해)
mongodump --host mongos:27017 --out /backup/$(date +%Y%m%d)
# 또는 각 샤드의 Replica Set을 개별 백업
mongodump --host shard1-1:27018 --out /backup/shard1/$(date +%Y%m%d)
mongodump --host shard2-1:27018 --out /backup/shard2/$(date +%Y%m%d)
정리
| 전략 | 적합한 케이스 | 주의점 |
|---|---|---|
| Range | 범위 쿼리 중심 | 핫스팟 발생 가능 |
| Hashed | 균등 분산 필요 | 범위 쿼리 비효율 |
| Compound | 다양한 쿼리 패턴 | 키 설계에 신중해야 함 |
| Zone | 데이터 지역성 필요 | 불균형 분산 가능 |
샤드 키 선택은 되돌리기 어려운 결정이므로, 실 데이터와 쿼리 패턴을 충분히 분석한 후 결정하세요.
✅ 퀴즈: MongoDB Sharding 이해도 점검 (7문제)
Q1. Sharded Cluster의 세 가지 구성 요소는?
Shard (데이터 저장), Config Server (메타데이터), mongos (라우터)입니다.
Q2. Range Sharding의 핫스팟 문제란?
단조 증가하는 키(created_at 등)를 사용하면 최신 데이터가 항상 마지막 샤드에 집중됩니다.
Q3. Targeted Query와 Scatter-Gather Query의 차이는?
Targeted는 샤드 키가 쿼리 조건에 포함되어 단일 샤드에서 처리되고, Scatter-Gather는 모든 샤드를 스캔합니다.
Q4. numInitialChunks를 설정하는 이유는?
Hashed Sharding에서 초기 청크를 미리 분할하여 데이터 로드 시 불균형을 방지합니다.
Q5. Zone Sharding의 주요 사용 사례는?
데이터 거주 규정(GDPR 등)에 따라 특정 지역의 데이터를 특정 샤드에만 저장해야 할 때 사용합니다.
Q6. 밸런서의 activeWindow 설정은 왜 필요한가요?
청크 마이그레이션은 I/O를 많이 사용하므로, 업무 시간 외에만 실행하여 서비스 영향을 최소화합니다.
Q7. MongoDB 5.0+에서 추가된 resharding 기능의 의미는?
기존에는 변경 불가했던 샤드 키를 온라인 상태에서 변경할 수 있게 되었습니다.