ユユユユユ

webエンジニアです

レプリケーションラグと結果整合性

 ひとつのリーダーが排他的に書き込みを管理する方式について検討してきた。この方式によって享受できるメリットはスケーラビリティとレイテンシの向上である。つまり、リードレプリカを追加すれば読み込みをスケールアウトさせられるし、ユーザーの近くにレプリカを配置すれば地理的制約によるレイテンシの問題も解消できる。

 他方で、リードレプリカの数が増えるに伴って、非同期レプリケーション化は避けられなくなる。同期レプリケーションにおける障害点がリードレプリカの数に比例するためである。そして非同期レプリケーションを導入すると、整合性の問題に否応なく向き合う必要が生じる。

 整合性の破綻とは、リーダーノードとリードレプリカの間でデータが一貫しなくなることである。同期が完了するまで一定の時間をおけば整合性が回復されることは保証される。また通常はコンマ数秒で同期が完了することも期待できる。しかしシステムの負荷が高まったり、ネットワーク全体が混雑するようになると、数秒から数分、あるいはそれ以上にわたって無制限に同期が遅延することは簡単に起こりうる。

 最終的にデータは整合するが、時点によっては整合しないことをとって「結果整合性」と呼ばれる。結果整合性は NoSQL の文脈で使われることが多いタームとはいえ、リレーショナルデータベースにおいても分散データベースの文脈では起こりうる問題であるので見落とすことはできない。

 結果整合性を前提に、不整合なデータがユーザーに提示されてしまいうるケースと、それに対する防止策を2パターンほど取り上げて検討してみることにしよう。

Read-after-write

 書き込みリクエストの直後にリードレプリカに読み込みリクエストを投げるとする。運が悪いと、自分の書き込みが反映されていないレスポンスが返されることになる。しばらく待てば結果整合がもたらされるとはいえ、ユーザーの視点からは自分の書き込みが失われたように見えてしまうわけで、これは小さからざる問題であるといえよう。ここで要求されるデータの一貫性は、書き込み直後の読み込みについての一貫性ということで read-after-write consistency と呼ばれる。

 最新の書き込み内容を見せるためには、リーダーノードに読み込みリクエストを投げるようにすればよい。また大切なのは自分自身の書き込み内容が見えることだけであって、他のユーザーにその書き込みを反映させるのが遅れてしまうのは問題にならない。こう考えると解消すべき問題のスコープはいくぶん小さく保てそうだ。ではどうやって、リクエストを転送する前に動的にリクエスト先を判定するか? いくつかの方法がありそうだ。

 例えば SNS のプロフィールページの更新が懸念になっているとする。「プロフィールを編集できるのは自分自身のみ」という前提に立つと、次のように整理することができそうだ。

  • 自分のプロフィールページは常にリーダーから読み込む
  • 他人のプロフィールページはレプリカから読み込む

 あるいは最終書き込み時刻をクライアントに記憶させておき、これを利用してリクエスト先を決定するようなことも考えられる。システム時刻を直接利用すると、クロックラグが生じたりクライアントが時刻を偽装することが可能になってしまうから、何かしらの単調増加する値を利用するなどといったことも考えられる。

 複数のクライアントをまたいで read-after-write consistency を実現するのは非常に困難である。具体的には、デスクトップクライアントで更新したデータをモバイルクライアントで閲覧したときにも最新のデータを取得させたい、というような要件のことである。

 異なるクライアントに横断的な状態を持たせることは不可能なので、判断材料となる何かしらのメタデータをサーバー側で管理する必要がある。これだけでも管理は複雑になりうる。その上リクエストが複数のデータセンターに分散している状況では、すべてのリクエストをひとつのデータセンターに集約するか、そうでなければメタデータをさらにレプリケーションして分散管理しないといけないことになる。いずれも大変な難事業となるだろう。

Monotonic Reads

 複数のリードレプリカ間に不整合が生じることもありうる。例えばまったく同じリクエストが二度繰り返されたとき、はじめにあるレプリカが最新のレコードを返した後で、異なるレプリカが少し古いレコードを返してしまうことがある。ユーザーからみると、まるで時間が巻き戻ったかのような見え方となり、好ましいものとはいえない。

 これを防止するために導入される整合性の考え方を Monotonic Reads という。これが保証するのは、新しい状態を一度受け取ったら、それ以降の読み込みではそれより古い状態は取得されないということである。

 具体的にいうと、あるユーザーのリクエスト先のレプリカを完全にランダムに振り分けるのではなく、例えばユーザーIDのハッシュ値などを使って常に同じレプリカから読み込むように設定すればこの整合性は保証できる。もちろん、リクエスト先のレプリカがダウンしたときのフォールバック先をどうするかをきちんと考慮するところまで検討しなければならない。

Consistent Prefix Reads

 書き込み順序にまつわる因果関係を管理し、正しい順序で情報を提示する整合性についての描写が含まれるが、これについては書き手の咀嚼力が足りずにうまく説明し直すことができないため割愛する。