プロンプトとスコープ
ある注文イベントが多くのチームによって生成および消費されています。新しいバージョンでは、オプショナルフィールドの追加、1つのフィールドの名前変更、別のフィールドの廃止が行われます。コンシューマーは一斉にアップグレードできず、過去のメッセージもリプレイされます。提案および互換性チェックからカナリアロールアウト、ロールバックに至るまでのゲートを設計してください。Avroのライター/リーダースキーマ、Schema Registryのモード、データ品質チェックがどのように連携するかを説明してください。
ここではデータ契約(data contracts)、スキーマの進化、ストリーミングロールアウト、ガバナンスが問われます。Schema Registryはバージョンと互換性チェックを一元管理しますが、詳細なルールはAvro、JSON Schema、Protobufで異なります。
面接官が見ているポイント
- 互換性を単一のブール値として扱うのではなく、backward、forward、full、transitive互換性を区別できているか。
- 実際のライター/リーダーのデプロイウィンドウに基づいてロールアウト順序を導き出せるか。
- 登録チェック、ランタイムスキーマID、コンシューマーラグ、データ品質を1つのリリースゲートとして構成できるか。
- 名前の変更、デフォルト値、削除、enum、不可逆なセマンティクス変更を、移行とロールバックを用いて適切に処理できるか。
最初に確認すべき質問
- フォーマットはAvro、JSON Schema、Protobufのどれですか?パース処理や互換性の詳細が異なります。
- コンシューマーはリアルタイム処理、バッチ処理、または履歴リプレイのどれですか?最も古いライターバージョンはどのくらいの期間読み取り可能である必要がありますか?
- どちら側が先にデプロイされますか?また、リージョン間やチーム間でロングテールなバージョンが存在しますか?
- サブジェクト(subject)はチーム、イベントタイプ、環境のいずれでグループ化されていますか?互換性はグローバルですか、それともサブジェクト単位ですか?
- ロールバックは古いプロデューサーの復元、ロールアウトの停止、または新旧フィールドのデュアルライトのいずれを必要としますか?
30秒で答えるフレームワーク
モードを選択する前に、バージョンをコンシューマーウィンドウにマッピングします。古いメッセージを消費する新しいリーダーにはbackward互換性、新しいメッセージを消費する古いリーダーにはforward互換性が必要です。双方向の場合はfull、保持されている履歴をカバーする場合はtransitiveチェックを行います。レジストリゲートですべてのスキーマを検証し、ライターの前にリーダーをロールアウトします。新しいフィールドにはデフォルト値を設定し、名前の変更はエイリアスやデュアルフィールドで移行し、コンシューマーウィンドウが経過するのを待ってから削除します。登録拒否、デシリアライズエラー、ラグ、デッドレター、フィールド品質を監視し、異常時には拡大を停止して古いバージョンを維持します。
ステップ・バイ・ステップの回答
1. ライターとリーダーのマトリクスを明示する
メッセージはライタースキーマを保持または参照し、コンシューマーは自身のリーダースキーマでそれを解決します。新旧プロデューサーと新旧コンシューマーの組み合わせを図示し、どれが動作する必要があるかをマークします。ストリーミングのローリングアップグレードでは通常、新しいプロデューサーが新しいデータを書き込む前に、新しいコンシューマーが古いデータを読み取ります。
2. モードをリスクと紐付ける
Backwardは新しいリーダーが古いライターを読めるかをチェックします。Forwardは古いリーダーが新しいライターを読めるかをチェックします。Fullは両方をチェックします。コンシューマーが過去の複数バージョンをリプレイする場合は、transitiveセマンティクスを使用するか、保持されているバージョンセットを明示的にテストします。1つのレジストリのデフォルト値があらゆるフォーマットで共通であると思い込んではいけません。
3. 変更ルールを定義する
フィールドの追加には、ビジネス上の意味が検証された安全なデフォルト値が必要です。名前の変更には、フォーマットでサポートされているエイリアスを使用するか、リーダーを移行する前に新旧フィールドをデュアルライトします。最も古いコンシューマーとリプレイウィンドウが終了した後にのみ削除します。新しいenum値については、古いリーダーが未知の値をどのように処理するかをテストします。型を絞り込んだり、その意味や単位を変更したりすることは破壊的変更(breaking change)です。
propose -> lint -> compatibility-check -> consumer-matrix-test
-> register -> canary-producer -> observe -> expand4. 登録およびCI/CDゲートを構築する
プルリクエスト時に、フォーマットのリント、正規化されたスキーマのdiff、サブジェクトレベルの互換性チェック、および代表的な履歴サンプルに対するデシリアライズを実行します。スキーマIDとバージョンはレジストリで管理し、本番環境のツールはチェックをバイパスする登録を拒否する必要があります。必要に応じてサブジェクトごとに互換性を管理し、監査された権限を持たせ、レビューなしのグローバルオーバーライドを排除します。
5. ライターの前にリーダーをロールアウトする
古いスキーマを読み取る新しいコンシューマーをカナリアデプロイし、デシリアライズエラーやラグを監視します。その後、デュアルライトを実施するか、新しいプロデューサーをリリースします。宣言されたウィンドウが経過した後にのみ、古いコンシューマーとフィールドを廃止します。ロングテールのコンシューマーには、オーナー、バージョン、期限、リプレイ計画が必要です。「全員がアップグレードするだろう」というのはコントロールプレーンではありません。
6. 品質を測定し、安全にロールバックする
未知のスキーマID、デシリアライズ失敗、デッドレター量、フィールド欠損率、単位の異常、コンシューマーラグ、リプレイ成功率、登録拒否を追跡します。新しいプロデューサーを停止し、互換性を維持しているライターを復元することでロールバックします。セマンティクスが変更された場合は、古いデータを新しい意味で解釈するのではなく、新しいトピックまたはバージョニングされたサブジェクトを分離します。
質の高い模範回答
ライター/リーダーのデプロイマトリクスを作成し、最も古いプロデューサー、コンシューマー、リプレイバージョンを特定した上で、実際のフォーマットに合わせたルールを選択します。新しいリーダーが古いメッセージを読むのはbackward、古いリーダーが新しいメッセージを読むのはforward、双方向にはfullが必要であり、保持された履歴にはtransitiveチェックが必要になる場合があります。すべての変更はリント、サブジェクトレベルの登録チェック、履歴サンプルデシリアライズを通過させます。
ロールアウト順序はリーダーが先、ライターが後です。新しいコンシューマーをカナリアデプロイし、その後にプロデューサーをデュアルライトまたは切り替え、コンシューマーウィンドウが経過した後にのみ古いフィールドを削除します。デフォルト値を追加し、エイリアスやデュアルフィールドで名前変更を移行し、削除やenum変更に対する古いクライアントの挙動をテストします。登録拒否、デシリアライズエラー、ラグ、デッドレター、フィールド品質を監視します。異常が発生した場合は拡大を停止して古いスキーマを維持し、古いプロデューサーに戻すか、セマンティクスが変更されている場合はバージョン管理されたトピックにルーティングします。互換性は選択したフォーマットで検証される必要があります。ある製品のデフォルトがAvro、JSON Schema、Protobufすべてで共通のセマンティクスになるわけではありません。
よくある失敗パターン
- リーダーとライターを特定せずに「backwardを有効化する」とだけ答える。
- エイリアス、デュアルライト、リプレイを無視して、フィールドの名前変更や削除を直接行う。
- 登録のみをチェックし、実際の履歴メッセージや古いコンシューマーをチェックしない。
- 新しいプロデューサーを先にデプロイして、古いコンシューマーを破壊してしまう。
- グローバルな互換性設定をサブジェクトのポリシーと見なし、権限やドリフト(乖離)を無視する。
- Kafkaのラグのみを注視し、フィールドの欠損、単位の変更、デッドレター、デシリアライズエラーを見逃す。
フォローアップの質問と模範回答
なぜ追加されたフィールドには多くの場合デフォルト値が必要なのですか?
古いメッセージにはそのフィールドが含まれていないため、新しいリーダーがレコードを構築するには確定的な値が必要になるからです。デフォルト値は、nullによって必須データの欠陥を隠すのではなく、セマンティクス的に有効なものでなければなりません。
名前の変更されたフィールドは直接変更すべきですか?
通常はすべきではありません。エイリアスを使用するか、新旧両方のフィールドを同時に書き込み、リーダーを移行させた後、リプレイウィンドウが経過してから古いフィールドを削除します。
fullとfull transitiveのリスクの違いは何ですか?
Fullは通常、隣接するバージョンとの双方向をチェックします。Full transitiveは保持されている履歴バージョンすべてにわたってチェックを拡張するため、ゲートがより厳格になり、アップグレードコストが高くなります。
登録済みのスキーマでもデータ損失が発生するのはなぜですか?
互換性がカバーするのは構造的な解決(structural resolution)であり、単位、enumの意味、フィールド品質、認可、下流のSQLまではカバーしないからです。サンプルリプレイ、品質ルール、ランタイム監視を追加してください。
ロングテールコンシューマーの終了条件はどのように設定しますか?
オーナー、バージョン、最終消費日時、リプレイの必要性を記録し、期限とアラートを設定します。期限前に移行手段を提供し、互換性を永続的に弱めるのではなく、古いバージョンを隔離または拒否します。
新しいサブジェクトやトピックを作成すべきなのはどのような場合ですか?
セマンティクス、単位、ライフサイクル、または認可境界が変更され、互換性ではその移行を表現できない場合です。分離によって誤認のリスクは低減しますが、デュアルライト、リプレイ、ガバナンスのコストが増加します。