プロンプトと対象となるロール
地理分散レプリケーションされたコメントシステムにおいて、親コメントとその返信が異なるレプリカに保存されています。ユーザーは返信が見えているのに後から親コメントが見えなくなったり、書き込みが成功した後に古いバージョンを読み取ったりしてはなりません。因果一貫性と線形化可能性(linearizability)および結果整合性(eventual consistency)の違いを説明し、セッション保証を設計した上で、実装に因果関係の逆転(causal inversion)がないことを検証する方法を示してください。
この質問は、分散システム、バックエンド、インフラストラクチャ、および一般的なエンジニアリングの面接に適しています。特定のデータベースを前提とする必要はありません。レプリカモデル、レプリケーション遅延、およびリクエストが伝播可能な因果コンテキストを保持するという事実を提示してください。
面接官が見ているポイント
優れた回答には4つの階層があります。happens-beforeを定義し一貫性の強度を区別すること、read-your-writes、monotonic reads、monotonic writes、writes-follow-readsをリクエストフローに対応付けること、依存関係を満たすためにレプリカがどのように待機または転送を行うかを説明すること、そして遅延レプリケーション、メッセージの並べ替え、フェイルオーバーを用いてそれらの特性を証明することです。また、因果一貫性は並行する書き込みに対して全順序(total order)を作成するわけではなく、競合解消は依然としてアプリケーション側の責務であることも述べる必要があります。
回答前に確認すべき明確化のための質問
「因果関係がある(causally related)」とは、通常、同一クライアントセッション内での書き込みに続く読み取り、別の書き込みの結果を読み取った上での書き込み、あるいはドメインにおける明示的な親子関係から生じます。happens-beforeエッジを持たない並行する書き込みは、異なるレプリカで異なる順序で現れる可能性があります。因果一貫性を「すべてのクライアントが単一の同一の順序を観測すること」と定義してはなりません。
次の4つの境界を明確にします。コンテキストがリトライや非同期キューをまたいで伝播するかどうか、レプリカが永続的に遅延したままになることが許容されるかどうか、読み取りがどの程度古くてもよいか(ステイルネス)、そして競合解決にLWW、CRDT、ドメイン固有ルールのいずれを使用するかです。これらの境界がなければ、主張する保証を検証することはできません。
30秒の回答フレームワーク
「因果一貫性は、因果関係のある書き込みが同一の因果順序で観測されることを要求しますが、並行する書き込みは異なる順序で現れる可能性があります。結果整合性よりも強力なユーザーから見える保証を提供しますが、線形化可能性やSpannerの外部一貫性とは異なります。それらのより強力なモデルは、結果が単一の実時間順序にも適合することを要求します。
私ならクライアントからバージョンベクトルまたは不透明な(opaque)因果トークンを伝播させます。各書き込みは既知の依存関係をレプリカに送信し、コミットされたバージョンをトークンにマージして戻します。読み取りはトークンを満たしているレプリカのみに向けるか、待機または転送を行います。コントラクトを満たせない場合は、明示的に性能劣化した(degraded)結果を返します。レプリケーション遅延、メッセージの並べ替え、リトライ、フェイルオーバーを注入し、待機レイテンシ、コンテキストサイズ、フォールバック率を測定しながら、親なしで返信が表示されたりセッションが自身の書き込みを見失ったりしないことを検証します。」
ステップごとの詳細解説
依存関係コンテキストとセッション保証の設計
クライアントのコンテキストをバージョンベクトルまたは不透明な因果トークンとして表現します。レプリカは自身が適用したバージョンを追跡し、読み取りを処理する前に依存関係を確認します。
context = client.context
write(key, value, context):
result = replica.write(key, value, dependency=context)
context = merge(context, result.version)
return result
read(key, context):
replica = chooseReplicaSatisfying(context)
result = replica.readAfter(context)
context = merge(context, result.context)
return resultRead-your-writesは、以降の読み取りがセッション自身の書き込みより古くならないことを意味します。Monotonic readsは、セッションが過去の状態に逆戻りしないことを意味します。Monotonic writesは、1つのセッションからの書き込みがコミット順に適用されることを意味します。Writes-follow-readsは、以降の書き込みが以前の読み取りによって観測された依存関係を引き継ぐことを意味します。レプリカに依存関係が不足している場合、待機するか、より新しいセーフポイントを持つレプリカに転送するか、プロダクトが明示的に許可している場合は性能劣化した(degraded)ステイルな結果を返すことができます。最後の選択肢を採用した場合、元の保証を主張し続けることはできません。
並行書き込み、障害、およびパフォーマンスのトレードオフ
因果一貫性が制約するのは、導出可能な順序関係のみです。2人のユーザーが同一テキストを並行して編集した場合、アプリケーションにとっての勝者を決定することはありません。LWWは更新を消失させる可能性があります。CRDTやドメインマージは、メタデータと実装コストを引き換えに、より多くの意図を保持できます。
コンテキストをより詳細にすると、依存関係の待機時間やテールレイテンシが増加する可能性があり、バージョンベクトルが肥大化することもあります。すべてのリクエストをプライマリに送信すれば保証は単純化されますが、リージョンごとのレイテンシと可用性が犠牲になります。結果整合性のレプリケーションは低コストですが、自動的にread-your-writesやmonotonic readsを提供するわけではありません。ビジネス要件としてコミット順序と実時間順序の一致が必要な場合は、より多くの協調(coordination)コストを受け入れた上で、線形化可能性や外部一貫性の採用を検討してください。
実行可能な検証計画
一意の因果チェーンを持つテストを構築します。親コメントを書き込み、それを読み取り、返信を書き込み、異なるリージョンのレプリカから読み取ります。レプリケーション遅延、ネットワーク分断、メッセージの並べ替えや重複、クライアントリトライ、プライマリのフェイルオーバーを注入します。すべての読み取りについて、因果トークン、観測されたバージョン、レプリカIDを記録します。
少なくとも次の項目をアサートします。返信を観測したリクエストは必ずその親も観測できること、書き込みの成功後に同一セッション内で古い読み取りが発生しないこと、セッションの読み取りバージョンが単調増加すること、並行書き込みは異なる順序で観測される可能性があるが宣言された競合ルールのもとで最終的に収束すること。トークンの欠落、不正確な依存関係チェック、レプリカのステイルなウォーターマークを識別できるように、違反が発生した最短のイベントシーケンスを保持します。
質の高い模範回答
「私は親から返信へのエッジをhappens-beforeとして定義します。因果一貫性はすべてのレプリカがそのエッジを維持することを要求しますが、同時に作成された2つのコメントは異なる順序で現れる可能性があります。線形化可能性はこれに加えて単一の実時間順序を要求し、結果整合性は書き込みが停止した後の収束のみを約束します。
クライアントはバージョンベクトルまたは因果トークンを保持し、リトライ、非同期ジョブ、サービス呼び出しを通じてそれを伝播します。書き込みはトークンを依存関係として送信し、成功時に新しいバージョンをマージします。読み取りはトークンを満たしているレプリカを選択するか、待機または転送します。これにより、明示的な待機およびフォールバックセマンティクスに従いながら、read-your-writes、monotonic reads、monotonic writes、writes-follow-readsが実装されます。
因果一貫性が並行する競合を解決するとは主張しません。同一フィールドに対しては依然としてLWW、CRDT、またはドメインマージが必要です。テストでは、レプリケーションの遅延と並べ替え、リクエストの重複、レプリカ間のフェイルオーバーを行い、返信が親から外れることがないか、セッションのバージョンが逆戻りしないか、テールレイテンシ、コンテキストサイズ、待機、フォールバック率が宣言されたバジェット内に収まっているかを確認します。」
よくある間違い
- 因果一貫性をグローバルな全順序と呼ぶこと → 並行する書き込みには必須の順序はありません → happens-beforeと並行性を明確に区別してください。
- 「レプリカはいずれ同期する」とだけ答えること → それではセッション保証になりません → トークン、依存関係チェック、レプリカ選択について説明してください。
- read-your-writesがデータベースのデフォルト機能であると見なすこと → レプリカ間のルーティングによって古いバージョンが返される可能性があります → 書き込みバージョンを伝播し、レプリカのウォーターマークを確認してください。
- 非同期キューでコンテキストを欠落させること → 返信ジョブが親への依存関係を失います → メッセージやリトライメタデータにトークンを含めて伝播させてください。
- LWWが因果競合を解決すると主張すること → LWWは並行する更新を上書きしてしまう可能性があります → マージポリシーは独立したアプリケーションの判断として設計してください。
- 正常系のみをテストすること → 遅延、並べ替え、フェイルオーバーによって順序の逆転が顕在化します → 障害を注入し、最短のイベントシーケンスを保持して検証してください。
フォローアップの質問と回答
フォローアップ1: 線形化可能性ではなく因果一貫性を選択するのはどのような場合ですか?
すべてのクライアントが単一の実時間順序を観測する必要があり、単一マシン上であるかのように操作のアトミック性が求められる場合は、線形化可能性またはより強力なセマンティクスを選択します。コメントのタイムラインでは親子関係の可視性とセッション保証が必要とされることが多いため、因果一貫性を採用することでローカル読み取りのパフォーマンスをより高く維持できます。決済残高などは、協調コストを選択する前に、まずビジネス上の不変条件を満たす必要があります。
フォローアップ2: 依存関係が不足している場合、レプリカはステイルな読み取りを返すことができますか?
APIがそれを性能劣化(degraded)としてラベル付けし、呼び出し元がそのコントラクトを受け入れる場合に限られます。プロダクトが『返信が見えるなら親も見える』と約束している場合、ステイルな読み取りはそのコントラクトに違反します。待機、転送、またはリトライ可能なエラーを返し、待機バジェットをSLOに含めてください。
フォローアップ3: バージョンベクトルは無制限に肥大化する可能性がありますか?
レプリカやクライアントが増加するとメタデータも増加します。トークンの圧縮、リース、因果安定点(causal stability points)、あるいは参加者セットの制限によってコストを抑えることができますが、各手法において必要な依存関係が失われないことを証明する必要があります。想定されるメンバーシップの変動のもとで、コンテキストサイズとマージのオーバーヘッドを測定してください。
フォローアップ4: テストが実際の因果関係の逆転をカバーしていることをどのように証明しますか?
すべてのイベントに対して書き込みID、依存関係トークン、適用済みウォーターマーク、レプリカ、読み取り結果を記録し、happens-beforeグラフを構築します。違反が発生した最短のチェーンを再生し、トークンの消失、重複配信、並べ替え、フェイルオーバーの各経路が障害を再現するか、あるいはアサーションを適切にトリガーすることを確認します。