LESSON 40分

ストーリー

高橋アーキテクト
アプリケーションサーバーは10台に増やせた。でもDBが1台のままだ。全てのサーバーが1台のDBに接続している。ここがボトルネックになっている

高橋アーキテクトが懸念を示した。

あなた
DBも水平スケーリングできるんですか?
高橋アーキテクト
できる。ただしアプリケーションよりはるかに難しい。Read Replicaとシャーディング、2つのアプローチを理解しよう

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分