物理机网卡设置为 tx off 导致 TcpRetransFail 爆炸

2 阅读4分钟

root@node3:~# ansible all -m shell -a "nstat -az | grep -Ei 'TcpAbortOnTimeout|TcpTimeouts|TcpRetransFail'"
node5 | CHANGED | rc=0 >>
TcpExtTCPTimeouts               1188892            0.0
TcpExtTCPAbortOnTimeout         290782             0.0
TcpExtTCPRetransFail            325                0.0
node3 | CHANGED | rc=0 >>
TcpExtTCPTimeouts               788233             0.0
TcpExtTCPAbortOnTimeout         253865             0.0
TcpExtTCPRetransFail            39                 0.0
node6 | CHANGED | rc=0 >>
TcpExtTCPTimeouts               89526              0.0
TcpExtTCPAbortOnTimeout         24005              0.0
TcpExtTCPRetransFail            34                 0.0
node4 | CHANGED | rc=0 >>
TcpExtTCPTimeouts               912459             0.0
TcpExtTCPAbortOnTimeout         275961             0.0
TcpExtTCPRetransFail            82454              0.0

Compacted conversation## 分析:事故 24h 的重传倍率

关键指标是 TcpRetransFail(重传最终失败 = 底层发不出去),node4 是唯一的异常节点:

原始数据归一化

节点Uptime (天)TcpRetransFailRetransFail/天TcpTimeouts/天AbortOnTimeout/天
node46282,4541,33014,7174,451
node5623255.219,1764,690
node355390.714,3314,612
node660340.61,492400

TcpRetransFail — 事故指纹

node4 的 TcpRetransFail 是其他节点的 ~620 倍(82,454 vs 平均 133)。

将异常量归因到 24h 事故窗口:

正常基线(peer 均值):   (5.2 + 0.7 + 0.6) / 32.2 次/天
node4 正常 62 天预期:   2.2 × 62136 次
事故产生的异常量:        82,454 - 13682,318 次(24h 内)

正常 24h 速率:  2.2 次/天
事故 24h 速率:  82,318 次/天

倍率:82,318 / 2.237,000

结论:事故期间 TcpRetransFail 速率是正常的 ~3.7 万倍

为什么只有 TcpRetransFail 爆炸?

指标含义node4 是否异常原因
TcpRetransFailtcp_retransmit_skb() 底层发送失败是,3.7万倍TSO/GSO off → 段数爆炸 → 重传队列溢出 → 底层无法发出
TcpTimeouts单次 RTO 超时触发否(与 node3/node5 相当)peer 节点也在等 node4 的回包,双向超时
TcpAbortOnTimeout连接因超时被 abort否(与 node3/node5 相当)同上,peer 侧也在 abort 连接

注意 node5/node3 的 TcpTimeouts 和 AbortOnTimeout 也很高 — 它们是 node4 连接震荡的 受害者(peer 侧超时),而 node6 数值低说明它与 node4 的交互较少。

TcpRetransFail 是 TSO/GSO off 的独特指纹:只有发送端(node4)会因段数爆炸导致 tcp_retransmit_skb() 在底层失败,接收端不会。

这不奇怪,这恰好是 TSO/GSO off 的病理特征

丢包低 + 重传炸 = 本地 TCP 栈溢出,不是线路问题

正常路径(TSO on):
  app write 64KB → NIC 硬件分段 → 1 个 GSO 段 → wire → 正常

故障路径(TSO off):
  app write 64KB → 内核软分段 → 441460B 段 → wire → 正常(不丢包!)
                                    ↓
                          但 TCP 需要逐段跟踪/ACK/重传
                          sk_wmem_alloc 爆炸
                                    ↓
                          RTO 触发 tcp_retransmit_skb()
                          → sk_wmem_alloc > sk_sndbuf
                          → return -ENOMEM
                          → TcpRetransFail++

核心机制tcp_retransmit_skb() 在尝试重传时检查 socket 写缓冲区:

// net/ipv4/tcp_output.c
if (atomic_read(&sk->sk_wmem_alloc) > sk->sk_sndbuf) {
    TCP_INC_STATS(TCP_MIB_RETRANSFAIL);  // ← 这就是 82,454 的来源
    return -ENOMEM;
}

段数 44 倍 → 每个段占 sk_buff + skb_shared_info(~240B 开销)→ wmem 消耗暴涨 → 重传根本发不出去

所以数据完全自洽

现象原因
丢包低线路/交换机正常,包确实发出去了
TcpTimeouts node4 不异常RTO 超时次数正常,peer 侧也有相同量级
TcpRetransFail node4 爆炸 3.7万倍只有发送端才会 wmem 耗尽,重传发不出
peer 节点 RetransFail 正常peer 的 TSO 没关,段数正常,wmem 够用

这就是为什么 TcpRetransFail 是 TSO/GSO off 的独特指纹 — 它不反映线路质量,而反映发送端 TCP 栈在段数爆炸下的本地资源耗尽。连接不是被"丢包"杀死的,是被"重传发不出"饿死的。

只有发送端才会 wmem 耗尽,重传发不出发送不是 tx 么? 为什么 rx 丢包?

好问题。这里有两个独立的症状链,不是因果关系:

为什么 tx 问题会出现 rx 丢包

根因:ethtool -K bond0 tx off(node4)
         │
         ├─→ 症状链 1(tx 侧):段数 44x → sk_wmem 耗尽 → TcpRetransFail 3.7万倍
         │   ↑ 这不是"丢包",是本地 socket buffer 满了发不出去
         │
         └─→ 症状链 2(rx 侧):node4 发出 44x 小包 → peer 节点 NIC rx ring 溢出
                                                       → peer 侧 rx drops

关键区分

TcpRetransFailrx 丢包
发生在哪node4 本地(发送端)peer 节点(接收端)
机制sk_wmem_alloc > sk_sndbuf,重传包没进 qdiscNIC rx ring buffer / softirq budget 耗尽
算不算丢包不算,包根本没到线路上,包到了 NIC 但被丢弃
谁的 counternode4 的 TcpRetransFailpeer 的 ethtool -S rx_dropped / ifconfig RX dropped

所以 "丢包低但重传炸" 不矛盾:

  • node4 tx 不会有 wire drops — 小包确实发出去了,NIC 能发 1460B 的包没问题
  • node4 TcpRetransFail 不是丢包 — 是重传包在本地 socket buffer 层就被拒绝了
  • peer rx drops 可能有一些 — 但 44x 小包未必超过现代 NIC ring buffer 容量,所以 "丢包低"

一句话:tx off 的直接后果不是丢包,是段数爆炸导致 TCP 栈内部资源耗尽。线路是通的,NIC 是好的,包也确实发出去了 — 只是 TCP 重传机制被自己的 wmem 压力卡死了。