MongoDB: シャーディング:水平スケーリング
最終更新:2026-08-26
シャードクラスターはMongoDB水平スケーリングの究極のソリューション—習得すればペタバイト規模のデータをサポートできます。
1. 学習内容
- シャーディングの概念(水平スケーリング)
- シャードキー選択戦略
- チャンク分割と移行
- 範囲 vs ハッシュシャーディング
- Config Server + Mongos + Shardアーキテクチャ
- Dockerシャードクラスター構築
2. シャーディングアーキテクチャ
概念説明: シャーディングはMongoDBの水平スケーリングソリューション—データを複数のシャードに分散し、各シャードは独立したレプリカセットです。アプリケーションはMongosルーティングを通じて透過的にデータにアクセスし、データ分散を意識する必要がありません。単一サーバーが容量または書き込みスループットの限界に達した場合、シャーディングが唯一のスケール方法です。
動作原理: Mongosはクエリルーターとして機能します。アプリケーションからリクエストを受信すると、Config Serverからシャードメタデータ(どのチャンクがどのシャードにあるか)を取得し、ターゲットシャードにリクエストをルーティングして実行し、結果をマージして返します。アプリケーションコードは単一ノードMongoDBと同一—シャーディングはアプリケーションに対して透過的です。
3つの主要コンポーネント:
graph TB
App[アプリケーション] --> M[Mongos<br/>クエリルーター<br/>ステートレス]
M --> CS[Config Server<br/>設定情報<br/>3ノードレプリカセット]
M --> S1[Shard 1<br/>シャードノード1<br/>レプリカセット]
M --> S2[Shard 2<br/>シャードノード2<br/>レプリカセット]
M --> S3[Shard 3<br/>シャードノード3<br/>レプリカセット]
subgraph "データフロー"
Q[クエリリクエスト] --> M
M -->|メタデータ照会| CS
M -->|データ照会| S1
M -->|データ照会| S2
M -->|データ照会| S3
end
style M fill:#cce5ff
style CS fill:#fff3cd
style S1 fill:#d4edda
style S2 fill:#d4edda
style S3 fill:#d4edda
| コンポーネント | 責務 | デプロイ要件 |
|---|---|---|
| Mongos | クエリルーティング、アプリケーション透過 | ステートレス、複数インスタンス対応 |
| Config Server | シャードメタデータ保存(クラスター設定) | レプリカセット必須(3ノード) |
| Shard | 実際のデータ保存(各シャードはレプリカセット) | 最低3ノードのレプリカセット |
シャード vs レプリカセット:
| 項目 | レプリカセット | シャードクラスター |
|---|---|---|
| 目的 | 高可用性(HA) | 水平スケーリング + HA |
| データ | 各ノードが全データセットを保存 | データは複数シャードに分散 |
| 書き込みスケール | なし(全書き込みはPrimaryへ) | ✅ 複数シャードに分散 |
| 読み取りスケール | Secondary読み取り負荷分散 | ✅ マルチシャード並列クエリ |
| 運用複雑さ | 中程度 | 高い |
| 適用規模 | < 1 TB | > 1 TB / > 10K ops/s |
3. シャードキー選択戦略
概念説明: シャードキーはデータがシャード間でどう分散されるかを決定するフィールドです—クエリパフォーマンス、データバランス、スケーラビリティに直接影響します。一度設定するとシャードキーは変更不可で、誤ったシャードキー選択は最も深刻なアーキテクチャミスの一つです。
動作原理: MongoDBはシャードキーに基づいてデータをチャンク(デフォルト64 MB)に分割し、各チャンクを特定のシャードに割り当てます。範囲シャーディングはシャードキー値の範囲でデータを分割し、ハッシュシャーディングはシャードキーのハッシュ値でデータを分割します。クエリ時、Mongosはシャードキーを使用してターゲットチャンクを含むシャードを特定し、ブロードキャストクエリを回避します。
シャードキー選択のESRTルール:
| 順序 | タイプ | 説明 | 例 |
|---|---|---|---|
| Equality(等価) | 等価フィルタリング | userId、_idを優先 | find({userId: 'u001'}) |
| Sort(ソート) | ソートフィールド | createdAtなど | sort({createdAt: -1}) |
| Range(範囲) | 範囲クエリ | 頻繁な範囲選択 | {price: {$gte: 100}} |
| Time(時系列) | 時系列データ | 作成日時など | {createdAt: 1} |
良いパーティションキーの4つの特性:
| 特性 | 説明 | 理由 |
|---|---|---|
| 高カーディナリティ | 多数の一意値 | より細かいチャンクに分割可能 |
| 低頻度更新 | 値がほとんど変更されない | シャードキー変更はチャンク移行を伴う |
| クエリヒット | クエリ条件にシャードキーを含む | Mongosはターゲットルーティング可能 |
| 均等分散 | 値が均等に分布 | ホットスポットシャードを回避 |
(1) 範囲シャーディング(Range Partitioning)
概念説明: 範囲シャーディングはシャードキー値の自然順序に基づいてチャンクを分割—連続する値の範囲が同じチャンクに配置されます。範囲クエリやソートに適していますが、データ偏在(ホットスポット)を引き起こしやすいです。
原理: 範囲シャーディングはシャードキーの値範囲を連続する区間[min, splitPoint1), [splitPoint1, splitPoint2), ...に分割し、各区間がチャンクに対応します。隣接する値を持つドキュメントは同じチャンク → 同じシャードに配置されます。
// === シャーディング有効化 ===
sh.enableSharding('shopdb');
// === シャードキー選択:ユーザーID範囲 ===
sh.shardCollection('shopdb.orders', { userId: 1 });
// userId 1-1000 → Shard 1
// userId 1001-2000 → Shard 2
| 項目 | 範囲シャード | 説明 |
|---|---|---|
| 範囲クエリ | ✅ 高効率 | 連続データが同一シャードに |
| ソート | ✅ 高効率 | インデックスは自然順序 |
| データ分散 | ⚠️ 偏在の可能性 | ホットキーが単一シャードに集中 |
| 適用シーン | 時系列、ID範囲 | createdAt、userId |
(2) ハッシュシャーディング
概念説明: ハッシュシャーディングはシャードキーのハッシュを計算し、ハッシュ値範囲でチャンクを分割します。データは均等に分散されホットスポットはありませんが、範囲クエリは全シャードへのブロードキャストが必要です。
// === ハッシュシャーディング ===
sh.shardCollection('shopdb.products', { sku: 'hashed' });
// skuのハッシュ値で均等にシャードに分散
| 項目 | ハッシュシャード | 説明 |
|---|---|---|
| データ分散 | ✅ 均等 | ハッシュ関数で自然に散らばる |
| 書き込み分散 | ✅ 均等 | ホットスポットなし |
| 範囲クエリ | ❌ ブロードキャスト必要 | 隣接値は異なるシャードに |
| 適用シーン | 主に等価クエリ | SKU、email、userID |
(3) 範囲 vs ハッシュ比較
| 項目 | 範囲 | ハッシュ |
|---|---|---|
| データ分散 | 偏在の可能性 | 均等 |
| 範囲クエリ | ✅ ターゲットルート | ❌ 全シャードにブロードキャスト |
| 等価クエリ | ✅ | ✅ |
| ソート | ✅ 順序付きインデックス | ❌ |
| 書き込みホットスポット | ⚠️ 単一ポイントホットスポット | ✅ 均等 |
| 複合シャードキー | ✅ 対応 | ❌ 単一フィールドのみ |
(4) 最適なシャードキー選択
// ✅ 良いシャードキー:高カーディナリティ、低頻度更新、クエリヒット
sh.shardCollection('shopdb.orders', { userId: 1, createdAt: -1 });
// 複合シャードキー、userId範囲 + 時系列ソート
// ❌ 不適切なシャードキー:低カーディナリティ
sh.shardCollection('shopdb.products', { category: 1 });
// categoryは5つの値しかなく、深刻な偏在を引き起こす
パーティションキーのアンチパターン:
| アンチパターン | 結果 | ベストプラクティス |
|---|---|---|
| 低カーディナリティフィールド(categoryなど) | 深刻なデータ偏在 | 高カーディナリティフィールドを選択(userId) |
| 単調増加フィールド(ObjectIdなど) | 新規データが同じシャードに書き込まれる | ハッシュまたは複合シャードキーを使用 |
| 頻繁に更新されるフィールド | 頻繁なチャンク移行 | 低頻度更新フィールドを選択 |
| クエリ条件に含まれない | 全クエリがブロードキャスト | 高頻度クエリフィールドを選択 |
4. チャンク分割と移行
概念説明: チャンクはシャーディングデータ管理の最小単位—デフォルト64 MB—で、特定の範囲のシャードキーを持つ全ドキュメントを含みます。チャンクは閾値を超えると自動的に分割され、シャード間でチャンク数が不均等になると自動的に移行されます。これがMongoDB自動負荷分散の中核メカニズムです。
動作原理:
- 分割: チャンクが64 MBを超える(または設定されたドキュメント数閾値を超える)と、Mongosはシャードキーの中央値を見つけてチャンクを2つに分割します。
- 移行: Balancerは各シャードのチャンク数を定期的にチェックし、チャンクが多いシャードから少ないシャードへチャンクを移行し、負荷をバランスさせます。
バランサーの動作:
graph TB
subgraph "バランサーワークフロー"
A[各シャードの<br/>チャンク数をチェック] --> B{差が>8?}
B -->|はい| C[移行元シャードを選択<br/>(最も多いチャンク)]
C --> D[移行先シャードを選択<br/>(最も少ないチャンク)]
D --> E[チャンク移行]
E --> F[Config Server<br/>メタデータ更新]
F --> A
B -->|いいえ| G[次回チェック待機<br/>デフォルト10秒]
end
style E fill:#cce5ff
style F fill:#d4edda
graph LR
A[Chunk 1<br/>1-1000] -->|分割| B[Chunk 1a<br/>1-500]
A -->|分割| C[Chunk 1b<br/>501-1000]
C -->|移行| D[Shard 2]
style B fill:#d4edda
style D fill:#fff3cd
チャンク操作コマンド:
| 操作 | コマンド | 説明 |
|---|---|---|
| 手動分割 | sh.splitAt(ns, key) |
指定キー値で分割 |
| 分散確認 | db.col.getShardDistribution() |
シャード別データ量 |
| クラスター状態 | sh.status() |
チャンク分散詳細 |
| バランサー状態 | sh.isBalancerRunning() |
バランシング実行中か |
| バランサー開始/停止 | sh.startBalancer() / sh.stopBalancer() |
メンテナンス時間中に一時停止可能 |
| チャンクサイズ変更 | db.settings.save({_id:'chunksize', value: 128}) |
単位: MB |
// === 手動でチャンクを分割 ===
sh.splitAt('shopdb.orders', { userId: 5000 });
// === チャンク分散を確認 ===
db.orders.getShardDistribution();
// === バランサー状態 ===
sh.status();
sh.isBalancerRunning();
ポイント解説:
- チャンク分割は論理操作(メタデータのみ変更)でデータは移動しません。チャンク移行は物理操作(実際にデータをコピー)。
- バランサーはバックグラウンドで実行され、移行中も読み書き操作に影響しません(二重書き込みで整合性を保証)。
- メンテナンス時間中(バッチインポートなど)は、インポートパフォーマンスへの影響を防ぐためバランサーを一時停止を推奨。
▶ サンプル 1:バランサー管理と手動チャンク分割
// ShopHub:大規模インポート前にバランサーを一時停止、インポート後に復旧
// 1. バランサー一時停止
sh.stopBalancer();
// 2. バッチインポートデータ
for (let i = 0; i < 1000000; i++) {
db.orders.insertOne({ userId: 'user_' + (i % 500), total: Math.random() * 1000, createdAt: new Date() });
}
// 3. ホットスポットチャンクを手動分割
sh.splitAt('shopdb.orders', { userId: 'user_100' });
sh.splitAt('shopdb.orders', { userId: 'user_200' });
sh.splitAt('shopdb.orders', { userId: 'user_300' });
// 4. バランサー復旧
sh.startBalancer();
// 5. 分散を確認
db.orders.getShardDistribution();
出力:
TEXT 📖 参照専用Shard shard1 at shard1/host1:27018 data: 500MB docs: 500000 chunks: 50 Shard shard2 at shard2/host2:27018 data: 500MB docs: 500000 chunks: 50
5. Dockerシャードクラスター構築
概念概要: シャードクラスターのデプロイはレプリカセットより複雑—Config Serverレプリカセット、複数のシャードレプリカセット、Mongosルーティングが必要です。Docker Composeでこれら全コンポーネントを一発でオーケストレーションでき、開発・テスト環境に最適です。
デプロイアーキテクチャ:
graph TB
subgraph "Config Serverレプリカセット"
CS1[config1:27019]
end
subgraph "Shard 1レプリカセット"
S1A[shard1a:27018]
end
subgraph "Shard 2レプリカセット"
S2A[shard2a:27020]
end
subgraph "Mongosルーティング"
MS[mongos:27017]
end
App[アプリケーション] --> MS
MS --> CS1
MS --> S1A
MS --> S2A
style MS fill:#cce5ff
style CS1 fill:#fff3cd
style S1A fill:#d4edda
style S2A fill:#d4edda
本番 vs 開発構成の比較:
| コンポーネント | 本番環境 | 開発環境 |
|---|---|---|
| Config Server | 3ノードレプリカセット | 1ノード(テストのみ) |
| 各シャード | 3ノードレプリカセット | 1ノード |
| Mongos | 複数インスタンス(負荷分散) | 1インスタンス |
| 合計mongod数 | 3 + 3 × 3 = 12+ | 1 + 2 + 1 = 4 |
# docker-compose-sharding.yml
version: '3.8'
services:
# Config Server(レプリカセット)
config1:
image: mongo:7.0
command: mongod --configsvr --replSet configReplSet --port 27019
# Shard 1(レプリカセット)
shard1a:
image: mongo:7.0
command: mongod --shardsvr --replSet shard1ReplSet --port 27018
# Mongosルーティング
mongos:
image: mongo:7.0
command: mongos --configdb configReplSet/config1:27019 --port 27017
ports:
- "27017:27017"
初期化ステップ:
| ステップ | コマンド | 説明 |
|---|---|---|
| 1 | config1でrs.initiate() |
Config Serverレプリカセット初期化 |
| 2 | shard1aでrs.initiate() |
Shard 1レプリカセット初期化 |
| 3 | mongosでsh.addShard() |
クラスターにシャード追加 |
| 4 | sh.enableSharding() |
データベースシャーディング有効化 |
| 5 | sh.shardCollection() |
シャードキーを選択 |
# === シャードクラスター初期化 ===
# 1. Config Serverレプリカセット初期化
mongosh --port 27019 --eval 'rs.initiate({_id: "configReplSet", members: [{_id: 0, host: "config1:27019"}]})'
# 2. Shard 1レプリカセット初期化
mongosh --port 27018 --eval 'rs.initiate({_id: "shard1ReplSet", members: [{_id: 0, host: "shard1a:27018"}]})'
# 3. Mongos経由でシャードを追加
mongosh --port 27017 --eval '
sh.addShard("shard1ReplSet/shard1a:27018");
sh.enableSharding("shopdb");
sh.shardCollection("shopdb.orders", { userId: 1 });
'
6. シャードクラスター監視
概念説明: シャードクラスターの監視は単一ノードやレプリカセットより複雑—各コンポーネントの健全性、データ分散バランス、チャンク移行状態などを監視する必要があります。適切な監視でデータ偏在、ホットシャード、設定問題を早期に発見できます。
監視項目:
| 項目 | コマンド | 主要メトリクス |
|---|---|---|
| クラスター概要 | sh.status() |
シャード数、チャンク分散、バランサー状態 |
| データ分散 | db.col.getShardDistribution() |
シャード間データ量がバランスしているか |
| バランサー | sh.isBalancerRunning() |
移行状態、移行進捗 |
| 設定情報 | db.settings.find() |
チャンクサイズ、バランサー設定 |
| シャード健全性 | sh.status().shards |
シャード到達可能性 |
// === シャード状態確認 ===
sh.status();
// === データ分散確認 ===
db.orders.getShardDistribution();
// === バランサー管理 ===
sh.startBalancer();
sh.stopBalancer();
sh.setBalancerState(true);
// === 設定確認 ===
db.settings.find();
よくある問題のトラブルシューティング:
| 問題 | 症状 | 診断 | 解決策 |
|---|---|---|---|
| データ偏在 | 特定シャードのデータ量が他より大幅に多い | getShardDistribution() |
ホットチャンクを手動分割 |
| ジャンボチャンク | 64MB超のチャンクが分割できない | sh.status()でjumbo表示 |
chunkSizeを増やすかキーを分割 |
| バランサー停止 | 移行が進まない | sh.isBalancerRunning() |
Config Server健全性を確認 |
| クエリブロードキャスト | 全シャードがクエリされた | explain()でSHARD_MERGE表示 |
クエリ条件にシャードキーを含める |
▶ サンプル 2:シャード監視とトラブルシューティング
// BobのDataFlowシステムでクエリが遅い、シャーディング問題を診断
// 1. クラスター状態確認
sh.status();
// Shard1: 8000 chunks, Shard2: 2000 chunks → 深刻な偏在を発見
// 2. データ分散確認
db.orders.getShardDistribution();
// Shard 1: 80GB (80%), Shard 2: 20GB (20%)
// 3. ジャンボチャンクがあるか確認
db.config.chunks.find({ jumbo: true }).count();
// 3つのJumbo Chunk
// 4. ホットスポットチャンクを手動分割
sh.splitAt('shopdb.orders', { userId: 'user_100' });
sh.splitAt('shopdb.orders', { userId: 'user_200' });
// 5. バランサーが実行中か確認
sh.startBalancer();
sh.isBalancerRunning(); // true
// 6. バランシング完了を待ち、再確認
db.orders.getShardDistribution();
// Shard 1: 50GB (50%), Shard 2: 50GB (50%) → バランス
7. いつシャーディングを使うべきか?
概念説明: シャーディングは早すぎても遅すぎてもいけません—スケーラビリティを提供しますが、運用複雑さも増します。早すぎるシャーディングは不要な運用コストを生み、遅すぎるシャーディングは単一サーバーのボトルネックとデータ移行の困難を招きます。データ量、書き込みスループット、成長傾向を総合的に評価して決定すべきです。
意思決定フレームワーク:
graph TB
A{データ量?} -->|< 100GB| B[❌ 単一サーバー + レプリカセット]
A -->|100GB-1TB| C{書き込みQPS?}
A -->|> 1TB| D[✅ シャーディング]
C -->|< 10K ops/s| E[⚠️ レプリカセット<br/>成長傾向を監視]
C -->|> 10K ops/s| D
B --> F[インデックス最適化 + 検索]
E --> G[シャードキー事前計画]
D --> H[パーティションキー選択 + シャードクラスター構築]
style B fill:#d4edda
style D fill:#cce5ff
style E fill:#fff3cd
| シーン | シャーディング | 理由 |
|---|---|---|
| データ量 < 100 GB | ❌ 単一サーバーで十分 | シャーディングはメリットなしで複雑さが増す |
| データ量: 100 GB – 1 TB | ⚠️ 状況による | 書き込みQPSと成長率を評価 |
| データ量 > 1 TB | ✅ 推奨 | 単一サーバーの容量・I/Oボトルネック |
| 書き込み > 10K ops/s | ✅ 推奨 | 単一Primary書き込みボトルネック |
| 継続的なデータ量増加 | ✅ 推奨 | 事前に計画し、強制シャーディングを回避 |
シャーディング前のチェックリスト:
| 準備項目 | 説明 |
|---|---|
| インデックス最適化が限界か確認 | まずexplain()でクエリ問題を除外 |
| 垂直スケーリングが限界か確認 | CPU、メモリ、SSDアップグレードで十分か? |
| 適切なパーティションキーを選択 | 高カーディナリティ、低更新頻度、クエリヒット |
| レプリカセットをデプロイ | シャーディングの基盤、各シャードはレプリカセット |
| 容量を計画 | データ成長を見積もり、シャード数を決定 |
| アプリケーション互換性テスト | クエリにシャードキーを含む、ユニークインデックスにシャードキーを含む |
▶ サンプル:Dockerでシャードクラスター構築 + データシャーディングのハンズオンガイド
# === 1. シャードクラスター起動(docker-compose-sharding.yml)===
# services:
# config1:
# image: mongo:7.0
# command: mongod --configsvr --replSet configReplSet --bind_ip_all --port 27019
# ports: ["27019:27019"]
#
# shard1a:
# image: mongo:7.0
# command: mongod --shardsvr --replSet shard1ReplSet --bind_ip_all --port 27018
# ports: ["27018:27018"]
#
# shard2a:
# image: mongo:7.0
# command: mongod --shardsvr --replSet shard2ReplSet --bind_ip_all --port 27020
# ports: ["27020:27020"]
#
# mongos:
# image: mongo:7.0
# command: mongos --configdb configReplSet/config1:27019 --bind_ip_all --port 27017
# ports: ["27017:27017"]
# depends_on: [config1, shard1a, shard2a]
docker-compose -f docker-compose-sharding.yml up -d
# === 2. Config Serverレプリカセット初期化 ===
mongosh --port 27019 --eval '
rs.initiate({
_id: "configReplSet",
members: [{ _id: 0, host: "config1:27019" }]
});
'
# === 3. シャードレプリカセット初期化 ===
mongosh --port 27018 --eval '
rs.initiate({ _id: "shard1ReplSet", members: [{ _id: 0, host: "shard1a:27018" }] });
'
mongosh --port 27020 --eval '
rs.initiate({ _id: "shard2ReplSet", members: [{ _id: 0, host: "shard2a:27020" }] });
'
# === 4. Mongos経由でシャードを追加 ===
mongosh --port 27017 --eval '
sh.addShard("shard1ReplSet/shard1a:27018");
sh.addShard("shard2ReplSet/shard2a:27020");
'
# === 5. データベースシャーディング有効化 ===
mongosh --port 27017 --eval '
sh.enableSharding("shopdb");
sh.shardCollection("shopdb.orders", { userId: 1, createdAt: -1 }); // 複合シャードキー
'
# === 6. テストデータ挿入(自動的に異なるシャードに分散)===
mongosh --port 27017 --eval '
for (let i = 0; i < 10000; i++) {
db.orders.insertOne({
userId: "user_" + (i % 100),
total: Math.random() * 1000,
createdAt: new Date(),
status: "paid"
});
}
'
# === 7. シャード分散確認 ===
mongosh --port 27017 --eval 'sh.status();'
# 出力:
# shards:
# { _id: 'shard1ReplSet', count: 5012 }
# { _id: 'shard2ReplSet', count: 4988 }
# データは2つのシャードに均等に分散
# === 8. Mongosクエリで自動ルーティング ===
mongosh --port 27017 --eval '
db.orders.find({ userId: "user_50" }).count();
'
# Mongosは自動的に対応シャードにルーティング(userId範囲に基づく)
// === 9. アプリケーションからMongosに接続(アプリ透過、シャーディング透過)===
const mongoose = require('mongoose');
await mongoose.connect('mongodb://localhost:27017/shopdb');
// アプリケーションコードの変更なし、スタンドアロンMongoDBと全く同じ
await Order.create({
userId: 'user_001',
total: 599,
items: [{ sku: 'PHONE-001', qty: 1 }]
});
// Mongosは自動的に適切なシャードにルーティング
// === 10. ハッシュシャーディング(データを均等分散)===
mongosh --port 27017 --eval '
sh.shardCollection("shopdb.products", { sku: "hashed" });
'
// skuはハッシュ後均等に分散、ホットスポットを回避
出力:
TEXT 📖 参照専用10,000件の注文が2つのシャードに自動分散(5,012 + 4,988)。アプリケーションはMongos経由で接続し、シャーディング詳細を意識する必要なし。
❓ よくある質問
📖 まとめ
- シャーディングアーキテクチャ:Mongos + Config Server + Shard
- シャードキー選択:ESRT(Equality / Sort / Range / Time)
- 範囲 vs ハッシュシャーディング
- 自動チャンク分割とバランシング
- Dockerシャードクラスター構築
- シャーディングのタイミング:データ量 > 1 TB / ops/s > 10,000
📝 練習問題
- 基礎問題(⭐): シャーディングアーキテクチャとコンポーネント(Mongos / Config Server / Shard)を理解する。
- 基礎問題(⭐): ビジネスシナリオを分析し、シャーディングが必要か判断する。
- 応用問題(⭐⭐): Docker ComposeでConfig 1つ + Shard 2つのクラスターを構築。
- 応用問題(⭐⭐): シャードキー選択をテスト(userId vs. sku vs. 複合)。
- チャレンジ(⭐⭐⭐): 完全なシャードクラスターをデプロイ(Configレプリカセット + 2シャードレプリカセット + Mongos + 監視)。