Day28 前半のゴール
「“DBは1台だけ”という前提を捨てて、“コピーして読ませる”をイメージする」
今日のテーマはレプリケーションです。 一言でいうと「あるDBサーバのデータを、別のDBサーバにリアルタイム(またはほぼリアルタイム)でコピーし続ける仕組み」です。
前半のゴールはこうです。 なぜレプリケーションが必要になるのかを、負荷と障害の観点から説明できる。 PostgreSQLの「プライマリ(マスター)」「スタンバイ(レプリカ)」の役割をイメージできる。 読み取り分散(リードレプリカ)という使い方の基本イメージを持てる。
まずは、「そもそも1台じゃダメになる瞬間っていつ?」からいきます。
なぜレプリケーションが必要になるのか
「“1台で全部やる”と、どこで詰むのか」
最初のうちは、DBサーバ1台で十分です。 アプリも少ない、ユーザーも少ない、トラフィックも少ない。 でも、サービスが育ってくると、次の2つの問題が見えてきます。
一つ目は「負荷」の問題です。 読み取り(SELECT)が増え続けると、1台のDBサーバがさばききれなくなります。 CPUが張り付き、ディスクI/Oが詰まり、レスポンスが遅くなり、最悪落ちます。
二つ目は「障害」の問題です。 DBサーバが1台しかないと、その1台が落ちた瞬間にサービス全体が止まります。 ハード故障、OSトラブル、設定ミス、アップデート失敗…。 どれか1つで「全部止まる」状態は、本番運用としてはかなり危険です。
レプリケーションは、この2つに対する答えの一つです。 データを別サーバにコピーしておくことで、
読み取りの負荷を複数台に分散できる。 片方が落ちても、もう片方を使ってサービスを継続できる(設計次第)。
という状態を作れます。
ここでの大事な気づきは、「レプリケーションは“性能のため”と“生存のため”の両方に効く」ということです。
プライマリとスタンバイの役割
「“書く人”と“読む人”を分けるイメージ」
PostgreSQLのレプリケーションの基本形は、「プライマリ(primary)とスタンバイ(standby)」です。 昔の言い方だと「マスター/スレーブ」ですが、今は primary / standby という用語が主流です。
プライマリ 書き込み(INSERT / UPDATE / DELETE)を受け付ける“本体”のDBサーバ。 アプリからの更新系クエリは、基本的にここに飛ぶ。
スタンバイ プライマリのデータをコピーしている“追従側”のDBサーバ。 通常は読み取り専用(SELECTのみ)として使う。
イメージとしては、「プライマリが日記を書いていて、その内容をスタンバイが後ろからずっと写経している」感じです。 プライマリで起きた変更は、WAL(後で説明するログ)を通じてスタンバイに流れ、スタンバイはそれを順番に適用していきます。
重要なのは、「スタンバイは基本的に“書き込み禁止”」ということです。 スタンバイに直接INSERTしたりUPDATEしたりはできません(やろうとするとエラーになります)。 あくまで「プライマリのコピー」であり、「読み取り専用の鏡」です。
PostgreSQLのレプリケーションのざっくり構造
「WALという“変更ログ”を流して、向こうで再生する」
中身の細かい設定は後半や実務で覚えればよくて、前半では「どういう仕組みでコピーしているか」のイメージだけ掴めれば十分です。
PostgreSQLは、データの変更を「WAL(Write-Ahead Log)」というログに必ず書き出します。 これは、「どのページのどの部分をどう変えたか」という変更履歴です。
プライマリでは、 アプリからのINSERT / UPDATE / DELETEを受ける。 その変更内容をWALに書く。 WALをディスクにフラッシュしてから、実際のデータファイルに反映する。
スタンバイでは、 プライマリからWALを受け取る。 受け取ったWALを順番に適用して、自分のデータファイルを更新する。
という流れで、「プライマリと同じ状態」を追いかけ続けます。
ここでのキモは、「スタンバイは“SQLを再実行している”わけではない」ということです。 INSERT文をもう一度実行しているのではなく、「ページのこの部分をこう書き換えろ」という低レベルな指示(WAL)を適用しています。 だからこそ、プライマリとほぼ同じ状態を効率よく再現できます。
例題:読み取り分散のシンプルな構成イメージ
「書き込みは1台、読み取りは2台でさばく」
具体的な構成を、図を頭に描くつもりで言葉にしてみます。
サーバA:PostgreSQL(プライマリ) サーバB:PostgreSQL(スタンバイ1) サーバC:PostgreSQL(スタンバイ2)
アプリケーションはこう接続します。
書き込み系(ユーザー登録、注文作成、更新など)は、必ずサーバA(プライマリ)に送る。 読み取り系(一覧表示、検索、レポートなど)は、サーバBとサーバCに振り分ける。
これにより、
プライマリは「書き込み+一部の読み取り」だけを担当する。 スタンバイは「読み取り専用」としてフルにCPUとI/Oを使える。
という状態になります。
例えば、1秒間に1000件のSELECTが飛んでくるサービスで、 1台ではギリギリだったところを、スタンバイ2台に分散することで「1台あたり約500件」にできる、というイメージです。
ここでの重要ポイントは、「アプリ側が“どのクエリをどのサーバに投げるか”を意識して設計する必要がある」ということです。 DBが勝手に「これは読み取りだからスタンバイへ」と振り分けてくれるわけではありません。
レプリケーションの“遅延”という現実
「“ほぼリアルタイム”は“完全リアルタイム”ではない」
レプリケーションは便利ですが、「プライマリとスタンバイの状態は、常に完全に同じ」とは限りません。 WALを送る・受け取る・適用する、というプロセスには、どうしてもわずかな遅延が発生します。
例えば、こんなケースを考えます。
ユーザーが新規登録をする。 そのINSERTはプライマリに書き込まれる。 すぐに「自分のプロフィールを表示する」画面に遷移する。 その画面のSELECTを、スタンバイに投げている。
このとき、「スタンバイへのレプリケーションがまだ追いついていない」と、 「さっき登録したはずのユーザーが見つからない」という現象が起きます。
これが「レプリケーション遅延」の典型的な問題です。
対策としては、
「直後に読む必要があるデータ」は、必ずプライマリから読む。 「少し古くてもいい読み取り」(ランキング、一覧、分析など)はスタンバイに任せる。
というように、「どのデータはどれくらいの鮮度が必要か」をアプリ側で設計する必要があります。
ここでの大事な気づきは、「読み取り分散は、“全部のSELECTをレプリカに投げればOK”ではない」ということです。 「一貫性」と「スケール」をどうバランスさせるかが、設計の肝になります。
レプリケーションと障害対応の関係
「“読めるだけ”から、“代わりに書けるようにする”まで」
Day28 前半では主に「読み取り分散」の話をしていますが、 レプリケーションは「障害時のフェイルオーバー」にも使われます。
ざっくり言うと、
平常時 プライマリが書き込みを受け、スタンバイがそれを追いかける。
障害時(プライマリが落ちた) スタンバイの1台を「新しいプライマリ」に昇格させる。 アプリの接続先を、その新しいプライマリに切り替える。
という流れです。
ただし、これは「自動で勝手に全部やってくれる」わけではなく、 専用ツールや仕組み(Patroni, repmgr, pgpool-II など)と組み合わせて設計する世界になります。 Day28 前半では、「レプリケーションがあるからこそ、“プライマリが死んでも復活しやすい”構成が取れる」というイメージだけ持っておけば十分です。
Day28 前半のまとめ
レプリケーションは「プライマリDBの変更をWALという変更ログとしてスタンバイDBに送り、スタンバイ側で順番に適用することで“ほぼ同じ状態のコピー”を維持する仕組み」であり、プライマリは書き込みを受け、スタンバイは基本的に読み取り専用として使うことで、「読み取り負荷を複数台に分散する」「プライマリ障害時にスタンバイを昇格させてサービス継続する」といった構成が取れる。 一方で、レプリケーションには必ずわずかな遅延があるため、「直後に読みたいデータはプライマリから読む」「少し古くてもよい集計・一覧はスタンバイに投げる」といった“鮮度に応じた読み先の設計”が必要であり、「全部のSELECTをレプリカに投げればOK」ではない――ここまでの構造と感覚が掴めていれば、Day28 前半としてはとても良いスタートラインに立てている。
