HTTP モードで HAProxy を使用してレスポンス ストリーミング呼び出し中に gRPC エラーを受信できない

HTTP モードで HAProxy を使用してレスポンス ストリーミング呼び出し中に gRPC エラーを受信できない

私は、http モードを使用して HAProxy の背後で提供される gRPC アプリケーションに取り組んでいます。サーバー アプリケーションがすぐに (つまり、応答を送信する前に) 特定のエラーで応答ストリーミング呼び出しを中止した場合、クライアント アプリケーションはCANCELLED送信されたエラーではなくエラーを受け取ります。エラーの詳細は次のとおりです。エラーコード 8 で RST_STREAM を受信しました私は HAProxy 2.3.2 と grpc 1.34.0 を使用しています。

このようなリクエストごとに、HAProxyログエントリSD-- にはフラグが設定されます。切断時のセッション状態フィールド、例:

<134>Jan  9 18:09:39 1a8328663d74 haproxy[8]: 172.28.0.4:41698 [09/Jan/2021:18:09:39.346] grpc-server-fe grpc-server-fe/grpc-server-be 0/0/0/-1/+0 -1 +115 - - SD-- 1/1/0/0/0 0/0 "POST http://proxy:6000/service.Service/StreamStream HTTP/2.0"

HAProxy ドキュメントでは、これらのフラグは次のように定義されています。

  • S: TCP セッションがサーバーによって予期せず中止されたか、サーバーによって明示的に拒否されました。
  • D: セッションはデータ フェーズでした。

さらに:

SDデータ転送中にエラーが発生し、サーバーへの接続が切断されました。これは通常、haproxy がサーバーとデータを交換中にサーバーから RST を受信したか、中間機器から ICMP メッセージを受信したことを意味します。これは、サーバーのクラッシュまたは中間機器のネットワークの問題によって発生する可能性があります。

それを知っており、HTTP 接続がデータ ストリーミングを目的として開かれたことを念頭に置いて、回避策として、エラーを発生させる前に空の gRPC メッセージを送信してみました。この解決策は部分的には役立ちました。ほとんどのリクエストでクライアントがエラー コードを受信できたかもしれませんが、それでも時々問題が発生しました。

次のステップとして、Wireshark を使用してネットワーク トラフィックを検査しました。以下は、即時呼び出し中止イベント時に gRPC サーバーによって提供される HTTP 応答のトレースです。

HyperText Transfer Protocol 2
    Stream: SETTINGS, Stream ID: 0, Length 0
        Length: 0
        Type: SETTINGS (4)
        Flags: 0x01
        0... .... .... .... .... .... .... .... = Reserved: 0x0
        .000 0000 0000 0000 0000 0000 0000 0000 = Stream Identifier: 0
    Stream: HEADERS, Stream ID: 1, Length 88, 200 OK
        Length: 88
        Type: HEADERS (1)
        Flags: 0x05
            .... ...1 = End Stream: True
            .... .1.. = End Headers: True
            .... 0... = Padded: False
            ..0. .... = Priority: False
            00.0 ..0. = Unused: 0x00
        0... .... .... .... .... .... .... .... = Reserved: 0x0
        .000 0000 0000 0000 0000 0000 0000 0001 = Stream Identifier: 1
        [Pad Length: 0]
        Header Block Fragment: 88400c636f6e74656e742d74797065106170706c69636174...
        [Header Length: 120]
        [Header Count: 4]
        Header: :status: 200 OK
        Header: content-type: application/grpc
        Header: grpc-status: 7
        Header: grpc-message: Details sent by the server
    Stream: RST_STREAM, Stream ID: 1, Length 4
        Length: 4
        Type: RST_STREAM (3)
        Flags: 0x00
        0... .... .... .... .... .... .... .... = Reserved: 0x0
        .000 0000 0000 0000 0000 0000 0000 0001 = Stream Identifier: 1
        Error: NO_ERROR (0)

そこでサーバーはエラーの詳細とEnd StreamフラグEnd Headersをセットしたレスポンスヘッダーを送信します。そしてNO_ERRORコードでストリームを閉じます。https://stackoverflow.com/questions/55511528/should-grpc-server-side-half-closing-implicitly-terminate-the-client/55522312現時点ではすべて問題ありません。また、RFC7540 の翻訳HTTP/2 プロトコルに関して問題となる点は見つかりませんでした。

引用された gRPC サーバーの HTTP 応答の後に HAProxy 発信 TCP が続きACK、次に HAProxy がその応答をクライアントに送信します。

HyperText Transfer Protocol 2
    Stream: HEADERS, Stream ID: 1, Length 75, 200 OK
        Length: 75
        Type: HEADERS (1)
        Flags: 0x05
            .... ...1 = End Stream: True
            .... .1.. = End Headers: True
            .... 0... = Padded: False
            ..0. .... = Priority: False
            00.0 ..0. = Unused: 0x00
        0... .... .... .... .... .... .... .... = Reserved: 0x0
        .000 0000 0000 0000 0000 0000 0000 0001 = Stream Identifier: 1
        [Pad Length: 0]
        Header Block Fragment: 885f106170706c69636174696f6e2f67727063000b677270...
        [Header Length: 120]
        [Header Count: 4]
        Header: :status: 200 OK
        Header: content-type: application/grpc
        Header: grpc-status: 7
        Header: grpc-message: Details sent by the server
    Stream: RST_STREAM, Stream ID: 1, Length 4
        Length: 4
        Type: RST_STREAM (3)
        Flags: 0x00
            0000 0000 = Unused: 0x00
        0... .... .... .... .... .... .... .... = Reserved: 0x0
        .000 0000 0000 0000 0000 0000 0000 0001 = Stream Identifier: 1
        Error: CANCEL (8)

フラグとHEADERSフレームのすべてのコンテンツが配置されているため、エラーの詳細がクライアントに渡されますが、 のコードRST_STREAMが に変更されています。実際には、クライアントは予想されるすべてのデータを受信しますが、その後、で指定されているように、gRPC エラーにマップされているCANCEL予期しないものを受け取ります。RST_STREAM(CANCEL)CANCELLEDgRPC ドキュメント

さらに調査を進める中で、HAProxyのソースコードを参照しました。コードはh2_do_shutrの方程式mux_h2.c(カスタム HAProxy ビルドの実験により、実際にこの場所であることが証明されました)。関連するコード ブランチには次のコメントがあります。

最終応答がすでに提供されているため、このストリームは不要です。これは、アップロードが終了する前にサーバーが応答し、すぐに閉じた場合に発生する可能性があります (リダイレクト、拒否など)

以上が、私がこの問題に関して収集できた詳細です。問題が gRPC コア (HTTP2 ストリーム処理の点で乱雑すぎる) にあるか、HAProxy (RST_STREAMコードの書き換え時に不注意すぎる) にあるかは完全にはわかりません。最後の質問は、即時呼び出し中止のイベントで正しく動作するように HAProxy と gRPC コア サーバーの構成を調整するにはどうすればよいかということです。問題を再現する最小限の HAProxy 構成は次のとおりです。

global
    log stdout local0

listen grpc-server-fe
    bind *:6000 proto h2

    mode http
    log global
    option logasap
    option httplog

    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

    server grpc-server-be server:6000 proto h2

私は準備しました最小限の例を含むリポジトリシンプルな Python クライアントとサーバーが含まれています。また、docker-compose構成された HAProxy を含むネットワーク環境も含まれています。

関連情報