TCP 网络性能排查实战:两个经典案例的深度学习总结

TCP 网络性能排查实战:两个经典案例的深度学习总结

概述

本文总结了两个真实的 TCP 网络性能问题排查案例,涉及 Kafka 消息队列在跨机房和同机房场景下的延迟问题。两个案例从不同角度揭示了 TCP 缓冲区、拥塞控制、内核参数对应用性能的深远影响。


案例一:跨机房 Kafka 消费延迟高达 10 分钟

问题描述

每天上午 9:30 开盘后,位于 Region-B 的 Kafka 消费者从 Region-A 的 Kafka Broker 拉取消息时,延迟不断增高到 10 多分钟,持续数小时才恢复。而 Region-A 同机房的消费者拉取同一 Topic 完全正常。

排查过程

整个排查经历了多个阶段,逐步逼近根因:

阶段一:排除基础设施问题

  • Broker/Client 进程状态、机器资源(CPU/内存/磁盘)均正常
  • 网络带宽充足,丢包率正常,wget 测试带宽足够
  • 初步怀疑方向:网络链路本身

阶段二:发现拥塞窗口异常(TCP 层面)

在客户端机器上抓包分析,发现关键线索:

1
2
3
4
5
6
# 查看 TCP 连接的拥塞窗口
ss -itmpn dst "x.x.x.x"

# 关键输出(问题连接)
cwnd:2 ssthresh:2 ← 拥塞窗口极小,只有 2 个 MSS
send 170.0Kbps ← 发送速率极低

RTT 约 130ms,拥塞窗口只有 2(约 2.9KB),理论吞吐量:2 × 1448 / 0.13 ≈ 22KB/s,远不够传输开盘时的行情数据。

尝试通过 wget 大文件把 ssthresh 撑大,然后重启客户端新建连接,问题暂时缓解。但第二天又复发。

阶段三:发现内核虚假重传(深入内核层面)

通过 Wireshark 分析抓包文件,发现一个异常现象:

Broker 发送响应后,如果 10ms 内没收到 ACK,就触发重传。而正常的 RTO 应该是 200ms+。

这种 10ms 的重传远低于 RTO,也不是快速重传或 TLP。最终定位到 Linux 3.10 内核的 tcp_early_retrans 参数:

1
2
3
4
5
6
# 查看当前值
sysctl net.ipv4.tcp_early_retrans
# 输出: net.ipv4.tcp_early_retrans = 3

# 这是 3.10 内核 backport 新版本 early retrans 功能时引入的 bug
# 导致大量虚假重传,进而让拥塞控制算法误判网络拥塞,不断缩小 cwnd

修复方法:

1
sysctl -w net.ipv4.tcp_early_retrans=0

修改后,全网 TCP 重传率从万分之 5 降到正常水平。

阶段四:发现 Send Buffer 限制

解决虚假重传后,抓包发现新的瓶颈:

Broker 发送一批数据包后,在途字节(Bytes in flight)达到约 50KB 就停止发送,等一个 RTT(~130ms)收到 ACK 后才继续。

原因:Kafka Broker 默认配置 socket.send.buffer.bytes=102400(100KB),内核实际分配约一半(~50KB),限制了发送窗口。

阶段五:定位根因 — 客户端接收 Buffer

将 Broker Send Buffer 限制去掉后仍然不行。最终在客户端日志中找到关键配置:

1
receive.buffer.bytes = 65536    ← Kafka 客户端默认只有 64KB 接收缓冲区

TCP 三次握手时,客户端根据 receive.buffer.bytes 通告接收窗口(rwnd)。64KB 的 rwnd 意味着:

1
最大吞吐量 = rwnd / RTT = 64KB / 130ms ≈ 490KB/s

而开盘高峰期需要数十 MB/s 的吞吐量,64KB 的接收窗口成为致命瓶颈。

根因与解决方案

根因:Kafka 客户端默认接收缓冲区 64KB 太小,叠加跨机房 130ms 的 RTT,导致 TCP 吞吐量被严重限制。

解决方案

1
2
3
4
5
6
7
8
9
# Kafka Consumer 配置
receive.buffer.bytes = 2097152 # 从 64KB 调整到 2MB(或设为 -1 让 OS 自动调优)

# Kafka Broker 配置
socket.send.buffer.bytes = -1 # 去掉 100KB 限制,让 OS 自动调优
socket.receive.buffer.bytes = -1

# Linux 内核参数(3.10 内核)
net.ipv4.tcp_early_retrans = 0 # 关闭有 bug 的 early retrans

效果:传输延迟从 10-60 分钟降低到 1-4 秒,性能提升约 1000 倍。

调整前后的 Wireshark 抓包对比:

1
2
调整前:485KB 数据需要 8 个 RTT(8 × 130ms = 1040ms)分批传输
调整后:485KB 数据在 3ms 内一次性传输完毕

案例二:同机房 TCP 订阅客户端异常缓慢

问题描述

同机房 4 台机器部署了相同的行情订阅服务,其中 1 台拉取行情数据异常缓慢。

排查过程

第一步:抓包发现 TCP Zero Window

在客户端抓包,发现大量 TCP Zero Window 通告——客户端在告诉服务端”我的接收缓冲区满了,别再发了”。

第二步:确认 Recv Buffer 堆满

1
2
netstat -ant | grep <server_port>
# 发现 Recv-Q 持续满载

第三步:发现 CPU 异常

1
2
top -Hp <java_pid>
# 发现一个线程 CPU 持续接近 100%,像死循环

第四步:定位代码问题

1
2
3
4
5
# 用 top 看到的线程 ID(十进制)转十六进制
printf '%x\n' <thread_id>

# 在 jstack 中查找对应线程堆栈
jstack <java_pid> | grep -A 30 <hex_thread_id>

找到问题代码:一个 for 循环中调用 read(),每次只读很少的数据(参数配置过小),导致:

  1. 应用层读取速度跟不上网络数据到达速度
  2. OS TCP Recv Buffer 被填满
  3. TCP 通告 Zero Window,服务端停止发送
  4. 该线程 CPU 100%(不停地循环读取微量数据)

根因与解决方案

根因:业务代码中 read() 的缓冲区参数配置过小,导致应用层消费速度远低于网络数据到达速度,TCP Recv Buffer 堆满触发 Zero Window。

解决方案:修复代码,增大 read() 的缓冲区大小。

效果

  • 网络吞吐量恢复正常(流量曲线从”平台”变为正常波动)
  • CPU 使用率从 40% 降到 20%

两个案例的关联与对比

维度 案例一(跨机房延迟) 案例二(同机房 Zero Window)
场景 跨机房,RTT ~130ms 同机房,RTT <1ms
表现 消费延迟 10+ 分钟 TCP Zero Window,数据停滞
瓶颈层 TCP 接收窗口 + 内核参数 应用层读取速度
根因 Kafka 默认 receive buffer 64KB 太小 业务代码 read() 缓冲区配置过小
为什么同机房没问题 RTT 小,64KB 窗口也够用 其他机器代码/配置正常
解决方式 调大 buffer + 修复内核参数 修复业务代码

两个案例本质上都是 TCP 接收端处理能力不足 导致发送端被限速:

1
2
案例一:接收窗口太小 → 发送端每个 RTT 只能发 64KB → 跨机房 RTT 大放大了问题
案例二:应用层读取太慢 → Recv Buffer 堆满 → Zero Window → 发送端完全停止

TCP 缓冲区与窗口的核心原理

1
2
3
4
5
6
7
8
9
10
11
发送方应用 write()

Send Buffer (sk_sndbuf)
↓ 受发送窗口限制
发送窗口 = min(拥塞窗口 cwnd, 接收窗口 rwnd)

网络传输 (in-flight data)

Receive Buffer (sk_rcvbuf)
↓ 通告 rwnd = buffer_size - 已占用空间
接收方应用 read()

关键关系:

  • 接收窗口 rwnd:由接收端 Recv Buffer 剩余空间决定,通过 ACK 通告给发送端
  • 拥塞窗口 cwnd:由发送端拥塞控制算法(如 CUBIC)动态调整
  • 发送窗口:取 rwnd 和 cwnd 的较小值
  • 最大吞吐量min(rwnd, cwnd) / RTT

为什么 RTT 大时 Buffer 小的问题会被放大?

1
2
同机房 (RTT=1ms):   吞吐量 = 64KB / 1ms   = 64MB/s   ← 够用
跨机房 (RTT=130ms): 吞吐量 = 64KB / 130ms = 490KB/s ← 严重不足

这就是案例一中”同机房正常、跨机房延迟”的根本原因。


关键排查命令速查

TCP 连接状态分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 查看 TCP 连接详细信息(拥塞窗口、RTT、重传等)
ss -itmpn dst "x.x.x.x"

# 关键字段解读
# cwnd:10 拥塞窗口(MSS 数量)
# ssthresh:2 慢启动阈值
# rtt:130/0.5 RTT 均值/方差(ms)
# rto:330 重传超时(ms)
# send 850Kbps 当前发送速率
# unacked:38 未确认的包数量
# retrans:0/3 当前重传/累计重传

# 查看 TCP 缓存指标
ip tcp_metrics show | grep <ip>

# 查看连接队列
netstat -ant | grep <port>
# Recv-Q: 接收队列(应用未读取的数据)
# Send-Q: 发送队列(已发送未确认的数据)

网络重传监控

1
2
3
4
5
# 实时观察 TCP 重传
watch -n 1 'nstat -az | grep TcpRetransSegs'

# 检查 early retrans 参数(3.10 内核)
sysctl net.ipv4.tcp_early_retrans

抓包分析

1
2
3
4
5
6
7
# 抓取指定端口的包
tcpdump -i eth0 port 9092 -w capture.pcap

# Wireshark 中关键过滤器
tcp.analysis.zero_window # Zero Window 事件
tcp.analysis.retransmission # 重传包
tcp.analysis.bytes_in_flight # 在途字节数

Java 应用排查

1
2
3
4
5
6
7
8
# 查看 Java 进程中各线程 CPU 使用
top -Hp <java_pid>

# 线程 ID 转十六进制
printf '%x\n' <thread_id>

# 导出线程堆栈并查找
jstack <java_pid> | grep -A 30 <hex_thread_id>

经验教训

1. 不要信任中间件的默认配置

Kafka 客户端默认 receive.buffer.bytes=64KB、Broker 默认 socket.send.buffer.bytes=100KB,这些值在同机房低延迟环境下够用,但在跨机房高 RTT 场景下会成为致命瓶颈。

建议:跨机房部署时,将 buffer 相关配置设为 -1(让 OS 自动调优),或根据 BDP = 带宽 × RTT 计算合理值。

2. 内核参数可能是隐藏的定时炸弹

Linux 3.10 内核的 tcp_early_retrans=3 导致大量虚假重传,拉高全网重传率到万分之 5,掩盖了真实的网络问题。这种问题平时不显现,只在高负载时爆发。

建议:关注内核版本差异,定期检查 TCP 重传率是否异常。

3. 学会用 Wireshark 做对比分析

排查网络问题最有效的方法是:抓一份有问题的包、一份正常的包,在 Wireshark 中对比分析。关注:

  • Bytes in flight 的变化曲线
  • ACK 之间的间隔(是否有等待平台)
  • 是否有异常重传

4. 应用层的 Bug 也会表现为网络问题

案例二中,业务代码 read() 缓冲区配置过小导致 TCP Zero Window,表面看是”网络问题”,实际是应用层 Bug。排查时不要只盯着网络层,要结合 top -Hp + jstack 看应用层行为。

5. 理解 BDP(带宽延迟积)

1
BDP = 带宽 × RTT

这是网络管道中能容纳的最大数据量。TCP Buffer 至少要等于 BDP 才能充分利用带宽。例如:

1
2
3
4
5
带宽 100Mbps, RTT 130ms:
BDP = 100Mbps × 0.13s = 13Mb = 1.6MB

→ TCP Buffer 至少需要 1.6MB 才能跑满带宽
→ 64KB 的 Buffer 只能利用 64KB/1.6MB ≈ 4% 的带宽

6. 排查要有层次感

1
2
3
4
5
6
7
8
9
应用层 → 是否有代码 Bug、配置错误

中间件层 → Kafka/MySQL 等的 Buffer 配置是否合理

TCP 层 → 拥塞窗口、接收窗口、重传情况

内核层 → tcp_early_retrans 等内核参数

网络层 → 带宽、丢包、RTT

从上到下逐层排查,每一层都可能是瓶颈。