はじめに
「レプリカを増やしても、書き込みが追いつかない」
レプリケーションは読み取りをスケールさせる。 でも、書き込みは1台のPrimaryに集中する。
ユーザーが100万人を超えたとき、1台のDBでは限界が来る。
そこで必要になるのが、シャーディングだ。
データを複数のDBに分割して、書き込みも読み取りもスケールさせる。
この記事では、シャーディングの本質から、実装パターン、落とし穴と解決策までを解説する。
パーティショニング vs シャーディング
パーティショニング(単一DB内の分割)
flowchart TB
subgraph DB["Database (単一DBサーバー内)"]
P1["Partition<br/>2023"]
P2["Partition<br/>2024"]
P3["Partition<br/>2025"]
end
style DB fill:#e3f2fd,stroke:#1976d2,stroke-width:3px
style P1 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style P2 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style P3 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
特徴:
- 同じDBサーバー内でテーブルを分割
- 管理が楽(1台のDB)
- スケールには限界がある
シャーディング(複数DBへの分割)
flowchart LR
S1["Shard 1<br/>(A-H)<br/>DB Server1"]
S2["Shard 2<br/>(I-P)<br/>DB Server2"]
S3["Shard 3<br/>(Q-Z)<br/>DB Server3"]
style S1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S3 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
特徴:
- 異なるDBサーバーにデータを分散
- 水平スケールが可能
- 複雑さが増す
使い分け
flowchart TD
Q1["1台のDBで性能が足りる?"]
Q2["レプリケーションで解決?"]
A1["パーティショニング<br/>or そのまま"]
A2["レプリケーション<br/>読み取りボトルネック"]
A3["シャーディング<br/>書き込みボトルネック"]
Q1 -->|"Yes"| A1
Q1 -->|"No"| Q2
Q2 -->|"Yes"| A2
Q2 -->|"No"| A3
style Q1 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style Q2 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style A1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style A2 fill:#e1f5ff,stroke:#2196f3,stroke-width:2px
style A3 fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
垂直分割 vs 水平分割
垂直分割(テーブル単位)
flowchart TB
subgraph Before["Before: 単一DB"]
ALL["users, orders, products, logs, ...<br/>(全テーブル)"]
end
subgraph After["After: 機能別に分割"]
direction LR
DB1["User DB<br/>━━━━━━<br/>users<br/>products"]
DB2["Order DB<br/>━━━━━━<br/>orders<br/>payments"]
DB3["Log DB<br/>━━━━━━<br/>logs<br/>metrics"]
end
Before --> After
style ALL fill:#ffebee,stroke:#c62828,stroke-width:2px
style DB1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style DB2 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style DB3 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
特徴:
- 機能単位でDBを分ける
- マイクロサービスとの相性が良い
- JOIN が制限される
実装例:
# 機能ごとに接続先を変える
class DatabaseRouter:
def __init__(self):
self.user_db = create_connection('user-db')
self.order_db = create_connection('order-db')
self.log_db = create_connection('log-db')
def get_connection(self, table):
if table in ['users', 'profiles', 'settings']:
return self.user_db
elif table in ['orders', 'order_items', 'payments']:
return self.order_db
elif table in ['logs', 'metrics', 'events']:
return self.log_db
水平分割(行単位)
flowchart TB
subgraph Before["Before: 単一テーブル"]
USERS["users (1億行)<br/>id=1, id=2, id=3, ... id=100000000"]
end
subgraph After["After: 行単位で分割"]
direction LR
S1["Shard 1<br/>━━━━━━<br/>users<br/>id 1-33M"]
S2["Shard 2<br/>━━━━━━<br/>users<br/>id 34M-66M"]
S3["Shard 3<br/>━━━━━━<br/>users<br/>id 67M-100M"]
end
Before --> After
style USERS fill:#ffebee,stroke:#c62828,stroke-width:2px
style S1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S3 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
特徴:
- 同じテーブルの行を複数DBに分散
- 書き込みがスケール
- 実装が複雑
シャーディング戦略
1. レンジベースシャーディング
flowchart LR
subgraph Ranges["ID範囲で分割"]
S1["Shard 1<br/>━━━━━━<br/>user_id<br/>1 ~ 1,000,000"]
S2["Shard 2<br/>━━━━━━<br/>user_id<br/>1,000,001 ~ 2,000,000"]
S3["Shard 3<br/>━━━━━━<br/>user_id<br/>2,000,001 ~ 3,000,000"]
end
style S1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S3 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
def get_shard(user_id):
if user_id <= 1_000_000:
return 'shard1'
elif user_id <= 2_000_000:
return 'shard2'
else:
return 'shard3'
メリット:
- 実装がシンプル
- 範囲クエリが効率的(例: 最近のユーザー)
デメリット:
- ホットスポット問題: 新規ユーザーが1つのシャードに集中
- シャード追加時のリバランスが大変
2. ハッシュベースシャーディング
flowchart LR
HASH["hash(user_id) % num_shards"]
U1["user_id: 12345<br/>hash % 3 = 0"]
U2["user_id: 67890<br/>hash % 3 = 1"]
U3["user_id: 11111<br/>hash % 3 = 2"]
S1["Shard 1"]
S2["Shard 2"]
S3["Shard 3"]
U1 --> S1
U2 --> S2
U3 --> S3
style HASH fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style U1 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style U2 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style U3 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style S1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S3 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
import hashlib
def get_shard(user_id, num_shards=3):
hash_value = int(hashlib.md5(str(user_id).encode()).hexdigest(), 16)
return f'shard{hash_value % num_shards + 1}'
メリット:
- データが均等に分散
- ホットスポットが発生しにくい
デメリット:
- 範囲クエリが非効率(全シャードをスキャン)
- リシャーディング問題: シャード数が変わると大量のデータ移動
3. 一貫性ハッシュ(Consistent Hashing)
flowchart TB
subgraph Ring["Hash Ring (0 → 2^32)"]
direction LR
H0["0"] --> S1["Shard1"]
S1 --> K1["Key K1"]
K1 --> S2["Shard2"]
S2 --> K2["Key K2"]
K2 --> S3["Shard3"]
S3 -.->|時計回り| H0
end
K1 -.->|"時計回りで最初に<br/>見つかるシャード"| S2
K2 -.->|"時計回りで最初に<br/>見つかるシャード"| S3
style S1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S3 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style K1 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style K2 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style H0 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
import hashlib
from bisect import bisect_right
class ConsistentHash:
def __init__(self, nodes, virtual_nodes=150):
self.ring = []
self.node_map = {}
self.virtual_nodes = virtual_nodes
for node in nodes:
self.add_node(node)
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node):
for i in range(self.virtual_nodes):
virtual_key = f"{node}:{i}"
hash_value = self._hash(virtual_key)
self.ring.append(hash_value)
self.node_map[hash_value] = node
self.ring.sort()
def remove_node(self, node):
for i in range(self.virtual_nodes):
virtual_key = f"{node}:{i}"
hash_value = self._hash(virtual_key)
self.ring.remove(hash_value)
del self.node_map[hash_value]
def get_node(self, key):
if not self.ring:
return None
hash_value = self._hash(key)
idx = bisect_right(self.ring, hash_value) % len(self.ring)
return self.node_map[self.ring[idx]]
# 使用例
ch = ConsistentHash(['shard1', 'shard2', 'shard3'])
print(ch.get_node('user:12345')) # → shard2
print(ch.get_node('user:67890')) # → shard1
# ノード追加時、影響を受けるのは隣接するノードのデータのみ
ch.add_node('shard4')
メリット:
- シャード追加・削除時のデータ移動が最小限
- スケールしやすい
デメリット:
- 実装が複雑
- Virtual Nodesの数の調整が必要
4. ディレクトリベースシャーディング
flowchart TB
subgraph Lookup["Lookup Table (ルックアップテーブル)"]
L1["user_id 1-100 → shard1"]
L2["user_id 101-200 → shard2"]
L3["user_id 201-300 → shard1 (移動後)"]
L4["user_id 301-400 → shard3"]
end
subgraph Shards["シャード"]
S1["Shard1"]
S2["Shard2"]
S3["Shard3"]
end
L1 --> S1
L2 --> S2
L3 --> S1
L4 --> S3
style Lookup fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style S1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S3 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
class DirectoryShard:
def __init__(self):
# ルックアップテーブル(Redis等に保存)
self.directory = {}
def assign_shard(self, user_id, shard):
self.directory[user_id] = shard
def get_shard(self, user_id):
return self.directory.get(user_id)
def migrate(self, user_id, new_shard):
# データ移動
old_shard = self.directory[user_id]
migrate_data(old_shard, new_shard, user_id)
# ディレクトリ更新
self.directory[user_id] = new_shard
メリット:
- 柔軟なデータ配置
- 任意のタイミングでリバランス可能
デメリット:
- ルックアップテーブルが単一障害点になりうる
- ルックアップのオーバーヘッド
シャーディング戦略の比較
| 戦略 | 分散の均一性 | 範囲クエリ | リシャーディング | 複雑さ |
|---|---|---|---|---|
| レンジ | ❌ 偏りやすい | ✅ 効率的 | ❌ 大変 | ✅ 簡単 |
| ハッシュ | ✅ 均一 | ❌ 非効率 | ❌ 大変 | ⚪ 普通 |
| 一貫性ハッシュ | ✅ 均一 | ❌ 非効率 | ✅ 最小限 | ❌ 複雑 |
| ディレクトリ | ✅ 制御可能 | ⚪ 条件による | ✅ 柔軟 | ❌ 複雑 |
シャードキーの選択
良いシャードキーの条件
- 高カーディナリティ: 多くのユニークな値を持つ
- 均等な分布: データが偏らない
- クエリパターンに合致: よく使うクエリで効率的
例: ECサイトの注文テーブル
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
user_id BIGINT,
shop_id BIGINT,
product_id BIGINT,
amount DECIMAL,
created_at TIMESTAMP
);
候補:
| シャードキー | 評価 |
|---|---|
| order_id | ⚪ 均等だが、user_idでのクエリが全シャードスキャン |
| user_id | ✅ ユーザーの注文履歴が1シャードで完結 |
| shop_id | ⚪ ショップ分析は効率的だが、ユーザークエリが非効率 |
| created_at | ❌ 最新データに書き込み集中(ホットスポット) |
推奨: user_id をシャードキーにする
def get_order_shard(user_id):
return hash(user_id) % NUM_SHARDS
これにより:
- 「このユーザーの注文一覧」が1シャードで完結
- ユーザーごとの書き込みが分散
【実務】クロスシャードクエリの課題
問題: JOINができない
-- シャーディング前
SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at > '2024-01-01';
-- シャーディング後
-- users と orders が別シャードにあると JOIN できない
解決策1: アプリケーション層でJOIN
def get_user_orders(user_id):
# 1. ユーザー情報を取得
user_shard = get_shard('users', user_id)
user = user_shard.query("SELECT * FROM users WHERE id = ?", user_id)
# 2. 注文情報を取得
order_shard = get_shard('orders', user_id)
orders = order_shard.query("SELECT * FROM orders WHERE user_id = ?", user_id)
# 3. アプリケーション層で結合
return {
'user': user,
'orders': orders
}
解決策2: データの非正規化
-- 注文テーブルにユーザー名を含める(非正規化)
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
user_id BIGINT,
user_name VARCHAR(100), -- 非正規化
amount DECIMAL,
created_at TIMESTAMP
);
メリット: JOINなしでデータ取得 デメリット: データの重複、更新時の整合性
解決策3: グローバルテーブル
flowchart LR
subgraph Global["Global Table (読み取り専用)"]
Master["Master<br/>(全データ)"]
R1["Replica<br/>(Shard1)"]
R2["Replica<br/>(Shard2)"]
Master -->|レプリケーション| R1
Master -->|レプリケーション| R2
end
style Master fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style R1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style R2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
更新頻度が低いマスタデータ(国リスト、カテゴリ等)は全シャードにレプリカを置く。
解決策4: Scatter-Gather パターン
flowchart TB
Coordinator["Coordinator<br/>(クエリを分散・集約)"]
S1["Shard1<br/>Query"]
S2["Shard2<br/>Query"]
S3["Shard3<br/>Query"]
Merge["Merge Results<br/>(結果をマージ)"]
Coordinator -->|クエリ分散| S1
Coordinator -->|クエリ分散| S2
Coordinator -->|クエリ分散| S3
S1 -->|結果| Merge
S2 -->|結果| Merge
S3 -->|結果| Merge
style Coordinator fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style S1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style S3 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style Merge fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
async def scatter_gather_query(query, params):
# 全シャードに並列でクエリを投げる
tasks = [
shard.query(query, params)
for shard in all_shards
]
results = await asyncio.gather(*tasks)
# 結果をマージ
return merge_results(results)
# 使用例: 全ユーザーの売上合計
async def get_total_sales():
results = await scatter_gather_query(
"SELECT SUM(amount) as total FROM orders WHERE created_at > ?",
['2024-01-01']
)
return sum(r['total'] for r in results)
【実務】トランザクションの課題
問題: 分散トランザクション
# user_id=1 のユーザーが user_id=2 のユーザーに送金
# しかし、user_id=1 と user_id=2 は別シャード!
def transfer(from_user, to_user, amount):
# Shard1: from_user の残高を減らす
shard1.execute("UPDATE accounts SET balance = balance - ? WHERE user_id = ?",
amount, from_user)
# ここで障害発生したら?
# → from_user の残高だけ減って、to_user に届かない
# Shard2: to_user の残高を増やす
shard2.execute("UPDATE accounts SET balance = balance + ? WHERE user_id = ?",
amount, to_user)
解決策1: 2フェーズコミット(2PC)
sequenceDiagram
participant C as Coordinator
participant S1 as Shard1
participant S2 as Shard2
Note over C,S2: Phase 1: Prepare
C->>S1: PREPARE (ロック取得)
S1-->>C: OK
C->>S2: PREPARE (ロック取得)
S2-->>C: OK
Note over C,S2: Phase 2: Commit (全員OKの場合)
C->>S1: COMMIT
C->>S2: COMMIT
S1-->>C: Done
S2-->>C: Done
Note over C,S2: Phase 2: Rollback (誰かNGの場合)
C->>S1: ROLLBACK
C->>S2: ROLLBACK
問題点:
- 遅い(ロック時間が長い)
- Coordinatorが単一障害点
- 障害時のリカバリが複雑
解決策2: Saga パターン
flowchart TB
Start["Saga開始"]
Step1["Step 1:<br/>Shard1で残高を減らす"]
Step2["Step 2:<br/>Shard2で残高を増やす"]
Success["成功"]
Compensate["Compensate:<br/>Shard1で残高を戻す<br/>(補償トランザクション)"]
Fail["失敗"]
Start --> Step1
Step1 -->|成功| Step2
Step2 -->|成功| Success
Step2 -->|失敗| Compensate
Compensate --> Fail
style Start fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style Step1 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style Step2 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style Success fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style Compensate fill:#ffebee,stroke:#c62828,stroke-width:2px
style Fail fill:#ffebee,stroke:#c62828,stroke-width:2px
class TransferSaga:
def execute(self, from_user, to_user, amount):
try:
# Step 1: 送金元の残高を減らす
self.debit(from_user, amount)
try:
# Step 2: 送金先の残高を増やす
self.credit(to_user, amount)
except Exception:
# 補償: 送金元の残高を戻す
self.compensate_debit(from_user, amount)
raise
except Exception as e:
raise TransferFailed(str(e))
def debit(self, user_id, amount):
shard = get_shard(user_id)
shard.execute(
"UPDATE accounts SET balance = balance - ? WHERE user_id = ? AND balance >= ?",
amount, user_id, amount
)
def credit(self, user_id, amount):
shard = get_shard(user_id)
shard.execute(
"UPDATE accounts SET balance = balance + ? WHERE user_id = ?",
amount, user_id
)
def compensate_debit(self, user_id, amount):
shard = get_shard(user_id)
shard.execute(
"UPDATE accounts SET balance = balance + ? WHERE user_id = ?",
amount, user_id
)
解決策3: シャード設計で回避
# 送金を同一シャード内で完結させる
# → 組織単位でシャーディング
組織A のユーザー同士の送金 → Shard1 内で完結
組織B のユーザー同士の送金 → Shard2 内で完結
組織間の送金 → 非同期処理(後述)
【実装】MySQL パーティショニング
レンジパーティション
CREATE TABLE orders (
order_id BIGINT AUTO_INCREMENT,
user_id BIGINT,
amount DECIMAL(10,2),
created_at DATETIME,
PRIMARY KEY (order_id, created_at)
) PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
-- パーティション追加
ALTER TABLE orders ADD PARTITION (
PARTITION p2026 VALUES LESS THAN (2027)
);
-- 古いパーティションの削除(高速)
ALTER TABLE orders DROP PARTITION p2022;
ハッシュパーティション
CREATE TABLE users (
user_id BIGINT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(255)
) PARTITION BY HASH(user_id) PARTITIONS 4;
リストパーティション
CREATE TABLE orders (
order_id BIGINT,
region VARCHAR(10),
amount DECIMAL(10,2),
PRIMARY KEY (order_id, region)
) PARTITION BY LIST COLUMNS(region) (
PARTITION p_east VALUES IN ('tokyo', 'osaka'),
PARTITION p_west VALUES IN ('fukuoka', 'hiroshima'),
PARTITION p_other VALUES IN ('other')
);
【実装】アプリケーション層シャーディング
シンプルな実装
import hashlib
from typing import Dict, Any
class ShardManager:
def __init__(self, shard_configs: Dict[str, str]):
"""
shard_configs: {'shard0': 'mysql://...', 'shard1': 'mysql://...', ...}
"""
self.shards = {
name: create_connection(url)
for name, url in shard_configs.items()
}
self.num_shards = len(self.shards)
def _hash(self, key: str) -> int:
return int(hashlib.md5(str(key).encode()).hexdigest(), 16)
def get_shard(self, shard_key: Any):
"""シャードキーから接続を取得"""
shard_index = self._hash(str(shard_key)) % self.num_shards
return self.shards[f'shard{shard_index}']
def get_all_shards(self):
"""全シャードを取得(scatter-gather用)"""
return list(self.shards.values())
# 使用例
shard_manager = ShardManager({
'shard0': 'mysql://host1/db',
'shard1': 'mysql://host2/db',
'shard2': 'mysql://host3/db',
})
# ユーザー作成
def create_user(user_id, name, email):
shard = shard_manager.get_shard(user_id)
shard.execute(
"INSERT INTO users (id, name, email) VALUES (?, ?, ?)",
(user_id, name, email)
)
# ユーザー取得
def get_user(user_id):
shard = shard_manager.get_shard(user_id)
return shard.query("SELECT * FROM users WHERE id = ?", (user_id,))
# 全ユーザー数(scatter-gather)
def count_all_users():
total = 0
for shard in shard_manager.get_all_shards():
result = shard.query("SELECT COUNT(*) as cnt FROM users")
total += result['cnt']
return total
Django でのシャーディング
# settings.py
DATABASES = {
'default': {},
'shard0': {
'ENGINE': 'django.db.backends.mysql',
'HOST': 'shard0.db.internal',
'NAME': 'app',
},
'shard1': {
'ENGINE': 'django.db.backends.mysql',
'HOST': 'shard1.db.internal',
'NAME': 'app',
},
'shard2': {
'ENGINE': 'django.db.backends.mysql',
'HOST': 'shard2.db.internal',
'NAME': 'app',
},
}
# routers.py
class ShardRouter:
def db_for_read(self, model, **hints):
if hasattr(model, 'shard_key'):
shard_key = hints.get('shard_key')
if shard_key:
return self._get_shard(shard_key)
return 'default'
def db_for_write(self, model, **hints):
return self.db_for_read(model, **hints)
def _get_shard(self, shard_key):
shard_index = hash(str(shard_key)) % 3
return f'shard{shard_index}'
# models.py
class User(models.Model):
shard_key = 'id' # シャードキーを指定
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=100)
class Meta:
app_label = 'users'
# 使用例
user = User.objects.using(router._get_shard(user_id)).get(id=user_id)
【実務】リシャーディング(シャード追加)
問題
シャード数: 3 → 4 に増やしたい
Before: hash(key) % 3
After: hash(key) % 4
→ 大量のデータ移動が発生
解決策1: 一貫性ハッシュ
前述の一貫性ハッシュを使えば、影響は最小限。
解決策2: ダブルライト
1. 新シャードを追加
2. 書き込みを新旧両方に行う(ダブルライト)
3. 旧シャードのデータを新シャードにコピー
4. 読み取りを新シャードに切り替え
5. 旧シャードへの書き込みを停止
6. 旧シャードを削除
class DoubleWriteShardManager:
def __init__(self):
self.old_shards = {...} # 旧シャード構成
self.new_shards = {...} # 新シャード構成
self.migration_mode = True
def write(self, shard_key, query, params):
if self.migration_mode:
# 両方に書き込み
old_shard = self._get_old_shard(shard_key)
new_shard = self._get_new_shard(shard_key)
old_shard.execute(query, params)
new_shard.execute(query, params)
else:
# 新シャードのみ
new_shard = self._get_new_shard(shard_key)
new_shard.execute(query, params)
def read(self, shard_key, query, params):
if self.migration_mode:
# 旧シャードから読み取り(移行完了まで)
old_shard = self._get_old_shard(shard_key)
return old_shard.query(query, params)
else:
# 新シャードから読み取り
new_shard = self._get_new_shard(shard_key)
return new_shard.query(query, params)
解決策3: Vitess / ProxySQL
シャーディングミドルウェアを使う。
# Vitess: シャード構成
keyspaces:
- name: commerce
sharded: true
vindexes:
- name: hash
type: hash
tables:
- name: users
column_vindexes:
- column: user_id
name: hash
シャーディングが必要なサインと判断基準
シャーディングを検討すべきサイン
| サイン | 説明 |
|---|---|
| 書き込みがボトルネック | レプリカを増やしても解決しない |
| テーブルサイズが巨大 | インデックスがメモリに収まらない |
| バックアップが長時間 | 数時間〜数日かかる |
| スキーマ変更が困難 | ALTER TABLE に何時間もかかる |
判断基準(目安)
データ量: 数百GB〜数TB
行数: 数億行以上
書き込み: 数万TPS以上
ただし、シャーディングは最後の手段。
まずは:
- クエリ最適化
- インデックス追加
- キャッシュ導入
- レプリケーション
これらで解決できないか検討する。
実務チェックリスト
設計時
- シャーディングが本当に必要か検討したか
- シャードキーは適切か
- クロスシャードクエリは最小限か
- トランザクションの要件は整理されているか
実装時
- シャードルーティングは実装されているか
- scatter-gatherが必要なクエリは特定されているか
- 監視・ログは各シャードで取れているか
運用時
- リシャーディングの手順は文書化されているか
- 各シャードの負荷は監視されているか
- ホットスポットは検知できるか
まとめ
シャーディングの本質は、データ分割による水平スケールだ。
いつ使うか
| 状況 | 対処 |
|---|---|
| 読み取りがボトルネック | レプリケーション |
| 書き込みがボトルネック | シャーディング |
| 両方 | レプリケーション + シャーディング |
戦略の選択
| 要件 | 戦略 |
|---|---|
| シンプルさ重視 | レンジ or ハッシュ |
| スケール重視 | 一貫性ハッシュ |
| 柔軟性重視 | ディレクトリ |
注意点
- シャードキーの選択が最重要
- クロスシャードクエリは避ける設計を
- トランザクションは複雑になる
- リシャーディングの計画を事前に
シャーディングは魔法ではない。複雑さとのトレードオフを理解して使おう。