レプリケーション
Replication ・ れぷりけーしょん
同じデータを複数のノードに複製し、故障に耐え、読み取りを分散する仕組み。複製の遅れとの付き合い方が要。
✦ 30秒で試す — 航海同じデータを3台に持たせても、失うときは失う開き直した画面に出るプロフィールは?予想してみる概要
レプリケーション(複製)は、同じデータを複数のノードにコピーして持たせ、故障に耐え(可用性)、読み取りを分散する(性能)ための仕組みです。データが1台にしかなければ、その1台が壊れた瞬間にデータもサービスも失われます。同じ内容を複数台に複製しておけば、1台が倒れても別の複製で処理を続けられます。データベースで最初に出会う冗長化の基本形です。
もっとも典型的なのが「プライマリ/レプリカ」構成です。書き込みを一手に引き受けるノード(プライマリ、リーダーとも)が1台あり、その内容を複製したノード(レプリカ、フォロワーとも)が複数ぶら下がります。読み取りを多数のレプリカに振り分ければ RDBMS の読み取り負荷を横に分散でき、プライマリが故障したらレプリカの1台を新しいプライマリに昇格させて処理を引き継げます。
よく対で語られるシャーディングとは、置き方が逆です。シャーディングはデータを分割し、各ノードに異なる一部を持たせて容量と書き込み性能を稼ぎます。レプリケーションは各ノードに同じ全体を持たせ、あえて重複させて可用性と読み取り性能を稼ぎます。大規模なシステムでは、分割した各シャードをさらに複製するのが普通です。
なぜ生まれたか
データベースが1台しかない構成には、二つの根本的な弱点があります。一つは可用性です。その1台がハードウェアの故障やメンテナンスで止まれば、サービス全体が止まります。ディスクごと壊れればデータそのものが失われ、バックアップから戻すまでの間の更新は消えます。もう一つは読み取り性能です。多くのサービスは書き込みより読み取りのほうがはるかに多く、利用者が増えると1台では読み取りをさばき切れなくなります。
レプリケーションは、この二つを「データを複製して持つ」という一つの手で同時に解きました。複製があれば1台を失っても代わりが利き、読み取りを複数の複製に分担させられます。別のデータセンターへ複製しておけば、拠点が丸ごと失われても事業を続けられる(ディザスタリカバリ)という価値も生まれました。ただし、複製が増えた瞬間に「複製どうしの中身がずれる」という新しい問題を抱え込みます。ノードやネットワークは部分的に壊れる、という前提で系を組む分散システムの難しさを、データストアに持ち込んだ最初の一歩がレプリケーションでもあります。
詳細
プライマリ/レプリカの基本構成
最も普及した形が、1台のプライマリと複数のレプリカからなる構成(シングルリーダー)です。アプリケーションからの書き込み(INSERT や UPDATE)はすべてプライマリに集まります。プライマリは自分に加えた変更を順番に記録した変更ログをレプリカに送り、レプリカはそれを同じ順番で再生して、自分のデータを同じ状態に追いつかせます。PostgreSQL は先行書き込みログ(WAL)をそのまま流し(ストリーミングレプリケーション)、MySQL は行の変更を記録したバイナリログを送ります。読み取りはプライマリでもレプリカでも行えるので、読み取りをレプリカ群に振り分けます。書き込みは1台に集まり、読み取りだけが横に伸びる、非対称な構成です。
同期と非同期 — 待つか、待たないか
プライマリの変更をレプリカへ「いつ反映したことにするか」で、大きく二つの方式に分かれます。同期(synchronous)レプリケーションは、プライマリが変更をレプリカに送り、レプリカから「受け取りました」の返事が来てから、アプリに「書き込み完了」を返します。非同期(asynchronous)レプリケーションは、レプリカの返事を待たずにすぐ完了を返し、複製は裏で追いかけます。この一点が、速さと安全さの取引をまるごと決めます。
同期方式では、完了が返った時点でレプリカにもデータが渡っているので、プライマリが直後に壊れても書き込みは失われません。その代わり、毎回レプリカの返事を待つぶん書き込みが遅くなり、遠い拠点のレプリカなら往復の時間がそのまま上乗せされます。レプリカが1台止まるだけで書き込みが止まる、というもろさもあります。非同期方式は待たないので書き込みが速く、レプリカの遅れや故障がプライマリに波及しません。ただし、複製が届く前にプライマリが壊れると、アプリには「完了」と返したはずの直近の書き込みが失われます。実務では両者の中間として、「複数のレプリカのうち1台の返事だけ待つ」準同期(MySQL の semi-sync、PostgreSQL の ANY 1 指定など)がよく使われます。
下の部品で確かめてください。「書く」を押すと、書き込み(金の粒)がプライマリから2台のレプリカへ遅れて流れます。方式を切り替えながら、書いた直後にレプリカから読む、プライマリの箱を押して倒す、を試すと、待ち時間・古い読み取り・消える書き込みの取引が見えます。
「書く」を押すと、書き込み(金の粒)がプライマリ A に届き、そこから2台のレプリカへ遅れて流れます。書いた直後に「レプリカから読む」と何が返るか、次に A の箱を押して倒すと何が残るかを試してください。
レプリケーションラグと読み取りの異常
非同期レプリケーションでは、プライマリへの書き込みがレプリカに届くまでに遅れが生じます。これをレプリケーションラグ(複製遅延)と呼びます。ふだんは1秒未満でも、レプリカに負荷がかかったり、大きな更新が流れたりすると数秒〜数分に伸びることがあります。ラグがあると、利用者が投稿した直後にレプリカから読んで「投稿したのに表示されない」、再読み込みのたびに別のレプリカに当たって「さっき見えたものが消える」といった異常が起きます。
このラグは、分断が起きたときの二者択一を述べた CAP定理の現れではありません。ネットワークが正常な平常時にも、「レプリカの返事を待てば遅くなり、待たなければ古い値が見えうる」という遅延と一貫性の取引があり、これを整理したのが PACELC という枠組みです(分断時は A か C か、平常時=Else は Latency か Consistency か)。非同期レプリケーションは平常時に遅延の短さを取った設計で、その結果として複製は「いずれそろう」結果整合性になります。対策としては、書いた本人の直後の読み取りだけはプライマリに向ける、同じ利用者は同じレプリカに張り付ける、レプリカの追いつき位置(ログの番号)を確かめてから読む、といった一貫性の作り込みが必要です。
フェイルオーバーとスプリットブレイン
プライマリが故障したとき、レプリカの1台を新しいプライマリに昇格させて処理を引き継ぐのがフェイルオーバーです。自動化されていることが多いものの、ここには難所があります。非同期構成では、昇格したレプリカに届いていなかった書き込みは失われます。故障から復帰した旧プライマリは、自分にしか無い書き込みを捨てて、新しいプライマリに従うしかありません。
さらに厄介なのは、旧プライマリが実は生きていて、ネットワークが切れていただけの場合です。新旧二つのプライマリが同時に書き込みを受け付ける「スプリットブレイン」が起き、データが取り返しのつかない形で食い違います。これを防ぐため、過半数のノードの合意アルゴリズムで選ばれた側だけがプライマリを名乗れるようにしたり、旧プライマリを確実に止めてから(フェンシング)昇格させたりします。故障の判定も難しい問題です。タイムアウトを短くすれば一時的な遅れを故障と誤認して無用な切り替えが起き、長くすれば停止時間が延びます。
シングルリーダーのほかの型
書き込みを受け付けるノードを増やした型もあります。マルチリーダー型は、データセンターごとにリーダーを置き、各拠点で書き込みを受け付けて、リーダー同士が変更を送り合います。利用者に近い拠点で書けるので速く、拠点間の回線が切れても各拠点は書き続けられます。オフラインでも使えるカレンダーやメモのアプリも、端末1台1台がリーダーになっている、と見ればこの型です。代償は、同じデータが二つの拠点で同時に書き換えられたときの衝突です。後から来たほうを採る、両方を残して利用者に選ばせる、衝突しないデータ型(CRDT)を使う、などの解決策を設計しなければなりません。
リーダーレス型は Amazon の Dynamo が広めた型で、Cassandra や Riak が採用しています。リーダーを置かず、書き手(あるいはその代理のノード)が複数のレプリカに直接書き、「N 台中 W 台に書けたら成功、R 台から読んで新しいほうを採る」というクォーラムで整合性を調整します。フェイルオーバーという概念がなく、どれかのノードが落ちても書き込みは止まりません。古くなった複製は、読み取りのついでに直す(read repair)か、定期的に差分を突き合わせて直します。
どの型を選んでも、レプリケーションは「複製すれば安心」ではなく、遅延・データの損失・衝突・スプリットブレインという固有のリスクとセットで設計する対象です。どの書き込みを失ってよいか、どの読み取りに古い値を許すかを決めることが、構成を選ぶことそのものです。
試問
アプリと、書き込みを受けるプライマリ A、その複製を持つレプリカ B(近い)と C(遠い)が並んでいます。いま C は止まっています。「OK を返すのは」で、アプリに OK を返す前に何台のレプリカの受け取りを待つか(同期=2台・準同期=1台・非同期=待たない)を選んで「書く」を押すと、OK が返った直後にプライマリ A が倒れ、残ったレプリカが新しいプライマリに昇格します。
遠いレプリカ C は止まっています。「OK を返すのは」で方式を選び、「書く」を押してください。アプリに OK が返った直後に、プライマリ A が倒れます。
