データベースのレプリケーションとは、複製して分けて置くと何を諦めるか

回 5-4 道 5 データベースと分散

レプリケーションは、データベースの主 (プライマリ) に書いた変更を、別の台の複製 (レプリカ) へ写し続けるしくみです。非同期の複製は少し遅れて届くので、複製から読むと古い値が返ることがあり、主が落ちて複製を主に上げると、まだ届いていなかった書き込みが消えます。同期にすれば消えませんが、書くたびに複製の返事を待つので、遅くなり、複製が止まると書けなくなります。

複製へ遅れて届く様子を動かし、分けて置くと「新しさ」「速さ」「止まらなさ」のどれを諦めるのかを切り分けられるようになります。

読みが増えて 1 台では足りないとき、どうしますか

同じデータを別の台に写し、読みをそちらに分けます。この「写し続ける」しくみがレプリケーション (英語で複製の意味) です。

ネットのお店で、商品の頁を見る人は、注文する人よりずっと多いのがふつうです。データベースが 1 台だと、読みも書きも全部その 1 台に集まり、いつか追いつかなくなります。しかもその 1 台が壊れると、お店ごと止まります。

そこで、書くのは主 (プライマリ) の 1 台に決め、主の変更を別の台の複製 (レプリカ) へ写します。読みは複製にも分けられ、主が壊れたら複製を新しい主に上げて続けられます。

ただし、写すのには時間がかかります。この回では、その少しの遅れが「古い値が返る」「書いたはずの注文が消える」につながる様子を Python で動かし、分けて置くと何を諦めるのかを確かめます。

書くのは原本だけ、読むのは写しに分け、書いた直後は写しがまだ古い様子を表した図

レプリケーションはどう動くのか

レプリケーションは、主が受けた変更を記録 (ログ) にして複製へ送り、複製が同じ順で当てはめることで、同じ中身を保ちます。違いが出るのは「複製に届いたことを確かめてから、書けたと返すかどうか」です。

主と複製

主と複製の役目は次の通りです。

台書く読む主が落ちたとき
主 (プライマリ)受ける受ける止まる
複製 (レプリカ)受けない受ける (少し古いことがある)新しい主に上げる

複製の使い道は、読みを分けること、主が落ちたときの控えにすること、離れた場所に置いて災害に備えることの 3 つが代表です。

PostgreSQL の公式文書は、主の変更を複製へ流し続けるやり方 (ストリーミングレプリケーション) は既定で非同期で、主で確定してから複製で見えるまでに小さな遅れがあり、複製の台が負荷に追いつけるなら、ふつうは 1 秒未満だと書いています。

同期と非同期の損得

非同期は、主に書けた時点で「書けた」と返し、複製へはあとから送ります。同期は、複製にも届いたと返事が来るまで「書けた」と返しません。

方式書けたと返すまで主が落ちたとき複製が止まったとき
非同期主だけで済み、速い届いていない書き込みが消える主はそのまま書ける
同期複製の返事を待ち、遅い消えない (主と複製が同時に落ちない限り)返事が来ず、書けたと返らない

PostgreSQL の公式文書は、非同期では主が落ちると確定済みの書き込みが複製に届いておらず消えることがあり、消える量は切り替えのときの遅れの大きさに比例すると書いています。同期なら、消えるのは主と複製が同時に落ちたときだけです。一方で、同期の複製のどれか 1 台が落ちると、書き込みの確定がいつまでも終わらないことがあるとも書いています。

速さと消えなさの間に、1 台にだけ届けば返す「準同期」という中間もあります。下の図で、遅い 1 台の遅れを動かして、書けたと返る時間と失う件数を比べてください。

COMMIT がいつ返り、何件を失うか

スケールアップとスケールアウト

1 台で足りなくなったときの手は、次の 2 つです。

手やること限り
スケールアップ1 台を強い台に替える買える台の上限で止まり、1 台が落ちれば全部止まる
スケールアウト台の数を増やして分ける台どうしで中身をそろえる手間が生まれる

レプリケーションは、読みのスケールアウトです。複製を足せば読める量は増えますが、書くのは主の 1 台のままなので、書き込みは増やせません。書き込みまで分けるには、次のシャーディングが要ります。

シャーディング、鍵で分けて置く

シャーディングは、データを鍵で分けて、台ごとに違う部分を持たせるやり方です。分けた 1 つ 1 つをシャードと呼びます。たとえば利用者の番号を 3 で割った余りで、0 なら 1 台目、1 なら 2 台目、2 なら 3 台目に置きます。

複製が「同じ中身を何台にも持つ」のに対して、シャーディングは「違う中身を分けて持つ」点が違います。書き込みも台ごとに分かれるので、書きのスケールアウトになります。

ただし、鍵の選び方で諦める物が変わります。利用者ごとに分けたなら、1 人の注文を引くのは 1 台で済みますが、「先月の全部の注文を数える」は全部の台に聞いて足し合わせることになります。多くの台では、複製とシャーディングを組み合わせ、シャードごとに主と複製を置きます。

動かして確かめる

複製の遅れを、刻み (時計の 1 目盛り) で動かします。主の在庫は 6 個で、刻み 0、1、2 に注文が入って 5、4、3 個に減ります。主の変更は 2 刻み遅れて複製に届きます。出力は左から、刻み、主の在庫、複製の在庫で、2 つが違う刻みに「古い」と出します。

動かす 複製へ遅れて届く在庫

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

刻み 主 複製
0 5 6 古い
1 4 6 古い
2 3 5 古い
3 3 4 古い
4 3 3
5 3 3

刻み 0 から 3 までの 4 刻みは、主と複製の在庫が食い違いました。刻み 2 では主が 3 個なのに、複製から読むと 5 個です。最後の注文が刻み 2 なので、2 刻み遅れの刻み 4 で、やっと主と複製がそろいました。

これが複製の遅れ (レプリケーションラグ) です。在庫を複製から読む画面は、売り切れたあとも「残りあり」と見せることがあります。書いた本人が直後に自分の変更を読み返す画面 (プロフィールを直して保存した直後など) は、主から読むのが安全です。

書くのをやめれば、遅れていた複製もいずれ追いつきます。このように「いずれはそろう」ことを結果整合性と呼びます。

分けて置くと何を諦めるのか

主が落ちたときと、網が切れたときに、分けて置いた前提が崩れます。非同期なら書き込みが消え、同期なら書けなくなり、網が切れると新しさと応答のどちらかを捨てることになります。

主が落ちると、届いていない書き込みが消える

主が落ちたとき、複製を新しい主に上げて動き続けることを切り替え (フェイルオーバー) と呼びます。次のコードは、刻み 0 から 4 までに注文 1 から 5 を 1 件ずつ受け、刻み 4 で主が落ちた所で、複製を主に上げます。遅れは 2 刻みです。出力は左から、やり方、客を待たせた刻みの合計、消えた注文の番号です。

動かす 主が落ちたときに消える注文

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

非同期 待ち 0 消えた [4, 5]
同期 待ち 10 消えた []

非同期では、客は 1 刻みも待たずに「注文できました」と言われました。ところが、刻み 3 と 4 の注文 4 と 5 は、主が落ちた時点で複製に届いておらず、新しい主には残っていません。客には書けたと返したのに、消えた注文です。

同期では、どの注文も消えませんでした。その代わり、1 件ごとに 2 刻み、合わせて 10 刻み客を待たせました。複製が遅いほど、遠いほど、この待ちは増えます。

消えた注文は、落ちた古い主の中にはまだ残っています。古い主を直して戻すと、古い主にしか無い書き込みと、新しい主にしか無い書き込みが両方できてしまい、どちらを正とするかを人が決めることになります。下の GitHub の事例が、まさにこれです。

網が切れると、一貫性か応答のどちらかを諦める

2 台を結ぶ網が切れたとき (ネットワークの分断)、片方に書いた値はもう片方に届きません。このとき、もう片方は次のどちらかを選ぶしかありません。

  1. 最新か分からないので、答えるのを断る。一貫性 (Consistency) を守り、応答 (可用性、Availability) を諦める。CP と呼ぶ
  2. 手元の古い値で答える。応答を守り、一貫性を諦める。AP と呼ぶ

網の分断 (Partition) が起きたら、一貫性と可用性の 2 つを同時には守れない、という主張が CAP 定理です。

次のコードは、台 A と台 B がどちらも 100 を持つ所から、A に 80 を書き、B から読みます。出力は左から、網、方針、書いた結果、B から読んだ値です。

動かす 網が切れたときの CP と AP

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

つながる CP 書けた 80
つながる AP 書けた 80
切れた CP 断る 断る
切れた AP 書けた 100

網がつながっている間は、CP も AP も 80 を返し、差は出ません。網が切れると、CP は書くのも読むのも断り、AP は書けたと返したうえで、B からは古い 100 を返しました。

選ばされるのは網が切れたときだけですが、網はいつか必ず切れる前提で設計します。だから「3 つ全部を守るデータベース」はなく、分断のときにどちらを捨てるかを先に決めておきます。図で、回線を切ってから CP と AP を切り替えてください。

網が切れた瞬間に選ばされる

仕事では、どう確かめるのか

仕事では、遅れの大きさを測り、失ってよい量を決め、切り替えを試しておきます。

  1. 遅れを見張る。PostgreSQL では、主の pg_stat_replication という一覧で、複製ごとに送った位置を見られ、主の今の位置との差が遅れの目安になる。差が広がり続けたら知らせる
  2. 失ってよい量を決める。障害で失ってよい時間の幅を RPO (目標復旧時点) と呼ぶ。お金の記録のように 1 件も失えない物は同期、失ってもやり直せる物は非同期と、データごとに分けられる
  3. 書いた直後の読み返しは主から読む。複製から読む画面には、少し古くてよいかを決めて書いておく
  4. 切り替えを本番の前に試す。複製を主に上げる手順、古い主を戻さない決まり、アプリの接続先の替え方を、止めてよい環境で通しておく
  5. 複製をバックアップの代わりにしない。間違って消した操作も複製へ写るので、時点を戻せる控えを別に取り、戻せるかを試す

関わる実例

GitHub、43 秒の網の切れ目から 24 時間 11 分の障害 (2018年10月21日)

2018年10月21日 22:52 UTC、GitHub は米国東海岸の網の拠点と主のデータセンターをつなぐ機器の交換作業で、43 秒のあいだ通信が切れました。事後報告によると、GitHub は書き込みを MySQL の主に送り、読みのほとんどを複製に回していました。

網が切れている間に、主の切り替えを自動で行う Orchestrator が、西海岸のデータセンターへ書き込み先を切り替えました。東海岸の主には、西海岸へ写される前の短い時間の書き込みが残っていました。両方のデータセンターに、相手に無い書き込みができたため、東海岸へ安全に戻せなくなりました。

GitHub は、利用者のデータの一貫性を守ることを選び、控えから戻して複製を追いつかせるまで、サービスの質を落とした状態が 24 時間 11 分続きました。特に忙しい束の 1 つでは、西海岸へ写されなかった書き込みが 954 件あったと書いています。再発の対策の 1 つ目は、地域をまたいで主を上げない設定にすることでした。

崩れた前提は「切り替えた先に、書き込みが全部届いている」です。非同期の複製の遅れと、自動の切り替えが重なると、上のコードの「消えた」の行が本番で起きます。

出典は October 21 post-incident analysis (GitHub Blog) です。

GitLab、複製の作り直しの途中で主の中身を消した (2017年1月31日)

2017年1月31日、GitLab.com のデータベースは主 1 台と、切り替え用の複製 1 台で動いていました。事後報告によると、23:00 UTC ごろ、負荷の高まりで複製の写しが遅れ、複製が要る記録 (WAL) を主が先に消してしまったため、複製は止まりました。

複製を作り直すには、複製の中身を空にしてから写し直します。その作業の途中、技術者は複製のつもりで主のデータの置き場を消し、気づいて止めるまでに約 300 GB が消えました。複製はすでに空にしてあったので、どちらの台からも戻せませんでした。

GitLab は、約 6 時間前に手で取ってあった控えから戻し、サービスは約 18 時間止まりました。1 月 31 日 17:20 UTC より後のデータベースの変更は失われ、少なくとも 5000 のプロジェクト、5000 のコメント、約 700 の利用者の登録が消えたと見積もっています。

崩れた前提は「複製があるから大丈夫」です。複製は主の今の中身を写すだけで、消す操作も写し、複製自体も止まります。時点を戻せる控えは別に要ります。

出典は Postmortem of database outage of January 31 (GitLab) です。

よくある問い

レプリケーションとバックアップは何が違うのですか

レプリケーションは、今の中身を別の台に写し続けて、すぐ切り替えられるようにするしくみです。バックアップは、ある時点の中身を取っておき、あとでその時点に戻せるようにするしくみです。間違って消した操作も複製には写るので、複製だけではその前に戻せません。GitLab の事例のように、両方を持ち、戻せるかを試しておきます。

レプリケーションと同期は同じ意味ですか

同じではありません。レプリケーションは「複製を作り、写し続ける」ことで、同期はそのやり方の 1 つです。複製の返事を待つのが同期、待たずにあとから送るのが非同期です。PostgreSQL は、非同期を既定にしています。

スケールアップとスケールアウトは、どちらを先に考えますか

台を強くするスケールアップは、アプリを変えずに済むので先に試しやすい手です。ただし上限があり、1 台が落ちれば止まります。読みが多いなら複製を足して読みを分け、書きまで足りなくなったらシャーディングを考える、という順で検討すると、諦める物を少しずつ増やせます。

シャーディングとパーティショニングは何が違うのですか

どちらも大きな表を鍵で分けることです。分けた部分を 1 台の中に置くのをパーティショニング、別の台に分けて置くのをシャーディングと呼び分けることが多いです。別の台に分けると、全部の台にまたがる集計や結び付けが重くなります。

CAP 定理で、3 つ全部を取ることはできないのですか

網が切れたときに、一貫性と可用性の両方を守ることはできません。網が切れない間は、どちらも守れます。網の分断は起きる前提で考えるので、実際の選択は「分断のときに CP と AP のどちらを取るか」です。

確認クイズ

答えだと思う選択肢を押すと、その場で正誤と理由が開きます。4 問すべてに正しく答えると、この回は修了で、石が緑に変わります (この端末のブラウザに残ります)。間違えた問いは、あとから解き直せます。

1 問目

非同期のレプリケーションで、主に書いた直後に複製から読むと何が起きることがありますか。

2 問目

このコードを動かすと何が出ますか。

3 刻み遅れて届く値

LAG = 3
log = [(0 + LAG, 10)]
replica = 7
for t in range(3):
    for a, v in log:
        if a <= t:
            replica = v
print(replica)

3 問目

同期のレプリケーションに替えると、非同期と比べて何を諦めることになりますか。

4 問目

2018年10月21日の GitHub の障害で、東海岸へすぐ戻せなかった直接の理由はどれですか。

説明課題

データベースを複製して分けて置くと何を諦めるのかを、「複製の遅れ」「切り替え」「同期」「網の分断」という言葉を使って、3〜4 文で説明してください。書いたら、下の模範解答を開いて比べてください。

模範解答

自分の説明を書いてから開いて、比べてください。

この回の先の一歩

読みを複製に逃がす、台を足して分ける、といった判断は、利用者が増えたサービスの設計とふだんの障害対応でよく話題になり、何を諦めたかを言えると原因の切り分けが速くなります。

もっと深く

この回の続きを、ゆめさくのコースの章や道具で深められます。頁の中の確認クイズに答えたあと、手を動かす量を増やしたいときに使ってください。

一次資料

どれも 2026年10月27日に開いて、題と中身を確かめました。

  • October 21 post-incident analysis (GitHub Blog、英語)。2018年10月30日の公開、22:52 UTC に 43 秒通信が切れたこと、24 時間 11 分の障害、書き込みは主へ読みは複製へ送っていたこと、Orchestrator が西海岸へ切り替えたこと、写される前の書き込みが東海岸に残ったこと、特に忙しい束の 1 つで 954 件、地域をまたいで主を上げない対策
  • Postmortem of database outage of January 31 (GitLab、英語)。2017年2月10日の公開、主 1 台と複製 1 台の構成、23:00 UTC ごろ複製が遅れて止まったこと、主の置き場を消して約 300 GB を失ったこと、約 6 時間前の控えから戻したこと、約 18 時間の停止、17:20 UTC より後の変更と失った件数の見積もり
  • Log-Shipping Standby Servers (PostgreSQL 公式文書、英語)。ストリーミングレプリケーションが既定で非同期なこと、遅れはふつう 1 秒未満なこと、非同期で消える量は遅れに比例すること、同期で消えるのは主と複製が同時に落ちたときだけなこと、同期の複製が落ちると確定が終わらないことがあること、pg_stat_replication で遅れを見られること

次へ

道 5 データベースと分散の回

  1. 5-1正規化重なりを無くす頁で読む
  2. 5-2インデックスと B-tree索引が速い理由頁で読む
  3. 5-3トランザクションACID、分離の度合い頁で読む
  4. 5-4分けて置くスケーリング、複製、CAPこの回

道 5 の頁へ戻るほかの道を見る