MongoDB Shardingが本当に必要になるとき
約8ヶ月前、MongoDB single instanceでe-commerceシステムをメンテナンスしていました。データセットが50GBを超え、concurrent usersが3000人以上に増えるまでは問題なく動いていました。リアルタイムクエリが徐々に遅くなり、50msから2〜3秒まで悪化しました。Vertical scaling(RAM・CPU増設)で数週間しのいでも、またすぐにボトルネックに逆戻りです。
そこで本格的にMongoDB Sharded Clusterに取り組み始めました。テスト環境にはDocker Composeが最速の手段です。10コンテナ全体を約2分でスピンアップでき、設定を間違えてもきれいにリセットできて、追加のサーバー費用も不要です。
3つのコンポーネントで構成されるアーキテクチャ
- Config Servers:クラスターのメタデータを保存します — どのシャードがどのデータレンジを持つかを管理。3ノードのreplica setとして動作します。
- Shard Servers:実際にデータを保存する場所。各シャードはHAを確保するために3ノードのreplica setで構成されます。
- mongos(Query Router):唯一のゲートウェイです。クライアントはここに接続し、mongosが自動的に適切なシャードへクエリをルーティングします。
環境のセットアップ
最小要件
- Docker Engine 24以上 および Docker Compose v2
- 最低4GB RAM(実際のテストには8GB推奨)
- MongoDB 7.0
プロジェクトディレクトリとkeyfileを作成します — replica set内のMongoDBインスタンス同士が相互認証するために必須です:
mkdir mongo-sharded && cd mongo-sharded
mkdir -p config/keyfile scripts
openssl rand -base64 756 > config/keyfile/mongo-keyfile
chmod 400 config/keyfile/mongo-keyfile
docker-compose.ymlファイル
クラスター全体の構成:3台のconfig server、6台のshard server(2シャード × 3ノード)、1台のmongos router。
version: '3.8'
networks:
mongo-cluster:
driver: bridge
x-mongo-common: &mongo-common
image: mongo:7.0
restart: unless-stopped
networks:
- mongo-cluster
services:
# --- Config Servers ---
configsvr1:
<<: *mongo-common
container_name: configsvr1
command: mongod --configsvr --replSet configReplSet --port 27017 --keyFile /etc/mongo/keyfile
volumes:
- configsvr1_data:/data/db
- ./config/keyfile/mongo-keyfile:/etc/mongo/keyfile:ro
ports:
- "27119:27017"
configsvr2:
<<: *mongo-common
container_name: configsvr2
command: mongod --configsvr --replSet configReplSet --port 27017 --keyFile /etc/mongo/keyfile
volumes:
- configsvr2_data:/data/db
- ./config/keyfile/mongo-keyfile:/etc/mongo/keyfile:ro
configsvr3:
<<: *mongo-common
container_name: configsvr3
command: mongod --configsvr --replSet configReplSet --port 27017 --keyFile /etc/mongo/keyfile
volumes:
- configsvr3_data:/data/db
- ./config/keyfile/mongo-keyfile:/etc/mongo/keyfile:ro
# --- Shard 1 ---
shard1rs1:
<<: *mongo-common
container_name: shard1rs1
command: mongod --shardsvr --replSet shard1ReplSet --port 27017 --wiredTigerCacheSizeGB 0.5 --keyFile /etc/mongo/keyfile
volumes:
- shard1rs1_data:/data/db
- ./config/keyfile/mongo-keyfile:/etc/mongo/keyfile:ro
ports:
- "27121:27017"
shard1rs2:
<<: *mongo-common
container_name: shard1rs2
command: mongod --shardsvr --replSet shard1ReplSet --port 27017 --wiredTigerCacheSizeGB 0.5 --keyFile /etc/mongo/keyfile
volumes:
- shard1rs2_data:/data/db
- ./config/keyfile/mongo-keyfile:/etc/mongo/keyfile:ro
shard1rs3:
<<: *mongo-common
container_name: shard1rs3
command: mongod --shardsvr --replSet shard1ReplSet --port 27017 --wiredTigerCacheSizeGB 0.5 --keyFile /etc/mongo/keyfile
volumes:
- shard1rs3_data:/data/db
- ./config/keyfile/mongo-keyfile:/etc/mongo/keyfile:ro
# --- Shard 2 ---
shard2rs1:
<<: *mongo-common
container_name: shard2rs1
command: mongod --shardsvr --replSet shard2ReplSet --port 27017 --wiredTigerCacheSizeGB 0.5 --keyFile /etc/mongo/keyfile
volumes:
- shard2rs1_data:/data/db
- ./config/keyfile/mongo-keyfile:/etc/mongo/keyfile:ro
ports:
- "27122:27017"
shard2rs2:
<<: *mongo-common
container_name: shard2rs2
command: mongod --shardsvr --replSet shard2ReplSet --port 27017 --wiredTigerCacheSizeGB 0.5 --keyFile /etc/mongo/keyfile
volumes:
- shard2rs2_data:/data/db
- ./config/keyfile/mongo-keyfile:/etc/mongo/keyfile:ro
shard2rs3:
<<: *mongo-common
container_name: shard2rs3
command: mongod --shardsvr --replSet shard2ReplSet --port 27017 --wiredTigerCacheSizeGB 0.5 --keyFile /etc/mongo/keyfile
volumes:
- shard2rs3_data:/data/db
- ./config/keyfile/mongo-keyfile:/etc/mongo/keyfile:ro
# --- Query Router ---
mongos:
<<: *mongo-common
container_name: mongos
command: mongos --configdb configReplSet/configsvr1:27017,configsvr2:27017,configsvr3:27017 --port 27017 --keyFile /etc/mongo/keyfile
volumes:
- ./config/keyfile/mongo-keyfile:/etc/mongo/keyfile:ro
ports:
- "27017:27017"
depends_on:
- configsvr1
- configsvr2
- configsvr3
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 512M
volumes:
configsvr1_data:
configsvr2_data:
configsvr3_data:
shard1rs1_data:
shard1rs2_data:
shard1rs3_data:
shard2rs1_data:
shard2rs2_data:
shard2rs3_data:
--wiredTigerCacheSizeGB 0.5フラグは実際の経験から得た教訓です。最初のデプロイ時はこれを省略していました — 制限が一切なかったのです。負荷テストを3時間ほど動かした後、mongosがホストのRAMをほぼ使い果たしてしまいました。原因を突き止めるのに2日かかりました。Dockerではメモリ制限は最初から宣言すべきものであり、障害が起きてから対処するものではありません。
起動後の詳細設定
クラスター全体を起動します:
docker compose up -d
# インスタンスが起動するまで約30秒待つ
Config Serverのreplica setを初期化します:
docker exec -it configsvr1 mongosh --port 27017 --eval '
rs.initiate({
_id: "configReplSet",
configsvr: true,
members: [
{ _id: 0, host: "configsvr1:27017" },
{ _id: 1, host: "configsvr2:27017" },
{ _id: 2, host: "configsvr3:27017" }
]
})'
Shard 1とShard 2のreplica setを初期化します:
docker exec -it shard1rs1 mongosh --port 27017 --eval '
rs.initiate({
_id: "shard1ReplSet",
members: [
{ _id: 0, host: "shard1rs1:27017" },
{ _id: 1, host: "shard1rs2:27017" },
{ _id: 2, host: "shard1rs3:27017" }
]
})'
docker exec -it shard2rs1 mongosh --port 27017 --eval '
rs.initiate({
_id: "shard2ReplSet",
members: [
{ _id: 0, host: "shard2rs1:27017" },
{ _id: 1, host: "shard2rs2:27017" },
{ _id: 2, host: "shard2rs3:27017" }
]
})'
mongos経由でシャードをクラスターに登録します:
docker exec -it mongos mongosh --port 27017 --eval '
sh.addShard("shard1ReplSet/shard1rs1:27017,shard1rs2:27017,shard1rs3:27017");
sh.addShard("shard2ReplSet/shard2rs1:27017,shard2rs2:27017,shard2rs3:27017");'
データベースとコレクションのShardingを有効化する
Shardingは自動的に有効になりません — シャーディングするデータベースとコレクションを明示的に指定する必要があります。最も重要なステップはshard keyの選択です。この決定はほぼ後から変更できません。コレクションにデータが入った後でshard keyを変更するには、全データをダンプし、コレクションを削除して、最初からインポートし直す必要があります。
docker exec -it mongos mongosh --port 27017 --eval '
// データベースのshardingを有効化
sh.enableSharding("myapp");
// Hashed sharding: 均等に分散、特定IDでのクエリに適している
sh.shardCollection("myapp.orders", { user_id: "hashed" });
// Range-based: rangeクエリに効率的だが、hotspotが発生しやすい
// sh.shardCollection("myapp.logs", { timestamp: 1 });'
使い分けの目安:特定IDでクエリすることが多い場合(user_id: 42)はhashedを使い、範囲クエリ(日付Aから日付Bまでのtimestamp絞り込み)にはrange-basedを使います。e-commerceのordersコレクションではuser_idによるhashedシャーディングの方が均等に分散できます — 特定のユーザー層がトラフィックを独占することがありません。一方、logsコレクションはtimestampによるrange-basedの方が合理的で、直近7日間のログを取得するようなクエリが大半を占めるためです。
動作確認とモニタリング
クラスターのステータス確認
# クラスター概要とチャンクの分布を確認
docker exec -it mongos mongosh --port 27017 --eval 'sh.status()'
# 各シャードへのデータ分布の詳細を確認
docker exec -it mongos mongosh --port 27017 --eval '
use myapp;
db.orders.getShardDistribution()'
実際の分散をテストする
docker exec -it mongos mongosh --port 27017 --eval '
use myapp;
for (let i = 0; i < 10000; i++) {
db.orders.insertOne({
user_id: i,
product: "item_" + Math.floor(Math.random() * 100),
amount: Math.random() * 1000,
created_at: new Date()
});
}
print("Total:", db.orders.countDocuments());
db.orders.getShardDistribution();'
10,000件のドキュメントを挿入すると、getShardDistribution()でデータが2つのシャードにほぼ均等に分散されていることを確認できます。最初はクラスター全体で1チャンクしかありませんが、データが増えるにつれてMongoDBが自動的に分割・マイグレーションを行います。
explain()によるクエリ分析
docker exec -it mongos mongosh --port 27017 --eval '
use myapp;
db.orders.find({ user_id: 42 }).explain("executionStats")'
出力のqueryPlanner.winningPlan.shardsフィールドを確認します:シャードが1つだけ表示されている場合 — クエリはtargetedで、mongosがどのシャードに問い合わせるかを正確に把握しています。両方のシャードが表示されている場合 — scatter-gatherで、mongosが全シャードに問い合わせて結果をマージします。シャードが2つなら、オーバーヘッドはまだ問題になりません。しかし8〜10シャードに拡張した状態でqueryが依然scatter-gatherのままだと、最も遅いシャードに引きずられてレイテンシが増大します。そうなったらshard keyを見直すか、適切なインデックスを追加する必要があります。
統合ヘルスチェックスクリプト
#!/bin/bash
# scripts/health-check.sh
echo "=== Config Servers ==="
docker exec configsvr1 mongosh --quiet --eval \
'rs.status().members.forEach(m => print(m.name, m.stateStr))'
echo "=== Shard 1 ==="
docker exec shard1rs1 mongosh --quiet --eval \
'rs.status().members.forEach(m => print(m.name, m.stateStr))'
echo "=== Shard 2 ==="
docker exec shard2rs1 mongosh --quiet --eval \
'rs.status().members.forEach(m => print(m.name, m.stateStr))'
echo "=== Cluster Shards ==="
docker exec mongos mongosh --quiet --eval \
'sh.status()' | grep -E "(shards|currently|chunks)"
このクラスターは6ヶ月間、本番環境で稼働しています — 1,000万ドキュメント、平均クエリレスポンスタイムは30ms以下。成功を左右するのは2点です:最初からshard keyを慎重に選ぶこと、そして全コンテナのメモリ制限を宣言すること。どちらも後から修正できるものではありません — 私は最も厳しい方法でそれを学びました。

