本页目录

网络性能与调优

从应用 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,带宽就跑不满。

链路BDPLinux 默认够吗
1Gbps × 30ms(国内跨省)3.75 MB够(tcp_rmem max 默认 ~6MB)
1Gbps × 150ms(跨洋)18.75 MB不够
10Gbps × 200ms250 MB远远不够
# 三元组: min / default / max —— 自动调节只在 min..max 之间伸缩
sysctl net.ipv4.tcp_rmem="4096 131072 67108864"   # 接收侧, 64MB 上限
sysctl net.ipv4.tcp_wmem="4096 16384 67108864"    # 发送侧
sysctl net.core.rmem_max=67108864                  # SO_RCVBUF 手动设置的上限

验证手段:ss -ti 看单连接的 cwndrttbytes_acked——如果 cwnd 长期顶着 buffer 上限而不是随链路波动,就是 buffer 卡了脖子。

注意⁠:buffer 不是越大越好。大 buffer 在低 BDP 链路上只是白占内存,在自己就是瓶颈的设备上还会加重排队延迟。按实际要跑的最大 BDP 设,留 2 倍余量即可。

加载时延迟飙升:Bufferbloat 与队列管理

经典症状:⁠空闲时 ping 10ms,下载一开跑 ping 涨到 500ms。原因不在带宽,在队列——路径上某个设备(往往是家用路由器/光猫)配了巨大的 FIFO buffer,CUBIC 这类丢包驱动的算法会一直发包直到填满它才收手,于是所有流量都在这个深队列里排队。

自己能控制的两端,用 AQM(主动队列管理)⁠替代 FIFO:

tc qdisc replace dev eth0 root fq_codel              # 多数发行版已默认
tc qdisc replace dev eth0 root cake bandwidth 950mbit # 家庭出口首选
  • fq_codel = 公平队列 + CoDel。CoDel 的思路是盯住包的驻留时间⁠:持续超过 5ms 就开始丢包(从队头丢,信号传得快),逼发送方降速——队列短了,延迟自然下来。公平队列部分保证一条大象流不会饿死旁边的 ssh。
  • cake 是 fq_codel 的工程完善版:内置整形器(把 bandwidth 设成略低于真实上行,把队列从光猫抢回自己手里)、per-host 隔离(一台设备开 BT 不拖累全家)、DiffServ 感知。

发送侧配合 bbr 效果最好——BBR 本身就不填队列(原理见 TCP 深度的拥塞控制一节):

sysctl net.ipv4.tcp_congestion_control=bbr

常用 TCP 旋钮

旋钮作用何时动它风险
tcp_congestion_control=bbr模型驱动的拥塞控制长肥/有丢包/bufferbloat 链路与 CUBIC 流共存时的公平性(v1)
tcp_fastopen=3SYN 里带数据,回头客省 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接收把同一条流的连续小包合并成大块再上交协议栈,中断和遍历都省了
ethtool -k eth0 | grep -v fixed     # 查看当前 offload 状态
ethtool -K eth0 tso off             # 排查问题时临时关闭
ethtool -G eth0 rx 4096 tx 4096     # 驱动 ring buffer, 突发流量丢包时加大

tcpdump 的经典陷阱⁠:抓包看到 64KB 的"巨包"不是异常——那是 TSO/GRO 生效,抓包点在分段之前/合并之后;线上实际以 MTU 尺寸传输。反过来,怀疑 offload 引发问题(尤其虚拟化环境的 checksum 错误)时,先 ethtool -K ... off 排除。

定位瓶颈的顺序

  1. 基线⁠:iperf3 -c <server> -R 双向各测,拿到裸带宽——先确认瓶颈到底在不在网络。
  2. 加载下测延迟⁠:一边 iperf3 一边 ping,延迟涨十倍 → bufferbloat,去调队列。
  3. 看单连接⁠:ss -ti 的 cwnd/rtt/retrans——cwnd 上不去看 buffer 与拥塞算法,retrans 多去查链路质量。
  4. 看系统⁠:nstat -a | grep -i retransethtool -S eth0 | grep -i drop(网卡层丢包)、softnet_stat 背压。
  5. 改一个变量,回到第 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