为什么想到用 tc 限速
个人站长的服务器带宽通常只有 5Mbps 到 20Mbps,一旦某个用户开始下载一个大文件,或者某个爬虫疯狂抓取,其他访客的页面就会卡到打不开。更糟的情况是,如果你的 VPS 是按流量计费的,一个失控的下载能把当月流量套餐吃光。
常规做法是在 Nginx 层用 limit_rate 限制单连接速度,但它只对 Nginx 自己发送的响应生效,管不住 rsync、FTP、SSH 隧道这些走系统层的流量。要真正在网卡出口做统一限速,Linux 内核自带的 tc(traffic control) 才是正确工具。它工作在内核网络栈,对上层应用完全透明。
先搞懂 tc 的三个基本概念
tc 的配置语法看起来吓人,因为它的术语来自网络学术界。但抓住三个概念就够用了:
- 队列规则(qdisc,queueing discipline):挂在网卡上的一根"队列"。默认是
pfifo_fast,即先进先出。要限速就要换成htb(分层令牌桶)或tbf(令牌桶过滤器)。 - 类(class):htb 队列下面可以分多个类,每个类有独立的速率上限。这样就能实现"总带宽 100Mbps,A 类最多 50Mbps,B 类最多 20Mbps"。
- 过滤器(filter):决定哪些数据包进入哪个类。靠 IP、端口、协议来匹配。
一个关键细节:限速是"出口方向"的概念。Linux 只能控制自己发出的流量,无法直接控制进入的流量。所以限制"服务器上传"用 egress 队列,限制"服务器下载"要用 ingress + ifb 虚拟网卡绕一下。很多人第一次配 tc 发现限制下载不生效,就是这个原因。
实战一:整体出口限速(最简单)
假设网卡是 eth0,要把出口带宽限制在 10Mbps。用 tbf 一句话搞定:
tc qdisc add dev eth0 root tbf rate 10mbit burst 32kbit latency 400ms三个参数的含义:
rate:长期平均速率上限。burst:允许瞬间突发的字节数。太小会导致速度上不去(因为每个包都要等令牌),一般设为 rate 的 1/10 左右。latency:包在队列里最多等多久。等超时的包会被丢弃,这会引发 TCP 重传,所以要留够余量。
查看当前配置和统计:
tc qdisc show dev eth0
tc -s qdisc show dev eth0删除配置(改参数前必须先删):
tc qdisc del dev eth0 root注意 tbf 是一个"无类"队列,简单但不够灵活——它只能对整个网卡限速,无法区分流量类型。
实战二:给不同服务分配带宽(htb)
更实用的场景:服务器总出口 100Mbps,网站上跑着三个服务——主站 80 端口、下载服务、备份同步。希望主站优先,下载限速 20Mbps,备份限速 10Mbps。
# 1. 建立 htb 根队列,总带宽 100Mbps
tc qdisc add dev eth0 root handle 1: htb default 30
# 2. 建立主类,绑定总速率
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit ceil 100mbit
# 3. 建立子类:主站(保证 60,最多 100)
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 60mbit ceil 100mbit prio 1
# 下载服务(保证 20,最多 40)
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 20mbit ceil 40mbit prio 2
# 备份同步(保证 10,最多 20)
tc class add dev eth0 parent 1:1 classid 1:30 htb rate 10mbit ceil 20mbit prio 3
# 4. 用过滤器把流量分到各类
# 主站:源端口 80/443
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 \
match ip sport 80 0xffff flowid 1:10
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 \
match ip sport 443 0xffff flowid 1:10
# 备份同步:目标端口 873(rsync 默认)
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 \
match ip dport 873 0xffff flowid 1:30这里的关键是 rate 与 ceil 的区别:rate 是保证速率,ceil 是允许借用的上限。当下载服务空闲时,主站可以借用它的带宽(最多到 ceil 100mbit);但当下载服务有流量时,主站永远能拿到 60mbit 的保证。这种"借还"机制是 htb 比 tbf 强大的核心。
实战三:限制下载方向(ingress + ifb)
要限制"别人从你服务器下载"这个方向的流量(即网卡的 ingress),必须借助 ifb 虚拟网卡。原理是把入口流量重定向到一个虚拟出口设备,再在虚拟设备上做出口整形。
# 1. 加载 ifb 内核模块
modprobe ifb numifbs=1
# 2. 启动虚拟网卡
ip link set dev ifb0 up
# 3. 把 eth0 的入口流量重定向到 ifb0
tc qdisc add dev eth0 handle ffff: ingress
tc filter add dev eth0 parent ffff: protocol ip u32 \
match u32 0 0 flowid 1:1 \
action mirred egress redirect dev ifb0
# 4. 在 ifb0 上做出口限速(比如限制每 IP 下载 2Mbps)
tc qdisc add dev ifb0 root handle 1: htb default 10
tc class add dev ifb0 parent 1: classid 1:1 htb rate 100mbit
tc class add dev ifb0 parent 1:1 classid 1:10 htb rate 2mbit ceil 20mbit这里 match u32 0 0 的意思是"匹配所有包",即把全部入口流量都镜像到 ifb0,然后在 ifb0 上统一整形。要针对单个 IP 限速,可以进一步用 hash filter 或者按 dst 做 u32 匹配。
一个必须搞懂的坑:QoS 只影响你自己的发送
很多人配完 tc 后发现"下载还是很快",原因往往是:你在客户端限速,却以为是服务端。比如你从服务器下载文件,服务器是发送方,所以要在服务器上对 egress 限速;而如果你在服务器上下载东西,服务器是接收方,就得用 ifb 那套方案。
另一个隐性坑是内核参数。tc 依赖 net.core.default_qdisc 和 net.core.somaxconn 等设置,某些云厂商的镜像默认把 qdisc 设成了 fq_codel,这并不会和 htb 冲突,但如果你的限速规则写在 NetworkManager 管理的网卡上,重启网络后 tc 规则会被清空——因为 tc 是运行时配置,不写入任何配置文件。必须用 systemd 或 NetworkManager dispatcher 持久化。
持久化:让规则在重启后自动生效
tc 规则重启就没了,所以要把命令写进脚本并挂到开机执行。推荐放在 NetworkManager 的 dispatcher 里,这样网卡一上来规则就跟着配好:
cat > /etc/NetworkManager/dispatcher.d/90-tc-limit.sh <<'EOF'
#!/bin/bash
IFACE="$1"
ACTION="$2"
[ "$ACTION" = "up" ] || exit 0
[ "$IFACE" = "eth0" ] || exit 0
/sbin/tc qdisc del dev eth0 root 2>/dev/null
/sbin/tc qdisc add dev eth0 root handle 1: htb default 30
/sbin/tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit
/sbin/tc class add dev eth0 parent 1:1 classid 1:10 htb rate 60mbit ceil 100mbit
/sbin/tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 80 0xffff flowid 1:10
EOF
chmod +x /etc/NetworkManager/dispatcher.d/90-tc-limit.sh注意脚本里的 2>/dev/null——重复添加规则会报错,而 dispatcher 在网卡状态变化时可能被触发多次,加个容错更稳。
验证与观测
配完之后,用 tc -s 看统计,重点看 Sent(发了多少字节)、dropped(丢了多少包)、overlimits(有多少次因为超过速率被限流)。如果 dropped 持续很高,说明 burst 或 latency 设小了,会导致 TCP 频繁重传反而更慢。
tc -s class show dev eth0再配合 iftop 或 nload 实时观察网卡流量,验证限速是否生效。实测时建议用 scp 传一个大文件,观察速度是否稳定在你设定的值附近。如果实测速度只有设定值的一半,通常是 burst 太小,把 burst 调到 rate 的两倍试试。
小结
tc 看着复杂,实际只需要记住:要限上传用 egress htb,要限下载用 ingress + ifb;rate 是保底、ceil 是上限;规则重启会丢,必须持久化。对个人站长来说,最实用的场景不是精细的流量工程,而是防止单个用户或爬虫把带宽吃光,让人人有页面可看。这是花十分钟配置、能长期受益的一件事。
补充:如何排查"限速规则没生效"
tc 规则配完之后,最常见的三个"看起来配了但没用"的原因,值得单独列出来:
原因一:数据包走了另一个网卡。 现在的服务器常有多个网卡(eth0、ens3、docker0、甚至 VPN 的 tun0)。如果你的服务绑定的是内网网卡,或者流量经过 Docker 的 NAT 转了一道,那么你在 eth0 上配的规则根本管不到。确认方法:
ip route get 8.8.8.8
ip -br addr show第一条命令会告诉你访问外网时实际走哪张网卡、源 IP 是哪个。规则要配在实际发包的那张网卡上。
原因二:Docker 容器流量被绕过。 Docker 会给容器创建 veth 对和 docker0 网桥,容器发出的流量在宿主机上是从 docker0 转发到物理网卡的。你在物理网卡上配的 htb 默认类可能因为 default 30 而被归到备用类,导致主站流量被限速得很惨。排查:
tc -s class show dev eth0 | grep -A2 "class htb"看各个类的 Sent 字节数,如果发现你不希望被限的类占了绝大多数流量,就是分类器(filter)匹配错了。
原因三:qdisc 被其他工具覆盖。 一些云厂商的镜像、Kubernetes 的 CNI 插件、或者 BBR 相关的调优脚本,会在启动时重置网卡的 qdisc。典型的竞争是 fq 与 htb——BBR 需要 fq 调度器才能发挥最佳效果,而限速需要 htb。折中方案是把 htb 作为根队列,在其叶子类上使用 fq_codel 作为子 qdisc:
tc qdisc add dev eth0 parent 1:10 fq_codel这样既有限速,又不牺牲拥塞控制的效果。如果发现规则被覆盖,检查 /etc/sysctl.d/ 下的 BBR 配置和云厂商的初始化脚本。
补充:给单个 IP 限速的写法
个人站更常见的需求不是按服务分类,而是"防止某个 IP 拖垮带宽"。这需要用 u32 过滤器匹配目标 IP。假设要限制 203.0.113.5 这个 IP 的下载速度为 1Mbps:
# 在 ifb0 上建根队列
tc qdisc add dev ifb0 root handle 1: htb default 99
tc class add dev ifb0 parent 1: classid 1:1 htb rate 100mbit
tc class add dev ifb0 parent 1:1 classid 1:99 htb rate 100mbit ceil 100mbit
tc class add dev ifb0 parent 1:1 classid 1:5 htb rate 1mbit ceil 5mbit
# 匹配该 IP 的流量,丢进限速类
tc filter add dev ifb0 parent 1: protocol ip prio 1 u32 \
match ip dst 203.0.113.5/32 flowid 1:5如果要动态地对所有"超额"IP 自动限速,那就要写脚本定期读取 ss -ti 的连接速率,然后动态增删 filter。对个人站来说,更省事的替代方案是直接用 Nginx 的 limit_req 或 limit_conn 模块在应用层限制,只有确实需要限制非 HTTP 流量时才动用 tc。