アーキテクチャ (IT)システムアーキテクト

スキーマ進化に耐性のあるイベントストリーミングバックボーンをどのように設計しますか?これにより、数千のマイクロサービスが異種のドメインイベントを公開しつつ、後方および前方の互換性を保証し、リアルタイムバリデーションを通じてデータ品質を強制し、分散したデータメッシュアーキテクチャにおけるスキーマポイズニング攻撃を防ぐことができますか?

Hintsage AIアシスタントで面接を突破

質問への回答

この課題の歴史は、ACIDトランザクションと中央集権型スキーママイグレーションが一貫性を確保していたモノリシックデータベースの時代にさかのぼります。組織がマイクロサービスおよびその後のデータメッシュパラダイムを採用するにつれて、ドメインチームはデータ契約を独立して進化させる権限を持つようになりました。この分散化は当初、混乱を引き起こしました。プロデューサーが業務時間中に破壊的変更をデプロイすると、Apache Kafkaの消費者はJavaPython、またはGoで作成されているためにクラッシュし、厳格なカラム構造を期待している下流のOLAPウェアハウスが壊れてしまいます。

根本的な問題は、プロデューサーの進化速度と消費者の安定性要件の間のインピーダンスミスマッチにあります。ガバナンスがなければ、チームはデフォルトのない必須フィールドを導入したり、安全でない型キャストを行ったり(例:INTからSTRING)、レガシーアナリティクスダッシュボードによってまだ参照されているカラムを削除したりすることができます。セキュリティの脆弱性は「スキーマポイズニング」を通じて発生します。これは、悪意のあるまたはバグのあるサービスが、メモリ不足エラーを引き起こすように設計された深く再帰的なネストオブジェクトを含む過剰なJSON Schema定義を登録することによって、デシリアライザで発生したり、Denial-of-Service攻撃中にパーサの脆弱性を利用したりします。

解決策は、中央集権的な強制を伴う分散型ガバナンスレイヤーとして機能するスキーマレジストリに中心を置いています。デプロイ前にCI/CDパイプラインゲートで厳格な互換性モード(BACKWARDFORWARD、およびFULL)を強制するために、Confluent Schema RegistryまたはApicurio Registryを実装します。コンパクトなバイナリシリアル化のためにApache AvroまたはProtocol Buffersを採用し、組み込みのスキーマ進化セマンティクスを利用します。Kafka InterceptorプラグインまたはEnvoy Proxyフィルターを使用してリアルタイムバリデーションを統合し、ブローカーに到達する前にネットワークエッジで非準拠メッセージを拒否します。スキーマ登録をサービスアカウントに制限するRBACポリシーを確立し、メモリ安全性とデシリアライゼーションパフォーマンスを検証するためのサンプルペイロードを生成する自動プロパティベースのテストを組み合わせます。

実生活からの状況

GlobalMartでは、毎時500,000件の注文を処理するフォーチュン500のeコマースプラットフォームで、私たちのOrder DomainチームはOrderCreatedイベントにfraudRiskScoreフィールドを追加する必要がありました。この変更は新しい機械学習パイプラインにとって重要でしたが、扱いを誤ると致命的でした。なぜなら、12の下流システムが既存のスキーマに依存していたからです。それには、レガシーのCOBOLベースのウェアハウスシステムや最新のApache Flinkストリームプロセッサが含まれていました。レガシーシステムは未知のフィールドを処理できずクラッシュし、Flinkジョブは予期しないプロパティで失敗する厳格なPOJOデシリアライゼーションを使用していました。

3つのアーキテクチャアプローチを評価しました。最初の戦略は、すべての12の消費者チームが4時間のメンテナンスウィンドウ中に同時に更新をデプロイする協調的なビッグバンデプロイを提案しました。これは即時の一貫性を提供しましたが、1時間あたり200万ドルの収益を生成するプラットフォームにとっては容認できないリスクを伴いました。どのチームのデプロイ失敗も、分散したKubernetesクラスター全体で複雑なロールバックを強いられ、ダウンタイムを延長し、エンタープライズクライアントとのSLA約束を違反する可能性がありました。

2番目のアプローチでは、デュアルトピックシャドウイングを使用し、プロデューサーは30日間orders-v1orders-v2の両方のトピックに同一のイベントを書き込むことを提案しました。これにより、調整リスクは排除されましたが、Kafkaのストレージコストが2倍(テラバイトの冗長データ)になり、モニタリングダッシュボードが複雑になり、ネットワークパーティションによって片方のトピックに成功した書き込みがもう片方に失敗するといった一貫性の危険が生まれ、古いパイプラインと新しいパイプラインの間に静かなデータの乖離が生じました。

私たちは3番目のアプローチを選択しました。Confluent Schema Registryを実装し、FULL_TRANSITIVE互換性をApache Avroを使用して強制しました。fraudRiskScoreはデフォルト値0.0のオプションフィールドとして追加され、レガシー消費者内のAvro SpecificDatumReaderは新しいメッセージをデシリアライズしつつ未知のフィールドを無視できました。私たちはGitHub Actionsを設定し、すべての履歴バージョンに対して新しいスキーマを検証するmaven-schema-registry-pluginチェックを実行しました。Prometheusメトリクスは消費者グループ全体でスキーマIDの使用状況を追跡し、古いバージョンの廃止前に採用率を確認しました。

その結果、2週間でダウンタイムゼロのマイグレーションが完了しました。レジストリは開発中に4つの破壊的変更の試みを防止し、開発者がcustomerIdフィールドの名前を変更しようとしたときにCIビルドが失敗しました。デプロイ後、私たちのGrafanaダッシュボードは150のマイクロサービス間でデシリアライゼーションエラーがゼロであることを示し、不正検出チームはデータレイクのParquet取り込みジョブに影響を与えることなく、高リスクトランザクションの特定が40%速くなったと報告しました。

候補者が見落とすことが多いこと

質問 1: すべての消費者がマイグレーションを終えた後、スキーマフィールドを安全に削除するにはどうすればよいですか?Kafkaログ保持により、古いメッセージが数ヶ月間保持される可能性があります。

回答:スキーマバージョンをレジストリから物理的に削除したり、フィールドのハード削除を行ったりすることは決して避けてください。むしろ、Avroのカスタムプロパティ"deprecated": trueProtobufのネイティブreservedキーワードおよびdeprecatedオプションを使用してフィールドを非推奨としてマークします。スキーマバージョンは無期限に保持してください。Kafkaブローカーは、数年にわたってそのスキーマで書かれたメッセージを保持する可能性があります(retention.msおよびretention.bytesポリシーに依存する)し、将来の消費者はイベントソーシング再構築のためにオフセットゼロからcompact topicを再生する必要があるかもしれません。Kafka StreamsBurrowを使用して消費者遅延モニタリングシステムを実装し、すべての消費者グループが非推奨フィールドを含む最後のメッセージのタイムスタンプを過ぎて処理を行ったことを確認します。最大保持期間が経過し、安全バッファが追加された時点でのみ、フィールドは「論理的に削除された」と見なされます。この時点でそのフィールドで新しいメッセージを生成することは止めることができますが、スキーマ定義は保持しておく必要があります。

質問 2: 消費者が今まで見たことのないスキーマバージョンを使用してメッセージをデシリアライズする必要がある場合(スキーマ進化ギャップ)、複数のバージョン間で伝播互換性をどう扱いますか?

回答:標準の互換性チェックは、最新のスキーマを直接前のバージョン(v4対v3)とだけ確認し、v5が導入されると、v1に閉じ込められた消費者を保護できません。レジストリで伝播の互換性を有効にして、新しいスキーマが全ての古いバージョンに対して検証されるようにします。デシリアライゼーションのギャップについては、Avroは「スキーマ解決」ルールを用いてこれを扱います。消費者がスキーマv1を持ち、v5で書かれたデータを受け取ると、SpecificDatumReaderはメッセージヘッダーに埋め込まれたライターのスキーマ(v5)を使用してデータを読み取り、フィールド名(位置ではなく)を一致させることによってリーダーのスキーマ(v1)にプロジェクトします。欠損フィールドにはデフォルト値が使用されます。Kafkaクライアントにはuse.latest.version=falseを使用し、消費者グループの再バランス中にレジストリに対する過剰なリクエストを避けるためにTTLでスキーマキャッシングを有効にします。

質問 3: 妨害されたマイクロサービスが消費者をクラッシュさせるように設計された技術的には有効だが悪意のあるスキーマ(入れ子になった再帰が100層やデフォルト文字列の値が50MBなど)を発行することでスキーマポイズニング攻撃を防ぐにはどうすればよいですか?

回答:4つの層で防御を実施します。まず、登録APIゲートウェイ(KongAWS API Gateway)で、500KBを超えるサイズや深さが5層を超える入れ子を含むスキーマを拒否する厳格な意味的バリデーションを強制します。次に、危険なパターン(無上下限の配列("maxItems": undefined)や終了条件なしの再帰型参照)を禁止するために、JSON SchemaまたはProtobufのリンティングルールを実装します。3番目に、提案されたスキーマに基づいて何千ものランダムな有効ペイロードを生成し、厳密なメモリ制限(例:512MB)を持つ孤立したDockerコンテナでデシリアライゼーションを試みる自動化テスト(Hypothesisjqwik)をCI/CDパイプラインで実行します。OOMKilledイベントまたはCPU制限を引き起こすスキーマは拒否します。最後に、スキーマが正しく登録されるのを防ぐために、登録時に特定のSPIFFEアイデンティティを持つ生産サービスアカウントのみがスキーマを登録できるように、相互TLS(mTLS)認証をレジストリに実装します。