データ指向アプリケーションデザイン第9章: 一貫性と合意

ch09 一貫性と合意

  • 耐障害性をもつシステムを構築する最も最善の方法は、有益な保証をもつ汎用的な抽象概念を見出し、それを一度だけ実装し、アプリケーションをその保証に依存させること

    • 例えば、トランザクション
      • アプリケーションはクラッシュすることなく(原子性)
      • データベースが並行にアクセスされることなく(分離性)
      • ストレージデバイスは完璧な信頼性をもつ(永続性)
      • かのように振る舞える
    • トランザクションがクラッシュ、レース条件、ディスク障害などのフォールトを隠蔽化し、アプリケーションが気にする必要がなくなる
  • 分散システムにおいても、分散システムにおける問題をアプリケーションがいくつか無視できるようにしてくれる抽象概念がある

    • 例えば、合意 (consensus): すべてのノードが何かについて同意すること

一貫性の保証

  • レプリケーションを行うデータベースのほとんどは少なくとも結果整合性を提供する

    • ある程度待てば最終的には読み取りリクエストに対してすべて同じ結果を返すようになること
    • 収束性という言葉の方が合っている
  • 分散一貫性モデルとトランザクション分離モデル

    • トランザクション分離モデル: 並行してトランザクションを実行することから生じるレース条件を避けることに主眼
    • 分散一貫性モデル: 遅延やフォールトに際してレプリカの状態を調整すること

線形化可能性

  • 考え方の背景

    • 同時に異なる二つのレプリカにクエリを投げると異なる結果が返ってくることがある(結果整合性)
    • データベースがレプリカが一つしかないように見せる方がシンプル
    • クライアントはデータを同じように見ることになり、レプリケーションラグを気にする必要がない
  • 線形化可能性

    • 原子的一貫性、強い一貫性、即時一貫性、外部一貫性と呼ばれることがある
    • 線形化可能性とは最新性の保証 (recency guarantee) を意味する
      • データベースから読み取りを行う場合、読み取られる値が最新であり、古くなったキャッシュやレプリカからの値でないことを保証する

システムを線形化可能にする条件

  • 3 つのクライアントが読み書きする例でどういう条件が必要か考える
  • パターン 1:

f:id:tmks0820:20210708114022p:plain

  • レジスタに対する操作

    • read(x) => v: クライアントがレジスタ x の値の読み取りをリクエストし、データベースが値として v を返したという意味
    • write(x, v) => r: クライアントがレジスタ x に値 v を設定し、データベースがレスポンスとして r (ok or error) を返したという意味
  • 線形可能性である場合のレスポンス

    • クライアント A の最初の読み取り => 間違いなく 0 が返ってくる
    • クライアント A の最後の読み取り => 書き込みが完了した後に始まっているので間違いなく 1 が返ってくる
    • 書き込み処理と重なって実行された読み取り => 0 または 1 が返ってくる
  • これだけだと線形可能性であるための条件としては不十分

    • 読み取りと書き込みが並行している場合に、新旧の値のいずれかが返ってくるとなると、読み取りのたびに異なる値が返ってくる可能性がある
    • これはデータのコピーが一つしかなく、常に最新の値が返ってくるという線形可能性に違反する
  • そこで制約を加える

f:id:tmks0820:20210708114104p:plain

  • どこかの時点で x の値が 0 から 1  にアトミックに変更され、あるクラインアントの読み取りに対して新しい値 1 が返されたら、 それ以降の読み取りはすべて新しい値を返さないといけない

  • もっと複雑な例

f:id:tmks0820:20210708114116p:plain

  • 追加の操作

    • cas(x, v_old, v_new) => r: クライアントがアトミックな compare-and-set 操作を要求したという意味
  • 線形化可能にするためには、操作のマーカーを連結する線が常に時間軸の中で前に進み、決して後ろに戻らないこと

    • 操作のマーカー(垂直線):データベースが処理を実行したタイミング
    • このモデルはトランザクションの分離は想定されていないため、値はいつでも他のクライアントによって書き換えられる
  • 線形化可能な直感イメージは上記の通りだが、形式的な定義も存在する。この定義にしたがっているかどうかテストすることも可能だが、演算負荷が非常に高い。

線形化可能性と直列化可能性

  • 直列化可能性

  • 線形化可能性

    • レジスタの読み書きにおける最新性の保証(ここのオブジェクトに関すること)
  • データベースは、直列化可能性と線形化可能性を同時に提供できる

    • 厳密な直列化可能性、強い単一コピーの直列化可能性と呼ばれる
    • 2PL による直列化可能性の実装や完全順次実行の実装はこれにあたる
  • 直列化可能なスナップショット分離 (SSI)は、線形化可能ではない

線形化可能性への依存

  • 線形化可能性が役立つ場面は?

ロックとリーダー選出

  • 単一リーダーのレプリケーションを利用するシステムは、単一のリーダーしかいないことを保証する必要がある
    • リーダー選出の方法の一つはロックを使うこと
    • このロックは線形化可能でなければならない

制約及びユニーク性の保証

  • ユーザー名、メールアドレス、ファイルストレージサービスのファイルパスに対するユニーク性
  • これらを書き込まれる際に保証したいなら線形化可能性でなければならない

クロスチャネルタイミングの依存関係

  • メッセージキューとファイルストレージのように二つの異なる通信チャネルが合った場合に、線形化可能性の最新性の保証がない場合にレース条件が発生する

線形化可能なシステムの実装

  • データのコピーが一つしかないように振る舞い、データに対するすべての操作はアトミックであるというのが線形化可能性

    • 本当にデータのコピーを一つにすると、障害があればデータが失われてしまう
    • なのでレプリケーションを使って、データを複数のホストに複数コピーする方法がある
    • レプリケーション + 線形化可能性をどうやって実現するのか?
  • シングルリーダーレプリケーション(潜在的に線形化可能)

  • 合意アルゴリズム(線形化可能)
  • マルチリーダーレプリケーション(線形化可能ではない)
  • リーダーレスレプリケーション(おそらく線形化可能ではない)

  • 線形化可能性とクオラム

    • 厳密なクオラムでも線形化可能にできない
    • パフォーマンス低下を受け入れることができるのであれば、Dynamo スタイルのクオラムを線形化可能にすることはできる
      • 読み取りの際にはアプリケーションに結果を返す前に読み取り修復を同期的に行う
      • 書き込みの際には書き込みの送信前にノードのクオラムの最新状態を読み取れるようにする

f:id:tmks0820:20210708114130p:plain

線形化可能にすることによるコスト

  • アプリケーションに線形可能化が必須である場合、一部のレプリカが他のレプリカからネットワークの問題で切り離されてしまったら、 切り離されているレプリカは利用できない(ネットワークの問題が解消されるのをまつか、エラーを返す)
  • アプリケーションに線形可能化が必須でない場合、レプリカが切り離されてしまったとしても、それぞれのレプリカが独立してリクエストを処理できるような方法で書き込みを受け付けることができる

    • ネットワークの問題が生じても利用できるが、その動作は線形化可能ではない
  • CAP 定理の対象範囲は非常に狭い

    • 1 つの一貫性モデル(線形化可能性)と 1 種類のフォールト(ネットワーク分離)だけを対象とする

線形化可能性とネットワーク遅延

  • 線形化可能性を持っているシステムは少ない...

    • マルチコア CPU の RAM でさえ線形化可能ではない
      • メモリバリア、フェンスが使用されないかぎり...メモリからの読み取りの最新性は保証されない
  • なぜ線形化可能性をもつシステムが少ないのか

    • ひとえにパフォーマンス影響のせい。耐障害性よりもパフォーマンスを重視するケースの方が多いため、線形化可能性を捨てて、 パフォーマンス向上を図る
      • 線形化可能にすると、そうしない場合と比較すると必ず読み書きのパフォーマンスが悪化する

順序の保証

  • 順序は重要な概念
    • シングルリーダーレプリケーションの場合、リーダーの主な目標はレプリケーションログにおける書き込みの順序の決定
    • 直列化可能性はトランザクションがあたかも何らかの順序にしたがって順次実行されたかのように振舞うことを保証する
    • タイムスタンプやクロックを利用して、二つの書き込みの順序を決定する

順序と因果関係

  • 順序は因果関係を保つのに役立つ
  • 因果律はイベント間に順序関係を発生させる
    • 原因は結果よりも先に生じていなければならない
    • システムが因果律から導かれる順序づけに従うのであれば、そのシステムは因果律において一貫している (casually consistency)

因果律に基づく順序と全順序の違い

  • 全順序があれば任意の二つの要素を比較できるので、必ず大小関係を判断できる

    • 自然数 -> 全順序: 5 は 13 より小さく、13 は 5 より大きい
    • 数学における集合 -> 全順序ではない: { a, b } は {b, c} より大きい...?
      • -> 比較不能
      • 半順序: 片方の集合が他の集合の全要素を含んでいる場合には、その集合は他の集合よりも大きいと判断できる
  • 線形化可能性: 操作に全順序がある

  • 因果律

    • 二つの出来事の間に因果関係があるなら、必ず順序関係がある
    • ただし、それらが並行に行われているなら、比較不能
    • つまり、因果律は半順序を定義する
      • 操作によっては順序付けられるが、比較不能な場合もある
  • 線形化可能は因果律を暗に含む

    • ただし、線形化可能はパフォーマンス、可用性に影響を与える
    • 因果律がほしいだけの場合に線形化可能なシステムはオーバー?
      • 因果律だけがほしい(線形化可能まではいらない)場合には別の方法もある

因果律における依存関係の補足

  • 因果律を保持するためには、ある操作に対してどの操作が先行したのかを知る必要がある
    • 因果律における依存関係を判断するためには、システム中のノードの知識を記述する何らかの方法が必要
    • 依存関係の順序を判断するためには、データベースはアプリケーションが読み取ったデータのバージョンを知っていなければいけない
      • SSI における衝突検出でも同様の考え方が適用される

シーケンス番号の順序

  • シーケンス番号、タイムスタンプを使ってイベントの順序づけを行うことができる

    • 物理クロックでなくて良い。論理クロック(操作を特定するための数値の並びを生成するアルゴリズム)で問題ない。
  • シングルリーダーレプリケーションの場合には、単調増加するシーケンス番号をレプリケーションログ中の各操作に割り当てられる

因果的でないシーケンス番号生成器

  • シングルリーダーではない、マルチリーダーやリーダーレスの場合、シーケンス生成は以下のようになる

    • 各ノードが独立して、自分用のシーケンス番号の集合を生成できる(A ノードは偶数、B ノードは奇数等)
    • 24 時間制のクロックから取得したタインプスタンプを各操作に割り当てる(LWW)
    • あらかじめ、シーケンス番号のブロックを割り当てておく(A ノードは 1-1000、B ノードは 1001 - 2000 等)
  • カウンタをインクリメントする方法よりもスケーラビリティがあり、パフォーマンスが高いかもしれない...

    • ただし、これらのシーケンス番号生成器は因果律に対する一貫性を持たない
      • 偶数カウンタと奇数カウンタでどちらが先の番号なのかは判断できない
      • 物理クロックから取得するタイムスタンプはクロックのスキューに影響される
        • 因果律としては後から行われた操作がタイムスタンプ上先の値が割り当てられるときがある
  • ランポートタイムスタンプ(lamport timestamp)

    • 一貫性をもつシーケンス番号を生成するシンプルな方法
    • 物理的なクロックには全く関係がない
    • 2 つのタイムスタンプがあるとき、カウンタの値が大きい方が大きいタイムスタンプ
    • カウンタの値が同じである場合ノード ID が大きい方が大きなタイムスタンプ
    • すべてのクライアントが過去に見た最大のカウント値を追跡し、その値をすべてのリクエストに含めるようにする
      • あるノードが受信したリクエストもしくはレスポンスに含まれるこの値がそのノード自身のカウンタ値よりも大きかったら、そのノードはすぐに自分のカウンタ値を最大値にする

f:id:tmks0820:20210708114147p:plain

  • ただし、全順序があるだけでは不十分なケースがある
    • 例えばユーザ名に対するユニーク制約のようなものを実装する場合
      • 順序がいつまでに確定するのかがわからなければならない
        • 同じユーザ名を全順序中でその操作よりも先に要求しているノードが他にはないことが確実になってはじめて、その操作が成功したと安全にいえる

全順序のブロードキャスト

  • シングルリーダーレプリケーションは、リーダーの単一の CPU コアですべての操作を並べることによって全順序を保証する

    • リーダーに障害があった場合にはどうする?
    • 単一ののリーダーで処理できる以上のスループットだった場合は?
  • これらの問題は全順序のブロードキャスト、アトミックブロードキャストと呼ばれる

  • 全順序のブロードキャストは通常ノード間のメッセージ交換プロトコルとして記述される

    • 非形式的には二つの安全性が常に満たされていなければならない
      • 信頼できる配信: メッセージがロストしてはいけない
      • 全順序づけされた配信: メッセージはすべてのノードに同じ順序で配信されなければならない
  • 全順序ブロードキャストの利用

    • 全順序ブロードキャストは、etcd や ZooKeeper のような合意サービスは全順序ブロードキャストを実装している
    • 全順序ブロードキャストはレプリケーションに必要不可欠
  • 全順序ブロードキャスト = 線形化可能な CAS = 合意

分散トランザクションと合意

  • 合意は分散コンピューティングにおいて最も基本的で重要な問題
  • 合意は難しい。少なくとも以下のことを理解していないといけない。

  • ノード群の合意をとることが重要な状況

    • リーダー選出
    • アトミックコミット
  • FLP 帰結(合意の不可能性)

    • ノードがクラッシュするリスクがあるならば、常に合意を達することができるアルゴリズムは存在しないという証明
    • FLP 帰結は非同期のシステムモデルを前提としている

アトミックなコミットと 2 相コミット

  • トランザクションの原子性: 複数の書き込みの途中で問題が生じた場合のシンプルなセマンティクス
    • 失敗する(中断する)か、成功するかのどちらか
    • データベースに中途半端な状態の結果を残さない
    • セカンダリインデックス(主たるデータとは別のデータ構造)と主たるデータの一貫性も保つ

単一ノードから分散アトミックコミットへ

  • 単一ノードの実行されるトランザクションは、原子性は一般にストレージエンジンによって実装される
  • 単一ノードのデータベースにおける永続性の提供の仕組み

  • 単一ノード上では、トランザクションのコミットはデータが永続性を持ってディスクに書き込まれる順序に依存している

    • データの書き込み -> コミット

2 相コミット(2PC)

  • コーディネータ(トランザクションマネージャー)が登場する
  • アプリケーションがコミットできる準備が整ったら、コーディネータはフェーズ 1 を開始する

    • コーディネータは準備リクエストを各ノードに送信し、それらがコミットできるかを問い合わせる
    • コーディネータは参加者からのレスポンスを追跡する
  • すべての参加者が yes を返し、コミットの準備が整っていることを示してきたら、コーディネータはフェーズ 2 でコミットリクエストを送信する => 実際にコミットが行われる

  • いずれかの参加者が no を返したら、コーディネータはフェーズ 2 ですべてのノードに中断リクエストを送信する

2PC 詳細
  1. 分散トランザクションを開始したい場合、アプリケーションはコーディネータに対してグローバルユニークなトランザクション ID を要求する
  2. アプリケーションは、各参加者上で単一ノードのトランザクションを開始し、それらのトランザクションに対してグローバルユニークなトランザクション ID を添付する
  3. アプリケーションコミットの準備が整ったら、コーディネータはグローバルなトランザクション ID をタグづけした準備のリクエストをすべての参加者に送信する

  4. 準備のリクエストを受信した参加者は、いかなる環境下でもそのトランザクションを間違いなくコミットできることを確認する

    • トランザクションの全データをディスクに書き込めること、制約違反の有無のチェックを行う
  5. コーディネータは準備のリクエストに対するレスポンスのすべてを受け取ったら、トランザクションをコミットするか中断するか最終的に判断する

    • コーディネータはコミットポイント(クラッシュが生じたとしてもコミットするか中断するかを下した判断がわかるようにするポイント)を残す
  6. コミットポイントの判断がディスクにかかれたら、コミットもしくは中断のリクエストがすべての参加者に送られる

    • このリクエストが失敗したり、タイムアウトしたりした場合、コーディネータは成功するまで永遠にリトライし続ける

コーディネータの障害

  • もしコーディネータに障害が発生した場合は、参加者はコーディネータのリカバリを待つこと以外に 2PC を完結させる方法はない
    • 参加者同士で通信して互いの状況を知って何らかの同意を得るということは可能だが、2PC にはこうしたプロトコルはない

スリーフェーズコミット

  • 2 相コミットはブロッキングアトミックコミットプロトコルと呼ばれる

    • これは 2PC がコーディネータのリカバリを持って行き詰まってしまう場合があることからきている
  • 2PC に代るものとして 3PC が提唱された

    • ただしネットワークの遅延に上限があり、ノードのレスポンスタイムにも上限があることを想定している
      • ので現実的なシステムでは 3PC で原子性を保証することはできない

分散トランザクションの実際

  • 2PC はパフォーマンス上のペナルティを伴うケースが多い

    • 例えば、 MySQL における 2PC は単一ノードのトランザクションと比べて 10 倍遅いという報告がある
    • その原因は、リカバリに必要なディスクへの強制書き込み (fsync)の増加と、ネットワークラウンドトリップの増加
  • 分散トランザクションとは何か?2 種類ある

exactly-once なメッセージ処理

  • ヘトロジニアスな分散トランザクションは多様なシステムを結合する
    • メッセージキューからきたメッセージを、メッセージを処理するデータベーストランザクションが成功した場合にかぎり、処理済みとして承認する、といったことができる
      • メッセージの配信、データベーストランザクションのどちらかが失敗したら、両方中断されるので再配信できる

XA トランザクション

未確定状態中のロックの保持

  • 未確定状態で行き詰まったトランザクションは、ロックの影響を受ける
    • Read Commited や Serializable な分離性を提供する場合、ロックを保持する。これはトランザクションが中断する、もしくはコミットされるまで保持され続ける
    • 未確定状態をそのままにしておくと、ロックされている部分にアクセスしようとしているすべてのトランザクションに影響を与えてしまい、アプリケーションの大部分が利用できなくなってしまう可能性がある

コーディネータからの障害のリカバリ

  • orphaned 未確定トランザクションが生じる可能性がある
    • トランザクションログが失われてしまったり、ソフトウェアのバグによって壊れてしまった場合
    • 自動で解決はできない
    • 2PC の正しい実装は、たとえ再起動をまたいだとしても未確定のトランザクションのロックは保持し続けなければならないので、データベースの再起動でも解決されない
    • 解決策は管理者が手作業で、未確定の各トランザクションの参加者を調べて、すでにコミット、ロールバックされている参加者があるかを確認し、同じ結果を他の参加者にも適用する

分散トランザクションの限界

  • コーディネータは一種のデータベース扱い
    • 単一マシン上でコーディネータが動作している場合にはシステム全体にとっての単一障害点になる
    • アプリケーションサーバはステートレスで、永続化すべきステートはデータベースに保存することがほとんど。コーディネータはアプリケーション側のライブラリとして提供されることがほとんどなので、ステートレスなアプリケーションサーバにステートが持ち込まれることになる
    • SSI と共に動作することはできない
    • 一つの参加者が壊れたら、他のすべてに影響が及ぼされる(つまり、XA トランザクションは障害を増加する傾向にある)

耐障害性をもつ合意

  • 合意とは、複数のノードが何かについて同意すること
  • 合意の問題は以下のように形式化される

    • 1 つ以上のノードが値を提案 (propose)する
    • それらの値の中から一つを決定 (deside)する
  • 合意が満たさなければいけない性質

    • 一様同意 (uniform agreement)
      • 2 つのノードが異なる決定をしていないこと
    • 整合性 (integrity)
      • 2 回決定をしているノードがないこと
    • 妥当性 (validity)
      • ノードが値 v を決定したら、v を提案しているノードがあること
    • 終了性 (termination)
      • クラッシュしていないすべてのノードは最終的に何らかの値を決定すること
  • 合意のシステムモデル

    • クラッシュしたノードは突然消え去り、決して戻ってこないことを前提としている
      • 2PC は終了性の要件を満たすことができない

合意アルゴリズムと全順序ブロードキャスト

  • 耐障害性をもつ合意アルゴリズムとして最も広く知られているもの

    • VSR (Viewstamped Replication)
    • Paxos
    • Raft
    • Zab
  • これらのアルゴリズムは、一様同意/整合性/妥当性/終了性という同意の形式的性質を直接利用せずに、値の並びを決定することによって全順序ブロードキャストになっている

    • 全順序ブロードキャストは、メッセージが厳密に一度だけ、同じ順序でメッセージがすべてのノードに配信されなければならない
    • 全順序ブロードキャストは、複数回に渡って合意をとることと等価

シングルリーダーレプリケーションと合意

  • リーダーの選出を自動で(管理者の手を介在することなく)行う場合には、合意が必要

エポック番号とクオラム

  • 合意プロトコルでは、内部的に何らかのリーダーを使っているがユニーク性は保証していない。その代わりに弱い保証を与える

    • エポック番号
      • Paxos の場合: 投票番号
      • VSR の場合: ビュー番号
      • Raft の場合: 期間番号
  • 現在のリーダーが落ちたと考えられるたびに、ノード間で新しいリーダーを選出するための投票が始まる

    • この選出に対してインクリメントされるエポック番号が与えられる
      • エポック番号は全順序をもち、単調増加する
      • 二つの異なるエポック番号があり、二つの異なるリーダーが選出されたら、大きいエポック番号をもつリーダーが優先される
  • リーダーは何かを決定する前にまず自分より大きいエポック番号を持ち、衝突する判断をするかもしれない他のリーダーがいないことを確認する必要がある

    • リーダーはクオラムから投票を集める
      • 投票には 2 回のラウンドがある
        • 1 回目はリーダー選出のための投票
        • 2 回目はリーダーの提案に対する投票
      • 2 つの投票のクオラムには重複部分が必要
        • 最初のリーダー選出の投票に参加したノードは、2 回目のリーダーの提案に対する投票にも参加していなければならない
  • 2PC と合意は似ているけれど違う

    • コーディネータは選出されないが、合意におけるリーダーは選出される
    • 2PC ではすべての参加者の yes が必要だが、合意は過半数で良い

合意の限界

  • 合意によって強固な安全性が提供されて、耐障害性を保てる
  • 全順序ブロードキャストを提供するため、線形化可能でアトミックな操作を実装できる

  • 限界点

    • 合意プロトコルにおける投票は同期レプリケーションの一種であるため、パフォーマンス上の問題が発生する可能性がある
    • 処理を行う上で厳密な過半数を必要とする
    • 投票に参加するノード集合が固定の集合であることが前提とされることが多い
      • 動的なメンバーシップの拡張を加えたアルゴリズムの理解はまだそれほど進んでいない
    • 障害が発生しているノードの検出をタイムアウトに依存している
      • ネットワーク遅延の影響によりリーダー選出プロセスが頻繁に起きてパフォーマンスを損なう可能性がある
      • Raft にはよく知られた問題がある
        • ネットワークが全体的には正しく動作しており、特定の一つのネットワークリンクで信頼性が低い状態が続いている場合、継続的にリーダー湿布が 2 つのノード間で行き来し続けてしまったりして効果的な処理を進められなくなる

メンバーシップと協調サービス

  • ZooKeeper, etcd は「分散キーバリューストア」「協調及び設定サービス」と説明される
  • 普通のユーザーがデータベースを利用するように ZooKeeper や etcd を使うことは滅多にない
  • 通常は何らかの他のプロジェクトを通じて間接的に利用することがほとんど
  • ZooKeeper の場合以下のプロジェクトから依存されている

    • HBase
    • Hadoop YARN
    • OpenStack Nove
    • Kafka
  • ZooKeeper や etcd は完全にメモリ内に収まる少量のデータを保持するように設計されている(永続性のためにディスクへの書き込みは行われる)

  • 分散システムを構築する上で有益な機能群

    • 線形化可能ででアトミックな操作
    • 操作の全順序
    • 障害検出
    • 変更通知