在 Ubuntu 客户端 (curl) 和服务器之间的 XML 对话期间,TCP 重传导致超时

TCP Retransmissions causing timeouts during XML conversation between Ubuntu client (curl) and server

我正在尝试确定并解决下面屏幕截图中概述的 ACK FIN 重传背后的原因。我是 运行 一个 ubuntu 18 apache 服务器 PHP,它通过 PHP curl (PHP 7.3) 与 XML 服务提供商通信.在 XML 对话期间,有问题的服务器似乎会随机超时。这会影响 xml 服务超时或 returning 不完整的 xml 结果集。 另一位开发人员告诉我他们遇到了类似的问题,并求助于 PHP 循环多次重试失败的请求,直到服务正确响应(从他们自己的客户端开发箱)。这指出了服务器端可能有问题的理论,但是在我排除所有可能性之前我不能接受这个。(如果其他开发人员使用与我相同的客户端 OS 怎么办?等等)

我已经在我的本地 windows 机器上通过 postman 进行了广泛的测试,但是我无法让 timeouts/errors 在这里发生。不确定 postman 是否有某种纠错机制,或者我的 windows 本身似乎更适合服务器计算机的 TCPIP 堆栈。

到目前为止我尝试过的: - 将连接从 https 断开到 http,这样我就可以在 Ubuntu 客户端上使用 wireshark 进行捕获(同时排除 https 作为原因) - 将 MTU 从 9001(ec2 实例)分别更改为 1500 和 1492,问题仍然存在,服务服务器的 MTU 似乎是 1500 - 在 curl 上启用 keepalive,没有效果 - 在 curl 设置中尝试不同的超时和连接超时,没有效果 - 尝试在 php 中使用相同的循环来重试请求。如果在 curl 中重试,添加了一个标志以完全切断 tcp 连接,没有效果,重试的请求有时仍会超时,其他时候它们 return xml 中的预期。看似随意。在 20 个请求中,可能有 2 个请求失败。

备注: 似乎在客户端发送 post xml 请求后,服务器以 ACK 响应,但从未发送状态 200,因为我在请求失败时捕获了它。这似乎导致客户端重复 FIN ACK 重传,这里似乎发生了一些错误更正,但是这不会冒泡到 XML 层来呈现完整的请求,而是 CURL 在等待时发出超时回复。在 wireshark 中,我可以看到不完整的答案,即在第 42 行重建了大约一半的 xml。我唯一的预感是服务器可能是一个 windows 框,它可能以某种方式与 ubuntu 的 tcp ip 堆栈,或者它只是服务器上的一个错误,除了重复请求之外,无论我做什么都可能无法修复。

有什么想法吗?我不是 TCPIP 专家,所以一般只理解 FINS 和 ACKS :) 接下来你会尝试什么?

wireshark screenshot

从网络捕获中可以解码的内容很多。首先请注意,帧 46 和 47(后续 TCP 连接的开始,因为我们没有看到相关连接的开始)协商的最大段大小 (MSS) 为 1460。但是,帧 53 有 4380 字节(3 次MSS)和帧 55 有 7300 字节(5 倍 MSS)。这很可能是因为网络捕获是在客户端主机上进行的,并且在 NIC 或驱动程序上启用了某种形式的接收卸载。通常,最好从网络上获取网络捕获(例如,通过跨越交换机端口并以混杂模式从跨越端口上的 NIC 获取捕获)。所以请记住,我们在捕获中看到的并不完全是电线上的。

我们看到正在考虑的请求在第 34 帧中发送,然后我们看到它在 11 毫秒后得到确认(序列 510)。

然后大约 20 秒没有任何反应。这在 TCP 级别是完全正常的。尽我们所能知道没有未确认的数据传输(因此 TCP 不会关闭连接,即使启用了 TCP 用户超时也不会起作用),因此此连接可能会像这样闲置一段时间,直到 TCP keepalive 启动在(如果启用)。

然后,在第 36 帧中,客户端发送序列为 510 的 FIN。在 TCP 级别,这表示客户端已完成发送,这将由应用程序调用 close()shutdown() 在插座上。由于这几乎是 20 秒,所以它确实感觉像是应用程序级别的超时,虽然它似乎不是 PHP Curl 的默认超时,但你显然已经在搞乱各种设置。

我们现在希望服务器确认传输的 FIN 并发送它需要发送的任何数据,然后当服务器应用程序调用 close()shutdown() 时,将发送一个 FIN服务器。

但是服务器没有ACK客户端的FIN,客户端TCP栈重传FIN 5次。您可以看到在每个 FIN 之后重传时间加倍,假设它没有放弃,预计第 6 次重传大约需要 31.38 秒。在 Linux 上,我相信这是由 tcp_orphan_retries 控制的,默认为 8,所以我最好的猜测是它没有放弃。

最后,在第 42 帧中,服务器在预期下一次 FIN 重新传输之前开始说话。它确认序列 511,表明它收到了一个或多个 FIN。并且它包含一个完整的 MSS 有效载荷,从协议级别来说这很好。

那么现在问题来了,为什么服务器TCP栈到现在才ACK FIN?完整的 MSS 有效负载是重新传输还是第一次发送?让我们暂时不去猜测,因为捕获中还有其他有用的信息。

客户端立即响应 RST。这要么是因为 TCP 确实放弃了服务器并且连接不再存在,要么是因为应用程序在套接字上调用了 close(),因此 TCP 堆栈无法将数据传递给客户端应用程序,所有希望有序关机不见了。我猜是后者。

然后,令人惊讶的是,在第 44 帧中,我们从服务器获得了序列为 16061 的 FIN。 WAT???。第 42 帧发送了响应的前 1460 个字节,因此有 16060-1460 个字节的数据,相当于 10 个 MSS 丢失。这里显然存在数据包丢失(这就是 WireSharks 使用 "Previous segment not captured" 注释数据包的原因)。

我不认为 FIN 是重传,因为有很多未确认的数据要重传(相当于 11 MSS),而且 FIN 通常会在 close() 上及时传输即使有出色的未处理数据。所以我猜这个 FIN 是在服务器在套接字上调用 close() 时发送的。

因此,如果 FIN 不是重传,我也猜测在 42 帧前 10 毫秒收到的前 1460 个字节可能不太好。我在想服务器做了它的思考,将 11 MSS 的数据写入套接字并关闭了套接字,导致 11 个带有有效负载的数据包(不可否认,所有数据包都未被处理,但是当代 implementation/configuration 的 TCP "slow start" 将允许这样做),然后是 FIN,但由于某种形式的数据包丢失,其中只有两个成功通过。我进一步猜测 TCP 堆栈可能正在确认多个 FIN,但也可能会丢失数据包。

所以我的建议是增加 PHP Curl 的超时时间。我最好的猜测是服务器用了 20 多秒来计算它的回复,丢包和重新传输超时只会导致进一步的延迟。