Skip to content
Published on

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

Authors
  • Name
    Twitter
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로 선출된다.

선거 조건:

  • 과반수(majority) 멤버가 서로 통신 가능해야 한다
  • priority가 0인 멤버는 Primary가 될 수 없다
  • hidden 멤버나 delayed 멤버는 투표만 가능하고 Primary 후보에서 제외된다
  • electionTimeoutMillis(기본 10초) 이내에 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 })

성능 튜닝 체크리스트

  • 인덱스 커버링: Shard Key를 포함하는 복합 인덱스로 쿼리가 단일 샤드에서 처리되도록 한다
  • Scatter-Gather 최소화: Shard Key를 쿼리 필터에 포함하여 특정 샤드만 대상으로 쿼리한다
  • Connection Pooling: mongos 연결 풀 크기를 적절히 설정한다 (기본 maxPoolSize=100)
  • Read Preference 활용: 분석 쿼리는 Secondary로 분산한다
  • 청크 크기 조정: 쓰기가 많은 워크로드는 청크 크기를 줄여 마이그레이션 영향을 최소화한다
  • WiredTiger 캐시: cacheSizeGB를 시스템 RAM의 50-60%로 설정한다

백업과 복구

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

프로덕션 백업 권장사항:

  • 소규모 클러스터는 mongodump로 충분하지만, 대규모 환경에서는 파일시스템 스냅샷(LVM/EBS) 또는 MongoDB Atlas Backup 사용을 권장한다
  • Oplog 크기는 최소 48시간 이상의 쓰기를 보관할 수 있도록 설정한다
  • Delayed Secondary 멤버를 활용하면 논리적 오류(실수로 데이터 삭제 등)로부터 빠르게 복구할 수 있다
  • 백업 복원 테스트를 정기적으로 수행한다 (최소 월 1회)

트러블슈팅 사례

1. Shard Key 핫스팟

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

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

해결:

  • Hashed Shard Key로 resharding 수행 (MongoDB 5.0+)
  • 복합 Shard Key를 사용하여 카디널리티를 높임
  • MongoDB 8.0의 sh.shardAndDistributeCollection()으로 빠른 재분배

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

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

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

해결:

  • Replica Set 멤버를 홀수(3, 5, 7)로 유지하여 항상 과반수 그룹이 하나만 존재하도록 구성
  • 네트워크 파티션 시 과반수에 속하지 못한 Primary는 자동으로 Secondary로 강등됨
  • w: "majority" Write Concern을 사용하여 과반수에 복제되지 않은 쓰기를 방지

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, 인덱스 빌드, 네트워크 병목

해결:

  • WiredTiger 캐시 크기를 확인하고 필요 시 증가
  • Secondary에서 실행 중인 무거운 쿼리를 확인하고 중단
  • 네트워크 대역폭을 확인하고 Oplog 전송 경로 최적화
  • 인덱스 빌드는 Rolling 방식(한 노드씩 순차적)으로 수행

운영 체크리스트

배포 전:

  • Shard Key를 쿼리 패턴, 카디널리티, 쓰기 분포를 기반으로 선택했는가
  • 각 Replica Set이 최소 3멤버(홀수)로 구성되어 있는가
  • Config Server가 별도 Replica Set(3노드)으로 구성되어 있는가
  • mongos가 2대 이상이고 로드 밸런서 뒤에 있는가
  • 인증(keyFile 또는 x.509)과 네트워크 암호화(TLS)가 설정되어 있는가

일상 운영:

  • 밸런서가 정상 동작하고 청크가 균등 분포되어 있는가
  • Replica Lag이 임계값(예: 10초) 이내인가
  • 커넥션 수가 maxIncomingConnections 한도 대비 적절한가
  • 슬로우 쿼리 로그를 정기적으로 분석하고 있는가
  • 디스크 사용률이 70% 이하인가

백업/복구:

  • 자동 백업이 일일 단위로 실행되고 있는가
  • Oplog 크기가 최소 48시간 이상의 데이터를 보관하는가
  • 백업 복원 테스트를 최근 30일 이내에 수행했는가
  • Delayed Secondary가 구성되어 논리적 오류에 대비하고 있는가

장애 대비:

  • 자동 페일오버 테스트를 최근 분기 내에 수행했는가
  • 모니터링 알림이 Replica Lag, 디스크 사용률, 연결 수에 대해 설정되어 있는가
  • Split-brain 시나리오에 대한 복구 절차가 문서화되어 있는가

참고자료