我正在开发一个 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 Stream
和End 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)。