왜 밸런서가 운영 문제가 되는가
MongoDB 샤딩 클러스터는 청크(chunk) 단위로 데이터를 분산한다. 밸런서는 샤드 간 청크 수가 불균형해지면 자동으로 청크를 이동시켜 균형을 맞춘다. 문제는 이 이동이 moveChunk 명령으로 실제 문서를 복사·삭제하는 작업이라 디스크 I/O, 네트워크, 그리고 대상 샤드의 캐시를 소모한다는 점이다. 트래픽 피크 시간에 밸런싱이 겹치면 쿼리 지연이 튀고, 잘못 잡힌 샤드 키로 인해 특정 샤드에 쓰기가 몰리면 밸런서가 끊임없이 청크를 옮기며 오히려 부하를 키운다.
밸런서 상태 확인과 기본 제어
운영에서 가장 먼저 하는 일은 현재 밸런서가 동작 중인지, 어떤 청크가 움직이고 있는지 파악하는 것이다.
// mongos에 접속해서 실행
sh.status() // 샤드별 청크 분포 요약
sh.getBalancerState() // 활성화 여부 (true/false)
sh.isBalancerRunning() // 지금 이동이 진행 중인지
// 진행 중인 moveChunk 확인
use config
db.migrations.find().pretty()
// 최근 이동 이력과 실패 원인
db.changelog.find(
{ what: /moveChunk/ }
).sort({ time: -1 }).limit(5).pretty()
changelog에서 moveChunk.error가 반복된다면 대상 샤드의 리소스 부족이나 인덱스 문제를 의심해야 한다.
밸런싱 윈도우로 시간대 분리
가장 효과적인 튜닝은 밸런싱을 트래픽이 낮은 시간대로 몰아넣는 것이다. 윈도우를 지정하면 그 외 시간에는 청크 이동이 시작되지 않는다.
use config
db.settings.updateOne(
{ _id: "balancer" },
{ $set: { activeWindow: { start: "02:00", stop: "05:30" } } },
{ upsert: true }
)
// 긴급 상황: 즉시 중단 (진행 중 이동은 끝까지 완료됨)
sh.stopBalancer()
// 특정 컬렉션만 밸런싱 제외
sh.disableBalancing("myapp.events")
주의할 점은 윈도우는 mongos 서버의 시간대를 기준으로 한다는 것이다. 컨테이너 환경이라면 UTC일 가능성이 높으니 실제 로컬 시간과 어긋나지 않는지 반드시 확인한다.
청크 크기와 이동 속도 조정
기본 청크 크기는 128MB(구버전은 64MB)다. 청크가 크면 이동 횟수는 줄지만 한 번의 이동이 무겁고, 작으면 이동이 잦아진다. 워크로드에 맞춰 조정한다.
use config
db.settings.updateOne(
{ _id: "chunksize" },
{ $set: { value: 256 } }, // 단위: MB
{ upsert: true }
)
// MongoDB 6.0+ : 청크 자동 분할은 사라지고
// 데이터 이동은 chunk가 아닌 "데이터 크기" 기준으로 판단
// 밸런서가 얼마나 자주 도는지 확인
db.settings.find({ _id: "balancer" })
버전별 밸런싱 동작 차이
| 항목 | ~5.0 | 6.0 이상 |
|---|---|---|
| 균형 기준 | 샤드별 청크 개수 | 샤드별 데이터 용량(byte) |
| 자동 분할 | 쓰기 시 auto-split | 이동 시점에 필요한 만큼만 분할 |
| 이동 병렬성 | 샤드 쌍당 1개 | 여러 샤드 쌍 동시 이동 지원 |
6.0부터는 청크 개수가 아니라 실제 용량으로 판단하기 때문에, 문서 크기 편차가 큰 컬렉션에서 예전보다 훨씬 안정적으로 균형이 맞는다. 다만 병렬 이동이 늘면서 순간 I/O 부하는 더 커질 수 있으니 모니터링 기준도 바꿔야 한다.
모니터링과 자동 알림
밸런서를 방치하면 새벽에 조용히 실패가 쌓인다. 주기적으로 상태를 수집해 이상 징후를 잡는 스크립트를 둔다.
from pymongo import MongoClient
from datetime import datetime, timedelta
client = MongoClient("mongodb://mongos-host:27017")
cfg = client.config
# 최근 1시간 이동 실패 건수
one_hour_ago = datetime.utcnow() - timedelta(hours=1)
errors = cfg.changelog.count_documents({
"what": "moveChunk.error",
"time": {"$gte": one_hour_ago}
})
# 샤드별 청크 편차 계산
pipeline = [{"$group": {"_id": "$shard", "cnt": {"$sum": 1}}}]
counts = [d["cnt"] for d in cfg.chunks.aggregate(pipeline)]
imbalance = max(counts) - min(counts) if counts else 0
if errors > 0 or imbalance > 20:
print(f"ALERT errors={errors} imbalance={imbalance}")
운영 시 주의점
첫째, stopBalancer()는 새 이동만 막을 뿐 진행 중인 이동은 완료된다. 급하게 멈춰도 몇 분은 기다려야 한다. 둘째, 샤드 키 설계가 잘못되면 밸런서로는 근본 해결이 안 된다. 단조 증가 키(예: ObjectId, timestamp)는 항상 마지막 샤드에 쓰기가 몰리는 hot shard를 만든다. 해시 샤드 키나 복합 키를 검토해야 한다. 셋째, 대량 삭제나 TTL로 청크가 비면 empty chunk가 남아 밸런서가 불필요하게 도는데, 이럴 땐 mergeChunks로 병합한다. 밸런서는 자동화 도구지 만능이 아니며, 윈도우·청크 크기·샤드 키 세 가지를 함께 조율할 때만 안정적으로 동작한다.