在 HTTP 模式下使用 HAProxy 进行响应流式调用期间无法收到 gRPC 错误

在 HTTP 模式下使用 HAProxy 进行响应流式调用期间无法收到 gRPC 错误

我正在开发一个 gRPC 应用程序,它将使用 http 模式在 HAProxy 后面提供服务。如果服务器应用程序立即(即在发送任何响应之前)因特定错误中止响应流式调用,则客户端应用程序将收到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:会话处于DATA阶段。

此外:

标准差与服务器的连接在数据传输过程中因错误而中断。这通常意味着 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 StreamEnd Headers设置标志的响应标头。然后用NO_ERROR代码关闭流。根据提供的答案https://stackoverflow.com/questions/55511528/should-grpc-server-side-half-closing-implicitly-terminate-the-client/55522312目前一切正常。我也简要回顾了RFC 7540并且没有发现任何与 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已更改为CANCEL。实际上,客户端收到了所有预期的数据,但之后它收到了意外的RST_STREAM(CANCEL),正在映射到 gRPCCANCELLED错误,如中所述gRPC 文档

在进一步调查的过程中,我参考了 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)。

相关内容