【TCP优化 02】为什么晚高峰 Ping 值正常,SSH 却卡到掉线?缓冲区膨胀(Bufferbloat)与 TCP 窗口调优
2026-08-14 · DevCraft Studio
晚高峰看视频还行、敲 SSH 却像幻灯片?这不是带宽问题,是缓冲区膨胀拖慢了交互。这篇讲清 Bufferbloat 原理,并给出 tc 配置 Cake/FQ-CoDel、sysctl 调 TCP 窗口与启用 BBR 的实操命令。
TCP 优化 · 共 2 篇
你有没有遇到过这种诡异场面:晚上用 VPS 的时候,开个视频或者下载速度还凑合,但一敲 SSH 命令行,字符半天蹦不出来,按一下回车要等好几秒才有反应,打字像在看幻灯片,甚至直接掉线重连。你顺手 ping 了一下,发现延迟也就 20~30 毫秒,明明"网络是通的",怎么交互就卡成这样?这其实不是带宽不够,也不是线路真的断了,而是一个很经典的坑——缓冲区膨胀(Bufferbloat)。这篇文章就把它讲明白,并手把手教你在 VPS 或软路由上用 Cake / FQ-CoDel 队列算法和 TCP 窗口调优把延迟压下去。
延伸阅读
更多相关攻略推荐:Web 与数据库分在两台 VPS?跨机房 MySQL 3306 安全、AWS / GCP 的"出站流量税":云巨头按 GB 扣费解析与 B、从 Git Push 到秒级上线:CI/CD 流水线与"无中断"发布、VPS 自建 CMS 横评:Ghost / Astro / Stra、VPS 自建 API 网关:用 KrakenD / APISIX /。
"能下载但交互卡"的矛盾现象
先说清楚这个现象的本质。Ping 测的是"空载延迟"——也就是网络上一没啥流量的时候,一个小小的探测包来回要多久。空载时队列是空的,探测包立刻就发出去了,所以 ping 看着很漂亮。但当你一边下载、一边看视频、一边跑备份的时候,链路被塞满,这时候再敲 SSH,你的按键数据包就得跟那些大文件的数据包一起排队。交互卡不卡,看的不是空载延迟,而是"满载延迟"(loaded latency)。
判断是不是 Bufferbloat 有个土办法:开着下载(比如跑个 iperf3 或者看个 4K 视频),同时另外一个窗口持续 ping 你的 VPS。如果空载时 20ms,一跑满速立刻飙到 200ms、500ms 甚至几秒,那就是典型的缓冲区膨胀。专业的测法去 waveform.com 的 Bufferbloat 测试页面,或者直接用 flent 跑 RRUL 测试,它会同时打满上下行并测延迟。
罪魁祸首:缓冲区膨胀 Bufferbloat
bufferbloat.net 这个项目对这个问题的解释非常经典:所谓 Bufferbloat,就是路由器或网络设备上"缓冲了太多数据"带来的讨厌延迟。为什么设备会缓冲太多?这要怪老式的 FIFO(先进先出)大缓冲区设计。
设想一下:你的 VPS 到外面是 100Mbps 的慢出口,但内部网卡是 1Gbps、10Gbps。当数据从内部飞快涌来、外面却慢慢发的时候,路由器本来应该"发不出去就丢几个包"来告诉发送方"慢点",但现在的设备为了不丢包,一律选择"先排队存着"。队列越积越长,有时能囤下几十甚至上百个数据包。每个被你 SSH 敲出来的按键包,就被卡在这长队末尾干等。队列里囤的这几百毫秒甚至几秒的数据,就是"膨胀的缓冲区",它直接变成你的延迟。
关键点在于:多囤一两个包是有益的(让慢链路别空转),但囤太多只会增加延迟,对吞吐量没有任何好处。可惜大多数消费级路由器和默认配置都选择了"尽量不丢包"的大缓冲策略,于是晚高峰一到,大家都在下载,队列全爆,你的 SSH 交互就被活活憋死了。这也是为什么"看视频还行、敲命令卡死"——视频是大块流量,多等几百毫秒你根本感觉不出来;SSH 是极小包、极强实时性,每等一秒你都浑身难受。
用 Cake / FQ-CoDel 给队列"瘦身"
解决办法叫"智能队列管理(SQM, Smart Queue Management)"。Linux 内核自带两个神器:fq_codel(公平队列 + 受控延迟)和更进阶的 cake(在 fq_codel 基础上加了限速和更好的流隔离)。它们的思路是:给每个网络连接单独开一条小队列,轮流公平地发一小批,不让某一个大流量独吞缓冲区;同时主动监控数据包在队列里等了多久,等太久就故意提前丢几个包通知发送方降速,把延迟压在目标值附近。
先看看你现在的队列算法是什么:
sysctl net.core.default_qdisc
tc qdisc show
如果看到 pfifo_fast 或 noqueue,那就该换了。最轻量的做法,直接把网卡根队列换成 fq_codel,一条命令搞定,立刻生效:
sudo tc qdisc replace dev eth0 root fq_codel
把 eth0 换成你实际对外的网卡名(用 ip addr 看)。fq_codel 适合大多数情况,零配置,Linux 路由器默认就是它。
如果你想更狠一点、顺便做限速(把瓶颈控制在自己手里而不是 ISP 的 modem 里),用 cake 并填上你实际带宽的 95% 左右(比如上传 1000Mbit,就填 950Mbit):
sudo tc qdisc replace dev eth0 root cake bandwidth 950mbit besteffort
如果你的发行版比较新(systemd 251+,比如 Debian 12、Ubuntu 22.04 之后),还可以用声明式配置,重启也不丢,比 tc 脚本优雅:在 /etc/systemd/network/ 下对应网卡的 .network 文件里加一段:
[CAKE]
Bandwidth=950M
FlowIsolationMode=dual-dst-host
PriorityQueueingPreset=besteffort
然后 sudo networkctl reload。适用场景:VPS 上如果它本身就是某个瓶颈出口(比如你拿它做网关、做 VPN 服务端、或者它自己对外的带宽有限),在出向接口上挂 cake/fq_codel 能显著压低满载延迟;家庭软路由同理,挂在 WAN 口上最有用。
TCP 缓冲区与窗口:高延迟下喂饱管道
队列问题解决的是"延迟",但还有一个问题是"吞吐"。在跨境、高延迟线路上(RTT 一两百毫秒),TCP 默认的接收/发送缓冲区太小,会导致"管道没喂满"——带宽明明很大,实际速度却上不去,而且重传、确认慢,交互也跟着迟钝。
这里要引出一个概念:带宽时延积(BDP, Bandwidth-Delay Product)。简单说,BDP = 带宽 × 往返延迟,它代表"线路里同时能塞下多少数据"。比如 500Mbps 带宽、120ms 延迟:BDP = (500,000,000 / 8) × 0.12 ≈ 7.5 MB。也就是说你的缓冲区至少得有 7.5 MB,才能把这条管道填满。默认缓冲区往往只有几百 KB,远远不够,于是 SSH 这种小交互虽然不占带宽,但窗口太小导致的慢启动和确认等待,会让每一轮往返都磨叽。
调大 TCP 缓冲区和窗口,编辑 /etc/sysctl.d/99-tcp-tuning.conf:
# 开启窗口缩放(让窗口能超过 64KB 的古老上限,RFC 7323)
net.ipv4.tcp_window_scaling = 1
# TCP 自动调优的接收/发送缓冲区范围:min default max(单位字节)
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# 全局套接字缓冲区上限,必须跟着调大,否则上面的 max 不生效
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
# 让内核自动调节接收缓冲区(默认就是 1,保持开启)
net.ipv4.tcp_moderate_rcvbuf = 1
改完执行 sudo sysctl -p /etc/sysctl.d/99-tcp-tuning.conf 生效。注意:tcp_rmem 的最大值不能超过 rmem_max,否则会被截断,这是新手最容易漏的一点。tcp_window_scaling=1 是关键开关——没有它,窗口最大只能 65535 字节,在 100ms 延迟下理论吞吐上限只有约 5.2 Mbps,再大的带宽也白搭。
拥塞控制算法:换成 BBR
最后一个大招是拥塞控制算法。Linux 默认是 cubic,它是"丢包驱动"的——靠丢包来判断该不该减速。这在高延迟、偶尔丢包的跨境线路上很吃亏,一丢包就猛砍发送窗口,吞吐和响应一起崩。谷歌的 BBR 是"带宽和延迟驱动"的,不靠丢包信号,能在大延迟、有丢包的线路上保持高吞吐,SSH 交互也明显更跟手。
开启 BBR(需要内核 4.9+,主流 VPS 都满足):
# 把默认队列调度也换成 fq,BBR 配合 fq 效果最好
echo "net.core.default_qdisc = fq" | sudo tee -a /etc/sysctl.d/99-tcp-tuning.conf
echo "net.ipv4.tcp_congestion_control = bbr" | sudo tee -a /etc/sysctl.d/99-tcp-tuning.conf
sudo sysctl -p /etc/sysctl.d/99-tcp-tuning.conf
验证有没有生效:
sysctl net.ipv4.tcp_congestion_control
sysctl net.ipv4.tcp_available_congestion_control
第二条能看到 bbr 在可用列表里,第一条显示当前用的是 bbr 就成功了。想看实际连接是不是走了 BBR,用 ss -tin state established | grep -w bbr | wc -l 数一下有多少条连接在用。
这里有个细节:BBR 和前面说的 fq_codel/cake 不冲突,它们是不同层面的东西。队列算法管的是"出向接口的排队和延迟"(AQM),BBR 管的是"发送端怎么控制速率",两者配合,一个压延迟一个保吞吐,晚高峰的 SSH 卡顿基本就解决了。另外可以顺手关掉空闲后的慢启动惩罚:net.ipv4.tcp_slow_start_after_idle = 0,让长连接保持窗口,不用每次敲命令都重新慢启动。
还有一个容易被忽视的延迟坑是 MTU。很多走 PPPoE 拨号或有隧道封装的 VPS,实际有效 MTU 只有 1452 甚至 1400,但系统默认按 1500 发包,包太大就要分片,而分片在跨境、过隧道时极易被丢弃或增加处理延迟,反而让 SSH 更卡。可以加上 net.ipv4.tcp_mtu_probing = 1 让内核自动探测路径 MTU,顺便避开分片带来的额外抖动;嫌麻烦的话用 ping -M do -s 1472 你的IP 一路把包大小往下调,找到"不分片"的最大尺寸,把网卡的 MTU 直接设成那个值也行。
实测与验证
调完别凭感觉,得测。SSH 交互顺不顺你自己敲两下就知道,但要量化延迟改善,推荐两个工具:
- 浏览器打开 waveform.com 的 Bufferbloat 测试,看满载延迟从几百毫秒降到 15~30 毫秒以内,就算调好了。
- 两端都装 iperf3,一端
iperf3 -s,另一端iperf3 -c 服务器IP -t 30,对比调前调后的吞吐;高延迟线路上 BBR + 大窗口下,单流吞吐常常能翻好几倍。 - 用
ss -ti看具体连接的 rtt 和 cwnd(拥塞窗口),确认窗口确实涨上去了。
总结
"Ping 正常但 SSH 卡"十有八九是 Bufferbloat,不是带宽不够。解法分两层:在出向接口上用 tc qdisc replace dev eth0 root cake bandwidth 950mbit(或 fq_codel)把队列压瘦、主动丢包控延迟;再用 sysctl 把 tcp_rmem/tcp_wmem、rmem_max/wmem_max 调大并开启 tcp_window_scaling=1,最后切到 BBR 拥塞控制,让高延迟线路既快又跟手。RackNerd、Vultr、Hetzner、DigitalOcean 这些 VPS 默认队列和 TCP 参数都偏保守,照着上面几条命令改一遍,晚高峰敲命令就能重回丝滑。