gRPCサービスの自動契約テストを実装するには、従来のREST検証とは根本的に異なるアプローチが必要です。なぜなら、Protocol Buffers(protobuf)は人間が読みやすいテキストではなく、バイナリシリアル化に依存しているからです。戦略は、スキーマの進化ガバナンス、バイナリペイロードの整合性、言語に依存しないシリアル化検証の3つの柱に焦点を当てる必要があります。
buf(Protocol Buffersビルドシステム)を利用して、CI/CDパイプライン内でのリンティングルールと破損変更検出を強化します。buf breakingコマンドを設定して、現在のproto定義を前のGitコミットまたはProtobufスキーマレジストリのベースラインと比較し、フィールド番号が不変に保たれ、削除されたフィールドが適切に予約されることを確認して、ワイヤフォーマットの破損を防ぎます。
言語間検証のためには、gRPCプラグインサポートを持つPactを採用するか、Java、Go、Pythonでスタブを生成するカスタムバイナリアサーションスイートを実装して、ある言語からのシリアル化されたメッセージが他の言語で正しくデシリアル化されることを確認します。これにより、言語固有の実装がデフォルト値やパックされた繰り返しフィールドを異なるように解釈する微妙な問題を捕らえることができます。
さらに、prototoolまたはbuf generateをBazelと統合して、生成されたクライアントライブラリがサービスデプロイメントと同期していることを確認し、消費者が古くなったproto契約に対してコンパイルする「インピーダンスミスマッチ」を防ぎます。
問題の説明
あるフィンテック企業が、Javaベースのモノリスとリスク計算を処理する新しいGoマイクロサービス間のレイテンシを改善するために、ペイメント処理をRESTからgRPCに移行しました。稼働から3週間後、Javaサービスは更新されたGoサービスと通信する際に不正確なリスクスコアを計算し始めました。調査の結果、Goチームがプロトフィールドの名前を変更し(risk_factorからrisk_scoreに)、フィールド番号を5から6に変更して同じデプロイメント内で行っていたことが明らかになりました。名前の変更は安全だと仮定されていました。しかし、Javaクライアントは依然としてタグ5のバイナリデータを送信しており、Goサービスはそれを異なるフィールド(ブーリアンのis_flagged)として解釈し、デシリアル化エラーではなく静的論理エラーを引き起こしました。
考慮された異なる解決策
プルリクエストによる手動プロトファイルレビュー:チームはプロトの変更についてプルリクエストを視覚的に検査し、コードオーナーが破損した変更をキャッチすることに依存しました。利点:インフラコストゼロ、既存のGitHubワークフローを活用。欠点:人間のレビュアーは、一度に名前が更新されたときのフィールド番号の変更を常に見逃す。バイナリペイロードが互換性を保つという自動保証もない。15以上のマイクロサービスでの日々のデプロイメントにはスケールしませんでした。
buf breaking検出を利用した静的分析:フィールドタグが変更または予約なしで削除された場合に、protoファイルをメインブランチと比較する自動buf breakingチェックをCIパイプラインに実装し、ビルドを失敗させます。利点:即時フィードバック(サブ秒実行)、特定のフィールド番号の変異問題を防止、軽量な統合。欠点:スキーマ定義のみを検証し、実際のバイナリシリアル化動作や言語固有のエッジケース(例:Goがnilスライスを処理する方法とJavaが空のリストを処理する方法)を検証しませんでした。両方のサービスが正しいスキーマを使用しているが、異なるprotobufライブラリのバージョンが異なるフィールドを解釈する問題もキャッチできませんでした。
バイナリペイロード検証を伴う双方向契約テスト:Javaクライアントが期待されるバイナリリクエスト/レスポンスペイロードを記録し、Goプロバイダーがそれを消費および生成できるかを検証するためにPactのgRPC拡張を利用します。さらに、提案された変更から生成されたprotoスタブで両方のサービスを起動するためにDocker Composeを使用した言語間統合テストを実装します。利点:実際のシリアル化/デシリアル化のラウンドトリップを検証し、言語固有のデフォルト値の不一致を捕え、デプロイメント前に両方のサービスがワイヤフォーマットに同意していることを保証します。欠点:両方のチームが共有契約リポジトリを維持する複雑な初期設定。マルチコンテナオーケストレーションのため、CI実行時間が1ビルドあたり4分増加しました。
選択された解決策と理由
チームは、プルリクエストビルド中のPact契約検証と機能ブランチでの即時開発者フィードバックのためにbuf breakingを組み合わせたハイブリッドアプローチを選択しました。bufツールは、初期のインシデントを引き起こしたフィールド番号の変異を防ぐための内ループ開発に必要なスピードを提供しました。Pactレイヤーは、バイナリ互換性のための重要な安全ネットを追加し、特に、Javaが空の文字列を長さ区切られたゼロバイトとしてシリアル化する一方で、Goがprotobufのoptional文字列に対して欠落フィールドを期待しているというエッジケースを捕えました。この組み合わせは、実行速度と包括的な安全をバランスさせました。
結果
実装後、パイプラインは最初の月に12の破損したproto変更を検出しました(フィールド番号の変異3件と予約フィールドの競合2件を含む)。全てがプロダクションではなく開発中にキャッチされました。デプロイ後の6ヶ月間、シリアル化に関連するインシデントはゼロでした。契約違反を検出する平均時間は4.2日(プロダクションデバッグ)から3分(CI失敗)に短縮され、言語間テストスイートがJavaおよびGoエンジニアリングチーム間のAPIバージョニングに関する議論の真実の源となりました。
契約テストシナリオでprotobufメッセージからフィールドを永久に削除する際に、後方互換性をどう処理しますか?
候補者は、単に.protoファイルからフィールド行を削除することを提案することがよくあります。正しい実装は、将来のフィールド番号の再利用を防ぐためにreservedキーワードを使用し、削除の前に少なくとも1つのメジャーバージョンサイクルの間フィールドを非推奨としてマークする必要があります。契約テストは、削除されたフィールドがデフォルトでゼロ値または明示的なデフォルトに戻ることを確認することによって、古い消費者が新しいメッセージ(前方互換性)をまだ解析できることを確認する必要があります。さらに、テストは新しいフィールドが予約されたタグを再利用しようとした場合にProtobufコンパイラが拒否することを検証する必要があります。これは、通常buf lintのルールPROTO3_FIELDS_NOT_RESERVEDや、対応する予約宣言がない削除フィールドをスキャンするカスタムCIゲートを通じて強制されます。
protobuf契約進化におけるフィールド番号とフィールド名の重要性は何ですか、またこの区別が自動テスト戦略にどのように影響を及ぼしますか?
多くの候補者は、人間が読みやすいJSON表現やデバッグツールに表示されるため、フィールド名に焦点を当てます。バイナリシリアル化では、フィールド番号(タグ)だけが重要です。「customer_id」を「user_id」に名前を変更してもバイナリ互換性は保たれますが、タグ1をタグ2に変更すると、すべての既存の消費者が壊れてしまいます。したがって、自動テストは名前の安定性よりもタグ不変性を優先する必要があります。戦略には、フィールドタグの変異に特有のbuf breakingルールを実装し、デシリアル化されたオブジェクトではなくバイナリワイヤ形式(ヘックスダンプやprotobuf-text-formatを使用)で主張するユニットテストを記述し、gRPCリフレクションサービスがバージョン間で一貫したフィールド番号を返すことを検証することが含まれます。テストはまた、名前が重要であるJSONトランスコーディングシナリオ(EnvoyやgRPC-Gatewayで一般的)をカバーする必要があり、REST-to-gRPC翻訳レイヤーのために別の検証が必要です。
gRPCストリーミングメソッド(サーバー側、クライアント側、双方向)を契約テストでどのようにテストしますか?単一のRPCメソッドと比較して?
ユニシメソッドは、単一のリクエスト/レスポンスペイロードを検証しますが、ストリーミングはメッセージの順序、フロー制御(バックプレッシャー)、接続ライフサイクル管理の周りで複雑さを導入します。サーバー側ストリーミングの場合、契約テストは消費者が部分ストリームの失敗を処理できることを検証し、適切なコンテキストキャンセレーションの伝播を実装する必要があります。クライアント側ストリーミングに対しては、テストはサーバーが正しくメッセージを蓄積し、ストリーム終了(半クローズ)イベントを適切に処理できることを検証する必要があります。双方向ストリーミングは、メッセージの相互交換と、長期間接続でのタイムアウト処理をテストする必要があります。実装には、手動検証のためのgRPCurl、ストリームスループットの負荷テスト用のghz、メッセージシーケンスを記録するためにストリーミングをサポートするPact v4を使用します。見逃されている重要な側面には、ストリームが異常に終了したときのリソースリークをテストすること(アクティブなストリーム数を示すPrometheus/grpcクライアントメトリクスで確認)、およびストリーミングコンテキストでのDeadline伝播が正しく機能することを確認し、生産でハング接続を防ぐことが含まれます。