为什么 dotnet keepalive Http 连接在第二个请求中失败,并显示“服务器关闭了预期保持活动的连接”?

问题描述

我有一个 dotnet 框架应用程序,它向运行 Apache 的远程服务器执行 POST api 请求。它间歇性地失败并显示错误

The underlying connection was closed: A connection that was expected to be kept alive was closed by the server. 

这发生在通过 keepalive TLS 连接向服务器发出的第二个请求时,因此在负载较重的生产系统中更频繁地发生,而在开发环境中则不那么频繁或根本不发生。

我们已经尝试过:

  • 禁用 Expect: 100-Continue 标头行为 (ServicePoint.Expect100Continue = false)
  • 启用 TCP 保持活动状态 (ServicePoint.SetTcpKeepAlive())

禁用 HTTP keep-alive 似乎可以解决此问题。 (HttpWebRequest.KeepAlive = false)

有没有办法在不禁用 http keep-alive 的情况下解决这个问题?

解决方法

Apache 设置 KeepAliveTimeout 默认为 5 秒不活动,然后空闲的保持活动连接将关闭。 (https://httpd.apache.org/docs/2.4/mod/core.html#keepalivetimeout)

这会导致以下情况:

  1. dotnet 打开与 apache 的连接并发出 POST
  2. apache 返回 200 OK。
  3. 连接处于“空闲”状态,正在等待另一个请求。
  4. 在 2 秒后 dotnet 打开一个新的 HttpWebRequest 并在其上调用 GetRequestStream() 准备写入请求。由于池中有空闲连接,因此使用该连接。
  5. 5 秒后 (KeepAliveTimeout),apache 发送一个 FIN 数据包以关闭底层连接。
  6. (比如说)30 秒后,dotnet 尝试写入到流中,该流尝试使用现已失效的套接字并立即失败并显示 The underlying connection was closed: A connection that was expected to be kept alive was closed by the server.

这在大型 POST 调用(例如调用 SOAP API)中尤其成问题,其中形成有效负载可能需要大量时间。

可能的解决方案是:

  1. 在开始发送数据之前不要调用 HttpWebRequest.GetRequestStream()
  2. 禁用保持活动状态 (HttpWebRequest.KeepAlive = false)。但是请注意,如果您的应用程序中的任何其他线程正在使用 keep-alive,则会出现问题(上述两个请求可以在完全不同的线程中)
  3. 最可靠的解决方案似乎是实施应用级重试。

请注意,这种行为(“将流锁定到套接字”)似乎只发生在 dotnet 框架中,而不是 dotnet 5/core 中。