本页目录
网络性能与调优
从应用 buffer 到 TCP 拥塞控制到网卡 offload——每层都有可调参数。BDP 决定需要多大 buffer 跑满带宽,BBR 用模型替代丢包检测在 bufferbloated 链路上优于 CUBIC,fq_codel/cake 主动管理队列防止 buffer 过度填满。
概述
网上流传的"TCP 调优秘籍"大多是不看场景的 sysctl 堆砌。有效的调优只有一条路:先搞清数据从应用到网线要经过哪些队列,测出瓶颈在哪一层,再动那一层的旋钮。一个包的发送路径上每一站都可能成为瓶颈,也各有各的调法:
flowchart LR
APP["📦 应用 buffer<br><small>write 多大块</small>"] --> SK["socket buffer<br><small>tcp_wmem / tcp_rmem</small>"]
SK --> CC["TCP 拥塞控制<br><small>cubic / bbr</small>"]
CC --> QD["qdisc 队列<br><small>fq_codel / cake</small>"]
QD --> DRV["驱动 ring buffer<br><small>ethtool -G</small>"]
DRV --> NIC["网卡 offload<br><small>TSO / GSO / GRO</small>"]
NIC --> NET(("🌐 网络<br><small>这里的队列你管不着<br>→ bufferbloat</small>"))
classDef knob fill:#4493f81f,stroke:#4493f8,stroke-width:2px
classDef wild fill:#f8514926,stroke:#f85149,stroke-width:2.5px
class APP,SK,CC,QD,DRV,NIC knob
class NET wild
三类典型问题对应三个不同的层:带宽跑不满多半是 socket buffer 小于 BDP 或拥塞算法不适配;加载时延迟飙升是队列问题(bufferbloat);小包吞吐/CPU 打满是中断和 offload 问题。下面按这三类展开。
跑不满带宽:BDP 与 Buffer Sizing
TCP 想跑满一条链路,"在路上飞着"的未确认数据必须填满整个管道。管道容量就是 BDP(带宽 × RTT),而 in-flight 上限受制于 min(cwnd, 接收窗口, 发送 buffer)——三者任何一个小于 BDP,带宽就跑不满。
| 链路 | BDP | Linux 默认够吗 |
|---|---|---|
| 1Gbps × 30ms(国内跨省) | 3.75 MB | 够(tcp_rmem max 默认 ~6MB) |
| 1Gbps × 150ms(跨洋) | 18.75 MB | 不够 |
| 10Gbps × 200ms | 250 MB | 远远不够 |
# 三元组: min / default / max —— 自动调节只在 min..max 之间伸缩
验证手段:ss -ti 看单连接的 cwnd、rtt、bytes_acked——如果 cwnd 长期顶着 buffer 上限而不是随链路波动,就是 buffer 卡了脖子。
注意:buffer 不是越大越好。大 buffer 在低 BDP 链路上只是白占内存,在自己就是瓶颈的设备上还会加重排队延迟。按实际要跑的最大 BDP 设,留 2 倍余量即可。
加载时延迟飙升:Bufferbloat 与队列管理
经典症状:空闲时 ping 10ms,下载一开跑 ping 涨到 500ms。原因不在带宽,在队列——路径上某个设备(往往是家用路由器/光猫)配了巨大的 FIFO buffer,CUBIC 这类丢包驱动的算法会一直发包直到填满它才收手,于是所有流量都在这个深队列里排队。
自己能控制的两端,用 AQM(主动队列管理)替代 FIFO:
- fq_codel = 公平队列 + CoDel。CoDel 的思路是盯住包的驻留时间:持续超过 5ms 就开始丢包(从队头丢,信号传得快),逼发送方降速——队列短了,延迟自然下来。公平队列部分保证一条大象流不会饿死旁边的 ssh。
- cake 是 fq_codel 的工程完善版:内置整形器(把
bandwidth设成略低于真实上行,把队列从光猫抢回自己手里)、per-host 隔离(一台设备开 BT 不拖累全家)、DiffServ 感知。
发送侧配合 bbr 效果最好——BBR 本身就不填队列(原理见 TCP 深度的拥塞控制一节):
常用 TCP 旋钮
| 旋钮 | 作用 | 何时动它 | 风险 |
|---|---|---|---|
tcp_congestion_control=bbr | 模型驱动的拥塞控制 | 长肥/有丢包/bufferbloat 链路 | 与 CUBIC 流共存时的公平性(v1) |
tcp_fastopen=3 | SYN 里带数据,回头客省 1 RTT | 短连接密集的 API 服务 | 中间盒可能丢 TFO 包 |
tcp_tw_reuse=1 | 客户端复用 TIME_WAIT | 高并发短连接的客户端 | 仅客户端安全;别碰早已移除的 tw_recycle |
somaxconn + tcp_max_syn_backlog | 全/半连接队列深度 | ss -lnt 看到 listen 队列溢出 | 只是加深队列,处理不过来还得扩容 |
tcp_notsent_lowat=16384 | 限制发送 buffer 里"还没发出去"的量 | 延迟敏感的流式应用 | 极端吞吐略降 |
原则:每改一个,压测对比一次。旋钮之间会互相作用,一把梭等于没调。
小包与 CPU:网卡 Offload
10Gbps 下每秒可达百万包,逐包中断 CPU 会先于带宽爆掉。思路是批处理——把分段/合并的活从内核搬到网卡:
| Offload | 方向 | 做什么 |
|---|---|---|
| TSO | 发送 | 内核丢给网卡一个 64KB 大块,网卡硬件切成 MSS 大小 |
| GSO | 发送 | TSO 的软件兜底(虚拟网卡、不支持 TSO 的硬件),在驱动前最后一刻才分段 |
| GRO | 接收 | 把同一条流的连续小包合并成大块再上交协议栈,中断和遍历都省了 |
|
tcpdump 的经典陷阱:抓包看到 64KB 的"巨包"不是异常——那是 TSO/GRO 生效,抓包点在分段之前/合并之后;线上实际以 MTU 尺寸传输。反过来,怀疑 offload 引发问题(尤其虚拟化环境的 checksum 错误)时,先 ethtool -K ... off 排除。
定位瓶颈的顺序
- 基线:
iperf3 -c <server> -R双向各测,拿到裸带宽——先确认瓶颈到底在不在网络。 - 加载下测延迟:一边 iperf3 一边
ping,延迟涨十倍 → bufferbloat,去调队列。 - 看单连接:
ss -ti的 cwnd/rtt/retrans——cwnd 上不去看 buffer 与拥塞算法,retrans 多去查链路质量。 - 看系统:
nstat -a | grep -i retrans、ethtool -S eth0 | grep -i drop(网卡层丢包)、softnet_stat背压。 - 改一个变量,回到第 1 步。
系统性的抓包分析见 tcpdump 与 Wireshark,故障排查思路见网络故障诊断方法论。
参考
- BBR: github.com/google/bbr · RFC 9438
- fq_codel/CoDel: RFC 8290, RFC 8289 · bufferbloat.net
- cake: man tc-cake · RFC 8290 的工程化后续
- Brendan Gregg: "Systems Performance" 第 10 章 (Network)
Keywords: BDP, BBR, CUBIC, bufferbloat, fq_codel, cake, AQM, TCP tuning, tcp_rmem, TSO, GSO, GRO, ethtool, iperf3