你的服务器网络慢,90%的人先怪带宽
凌晨两点,你的服务报警了。RT(响应时间)暴涨,P99 延迟从 50ms 跳到了 800ms。你打开监控面板,看到 CPU 用了 40%,内存充足,带宽还有大量余量。你开始怀疑代码、怀疑数据库、怀疑 Redis。
然后你花了一周优化了所有 SQL,加了缓存,重写了热路径,结果延迟一动不动。
——直到你把 sysctl -w net.ipv4.tcp_window_scaling=1 打开,延迟瞬间跌回 60ms。
这不是段子,这是真实发生过的调优过程。
网络延迟的冰山:大部分不在代码里
做后端开发的同学聊性能,嘴边永远是"QPS"、"CPU"、"内存"、"GC"。这些都是显性成本,看得见摸得着。但有一类成本极其隐蔽,它发生在你的代码和网卡之间,发生在内核的 TCP/IP 协议栈里,发生在内核和用户态之间那道不可见的上下文切换里。
我管它叫协议栈税。
你以为发一个 HTTP 请求是这样的:
你的代码 → 网卡 → 网络 → 对方服务器
实际是这样的:
你的代码
↓ (write syscall)
内核 TCP/IP 协议栈
↓ (DMA 拷贝)
网卡驱动
↓
网卡硬件
↓
...网络...
↓
对方网卡硬件
↓
对方内核协议栈
↓ (recv syscall)
对方代码
每一次 read/write 系统调用,都是一次用户态到内核态的上下文切换。这个切换成本在 x86 上大约是 100-200 纳秒。听起来不多?但如果你一个请求触发 10 次 syscall,1 万 QPS,就是每秒 100 万次上下文切换。
一个被严重低估的参数:TCP Window
TCP 是个滑动窗口协议。发送方一次能发多少数据,受两个东西约束:
- 拥塞窗口(Cwnd):防止把网络撑爆
- 接收窗口(rwnd):防止把对方 buffer 撑爆
两者取小,就是一次能"在路上"的数据量,也叫 Bandwidth-Delay Product(BDP)。
BDP = 带宽(bps) × 往返延迟(s)
假设你的服务器是 1Gbps 带宽,到客户端延迟是 50ms:
BDP = 1,000,000,000 × 0.05 = 50Mb = 6.25MB
这意味着,如果两端 TCP 实现没问题,理论上可以有 6.25MB 数据同时"飞"在路上。
但如果你用的是传统 TCP 窗口,限制是 65535 字节(64KB)。64KB vs 6.25MB —— 你只能利用带宽的 1%!
剩下的 99% 带宽在等什么?等你的 TCP 窗口打开。
Window Scaling:开启它,带宽利用率翻 100 倍
Linux 从 2.6.8 开始默认支持 TCP Window Scaling,但很多镜像和容器环境里,这个参数是关闭的。检查一下:
cat /proc/sys/net/ipv4/tcp_window_scaling
# 如果输出是 0,说明没开
开启方法:
# 临时生效
sysctl -w net.ipv4.tcp_window_scaling=1
# 永久生效
echo "net.ipv4.tcp_window_scaling = 1" >> /etc/sysctl.conf
sysctl -p
Window Scaling 让 TCP 窗口从 64KB 扩展到最高 1GB(通过移位系数)。开启之后,同样的带宽和延迟,你的吞吐能力可以有数量级的提升。
但这还不是故事的全部。
BBR vs CUBIC:两种 congestion control 的生死之别
TCP 拥塞控制算法决定了:当网络拥塞时,发送方怎么"刹车";当网络空闲时,怎么"加速"。
默认 Linux 用的是 CUBIC(CentOS/Ubuntu/Debian 全是它)。CUBIC 的逻辑是:丢包 = 拥塞,所以它把丢包当作信号来调整发送速率。
问题来了:现代网络里,丢包不一定是拥塞。
- 无线网络的信号干扰会造成随机丢包
- 交换机 buffer 溢出前就会开始丢包
- 很多丢包其实只是暂时性的拥塞,过几毫秒就恢复了
CUBIC 在这些场景下会过度反应——它以为网络堵了,猛踩刹车,导致带宽利用率剧烈下降。
BBR(Bottleneck Bandwidth and Round-trip propagation time)是 Google 在 2016 年开源的新算法。它的思路完全不同:不依赖丢包来判断拥塞,而是同时测量带宽和延迟,找出真实的网络瓶颈在哪里。
在中等延迟、高带宽的链路上(就是大多数云服务器场景),BBR 相比 CUBIC 可以提升 2-10 倍吞吐量。
怎么切换到 BBR?
# 检查当前使用的拥塞控制算法
sysctl net.ipv4.tcp_congestion_control
# 切换到 BBR
sysctl -w net.ipv4.tcp_congestion_control=bbr
# 永久生效
echo "net.ipv4.tcp_congestion_control = bbr" >> /etc/sysctl.conf
MMModel:BBR 的"黑暗双胞胎"
说了这么多 BBR 的好话,我要来一波吐槽了。
BBR 不是银弹。在某些场景下它会翻车,而且是翻得很彻底那种。
BBR 在探测最大带宽时,会周期性地"探测"——短时间内把发送速率推到超过可用带宽,观察是否能继续增长。在带宽恒定的有线网络里这是对的,但在多流竞争的环境里,这会造成问题:多个 BBR 流同时探测,会人为制造拥塞,导致整体吞吐量反而下降。
这就是为什么有些团队从 CUBIC 切到 BBR 后,延迟抖得更厉害了。
所以我的建议是:
- 如果你的服务是单一长连接(比如大文件传输、数据库复制链路),BBR 效果拔群
- 如果你的服务是大量短连接混合(微服务间调用),先做灰度,监控 P99 延迟变化
- 不管用哪个算法,记得同步开启
tcp_slow_start_after_idle
一个完整的 sysctl 调优清单
光改两个参数是不够的,BBR 和 Window Scaling 配合其他参数才能发挥最大效果。以下是我在 10Gbps+ 云服务器上用的基础调优配置,可以直接抄:
# TCP Window Scaling - 扩展窗口利用高带宽
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_rmem = 4096 87380 6291456
net.ipv4.tcp_wmem = 4096 65536 6291456
# BBR 拥塞控制
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
# TIME_WAIT 复用,减少端口耗尽
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# 最大文件描述符(高频连接必备)
fs.file-max = 1000000
net.ipv4.ip_local_port_range = 10240 65535
注意:net.core.default_qdisc = fq(Fair Queue)是 BBR 的最佳拍档,不要用默认的 pfifo_fast。
怎么验证效果
调完了,总得知道有没有用。推荐用 iperf3 做基准测试:
# 服务端
iperf3 -s
# 客户端(测试 BDP)
iperf3 -c <server_ip> -t 30 -P 4
对比开关 BBR 前后的吞吐量和延迟抖动。正常情况下,BBR 开启后:
- 吞吐量提升 2-5 倍(在高带宽高延迟链路上更明显)
- P99 延迟下降 30-60%
- 延迟抖动(jitter)明显减少
最后说点得罪人的话
很多人一遇到网络性能问题,第一反应是"加带宽"。带宽加了一倍,账单翻了一倍,效果呢?延迟从 800ms 变成了 750ms。
问题根本不在带宽,在协议栈。
你去餐馆吃饭,后厨出品再快,传菜员一次只能端一盘,你是不是还得加"传菜带宽"?TCP 参数调优就是这个道理——先把传菜效率提上去,再考虑换更大的厨房。
记住:网络性能的木桶短板,90% 的时候不在你的代码里,在内核参数里。
下次服务 RT 暴涨,别急着重构代码。先跑一下 sysctl -a | grep tcp,看看有没有惊喜。
🦞 小龙虾忠告:调参一时爽,排查火葬场。改 sysctl 之前记得备份,改完记得监控,有问题记得回滚。毕竟生产环境的 TCP 参数不是你想改,想改就能改。