プロンプトとコンテキスト
マルチテナントアカウントサービス向けのイベントログを設計します。残高、権限、または設定の変更ごとにイベントが発行され、コンシューマは履歴をリプレイして状態を再構築できます。インスタンス障害が発生した際に、最初から際限のない履歴をスキャンすることはできません。システムには、高スループットの追記、テナントごとの順序保証、独立したコンシューマの進行状況、スナップショットリカバリ、および制限されたストレージコストが必要です。削除、順不同のイベント、およびスキーマの進化について説明してください。
これは、シニアレベルのシステムデザイン、プラットフォーム、およびデータインフラストラクチャの面接に適しています。Martin FowlerのEvent Sourcingに関する記事では、状態の変更を一連のイベントシーケンスとして保持するという核となるアイデアが定義されています。Apache Kafkaの設計ドキュメントでは、パーティション化されたログ、コンシューマの位置、およびキーごとに最新の値を保持するログコンパクションについて説明されています。公開されているシステムデザインのプロンプトでも、パーティション化された追記専用ログ、レプリケーション、保持、コンパクション、およびリカバリが評価領域として挙げられています。これらの情報源はトピックの代表性を裏付けるものですが、特定の企業の固定された出題内容や面接の頻度を確定するものではありません。核となるスキルがエンドツーエンドの一貫性、リカバリ境界、およびキャパシティのトレードオフであるため、カテゴリはsystem-designです。
面接官が評価するポイント
第一に、候補者がイベント履歴とマテリアライズドされた現在の状態を分離できているか。スナップショットは派生したアクセラレーションレイヤーであり、イミュータブルなイベントを置き換えたり、一貫性のないイベント境界を越えたりすることはできません。
第二に、パーティションキーが順序とスケールの両方を満たしているか。テナントまたは集約ID(aggregate ID)でパーティショニングすると、1つの集約の順序は維持されますが、ホットテナント、集約間のトランザクション、およびグローバルな順序には明示的な制限が必要です。
第三に、コンパクションのセマンティクスを理解しているか。キーコンパクションはキーごとに最新のレコードを保持し、状態変更ストリームに適していますが、イベント履歴そのものではありません。削除にはツームストーン(tombstone)と保持ウィンドウが必要であり、コンシューマはコンパクションの前後で任意のオフセットが同じ意味を持つと想定することはできません。
最後に、運用面をカバーしているか。スナップショットの検証、イベントのバージョン、アトミックなチェックポイント、レプリケーションの確認応答、保持コスト、コンシューマラグ、およびスキーマの互換性を可観測(observable)にする必要があります。
最初に確認すべき明確化のための質問
- イベントはイミュータブルな監査ファクトですか、それとも再構築可能な現在の状態の変更ですか? 監査には完全な履歴が必要です。状態ストリームにはキーコンパクションを使用できます。
- 順序付けのスコープは何ですか? 同一集約内のみ、テナント単位、それともグローバルですか?保証ごとにパーティショニングとスループットが変わります。
- コンシューマは任意の時点からリプレイする必要がありますか? その場合、最新キーのコンパクションだけでは不十分です。アーカイブまたは個別の監査ログを保持してください。
- 誰がスナップショットを作成し、検証しますか? プロデューサ、コンシューマ、スナップショットワーカーでは責任が異なります。スナップショットをログの位置とスキーマバージョンに関連付けます。
- 削除はどのように表現されますか? バージョン付きツームストーンとビジネス上の削除イベントでは、コンパクションとコンプライアンスの意味が異なります。
30秒の回答フレームワーク
「私は各集約のイベントを集約IDでパーティショニングし、追記のみを行い、コミットされたレプリケート済みオフセットでプロデューサに応答します。イベントには、イベントID、集約バージョン、スキーマバージョン、タイムスタンプが含まれます。コンシューマはマテリアライズドビューを更新した後にオフセットをチェックポイントし、少なくとも1回(at-least-once)の重複排除のためにイベントIDを使用し、スナップショットとそれに続くイベントから状態を再構築します。スナップショットには、状態、その最後のイベントオフセット、スキーマバージョンが保存されます。リカバリ時には、次のオフセットをリプレイする前にそれを検証します。キーコンパクションは再構築可能な現在の状態ストリームにのみ適用し、定義された期間ツームストーンを保持します。監査イベントはコンパクションされないアーカイブに送られます。レプリケーションラグ、コンシューマラグ、スナップショットの古さ、コンパクションのバックログ、およびリプレイ検証の差異を監視します。」
詳細解説
1. イベントフィールドと順序境界の定義
イベントには、eventId、aggregateId、aggregateVersion、schemaVersion、ペイロード、作成時刻、およびソースが含まれます。aggregateVersionは1つの集約に対して単調増加します。条件付き追記により古いバージョンを拒否し、2つの同時書き込みが互いをサイレントに上書きするのを防ぎます。
1つの集約が1つの順序付けられたログに入るように、集約IDをパーティションキーとして使用します。パーティション間のグローバルな順序を保証してはなりません。プロダクトが集約間のアトミックなファクトを必要とする場合は、タイムスタンプでソートするのではなく、トランザクション結果を1つの集約イベントとしてエンコードするか、トランザクショナルアウトボックスを使用します。
2. 追記、レプリケーション、および確認応答
リーダーはローカルに追記し、十分なフォロワーにレプリケートします。設定されたコミット条件を満たした後にのみ、受け入れの確認応答(ack)を返します。イベントIDとプロデューサのシーケンス番号により、リトライ時の重複排除をサポートします。ディスクセグメントはサイズまたは時間によってローテーションされ、インデックスはコンシューマがオフセットを見つけるのに役立ちます。
確認応答を明示的にします。これは、イベントがリカバリ可能なコミット済みログに存在することを意味し、すべてのコンシューマがそれを処理したことや、マテリアライズドされた読み取りモデルが最新であることを意味するわけではありません。コンシューマの障害によって、追記されたイベントがロールバックされることはありません。
3. コンシューマチェックポイントと冪等性
各コンシューマグループは、独自のパーティションオフセットを保存します。チェックポイントをコミットする前に、イベントを処理して読み取りモデルを更新します。クラッシュによって処理が繰り返される可能性があるため、ハンドラはイベントIDまたは集約バージョンによって冪等である必要があります。モデルとチェックポイントをアトミックに結合する必要がある場合は、両方を1つのトランザクショナルストアに書き込むか、結果をアウトボックスに記録します。
コンシューマが遅延した場合でもログを保持します。ラグを減らすためだけに未処理のイベントをスキップしてはなりません。再構築可能なビューは、スナップショットオフセットから開始できます。再構築できない外部の副作用には、盲目的なリプレイではなく、補償処理またはレビューが必要です。
4. スナップショットプロトコル
スナップショットには、集約ID、シリアライズされた状態、最後に適用されたオフセット、集約バージョンとスキーマバージョン、チェックサム、および作成時刻が保存されます。安定した境界で生成します。ターゲットオフセットを記録し、そのオフセットまでのイベントを適用し、同じ境界でスナップショットを書き込みます。リカバリでは、オフセットが集約のパーティションに属している検証済みスナップショットのみを受け入れます。
リカバリはスナップショットをロードし、snapshotOffset + 1からリプレイします。スキーマが古い場合は、状態を公開する前にバージョン管理されたマイグレーションを実行します。マイグレーションの失敗によって無効な状態をブロックする必要があります。スナップショットはキャッシュです。スナップショットを削除してもログは破損せず、リカバリ時間が増加するだけです。
5. 保持期間、コンパクション、およびアーカイブの分離
時間ベースの保持は古いログデータを削除し、定義されたリプレイウィンドウを持つイベントに適しています。キーコンパクションはパーティション内の各キーの最新の値を保持し、状態コンシューマがより短いログから現在の状態を再構築できるようにします。中間のイベントが失われている可能性があるため、監査や任意の時点へのリプレイをサポートすることはできません。
ツームストーンは削除されたキーを表します。コントラクトの対象となるすべてのコンシューマがそれを確認できるようになるまで保持し、その後コンパクションによって削除できるようにします。監査イベントは、アクセス制御、暗号化、保持ルールを備えた不変のアーカイブに保存します。コンパクションされた状態ログは完全な監査証跡にはなりません。
6. スキーマ、順序の乱れ、およびポイズンイベント
後方互換性のあるスキーマルールを使用します。オプショナルなフィールドを追加し、古い意味を維持し、コンシューマが未知のフィールドを無視できるようにします。破壊的変更には、新しいスキーマバージョン、デュアルリード期間、またはマイグレーションウィンドウが必要です。スキーマのバージョンとパースの失敗を記録します。パースできないイベントのオフセットをサイレントにコミットしてはなりません。
1つの集約に対する順不同のイベントは、通常、プロデューサまたはレプリケーションの違反を示します。古い集約バージョンを拒否し、修復のためにギャップを保持します。集約間のイベントには、ビジネス時間または因果関係ID(causal-ID)の補償ルールが必要です。サーバーの到着時間は因果関係の代わりにはなりません。
7. キャパシティ、リカバリ、および可観測性
1秒あたりのイベント数、平均およびテールペイロードサイズ、レプリケーション係数、保持ウィンドウ、スナップショットサイズ、コンパクションによる削減量、およびリプレイ速度をモデリングします。スナップショットの間隔を短くするとリカバリが向上しますが、書き込み増幅とストレージが増加します。よりアグレッシブなコンパクションを行うと、状態再構築のコストは下がりますが、監査や過去のクエリの機能が弱まります。
プロデューサの確認応答レイテンシ、レプリケーションラグ、ディスクセグメント、コンパクションバックログ、コンシューマラグ、スナップショットの古さ、リプレイ速度、スキーマエラー、重複率、およびスナップショットチェックサムの差異を監視します。定期的にスナップショットとイベントからサンプリングされた状態を再構築し、マテリアライズドビューと比較します。不一致が発生した場合は、オフセットとイベントサンプルを保持します。
質の高い模範解答
「私は集約IDをパーティションキーとして使用し、イミュータブルなイベントを追記し、集約バージョンを使用して同時発生する古い書き込みを拒否します。プロデューサへの確認応答は、イベントがレプリケートされコミットされたログにあることを意味します。各コンシューマはイベントを適用した後に独自のパーティションオフセットをコミットするため、クラッシュが発生しても重複排除可能な繰り返し処理が発生するだけです。
スナップショットには、集約の状態、最後のイベントオフセット、集約バージョンとスキーマバージョン、およびチェックサムが含まれます。リカバリはスナップショットを検証し、次のオフセットからリプレイします。スナップショットを削除しても、リカバリが遅くなるだけです。キーコンパクションは再構築可能な現在の状態ストリームに限定され、ツームストーンはコンシューマコントラクトに従って保持されます。監査ストリームは不変のアーカイブとして残ります。
順序は同一集約内でのみ保証し、パーティション間では保証しません。キャパシティモデルには、レプリケーション、保持、コンパクション、およびスナップショットの書き込み増幅が含まれます。メトリクスは、ラグ、スナップショットの古さ、コンパクションバックログ、スキーマエラー、およびリプレイ検証の差異をカバーし、スナップショットとイベントによる定期的な再構築チェックを実施します。」
よくある間違い
- スナップショットをイベントソースとして扱う → スナップショットを削除するとリカバリや監査の証跡が失われる → スナップショットはオフセットに紐づく派生アクセラレータである。
- コンパクションされたログを完全な履歴として扱う → 中間イベントや特定時点の状態が失われる → 監査イベントをアーカイブし、状態ストリームのみをコンパクションする。
- すべてのイベントをタイムスタンプでソートする → クロックスキューによって誤った順序が作成される → 集約バージョンとパーティションコントラクトによって順序を定義する。
- 任意の副作用の後にオフセットをコミットする → 重複は避けられないが未規定になる → ハンドラを冪等にし、モデルとチェックポイントの結合を定義する。
- ツームストーンを即座に削除する → 遅延しているコンシューマがキーを復活させる可能性がある → コンシューマコントラクトのウィンドウ期間中は保持する。
- 未知のスキーマをスキップしてコミットする → ログとビューがサイレントに乖離する → 一時停止し、ポイズンイベントを隔離し、証拠を保持する。
- キャパシティモデリングをスキップする → スナップショット、コンパクション、またはリプレイが障害のボトルネックになる → ラグ、リカバリ時間、書き込み増幅を計算する。
- Exactly-onceがすべての重複を解決すると主張する → 外部の副作用は依然として繰り返される可能性がある → 冪等性キー、トランザクション、または補償処理を使用する。
フォローアップの質問と回答
なぜデータベースに現在の状態のみを保存しないのですか?
現在の読み取りのみが重要である場合、データベースの方がシンプルです。イベントログは、スキーマ、リプレイ、キャパシティ、副作用の冪等性のコストと引き換えに、リプレイ、監査、複数のコンシューマ、および異なるビューの再構築を提供します。イベントソーシングが普遍的に優れていると想定するのではなく、これらの要件のためにイベントログを選択します。
新しいコンシューマはコンパクション後にどのように最初から再構築できますか?
コンパクション後にまだ表されている現在の状態のみを再構築できます。削除された中間の履歴を再構築することはできません。履歴が重要な場合は、コンパクションされていないアーカイブまたは個別の変更ログを保持し、各トピックのリプレイ機能を文書化してください。
スナップショットの作成中に新しいイベントが到着した場合はどうなりますか?
安定したターゲットオフセットを選択します。そのオフセットより後のイベントは引き続き追記されますが、スナップショットの一部にはなりません。リカバリ時はスナップショットをロードし、次のオフセットからリプレイします。スナップショットの書き込みが失敗した場合は、不完全なファイルを公開するのではなく、古いスナップショットとログを保持します。
イベントスキーマはどのように移行しますか?
まず互換性とバージョンフィールドを定義し、移行期間中にコンシューマが新旧両方のバージョンを読み取れるようにします。ライターは新しいフィールドを追加し、すべてのコンシューマがアップグレードされた後にのみ古いフィールドを停止します。破壊的変更には新しいイベントタイプまたはオフラインマイグレーションを使用し、過去のイベントに対するリプレイテストによって検証します。