TCP 重传导致工业相机通信中断的原因与排查

· · TCP, 网络, 故障排查, Windows开发, 工业相机

在工业相机或设备控制的通信中,最麻烦的现象就是 平均速度很快,但偶尔会停顿数秒。由于复现率很低,平时又不会出现任何异常,UI、线程、GC、相机 SDK、NIC、交换机——所有环节看起来都或多或少有点可疑。

本文要讨论的是控制工业相机的应用与主机之间的 TCP 通信中,偶尔会出现数秒通信停顿的现象。经过调查后发现,其真正原因并不是应用停止,而是 由丢包引发的 TCP 重传等待。此外,通过启用 RFC1323 系列的时间戳功能(在当前的整理中对应 RFC 7323),在这套系统中能把等待时间压缩到最低限度。

文中的设备名称、架构和数值都做了一般化处理,但思路本身可以直接用于实务。

目录

  1. 先说结论(一句话)
  2. 症状的表现
    • 2.1. 应用本身正常运行,但响应偶尔会停顿数秒
    • 2.2. 由于发生频率低,仅凭日志很难察觉
  3. 到底发生了什么(图)
    • 3.1. 从丢包进入重传等待
    • 3.2. 秒级停顿与 RTO 的模式相吻合
  4. 排查时关注的要点
    • 4.1. 首先排除应用内部的停顿因素
    • 4.2. 通过抓包确认重传
    • 4.3. 查看协商到的 TCP 选项
  5. RFC1323 时间戳为何有效
    • 5.1. 时间戳是为了 RTTM 与 PAWS
    • 5.2. 可以消除重传时 RTT 测量的模糊性
    • 5.3. 本案例中能够压缩等待时间的原因
  6. 实际采取的对策
    • 6.1. 启用时间戳
    • 6.2. 在 SYN / SYN-ACK 中确认 TSopt
    • 6.3. 即使这样仍无效时该查看的地方
  7. 在 Wireshark 中要看的要点
  8. 大致的使用区分
  9. 总结
  10. 参考资料

1. 先说结论(一句话)

  • 偶尔停顿数秒的 TCP 通信,其真正原因有时并不是应用停止,而是 丢包之后的重传等待
  • 如果在抓包中能看到 Retransmission 以及明显的时间差,且停顿时长与 RTO 的等待方式相吻合,那就相当可疑
  • TCP timestamps option 是为 RTT 测量与 PAWS 而设计的机制,也可以消除重传时 RTT 测量的模糊性
  • 在本案例中,通过启用 RFC1323 系列的时间戳功能,减少了 RTO 估计过度保守而持续偏高的时间,把秒级停顿压缩到了最低限度
  • 不过,这并不是 能消除丢包本身的魔法。物理层、NIC、交换机、中间设备、驱动、缓冲区设计等方面的排查仍然是另外需要做的事

总而言之,如果「偶尔停顿数秒」的真正原因是 TCP 内部的等待时间,那么单靠应用层的重试再努力也是打不到点上。先查看 wire(线路上的实际数据),确认是否处于重传等待状态,会更快。

2. 症状的表现

2.1. 应用本身正常运行,但响应偶尔会停顿数秒

最初让人费解的地方在于,整个应用看起来并没有卡死。

  • UI 没有完全失去响应
  • 进程也没有崩溃
  • CPU 也没有被占满
  • 但是,相机控制命令的响应 偶尔 会消失数秒

这类症状与应用内部的 deadlock 或死循环很难区分。而且在设备控制场景中,一次数秒的停顿就会直接给人「产线停止」的印象。即便平均值看起来很正常,现场的实际体感也会相当差。

2.2. 由于发生频率低,仅凭日志很难察觉

这类故障麻烦的地方在于发生频率很低。表现出来可能是一小时一次、半天一次,或者只在多个条件叠加时才出现。

如果只靠日志追踪,大致会是这样:

  • 应用日志上停在「已发送」「没有返回」
  • 接收方日志看起来「什么都没收到」
  • 恰好同一时间段又发生了其他事件,导致「嫌疑对象」变得分散

在这种情况下,如果只靠应用日志来还原因果关系,很容易陷入泥潭。下降到通信层来查看会更快。

3. 到底发生了什么(图)

3.1. 从丢包进入重传等待

这次的脉络很简单:数据包在传输途中的某处丢失,发送方在等待 ACK,由于一直没有收到,就等待 RTO 超时后再进行重传。

相机侧网络主机应用相机侧网络主机应用在此处发生丢包因为没有收到 ACK 所以等待本次请求的恢复中包含 RTO 等待通信在此处恢复控制命令 (Seq=N)重传控制命令重传数据包到达ACKACK

从应用的角度来看,这呈现为「停顿了数秒」,但从 TCP 的角度来说,只是「因为还没收到 ACK,所以在等待重传计时器超时」而已。虽然不起眼,但这种停顿方式其实很常见。

本次的控制通信以小型的 request/response 为主,一次交互中并没有发送大量未经 ACK 确认的数据。因此,在能够收集到足够的 duplicate ACK 从而触发 fast retransmit 之前,RTO 等待 更容易先表现出来,这正是本次系统的构成特点。

3.2. 秒级停顿与 RTO 的模式相吻合

TCP 的重传等待尽管在具体实现上略有差异,但整体上都采用比较保守的等待方式。根据 RFC 6298,初始 RTO 以 1 秒为基准,如果计算结果小于 1 秒则向上取整为 1 秒,并且每次超时都会翻倍。

丢包未收到 ACKRTO 等待重传是否收到 ACK?通信恢复RTO 翻倍

因此,即便本该在几百毫秒内结束的场景,在条件不利时也可能呈现出 1 秒、2 秒、4 秒这样的等待模式。本次「偶尔停顿数秒」的现象,与这种模式相当契合。

4. 排查时关注的要点

4.1. 首先排除应用内部的停顿因素

并没有一开始就断定是 TCP 的问题,而是先排除了应用侧的典型因素。

确认对象 排查理由 本次结论
UI 线程 / 工作线程 确认是否存在挂起或相互等待 并非主要原因
CPU 使用率 确认是否因高负载导致处理延迟 停顿时也没有出现占满的情况
GC / 内存压力 确认是否存在暂停 停顿时长的形态不吻合
相机 SDK 调用 确认 SDK 内部是否存在等待 与 wire 上的延迟不一致
抓包 确认通信层的重传情况 在此处看清了原因的脉络

这里重要的是,不要仅凭应用日志的时间戳来判定「元凶」。在设备控制类应用中,上层的等待往往只是下层等待的一种映射而已。

4.2. 通过抓包确认重传

抓包之后可以看到,在通信停顿的时间段内出现了 TCP Retransmission,并且能确认在此之前 ACK 一直没有返回。

需要重点关注的地方包括:

  • 是否出现了同一个 Seq 的重传
  • 重传发生前的时间差是否与停顿时长一致
  • 看起来是不是 RTO 超时等待,而不是 Dup ACKFast Retransmission
  • 出问题的连接是否每次都对应同一个 tcp.stream

如果这些都吻合,那么「不是应用停止,而是 TCP 在等待重传」的可能性就会大大增加。

4.3. 查看协商到的 TCP 选项

接下来查看的是连接建立时的 SYN / SYN-ACK。由于时间戳是在 TCP 连接的 3-way handshake 中协商的,如果这里没有出现 TSopt,那么该连接就不会使用时间戳功能。

相机侧主机相机侧主机只有在此处协商成功,之后的分段才能使用 TSoptSYN + TSopt ?SYN/ACK + TSopt ?ACK

如果不查看这一步,只是改动 OS 的设置,就会出现「明明启用了却没生效」这种同样不起眼的问题。相比设置值,wire 上的实际情况才更有说服力。

5. RFC1323 时间戳为何有效

在实务中,「RFC1323 时间戳」这种叫法目前仍然沿用,但当前正式的整理版本是 RFC 7323。本文为了遵循习惯用法而写作 RFC1323,但含义上指的是 TCP timestamps option。

5.1. 时间戳是为了 RTTM 与 PAWS

TCP 的 timestamps option 主要用于两个目的:

  • RTTM(Round-Trip Time Measurement,往返时延测量)
  • PAWS(Protect Against Wrapped Sequences,防止序列号回绕)

本次真正起作用的是 RTTM 一侧。发送方发出的分段中携带 TSval,对方在 ACK 中通过 TSecr 返回该值,这样发送方就能更精细、更准确地测量 RTT。

5.2. 可以消除重传时 RTT 测量的模糊性

一旦发生重传,如果没有时间戳,就会出现「这个 ACK 到底是针对最初的发送,还是针对重传」这种模糊性。这正是所谓 Karn 算法所关注的问题。

RFC 6298 规定,对于被重传过的分段不应该采集 RTT 样本,原因就在于无法判断这个 ACK 究竟对应哪一次发送。不过,如果有 timestamps option,就可以消除这种模糊性,因为只要查看 ACK 中携带的 TSecr,就能识别出到达的是携带哪个 TSval 的分段。

接收方发送方接收方发送方该分段发生丢失因为没有收到 ACK 所以等待可以判断这是对哪次发送的响应Seq=N, TSval=1000重传 Seq=N, TSval=2000ACK, TSecr=2000

这正是本次改善的核心所在。

5.3. 本案例中能够压缩等待时间的原因

在本案例中,丢包偶尔发生,每次发生时 RTT / RTO 的估计都容易偏向保守。启用时间戳之后,即便在包含重传的场景下,也更容易更新 RTT 的估计值,从而抑制 RTO 估计值持续偏高、迟迟得不到修正的时间。

换句话说,本次所做的并不是让 TCP 变快的魔法,而是 减少 TCP 不必要地长时间「观望」的时间

当然,RFC 7323 并没有说「只要增加 RTT 样本,一切问题都能完美解决」。它对 RTO 优化的效果确实存在一定的局限性。不过,能够消除重传时的模糊性 这一点,在类似本次这样的系统中确实能够直接见效。

也有一些需要注意的地方:

  • 这在一定程度上取决于 TCP stack 的具体实现
  • 仅靠时间戳并不能消除丢包本身
  • 如果物理层或中间设备存在问题,根本原因还在别处
  • SACK、NIC 驱动、offload 设置、交换机侧的问题,也需要单独排查

不过,像本次这样「丢包并非为零」「但真正让人头疼的是秒级等待」的系统,这种做法确实能相当有效。

6. 实际采取的对策

6.1. 启用时间戳

作为对策,让连接两端都能够协商 timestamps option。在 Windows 系统中,这有时会被作为 RFC 1323 选项来处理,并受到 OS 设置和网络设置的影响。

不过,在实务中,比起「设置界面上显示为已启用」,「SYN / SYN-ACK 的实际数据包中携带了 TSopt」这件事更为重要。这一点确实如此。

6.2. 在 SYN / SYN-ACK 中确认 TSopt

启用之后,确认了以下 3 点:

  • 出问题的连接的 SYN 中是否带有 TSopt
  • SYN/ACK 一侧是否也返回了 TSopt
  • 之后的数据分段和 ACK 中是否持续携带 TSopt

只有确认了这几点,才能说「该连接实际上确实在使用 timestamps」。

6.3. 即使这样仍无效时该查看的地方

即便启用了时间戳,在以下情况中改善效果也可能不明显:

  • 丢包率本身就很高
  • 中间设备破坏、丢弃或篡改了 TCP 选项
  • NIC / 驱动 / offload 相关存在其他问题
  • 应用把整体流程都挂在一次同步调用上,导致一次等待看起来就是整体停止
  • 实际上主因并非 TCP,而是相机侧处理停滞或设备内部队列拥堵

因此,按下面的顺序推进对策会比较清晰:

  1. 先通过 wire 确认是否处于重传等待
  2. 查看是否协商了 TSopt
  3. 启用 timestamps 并观察改善的差异
  4. 如果问题依然存在,就分别排查丢包源与应用设计

7. 在 Wireshark 中要看的要点

下面列出一些便于排查的显示过滤器。

tcp.stream eq <目标流>
tcp.analysis.retransmission
tcp.analysis.fast_retransmission
tcp.analysis.lost_segment
tcp.options.timestamp.tsval
tcp.options.timestamp.tsecr

查看时有几个技巧:

  • tcp.stream 把范围缩小到目标连接
  • 显示 Time delta from previous displayed packet,直接查看停顿的秒数
  • 确认问题发生的瞬间是否出现了 Retransmission
  • 确认连接建立时的 SYN / SYN-ACK 是否协商了 TSopt
  • 查看 ACK 的 TSecr 是否已经返回

在把日志与数据包对照时,还需要注意应用时间与抓包时间之间的基准差异。如果这里出现偏差,很容易把无关的事件当成「元凶」。

8. 大致的使用区分

症状 首先怀疑的对象 最先要做的事
偶尔以数秒为单位停顿 TCP 的 RTO 等待 通过抓包确认重传与时间差
每次几乎在相同时机停顿 应用内部等待、设备侧处理、固定超时 查看线程、SDK 调用、设备日志
只在高负载时恶化 CPU、GC、队列拥堵 查看 CPU、中断、内存、队列长度
大范围连接同时恶化 物理层、交换机、中间设备 查看 NIC、线缆、端口统计、中间设备日志
修改了设置却没有变化 TCP option 没有被协商 重新确认 SYN / SYN-ACK

最后一行的情况其实非常常见。「改动了设置」带来的满足感,和「wire 上实际在使用」这一事实,是两件不同的事。

9. 总结

本次的要点:

  • 「偶尔停顿数秒」有时并不是应用停止,而是 TCP 处于重传等待
  • 如果停顿时长与 RTO 的等待方式吻合,并且能看到 Retransmission,那么这条思路相当靠谱
  • TCP timestamps option 是为 RTTM 和 PAWS 而设计的机制,也能消除重传时 RTT 测量的模糊性
  • 在本案例中,通过启用 RFC1323 系列时间戳,抑制了 RTO 过度保守而持续偏高的时间

应当避免的做法:

  • 只凭应用日志就判定通信停顿的元凶
  • 只看 OS 设置,不查看实际数据包
  • 以为启用时间戳就能连丢包原因也一并消除

实务中真正有效的做法:

  • 先查看 wire
  • 确认重传与等待时间的模式
  • 确认 TSopt 的协商情况
  • 即便改善之后,丢包源与应用设计仍需分开排查

也就是说,这类问题「找准在哪里等待」比「让它变快」更重要。只要不在这一点上出错,排查过程就会大幅缩短。

10. 参考资料

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

什么是 TCP Retransmission(TCP 重传)?
当发送的数据包没有收到对应的 ACK(确认应答)时,TCP 会重新发送同一份数据的机制。如果数据包在传输途中的某处丢失,发送方会等待 ACK,如果 ACK 一直没有到达,就会等待重传计时器(RTO)超时后再进行重传。从应用程序的角度来看,这会呈现为「停顿了几秒」,但从 TCP 的角度来说,只是「因为还没收到 ACK,所以在等待重传计时器超时」而已,这种停顿方式其实很常见。在抓包中可以观测为 TCP Retransmission。
TCP Retransmission 发生的原因是什么?
直接的触发原因是丢包,由于 ACK 没有返回而发生重传。至于丢包的来源,需要分别排查物理层(线缆或端口)、NIC 及其驱动 / offload 设置、交换机或中间设备等问题。虽然启用 TCP timestamps 等手段可以压缩重传后的等待时间,但这并不是能消除丢包本身的魔法,因此仍需单独排查丢包源。此外还存在中间设备破坏或丢弃 TCP 选项的情况,也有实际上主因并非 TCP,而是设备侧处理停滞的情况。
为什么 TCP 重传会导致通信停顿长达几秒?
这是因为 TCP 的重传计时器(RTO)采用了比较保守的等待方式。根据 RFC 6298,初始 RTO 以 1 秒为基准,即使计算结果小于 1 秒也会向上取整为 1 秒,并且每次超时都会翻倍。因此在条件不利时,等待时间会呈现出 1 秒、2 秒、4 秒这样的模式。在以小型 request/response 为主的控制通信中,往往还没能收集到足够的 duplicate ACK 来触发 fast retransmit,RTO 等待就已经先表现出来了,从而呈现为偶发的数秒停顿。启用 TCP timestamps option 后,可以消除重传时 RTT 测量的模糊性,从而在某些情况下抑制 RTO 估计过度保守而持续偏高的时间。
在 Wireshark 中该如何排查 TCP Retransmission?
可以使用 tcp.analysis.retransmission、tcp.analysis.fast_retransmission、tcp.analysis.lost_segment 等显示过滤器。先用 tcp.stream 把范围缩小到目标连接,再显示 Time delta from previous displayed packet 来直接确认停顿的秒数,观察问题发生的瞬间是否出现了 Retransmission,以及重传的时间差是否与停顿时长一致。是否使用了 TCP timestamps,需要通过连接建立时的 SYN / SYN-ACK 是否协商了 TSopt 来确认。比起在设置界面上显示为「已启用」,实际数据包中是否携带 TSopt 这一事实才更重要。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表