LabHub

블로그

MongoDB Sharding과 Replica Set 운영 가이드: 클러스터 설계부터 트러블슈팅까지

한국어English日本語

MongoDB Sharding and Replica Set

들어가며

MongoDB는 대규모 데이터 처리와 고가용성을 위해 Sharding과 Replica Set이라는 두 가지 핵심 분산 메커니즘을 제공한다. Sharding은 데이터를 여러 서버에 수평으로 분산하여 저장 용량과 처리량을 확장하고, Replica Set은 데이터를 복제하여 장애 발생 시 자동 페일오버를 보장한다. 프로덕션 환경에서 이 두 메커니즘을 올바르게 조합하는 것이 안정적인 MongoDB 운영의 핵심이다.

이 글에서는 Sharding 아키텍처의 구성 요소부터 Shard Key 선택 전략, Replica Set 페일오버 메커니즘, Chunk 마이그레이션과 밸런서 관리, 읽기/쓰기 관심사 설정, 모니터링과 성능 튜닝, 백업/복구, 그리고 실전 트러블슈팅 사례까지 프로덕션 클러스터 운영에 필요한 전체 지식을 다룬다.

Sharding 아키텍처 개요

MongoDB Sharded Cluster는 세 가지 구성 요소로 이루어진다.

mongos (Query Router): 클라이언트 요청을 적절한 샤드로 라우팅하는 프록시 역할을 한다. 상태를 가지지 않으므로(stateless) 여러 대를 배포하여 부하를 분산할 수 있다.

Config Server: 클러스터의 메타데이터(샤드 목록, 청크 범위, 데이터 분포 정보)를 저장하는 Replica Set이다. MongoDB 3.4 이후 반드시 Replica Set으로 구성해야 한다.

Shard: 실제 데이터를 저장하는 Replica Set이다. 각 샤드는 전체 데이터의 일부(청크)를 담당한다.

# Sharded Cluster 기본 구조 (최소 구성)
#
# Client
#   |
#   v
# mongos (Router) x 2  -- 로드 밸런서 뒤에 배치
#   |
#   v
# Config Server Replica Set (3 nodes)
#   |
#   +--- Shard 1 Replica Set (Primary + 2 Secondary)
#   +--- Shard 2 Replica Set (Primary + 2 Secondary)
#   +--- Shard 3 Replica Set (Primary + 2 Secondary)
#
# 총 노드 수: 2 (mongos) + 3 (config) + 9 (shard) = 14

Sharded Cluster 초기 구성

// mongos에 접속하여 Shard 추가
sh.addShard('shard1/mongo-shard1-a:27018,mongo-shard1-b:27018,mongo-shard1-c:27018')
sh.addShard('shard2/mongo-shard2-a:27018,mongo-shard2-b:27018,mongo-shard2-c:27018')
sh.addShard('shard3/mongo-shard3-a:27018,mongo-shard3-b:27018,mongo-shard3-c:27018')

// 데이터베이스에 Sharding 활성화
sh.enableSharding('myapp')

// 컬렉션에 Shard Key 지정 (Hashed)
sh.shardCollection('myapp.orders', { customerId: 'hashed' })

// 컬렉션에 Shard Key 지정 (Ranged)
sh.shardCollection('myapp.logs', { timestamp: 1 })

// 클러스터 상태 확인
sh.status()

Shard Key 선택 전략

Shard Key는 한 번 설정하면 변경이 매우 어렵기 때문에(MongoDB 5.0부터 resharding 지원) 설계 단계에서 신중하게 선택해야 한다. 좋은 Shard Key는 높은 카디널리티(cardinality), 낮은 빈도(frequency), 비단조(non-monotonic) 특성을 가져야 한다.

Shard Key 전략 비교표

전략설명장점단점적합한 사례
Hashed Sharding필드 값의 해시로 분산데이터 균등 분배, 핫스팟 방지범위 쿼리 비효율, 모든 샤드 스캔 필요고빈도 쓰기, 범위 쿼리 불필요
Ranged Sharding필드 값의 범위로 분산범위 쿼리 효율적, 특정 샤드만 접근핫스팟 가능, 단조 증가 키 문제시간 기반 로그, 범위 쿼리 빈번
Zone Sharding지역/조건별 데이터 배치데이터 로컬리티, 규정 준수설정 복잡, 불균형 가능멀티 리전, 데이터 주권 요구
Compound Key복합 필드 조합카디널리티 향상, 쿼리 최적화설계 복잡도 증가단일 필드로 부족할 때

Shard Key 설정 예제

// 1. Hashed Shard Key - 균등 분배가 최우선일 때
sh.shardCollection('myapp.users', { _id: 'hashed' })

// 2. Compound Shard Key - 카디널리티와 쿼리 패턴 동시 충족
sh.shardCollection('myapp.events', { tenantId: 1, createdAt: 1 })

// 3. Zone Sharding - 지역별 데이터 분리
sh.addShardToZone('shard1', 'KR')
sh.addShardToZone('shard2', 'US')
sh.addShardToZone('shard3', 'EU')

sh.updateZoneKeyRange(
  'myapp.users',
  { region: 'KR', _id: MinKey },
  { region: 'KR', _id: MaxKey },
  'KR'
)
sh.updateZoneKeyRange(
  'myapp.users',
  { region: 'US', _id: MinKey },
  { region: 'US', _id: MaxKey },
  'US'
)

// 4. MongoDB 8.0+ resharding - Shard Key 변경이 필요할 때
sh.reshardCollection('myapp.orders', { customerId: 1, orderId: 1 })

Shard Key 안티패턴

단조 증가하는 필드(예: ObjectId, timestamp)를 Ranged Shard Key로 사용하면 새 데이터가 항상 마지막 청크에 집중되어 핫 샤드(Hot Shard)가 발생한다. 이 경우 Hashed Sharding을 사용하거나 복합 키를 조합해야 한다. 또한 카디널리티가 너무 낮은 필드(예: boolean, status 같은 열거형)는 데이터를 고르게 분산할 수 없어 점보 청크(Jumbo Chunk)를 유발할 수 있다.

Replica Set 구성과 페일오버

Replica Set 초기화

// Primary에서 Replica Set 초기화
rs.initiate({
  _id: 'shard1',
  members: [
    { _id: 0, host: 'mongo-shard1-a:27018', priority: 10 },
    { _id: 1, host: 'mongo-shard1-b:27018', priority: 5 },
    { _id: 2, host: 'mongo-shard1-c:27018', priority: 1 },
  ],
  settings: {
    electionTimeoutMillis: 10000, // 기본 10초
    heartbeatTimeoutSecs: 10, // 하트비트 타임아웃
    chainingAllowed: true, // 세컨더리 간 체이닝 허용
  },
})

// Replica Set 상태 확인
rs.status()

// 복제 지연 확인
rs.printReplicationInfo()
rs.printSecondaryReplicationInfo()

페일오버 메커니즘

MongoDB Replica Set의 페일오버는 Raft 기반 합의 알고리즘으로 동작한다. Primary가 응답하지 않으면 나머지 멤버가 선거를 시작하고, 과반수 이상의 투표를 얻은 멤버가 새 Primary로 선출된다.

선거 조건:

// 수동 페일오버 실행 (유지보수 시)
rs.stepDown(60) // 60초 동안 Primary 역할 포기

// 특정 멤버를 Primary로 승격시키려면 priority 조정
cfg = rs.conf()
cfg.members[1].priority = 100
rs.reconfig(cfg)

// Hidden 멤버 설정 (백업 전용)
cfg = rs.conf()
cfg.members[2].hidden = true
cfg.members[2].priority = 0
rs.reconfig(cfg)

// Delayed 멤버 설정 (데이터 복구용, 1시간 지연)
cfg = rs.conf()
cfg.members[2].secondaryDelaySecs = 3600
cfg.members[2].hidden = true
cfg.members[2].priority = 0
rs.reconfig(cfg)

Chunk 마이그레이션과 밸런서

밸런서 동작 원리

밸런서는 Config Server의 Primary에서 실행되며, 샤드 간 청크 수 차이가 임계값(migration threshold)을 초과하면 청크를 이동시킨다. n개의 샤드가 있는 클러스터에서 최대 n/2개의 동시 마이그레이션이 가능하다.

// 밸런서 상태 확인
sh.getBalancerState()
sh.isBalancerRunning()

// 밸런서 중지/시작
sh.stopBalancer()
sh.startBalancer()

// 밸런서 시간 윈도우 설정 (새벽 2-6시에만 동작)
db.adminCommand({
  balancerStart: 1,
  condition: {
    activeWindow: { start: '02:00', stop: '06:00' },
  },
})

// 특정 컬렉션 밸런싱 비활성화
sh.disableBalancing('myapp.orders')

// 청크 수동 이동
sh.moveChunk('myapp.orders', { customerId: 'user123' }, 'shard3')

// 현재 마이그레이션 상태 확인
db.adminCommand({ currentOp: true, desc: /migrate/ })

청크 관리

// 청크 크기 변경 (기본 128MB, MongoDB 7.0+)
use config
db.settings.updateOne(
  { _id: "chunksize" },
  { $set: { value: 64 } },
  { upsert: true }
)

// 점보 청크 확인
db.chunks.find({ jumbo: true }).forEach(function(chunk) {
  print("Jumbo chunk: " + chunk.ns + " on " + chunk.shard)
})

// 점보 청크 강제 분할 시도
sh.splitAt("myapp.orders", { customerId: "splitPoint" })

// 청크 분포 확인
db.chunks.aggregate([
  { $group: { _id: "$shard", count: { $sum: 1 } } },
  { $sort: { count: -1 } }
])

읽기/쓰기 관심사 설정

Read Preference 모드 비교

모드읽기 대상일관성지연 시간적합한 사례
primaryPrimary만강한 일관성기준값실시간 데이터 필요
primaryPreferredPrimary 우선, 불가 시 Secondary대부분 강한 일관성기준값Primary 장애 대비
secondarySecondary만최종 일관성낮음 (가까운 노드)보고서, 분석 쿼리
secondaryPreferredSecondary 우선, 불가 시 Primary대부분 최종 일관성낮음읽기 부하 분산
nearest네트워크 지연 최소 노드최종 일관성최소지역 분산 배포

Write Concern 레벨 비교

Write Concern설명내구성성능적합한 사례
w: 0응답 대기 없음매우 낮음최고로그, 메트릭 (유실 가능)
w: 1Primary 확인만보통높음일반 쓰기
w: "majority"과반수 확인높음보통중요 데이터 (권장 기본값)
w: NN개 노드 확인매우 높음낮음금융 등 엄격한 요구
j: true저널 기록 확인최고낮음내구성 최우선
// 읽기/쓰기 관심사 설정 예제

// 1. 컬렉션 수준에서 Read Preference 설정
db.orders.find({ status: 'pending' }).readPref('secondaryPreferred')

// 2. 연결 문자열에서 설정
// mongodb://mongos1:27017,mongos2:27017/myapp?readPreference=secondaryPreferred&w=majority

// 3. Write Concern 지정
db.orders.insertOne(
  { customerId: 'user123', amount: 50000 },
  { writeConcern: { w: 'majority', j: true, wtimeout: 5000 } }
)

// 4. 전역 기본 Write Concern 설정
db.adminCommand({
  setDefaultRWConcern: 1,
  defaultWriteConcern: { w: 'majority', j: true },
  defaultReadConcern: { level: 'majority' },
})

모니터링과 성능 튜닝

핵심 모니터링 쿼리

// 1. 샤드별 데이터 분포 확인
db.orders.getShardDistribution()

// 2. 실행 중인 오퍼레이션 확인
db.currentOp({ secs_running: { $gt: 10 } })

// 3. 슬로우 쿼리 프로파일링 활성화
db.setProfilingLevel(1, { slowms: 100 })

// 4. 슬로우 쿼리 분석
db.system.profile
  .find({
    millis: { $gt: 100 },
    ns: /myapp\.orders/,
  })
  .sort({ ts: -1 })
  .limit(10)

// 5. 서버 상태 확인
db.serverStatus().opcounters // 연산 카운터
db.serverStatus().connections // 연결 수
db.serverStatus().wiredTiger.cache // 캐시 상태

// 6. Replica Set 복제 지연 확인
db.adminCommand({ replSetGetStatus: 1 }).members.forEach(function (m) {
  if (m.stateStr === 'SECONDARY') {
    var lag = (new Date() - m.optimeDate) / 1000
    print(m.name + ' lag: ' + lag + 's')
  }
})

// 7. 인덱스 사용 통계
db.orders.aggregate([{ $indexStats: {} }])

// 8. Config Server 메타데이터 일관성 검증 (MongoDB 7.0+)
db.adminCommand({ checkMetadataConsistency: 1 })

성능 튜닝 체크리스트

백업과 복구

mongodump/mongorestore를 이용한 백업

# 1. 백업 전 밸런서 중지 (샤드 클러스터)
mongosh --host mongos1:27017 --eval "sh.stopBalancer()"

# 2. 개별 샤드 백업 (--oplog로 시점 일관성 확보)
mongodump --host shard1/mongo-shard1-a:27018 \
  --oplog --out /backup/shard1-$(date +%Y%m%d)

mongodump --host shard2/mongo-shard2-a:27018 \
  --oplog --out /backup/shard2-$(date +%Y%m%d)

# 3. Config Server 백업
mongodump --host configRS/config1:27019 \
  --oplog --out /backup/config-$(date +%Y%m%d)

# 4. 밸런서 재시작
mongosh --host mongos1:27017 --eval "sh.startBalancer()"

# 5. 복원 (--oplogReplay로 시점 복구)
mongorestore --host shard1/mongo-shard1-a:27018 \
  --oplogReplay /backup/shard1-20260313

시점 복구(PITR)와 Oplog

// Oplog 크기 및 보관 시간 확인
db.getReplicationInfo()
// 출력 예시:
// configured oplog size:   2048MB
// log length start to end: 172800secs (48hrs)
// oplog first event time:  ...
// oplog last event time:   ...

// Oplog 크기 변경 (최소 48시간 이상 보관 권장)
db.adminCommand({ replSetResizeOplog: 1, size: 4096 }) // 4GB

프로덕션 백업 권장사항:

트러블슈팅 사례

1. Shard Key 핫스팟

증상: 특정 샤드의 CPU/디스크 I/O만 높고, 나머지 샤드는 유휴 상태

원인: 단조 증가 필드(ObjectId, timestamp)를 Ranged Shard Key로 사용하여 모든 새 쓰기가 마지막 청크에 집중

해결:

2. Split-brain (네트워크 파티션)

증상: 두 개 이상의 노드가 동시에 Primary로 동작하여 데이터 불일치 발생 가능

원인: 네트워크 파티션으로 Replica Set이 두 그룹으로 분리되고, 각 그룹이 별도로 Primary를 선출

해결:

3. Chunk 마이그레이션 실패

증상: 밸런서 로그에 "Data transfer error" 또는 "WriteConcernFailed" 오류

원인: 복제 지연이 심하거나 네트워크 대역폭 부족, 대상 샤드의 디스크 공간 부족

해결:

// 마이그레이션 속도 제한으로 복제 부하 완화
db.adminCommand({
  configureFailPoint: "moveChunkHangAtStep4",
  mode: "off"
})

// _secondaryThrottle 활성화로 복제 동기화 보장
use config
db.settings.updateOne(
  { _id: "balancer" },
  { $set: { "_secondaryThrottle": { w: 2 } } },
  { upsert: true }
)

// rangeDeleter 배치 크기 줄이기
db.adminCommand({
  setParameter: 1,
  rangeDeleterBatchSize: 32,
  rangeDeleterBatchDelayMS: 100
})

4. Replica Lag (복제 지연)

증상: Secondary의 optimeDate가 Primary보다 수십 초 이상 뒤처짐

원인: 대량 쓰기 부하, 느린 디스크 I/O, 인덱스 빌드, 네트워크 병목

해결:

운영 체크리스트

배포 전:

일상 운영:

백업/복구:

장애 대비:

참고자료

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다