DB 上級 — レプリケーション・シャーディング・キャッシュ・コネクションプール・パーティショニング
DB 上級 — レプリケーション・シャーディング・キャッシュ・コネクションプール・パーティショニング
🎯 このレッスンを読み終えたら
このレッスンをすべて読み終えると、以下の 3 つを自信を持って説明できるようになります。
- ▸✅ パーティショニング Range / Hash / List の選択基準
- ▸✅ シャーディング vs パーティショニングの違い
- ▸✅ DB バックアップ戦略(Logical vs Physical)の説明
学習目標をチェックリストとして手元に置き、すべて答えられるようになったらレッスンを閉じてください。
レプリケーション — *DB を複数台に増やす*
レプリケーションとは
ウェブサービスが成長すると、読み取りリクエストが爆増します。1 つの記事を1 万人が読むというパターンはよくあることです。DB 1 台ではさばけなくなったらどうするか?
レプリケーション = DB を複数台に増やして負荷を分散する方法。1 台を Primary(書き込み専用)とし、複数台の Replica(読み取り専用)が Primary の変更をコピーして受け取ります。
同期 vs 非同期レプリケーション
同期レプリケーション — Primary が Replica の応答を待ってからユーザーに成功応答を返す。データ損失ゼロだが、遅い。
非同期レプリケーション — Primary はすぐに応答し、Replica は後からコピーする。速いが、Replica がわずかに遅れる(lag)。Primary が突然ダウンすると、直近のデータが失われる可能性あり。
ほとんどのサービスは非同期を採用します。99% のケースで lag はミリ秒単位なので問題なく、パフォーマンスの恩恵が大きいためです。
Lag — 最も重要な監視指標
Replica がどれだけ遅れているかが核心です。lag が 1 秒になると、ユーザーが書いたばかりの投稿が表示されないという事故につながります。
lag が増え続けるなら → ネットワーク・ディスク・CPU のどこかがボトルネック。アラート設定は必須。
アプリケーション側 — 読み書き分離
ほとんどの ORM・DataSource ルーターがこれをサポートします。読み取り専用クエリは自動的に Replica へ、書き込みは Primary へ。
> ⚠️ 落とし穴: 書き込み直後の読み取りは、lag の影響で古いデータが返る可能性があります。そのような場合は、そのリクエストだけ Primary から読むといった例外処理が必要です。
まとめ
レプリケーションは読み取りスケールアウト+障害復旧の標準的な手法です。非同期がデフォルトであり、lag 監視が運用の核心。読み書き分離によって Primary の負荷を大幅に減らすことができます。
シャーディング — *データを分割する*
シャーディングとは
レプリケーションは読み取りを増強しますが、書き込みは 1 台のみです。書き込みも爆増したら? — Twitter のように1 秒に数十万ツイートが来れば、1 台では不可能です。
シャーディング = データを複数の DB に分散する方法。ユーザー ID が 1〜100 万は DB1、100 万〜200 万は DB2 — このような水平分割。
各シャードは独立した DBなので、書き込み負荷が N 等分されます。デメリットは複雑性の爆増です。
シャードキー — 最も重要な決断
どのカラムを基準に分割するかが運命を左右します。
良いシャードキー:
- ▸
user_id— ユーザーごとに均等分散、クロスシャードクエリが少ない - ▸
tenant_id(B2B SaaS)— 企業ごとの分離
悪いシャードキー:
- ▸
created_at— 最新データへのホットスポット。古いシャードが暇な一方、新しいシャードが爆発。 - ▸
auto_increment id— レンジシャーディング時に同様のホットスポット。
シャードキーは後から変更がほぼ不可能です。最初に慎重に決定する必要があります。
クロスシャードクエリ — 最大の落とし穴
両方のシャードをクエリする必要があります。JOIN も不可能。トランザクションも分散トランザクションになった瞬間、非常にコストが高くなります。
だからこそ、クロスシャード操作が少なくなるようデータモデルとシャードキーを設計することが核心です。
リシャーディング — 運用上の悪夢
データが増えてシャードを5 個から 10 個に増やす必要が生じたら?すべてのデータを再分配しなければなりません。その間サービスに影響を与えずに進めるのは非常に難しいです。
だから最初から十分な数のシャードでスタートするか(例: 64 個で始めてノードに分散)、consistent hashing などの手法で負担を軽減するのが一般的です。
マネージドオプション — 自分でやらないのが正解
ほとんどの企業はシャーディングを自前で実装しません。難しすぎて危険です。
- ▸DynamoDB / MongoDB Atlas — 自動シャーディング NoSQL
- ▸Vitess — MySQL シャーディング(YouTube が開発、CNCF 卒業プロジェクト)
- ▸Citus — PostgreSQL 拡張(Microsoft)
特別な理由がなければ、レプリケーションで十分です。シャーディングが本当に必要になるのは、数億行・秒間数万書き込みレベルです。
まとめ
シャーディングは最後の手段。レプリケーション → キャッシュ → パーティショニングをすべて試してもなお足りない場合にのみ。開始時のシャードキーの決定が運命を左右し、クロスシャードクエリの回避がパフォーマンスの鍵です。
キャッシュ + コネクションプール — *DB 負荷を減らす 2 つの方法*
キャッシュとは
DB クエリは遅い(ミリ秒)。メモリは速い(マイクロ秒)。同じクエリを繰り返し実行するなら、結果をメモリに保存して再利用すれば 1000 倍速くなります。これがキャッシュの基本的なアイデアです。
Redis が事実上の標準。メモリベースの key-value ストア。非常に速く、TTL(自動期限切れ)・pub/sub などの付加機能も備えています。
Cache-Aside パターン
最も一般的なパターン:
5 分間は DB を介さずに Redis からすぐにレスポンス。DB 負荷が劇的に減ります。
キャッシュ無効化 — コンピュータサイエンスの二大難問の一つ
> "There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton
データが変更されたとき、古いキャッシュをどう扱うか?
1. TTL — 一定時間後に自動期限切れ。最もシンプルだが、最大 N 秒間は古いデータになる可能性あり。
2. 明示的無効化 — データ変更時に redis.del('user:42') を呼ぶ。即時反映。ただし、すべての変更コードで忘れずに呼び出す必要あり。
3. イベントベース — DB 変更 → イベント発行 → キャッシュのサブスクライバーが無効化。分散システムで有利。
4. バージョンキー — user:v2:42 のようにキー自体を変える。古いキーは自然に廃棄。TTL と組み合わせるとすっきり。
> 💡 実際のところ: TTL + 明示的無効化の組み合わせが 90%。完全な一貫性はコストが高く難しい。短い stale period を許容するのが一般的。
コネクションプール — DB 接続の再利用
DB 接続を毎回新たに作成するのはコストがかかります。TCP ハンドシェイク・認証・TLS ネゴシエーションがリクエストのたびに発生し、接続だけで約 50ms かかります。
コネクションプール = N 個の接続を事前に作成しておき、再利用するプールです。リクエストが来ると、プールから借りて使い、終わったら返却します。新規接続のコストがなくなります。
プール設定の核心
最も重要なのはプールサイズです。小さすぎると待ち行列、大きすぎると DB に過負荷がかかります。
公式: app servers × pool size ≤ DB max_connections
例: app 4 台 × pool 20 = 80 ≤ Postgres max=100 ✅
HikariCP(Java 標準)などのライブラリがインテリジェントに管理してくれます。min = max 推奨(予測可能な動作)。
よくある落とし穴
1. 接続リーク — conn.close() を忘れるとプールが枯渇。try-with-resources / コンテキストマネージャーは必須。
2. プールが大きすぎる — DB の max_connections を超過。「too many connections」エラーで爆発。
3. アイドルタイムアウトが短すぎる — ファイアウォールや LB がアイドル接続を切断する環境では新規接続が頻発。max-lifetime で定期的に交換することを推奨(30 分程度)。
まとめ
キャッシュ + コネクションプールは DB 負荷を減らす 2 つの標準ツール。キャッシュは繰り返しクエリのコストを、コネクションプールは接続コストを削減します。両方を適切に設定すれば、同じ DB で10 倍のトラフィックを受けられます。
パーティショニング — *大きなテーブルを小さな断片に*
パーティショニングとは
テーブル 1 つが 100 GB を超えると様々な問題が生じます。インデックスも巨大化し、古いデータの削除が遅くなり、一部のデータしか使わないクエリでも全体をスキャンするようになります。
パーティショニング = 大きなテーブルを論理的に複数の断片に分割しながら、ユーザーには 1 つのテーブルに見えるようにする技術。シャーディングに似ていますが、同じ DB 内での分割という点が異なります。
RANGE パーティション — 時系列データに最適
最も一般的なパターン。日付範囲で分割します。
INSERT INTO logs を実行すると、created_at の値を見て自動的に適切なパーティションに格納されます。
真の恩恵 — パーティションプルーニング
このクエリは logs_2025_03 パーティションだけをスキャンします。他のパーティションはスキップされます。100 GB 中の 1 GB だけ見ればよいので 100 倍速くなります。
DB が自動的にどのパーティションを見るべきかを推論します。これがプルーニング(枝刈り)です。
古いデータの削除が速い
3 年以上前のログを削除する場合を考えてみましょう。
パーティション単位の DROP はディスクから丸ごと取り外すような操作なので非常に速いです。ログ・イベント処理では圧倒的に優れています。
自動化 — pg_partman
毎月新しいパーティションを手動で作成するのは運用負担です。pg_partman(Postgres 拡張)が自動的に処理してくれます。
TimescaleDB は PostgreSQL 上の時系列特化拡張。パーティショニングを自動的に管理し、圧縮まで対応します。
LIST・HASH パーティション
LIST — 地域・カテゴリのような明確な分類:
HASH — 均等分散(Postgres 11+):
パーティショニングはいつ使う?
- ▸✅ 単一テーブルが 100 GB 以上
- ▸✅ 時間ベースのデータ(ログ・メトリクス・イベント)
- ▸✅ 古いデータの一括削除が必要
- ▸❌ 小さなテーブル — オーバーヘッドのみ増加
まとめ
パーティショニングは大きなテーブルのパフォーマンスと運用負担を劇的に軽減します。時系列データにはデフォルトの選択肢。pg_partman・TimescaleDB で自動化すれば運用も楽になります。シャーディングよりはるかに簡単で安全な選択肢なので、まず検討する価値があります。
💾 実践デモ — キャッシュヒット vs ミスのパフォーマンス差
-- 計測結果(本番環境の平均)
-- DB 直接クエリ(インデックス使用)
SELECT * FROM users WHERE id = 42;
-- 平均: 8 ms
-- Redis キャッシュ hit
-- GET user:42
-- 平均: 0.3 ms
-- 1000 回繰り返し:
-- DB のみ: 8 ms × 1000 = 8,000 ms(8秒)
-- Cache+DB: 初回 8ms + 999回 × 0.3ms = 308 ms(0.3秒)
-- → 26 倍高速
🤖 AI にこう依頼してみてください
このレッスンの概念を理解すれば、AI に具体的に指示できます。漠然とした「直して」ではなく、語彙を持ったリクエスト — それがトークン節約の出発点です。
- ▸「この logs テーブルを created_at の YEAR Range パーティションに変換して」
- ▸「この SELECT クエリを必要なカラムだけ選択してカバリングインデックスが効くようにして」*
- ▸「シャーディング vs パーティショニングの違い+うちのケースにはどちらが適切か診断して」
なぜこれがトークンを節約するのか
概念を知らないと、AI の回答を受け取っても「それって何ですか?」と再度聞き返す必要があります。その「聞き返し」がトークンを消費します。概念を一度理解しておけば、会話が一度で完結します。