レプリケーションの目的は
- データを地理的にユーザの近くで保持しておくことでレイテンシを下げる
- 一部に障害があってもシステムが動作し続け可用性を高める
- 読み取りのクエリを処理するマシン数をスケールアウトし、スループットを高める
リーダーとフォロワー
レプリカとはデータベースのコピーを保存する各ノードのこと。全てのレプリカにはデータベースのコピーが行きわたっている必要がある。そのための仕組みがリーダーとフォロワー。
- レプリカの一つはリーダーと呼ばれる。書き込みリクエストは必ずリーダーに送られる。リーダーは新しいデータをローカルに書き込む。
- 他のレプリカをフォロワーと呼ぶ。リーダーはローカルに書き込むとその変更データを各フォロワーに送信する。各フォロワーは受け取ったデータを順番にローカルに書き込む。
- クライアントがデータの読み取りをしたい場合は、どのレプリカから行っても良い。ただし、書き込みはリーダーのみ行える。
Cosmos の物理パーティションは最低でも 1 つのリーダーと 3 つのフォロワーにより構成されている。コンテナがどれほど小さくでも、物理パーティション一つに付き最低でも 4 つのレプリカを持つ。そのうち 1 つがリーダーで、残り 3 つがフォロワーとなる。このレプリカのまとまりをレプリカセットと呼んだりする。
5 つの一貫性レベルがあるが、あれはレプリカセット内での話か?もちろんリージョン跨いで分散されるものだろうけど。
Cosmos DB は同一論理パーティションであればトランザクション処理が行えるが、それって
って理由なのじゃなかろうか?論理パーティションキーから物理パーティションを逆引きも出来るから異なる論理パーティションでも可能かもしれないけど、SDK 側でそのチェックやると実行時エラー置きまくって使い勝手悪そう...。アプリ側で考慮すると複雑化するし、そもそも意識する必要ないものだし?
learn.microsoft.com
リーダーは変更データを各フォロワー向けに送信するとあったが、これは Change Feed の仕組みと関係あったりするのかな?この辺の解像度を上げるのは別でやる必要がある。
learn.microsoft.com
SQL Server には Always On availability group
というものがある。この辺は Hyperscale と関係してんのかなぁ。これも別でやる。
learn.microsoft.com
レプリケーションは同期でやるのか非同期でやるのか問題。同期でやるメリットは、常にリーダーとフォロワーの間で読み取れるデータが一致する事。一貫性が保たれるというわけですな。非同期でやる場合はフォロワーが壊れてもリーダーへの書き込みは成功する事。可用性が高まるという事ですな。CAP 定理の話だね。
この本では一つのフォロワーを同期的に更新し、残りのフォロワーを非同期にする方法が紹介されていた。同期的に更新していたフォロワーが壊れると、非同期でやっていたフォロワーをどれか一つ同期的に変更する。これで一貫性と可用性のバランスを取る。この方法は準同期型 (semi-synschronous)
って呼ばれてるらしい。
Azure Storage はチェーンレプリケーションが採用されている。これは同期型の発展形。
https://sigops.org/s/conferences/sosp/2011/current/2011-Cascais/printable/11-calder.pdf
http://www.umbrant.com.s3-website-us-west-1.amazonaws.com/blog/2016/windows_azure_storage.html
チェーンレプリケーションっていうのは、書き込みリクエストと読み取りリクエストを受けつけるノードを分け、レスポンスを返すノードを一つにするもの。書き込みはHEADから始まり、差分を全ノードに順番に送る。TAILがその差分を受け付けると、そのままレスポンスを返す。同期的なレプリケーションは書き込みリクエストを受け付けたノードが、レプリケーション先のノードからACKが返ってくるのを待機している。チェーンレプリケーションは ACKは非同期で返すからスループットが良いって事なのかな?
Chain replication : how to build an effective KV-storage (part 1/2) | by Anton Zagorskii | Coinmonks | Medium
フォロワーのセットアップはある時点のスナップショットの適応と、それ以降の変更の要求により行う。だいたいはストレージの機能として用意されてるんじゃないか?増分バックアップとかに似た考え方かも。ストレージ自体を別者に置き換えるとかなら、スナップショットを適応して W write をするとかあるなぁ。
ノード障害への対応
- フォロワーの障害: キャッチアップリカバリ
- フォロワーはローカルにリーダーからの変更通知の処理結果と、どこまで処理したのかの情報を持っている。それ以降の情報を障害からの復旧時にリーダーに要求すればよい。
- リーダーの障害: ファイルオーバー
- フォロワーのいずれかを新しいリーダーとして設定しなおさなければならない
- リーダーの障害検出には、ノード間で通信を行い一定時間以内にレスポンスが返ってくるかどうかで判断している
- Elasticsearch のノードの間の監視も同じ考え方だろう
- 新しいリーダーの選定にもいくつか方法がある
- ノード同士の投票
- これも Elasticsearch で採用されているはず
- 事前にコントローラノードを決めておき、それをリーダーにする
- これは新しいリーダーを決めた時に一緒に新しいコントローラノードも決めるのだろうか?
- 新しいリーダーを書き込み先として設定する
- 旧リーダーが復旧した時に、そのノードにも新しいリーダーを通達する必要がある
フェイルオーバーの問題
リーダーに書き込まれたが、他ノードのレプリケーションが終わる前にリーダーに障害があった場合。新しくリーダーになったノードには、その書き込み結果が失われ、旧リーダーが復旧した時にデータが競合する可能性がある。その場合は、旧リーダーへの書き込みを破棄するという手がある。クライアントから見たら最新の書き込みが結果に反映されることになるはず。ただ、新しい書き込みがなければ、書き込んだはずのデータが見えないって事が起きうるのかぁ。
Git Hub で起きたインシデントがわかりやすかった。異なるストレージ間で整合性を保たなきゃならん時って事例。MySQL のフォロワーをリーダーに昇格させた。あるテーブルは PK にオートインクリのカラムを使っており、レプリケーションが間に合わずに新リーダーが PK を再利用してしまった。PK は Redis にも保存していたので、同じ PK なのに中身が違うという不整合が発生した。MySQL -> Redis の順に保存してたんだろうけど、MySQL のリーダー -> フォロワーへのレプリケーションが間に合わなかったんだろうな。
新しいリーダーを作った時に、複数のフォロワーをリーダーにしてしまうという障害もある。複数のリーダーが誕生してしまうという事。これはスプリットブレインという問題。これが起きた場合は片方をシャットダウンさせるアプローチ (フェンシング) がとられる。そのアルゴリズムの検討もむずい。
リーダーに障害があったと判定するには、レスポンスタイムアウトをいくつに設定すればよいだろう?長くとれば復旧までの時間 (気付くまでの時間) がかかってしまうし、短くとれば不必要なフェイルオーバーが発生してしまうかもしれない (リーダーの一時的な過負荷で遅れたとかネットワークの問題とか)。その場合のフェイルオーバーは問題をさらに悪化させてしまうかもしれない。過負荷が別のノードに移るだけで、またタイムアウトを起こしてしまうとか、無限フェイルオーバーに繋がりそう?
Elasticsearch のノードの監視のアルゴリズムはどうなってるんやろう。-> 調べてみたけどわからんかった...。minimum_master_nodes
をいくつにするのかとかは勉強になった。
https://www.elastic.co/jp/blog/how-to-configure-elasticsearch-cluster-better
リーダーが受け取った SQL をフォロワーにも送信し実行させる方法。SQL の中に NOW()
や RAND()
といった実行時に値が確定するような式が含まれていると、リーダーとフォロワーで結果が変わる可能性がある。フォロワーでも同じ SQL を実行するため。
オートインクリメントのカラムを使ってたりすると、リーダーとフォロワー間でカウンタがずれていると不整合が生じる。Git Hub のインシデントがそんな感じだったね。複数トランザクションが並列で走って競合エラーが追起きた時、予期せぬ形でカウンタがインクリメントされることがあるからなぁ。直列にしないと怖い。
ストアドプロシージャのような非決定的な操作も不整合のきっかけになりうる。非決定的にあまりなじみがなかったが、簡単に言うと実行するたびに値が変わる可能性のあること。下の記事が勉強になった。
Deterministic and Nondeterministic Functions - SQL Server | Microsoft Learn
WAL の転送
リーダーがトランザクションログをディスクに書き込むと同時にフォロワーに送信する方法。フォロワーはトランザクションログからデータをレプリケーションすればよい。ロールフォワードみたいなもんだね。
トランザクションログってディスクブロック状のどのバイトが変更されたのかっていうかなり低レベルな内容が書かれているらしい。ディスク上の配置を変えるような操作をしたらどうなるのだろう?それ含めてフォロワーに通達されるのかな?トランザクションログは知ってたけど、具体的に中身がどんなものかは想像した事がなかった。
ストレージエンジンに依存しない形のログフォーマットをを使う。WAL とは対照的?論理ログと言われたりする。
一ログが一行の変更を表す。一ログに変更のあった行を特定する情報 (PK とか)や変更後の各列の値が入っている。論理ログでテーブルが出来そうなイメージ。ログのタイプ (Insert/Update/Delete) も持っているんだろう。
トランザクションを貼って更新された場合は、ログとトランザクションのコミットログがセットになる。トランザクション ID とかで関連が作られるのだろうか?コミットログがなければその変更は破棄されるって事かなぁ。SQL Server の CDC ってこの仕組みが関係してたりして?-> 読んだけどトランザクションログをベースにしてるらしい。
What is change data capture (CDC)? - SQL Server | Microsoft Learn
論理ログには依存性がないので、ストレージエンジンが異なってもレプリケーションが出来ると。
データベースのトリガやストアドプロシージャを使ってレプリケーションを行う方法。一部のテーブルや一部の行のみをレプリケーションしたい時に使う。アプリケーションコードをかますことも出来るので柔軟性が高い。
そもそもレプリーションの目的は耐障害性だけでなく、スケーリングなどもある。読み取りのスケーリングを高めたければフォロワーを追加することもある。
複数のフォロワーが存在するときに同期的なレプリケーションを選択してしまうと信頼性が低くなる可能性がある。一つのノードでもダウンすると、書き込みが行えなくなってしまうため。スケールさせようとノードを増やせばダウンする確率は上がってしまう。
非同期的なレプリケーションを採用するしかないが、それでも問題はある。それがレプリケーションのラグ。リーダーに書き込みは成功したが、フォロワーへの反映が奥rてしまってクライアントが古いデータを読み取ってしまうことがある。
自分が書いた内容の読み取り
read-after-write 一貫性の説明。これはユーザがページを読み込みなおしたときに、自分の行った更新が必ず反映されているということ。
ユーザのプロフィールみたいに、特定のユーザしか更新ができないようなデータは話が単純。自分自身のプロフィールを読み取りたい時は確定でリーダーから読み取れば良い。
他にもいくつか実装方法はあって、
- 最終の更新から 1 分以内 (レプリケーションのラグ考慮) のデータは全てリーダーから読み取る
- フォロワーのレプリケーションをモニタリングし、遅れている場合にはそのフォロワーからは読み取らない
- クライアント側にデータの更新日時を持たせておき、その日時までのレプリケーションが完了しているフォロワーから読み取るようにする
- セッションとかに持たせておくのだろうか
- デバイスまたぎのアクセスを考慮して、別ストレージへ持たせておくこともある
一貫性にもいろんな種類があるんや。
Eventual Consistencyまでの一貫性図解大全 #分散システム - Qiita
モノトニックな読み取り
非同期にレプリケーションを行っているフォロワーから読み取る場合に、過去のデータを読み取ってしまう問題がある。
複数のフォロワーがあって、それぞれのレプリケーションが完了するまでに差があるとする。あるフォロワーは完了してて、そこからデータを読み取ったのち、完了していないフォロワーからデータを読み取ってしまったとする。するとクライアントはなぜかデータが古いものに戻ってしまうという体験をする。
この問題の解決策として、あるユーザが読み取るフォロワーを一つに固定してしまうというものがある。ユーザが異なれば読み取るフォロワーが異なっても OK。
レプリケーションのラグにより、書き込みを行った順序でデータが反映されなかったことで読み取り者にとって不可解な状態が生じてしまうこと。
例えば、チャットアプリがあって、書き込みとしては A さん -> B さんの順に行ったのに、レプリケーションが B さん -> A さんの順になり観測者から不可解な状態になっているように見えてしまうこと。
パーティション跨ぎのデータベースで問題になる。対策としては因果関係のある書き込みは同じパーティションに書き込むようにするということ。因果関係のある書き込み
というのがピンとこない。
例えばステートメントベースのレプリケーションで、書き込み順序が入れ替わったことにより不整合な状態が発生してしまうとかなら想像がつく。順序の問題て、where 句に引っ掛からなくなってしまったとかね。
マルチリーダーレスアプリケーション
前提として得られるメリットと加わる複雑さにより、単一のデータセンター内では実装されない。複数のデータセンターにそれぞれリーダーを配置する構成が取られる。
以下の観点からマルチリーダ構成の特徴を考える。
|
単一リーダー |
マルチリーダー |
パフォーマンス |
書き込みは必ずリーダに行われるので、データセンターを跨いだリクエストが飛ぶ。レイテンシを防ぐためにユーザとの距離を近くしたいという目的があれば、それに反することにもなる。 |
ローカルのデータセンターのリーダーにのみ書き込みを行えば良いので、単一リーダーより優れる可能性がある。 |
耐障害性 |
データセンターに障害があれば、別データセンターのフォロワーをリーダーにする |
各データセンターは独立して動き、データセンターが復旧したらレプリケーションを追いつかせるだけで良い |
それぞれのリーダーで同じデータが更新された時のコンフリクト解消はどうやってるんだろうなぁ。
クライアント用のデバイスもある種のリーダーとして考えることができる。アプリがオフラインになったときに書き込まれたデータがオンラインになった瞬間に反映されなければならないとする。この場合、デバイスのローカルストレージがリーダーであり、オンラインになった瞬間に別リーダーにレプリケーションしていると見ることもできる。
PWA とか作ってたけど、この辺全然考えたことなかったなぁ。
書き込みの衝突処理
同じデータに対して複数のクライアントから同時に書き込みを行う場合を考える。マルチリーダーの場合、ローカルリーダーをそれぞれ持つので、両書き込みリクエストは成功する。問題はそれをレプリケーションしたときにコンフリクトが発生してしまうということ。
シングルリーダーであれば先の書き込みリクエストを受け付けたときに対象データにロックをかけられるが、マルチリーダーの場合はそんなことできない。
衝突を発生させなければいいという考え方もあって、その場合はあるデータに対する書き込みは全て同じリーダーにルーティングするという方法がある。内部実装はシングルリーダーだね。データが異なれば別リーダーに書き込めばいい。
一貫した状態への収束
レプリケーション時に衝突が発生したデータは、最終的にどの状態になるのか全てのリーダーで収束させなければならない。でないと不整合な状態になってしまう。収束の方法はいくつかあって、
- 書き込み時にタイムスタンプを記録し、最大のデータで収束させる
- タイムスタンプが古いデータはロストしてしまう
- リーダーに ID をつけておき、ID が最大のリーダーに書き込まれたデータに収束させる
- ID の小さなリーダーへの書き込みはロストしてしまう
- 書き込まれたデータをソートし、結合させる (B, C みたいに)
- 同じデータへの書き込み内容を全て記録しておき、後でユーザに選択させる
ユーザの属性によって何に収束させるのか決めてもいいと思った。例えばユーザの権限とか (管理者であればそれに収束させる)、作成者の書き込みを優先させるとか。複雑なのと、仕様としてどうなんだろうという気持ちになる。
リーダーたちが取りうるトポロジーはいくつか種類がある。
SQL Server はどのトポロジーだろうと思って調べたけど、いい記事が見つからなかった。代わりに骨太な記事を見つけたから、どこかで読まねば。
Types of Replication - SQL Server | Microsoft Learn
以下のような時系列を考える。
- リーダー A へ 1 という値を書き込む
- リーダー A の変更を B,C,D へレプリケーションする
- リーダー B に A の変更を反映させる
- リーダー B に 1 を 3 にするという書き込みを行う
- リーダー B の変更を A,C,D にレプリケーションする
- リーダー C に A, B の変更が逆の順序で到着する
これは一貫性のあるプレフィクス読み取りに似た因果律の問題。解決方法としてはバージョンベクトルと呼ばれる方法がある。
Amazon の Dynamo はこのアーキテクチャを採用している。クライアントは書き込みリクストと読み取りリクエストを並列に複数のレプリカに送っている。この時、帰ってくるデータは異なる可能性があるが、最も新しいものをクライアントに返す。そのためにデータにバージョンを持たせている。MVCC の分散システムみたいなイメージ?
複数レプリカのうち、いくつのレプリカへの書き込み失敗なら許容できるのか、また読み取りを行うレプリカ数を幾つにすれば良いのか。この辺りは定義がなされていて、全レプリカ数を n
、書き込み時の成功数の最低数を w
、最低読み取り数を r
とすると、w + r > n
が成り立っていれば書き込みに失敗しても許容できる。
ワークロードが読取に特化しているなら、w
を 1
、r
を n-1
と調整しても良い。一般的には n は奇数で、w = r = (n+1)/2 (切り上げ)
が成り立つように調整される。
この式どこかで見たことあると思ったが、Elasticsearch の master node を決める投票数の推奨値と似てるな。関係ない気がするが。
Elasticsearchの運用に関する典型的な4つの誤解 | Elastic Blog
レプリカがダウンしてこの士気を満たさなくなった時、書き込みや読み取りをエラーとする