隧道上的交互式 shell 在一次文件复制运行时冻结数秒,随后又若无其事地恢复。出问题的只有下载,可 shell 也跟着等待,而且两者在同一时刻恢复。这一模式正是单条可靠流承载众多会话的特征,而且它几乎经受得住人们最先尝试的每一种调参。

这个症状值得细读,因为它指向的是机制而不是某个设置:停顿是共享的,所以原因是共享的,而共享的对象就是外层连接。事情发生时,日志里的其他一切看起来都正常。

症状:所有东西同时停顿

两条内层连接一起卡住就是线索。缓慢的下载和缓慢的 shell 是两条独立的 TCP 会话,各有各的序号和拥塞状态,所以其中一条出故障不该影响另一条。当它们步调一致地停顿时,是它们上方的某个东西在给它们的字节排序。

那个东西就是隧道的传输层。在 TCP 模式下,OpenVPN 把封装后的数据包写入一条通往对端的 TCP 连接,而 TCP 的契约是单条字节流按序交付。后面的字节无论属于哪条内层会话,都不会在缺失的字节之前交给应用。

注意症状没有告诉你什么。它并不说明路径坏了、服务器过载了,或者加密太慢。那些会呈现不同的形态:整齐偏低的吞吐、超时,或只影响一个方向的故障。因某个丢失的段被重传而结束的共享停顿,恰恰就是排序效应。

一条外层字节流,众多内层会话

设想隧道同时承载一次下载和一个 shell。外层连接的一个段丢失了。后续的段可以到达接收主机,接收端的 TCP 栈也会确认它们,但在空洞被填补之前无法向上交付。因此 OpenVPN 拿不出任何东西给 shell 读,尽管 shell 自己的字节早就到了。

这就是单条连接内部的队头阻塞,它并不要求两个应用共享一条内层连接。它们共享的是外层连接,这就够了。

二阶效应让它更糟。看不到确认的内层发送方可能重传外层已经在设法交付的数据。这种重传在一条刚刚证明会丢段的路径上变成额外的隧道流量,于是恢复工作与真正要紧的恢复相互竞争。

这一切都不保证会崩溃。在往返时间短、队列宽裕的干净路径上,这种安排可以多年完全可用。故障在丢包、延迟与队列深度同时具备时出现,这也是报告集中在拥塞的上行链路和移动链路上、而不是配置良好的办公室里的原因。

为什么加长内层定时器帮不上忙

用 rto_min 提高某条路由上的最小重传超时,会把使用该路由的本地产生的 TCP 流量的定时器重传往后推。这是一个确有实效的真实旋钮,它针对的是二阶问题而不是一阶问题。

ip route show
ip route show cache 2>/dev/null | head
ss -tin dst 10.70.0.1

它做不到的是:让外层字节乱序放出、更快找回丢失的段,或者让对端的应用服务器采用同一个定时器。接收端协议栈仍然按序交付,远端的发送方仍然使用自己的往返测量。

它也不会关掉其他每一种丢包恢复机制。在真正的丢包需要定时器恢复的地方,更大的最小值会让应用等更久才行动,这与对延迟敏感的会话所需要的正好相反。

十二秒的固定下限出现在这个主题的建议里,仿佛存在一条 TCP 隧道就理所应当。RFC 6298 中描述的重传定时器源自实测往返时间并采用指数退避,所以固定下限是针对特定路径的策略选择,而非协议要求。广泛套用会掩盖症状,同时改变该路由覆盖的每条流的恢复方式。

tcp-nodelay 做得到与做不到的事

在传输套接字上关闭 Nagle 式缓冲的隧道选项有一个狭窄的用途:阻止小的写入在发送前等待更多数据。这有助于交互式流量在本来健康的路径上避免额外延迟。

它不会把传输变成无序的,也不会缩短丢段之后的恢复。因重传超时而停顿的 shell 在开启该选项时仍会停顿,因为这个选项从未触及排序规则。

两者容易混淆,因为都被描述成延迟修复。一个消除的是缓冲带来的额外等待,另一个则需要改变传输所保证的东西,而任何套接字选项都做不到这一点。

如何公平地比较传输方式

如果运营方允许且两种服务都存在,就在到同一台服务器的同一条接入路径上,比较承载同样工作负载的 UDP 服务与 TCP 服务。在两次测试之间更换国家、提供商或服务器代次,会引入一个将主导结果的变量。

要观察共享停顿,而不是某个吞吐数字。外层丢包期间多条内层应用一起卡住,才是识别队头阻塞的观察结果;单次测速数字偏低则不是。

在隧道接口和物理接口上进行的获准抓包能干净地区分这两层,因为内层与外层的重传属于不同的连接、不同的序号空间。在外层流显示空洞时看到内层重传,就是直接证据。

改用 UDP 会去掉那条额外的可靠流,让内层 TCP 自行恢复。它并不承诺零丢包或低延迟,饱和的上行链路、过大的队列或 MTU 问题仍然需要处理。隧道之外的流量永远不会被挡在隧道的序号空洞后面,但当两条路径共用一条拥塞链路时它仍可能受影响,而这个共享瓶颈值得单独调查,而不是并进这里。

相关文章