ストーリー
高橋アーキテクトが懸念を示した。
Read Replica(読み取りレプリカ)
書き込みはPrimary(マスター)に、読み取りはReplica(スレーブ)に分散します。
// Read Replica パターンの実装
class DatabaseRouter {
constructor(
private primary: DatabaseConnection, // 書き込み用
private replicas: DatabaseConnection[], // 読み取り用(複数)
) {}
// 書き込みクエリ → Primary
async write(query: string, params: any[]): Promise<any> {
return this.primary.execute(query, params);
}
// 読み取りクエリ → Replica(ラウンドロビン)
private replicaIndex = 0;
async read(query: string, params: any[]): Promise<any> {
const replica = this.replicas[this.replicaIndex % this.replicas.length];
this.replicaIndex++;
return replica.execute(query, params);
}
// 書き込み直後の読み取り → Primary(レプリケーション遅延を回避)
async readAfterWrite(query: string, params: any[]): Promise<any> {
return this.primary.execute(query, params);
}
}
// 使用例
class ProductRepository {
constructor(private db: DatabaseRouter) {}
async findById(id: string): Promise<Product> {
// 読み取り → Replica
return this.db.read('SELECT * FROM products WHERE id = $1', [id]);
}
async update(id: string, data: ProductUpdate): Promise<Product> {
// 書き込み → Primary
await this.db.write(
'UPDATE products SET name = $1, price = $2 WHERE id = $3',
[data.name, data.price, id]
);
// 書き込み直後の読み取り → Primary(遅延回避)
return this.db.readAfterWrite('SELECT * FROM products WHERE id = $1', [id]);
}
}
レプリケーション遅延
// Primary → Replica への反映には遅延がある
const replicationLag = {
synchronous: {
lag: '0ms',
description: '同期レプリケーション。遅延なしだが書き込み性能が低下',
useCase: '金融系など強い一貫性が必要',
},
asynchronous: {
lag: '10-1000ms',
description: '非同期レプリケーション。遅延ありだが高速',
useCase: '大半のWebアプリケーション',
},
};
| メリット | デメリット |
|---|---|
| 読み取り性能が台数分向上 | レプリケーション遅延がある |
| 実装が比較的シンプル | 書き込み性能は向上しない |
| 可用性の向上(Primary障害時にフェイルオーバー) | Primaryが単一障害点 |
シャーディング(Sharding)
データを分割して複数のデータベースに分散します。書き込みも分散できます。
// シャーディングの実装例
class ShardRouter {
constructor(private shards: DatabaseConnection[]) {}
// シャードキーに基づいてルーティング
getShardIndex(shardKey: string): number {
const hash = this.hashFunction(shardKey);
return hash % this.shards.length;
}
async query(shardKey: string, sql: string, params: any[]): Promise<any> {
const shardIndex = this.getShardIndex(shardKey);
return this.shards[shardIndex].execute(sql, params);
}
private hashFunction(key: string): number {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = ((hash << 5) - hash) + key.charCodeAt(i);
hash = hash & hash; // 32bit整数に変換
}
return Math.abs(hash);
}
}
// シャーディング戦略
const shardingStrategies = {
// 範囲ベース: ユーザーID 1-1000 → Shard 1, 1001-2000 → Shard 2
range: {
pros: '範囲クエリが効率的',
cons: 'ホットスポットが発生しやすい',
},
// ハッシュベース: hash(userId) % shardCount
hash: {
pros: 'データが均等に分散',
cons: 'リシャーディング(シャード数変更)が困難',
},
// ディレクトリベース: ルックアップテーブルで管理
directory: {
pros: '柔軟なルーティング',
cons: 'ルックアップテーブルが単一障害点',
},
};
シャーディングの課題
// 課題1: クロスシャードクエリ
// ユーザーテーブルがシャードされている場合、全ユーザー検索はどうする?
async function searchAllUsers(query: string): Promise<User[]> {
// 全シャードに並行クエリ → 結果をマージ(遅い)
const results = await Promise.all(
shards.map(shard => shard.query('SELECT * FROM users WHERE name LIKE $1', [`%${query}%`]))
);
return results.flat().sort((a, b) => a.name.localeCompare(b.name));
}
// 課題2: JOIN
// 異なるシャードにあるデータのJOINは不可能
// → アプリケーションレベルでのJOIN(N+1問題のリスク)
// 課題3: トランザクション
// 複数シャードにまたがるトランザクションは分散トランザクションが必要
| メリット | デメリット |
|---|---|
| 読み書き両方がスケール | 実装が非常に複雑 |
| データ量の制約がなくなる | クロスシャードクエリが遅い |
| 各シャードのサイズを小さく保てる | リシャーディングが困難 |
選択ガイド
DBがボトルネック
│
├─ 読み取りが中心?(読み書き比率 > 8:2)
│ └─ Yes → Read Replica が第一候補
│
├─ 書き込みも多い?
│ └─ Yes → シャーディングを検討
│
├─ データ量が1台のDBに収まらない?
│ └─ Yes → シャーディングが必須
│
└─ まずは以下を先に試す
1. クエリ最適化(インデックス追加)
2. キャッシュ導入
3. コネクションプール最適化
まとめ
| ポイント | 内容 |
|---|---|
| Read Replica | 読み取りを複数レプリカに分散。実装が比較的容易 |
| シャーディング | データを分割して分散。読み書き両方がスケール |
| レプリケーション遅延 | 同期(遅延なし、性能低下)vs 非同期(遅延あり、高速) |
| シャーディングの課題 | クロスシャードクエリ、JOIN、トランザクション |
| 優先順位 | クエリ最適化 → キャッシュ → Read Replica → シャーディング |
チェックリスト
- Read Replicaの仕組みとレプリケーション遅延を理解した
- シャーディングの戦略(範囲、ハッシュ、ディレクトリ)を把握した
- シャーディングの課題を3つ挙げられる
- DBスケーリングの優先順位を理解した
次のステップへ
次は「非同期処理とキューイング」を学びます。すぐに応答を返さなくてよい処理を非同期化して、スケーラビリティを高める方法です。
推定読了時間: 40分