Linux tc 流量控制实战:用 HTB 给 VPS 做带宽整形、优先级与端口/IP 限速全流程

为什么个人站长需要 tc 流量控制

很多人买了 VPS 之后,对带宽的认知就停留在「标称多少 Mbps」这个数字上。但真实情况是:你很少会跑满这条带宽,更多时候是某个进程或者某个 IP 悄悄把上行占满,导致 SSH 卡顿、网站响应变慢、备份任务把出口堵死。操作系统本身不会主动替你分配「谁先走、谁让路」,它默认是尽力而为(best effort)——谁发包快谁就赢。要让这台机器上的带宽变得「可预期」,就需要在内核层面做流量控制,也就是 Linux 里的 tc(traffic control)。

tc 属于内核 netem/sched 子系统,工作在协议栈的出口方向(egress)。它的核心思路是:数据包在离开网卡之前,先进入一个「队列规则」(qdisc),由队列规则决定什么时候发、发多少、超过配额的包是丢弃还是延迟。你可以把它理解成网卡前面的一个「调度员」。对个人站长来说,tc 最实用的三个场景是:给某个端口或某类流量限速、给关键业务(SSH、网站 80/443)留出优先通道、以及给备份/下载类任务设置一个「最高不得超过 X Mbps」的上限,防止它们吃掉全部带宽。

本文不堆砌 tc 的所有参数,而是围绕一个真实需求展开:一台 5 Mbps 上行的小内存 VPS,既要跑网站,又要定时 rsync 备份,还要保证 SSH 随时可用。我们会用 HTB 队列规则搭一套「分层带宽分配」,把这条总带宽切成分保底、有上限的几档,并讲清楚每个参数为什么这么填。

先搞清楚出口方向与限速的本质

一个常见的误解是「tc 能限制下载速度」。严格来说,tc 只能控制本机往外发的包。你要限制「这台机器下载的速度」,本质上是间接的:下载时对方发给你,你回 ACK;tc 通过控制 ACK 的发送节奏、或者控制你发出的数据流,间接影响对方的发送窗口。但真正精确、可靠的用法是限制上行和本机转发的流量。如果你有一台机器做 NAT 网关,tc 作用在它身上,就能控制内网所有机器的出口速度,这才是 tc 最经典的应用。

所以第一步永远是先确认「你要控制的流量是不是从这台机器发出去的」。对于 VPS 上跑网站的场景,主要出口流量是:网站响应给访客的数据(下行方向从服务器看是上行)、发出去的邮件、备份上传、以及各种更新下载。其中备份上传和软件更新是最容易失控的两个。

HTB 的核心概念:根队列、分类与令牌桶

tc 有几种队列规则,最常用的是 htb(Hierarchical Token Bucket,分层令牌桶)和 fq_codel(公平队列 + 抗缓冲膨胀)。HTB 的特点是「分层」:你可以先建一个根类,给出总带宽,然后在它下面挂若干子类,每个子类有自己的保证速率(rate)和最高速率(ceil)。当某个子类闲着时,它没用的带宽可以借给正在忙的兄弟类,但每个类最多只能借到自己的 ceil 上限。

HTB 的两个关键词是 rate 和 ceil。rate 是「保证速率」——只要这个类有流量,系统会尽量给它这个速率。ceil 是「封顶速率」——再闲也不能超过。举个小 VPS 的例子:总出口 5 Mbps,你可以这样分:网站与 SSH 这类交互流量 rate 1 Mbps、ceil 4 Mbps;备份任务 rate 500 Kbps、ceil 2 Mbps;其余默认流量 rate 500 Kbps、ceil 1 Mbps。这样平时备份跑起来最多占 2 Mbps,网站依然有 2 Mbps 以上的富余;而当备份停下来,网站可以独享到 4 Mbps。

另外一个必须搭配使用的规则是 fq_codel。HTB 负责「分配多少带宽」,但队列内部的排队和延迟需要 fq_codel 来治理。启用了 fq_codel 之后,缓冲区膨胀(bufferbloat)带来的延迟抖动会明显改善,交互体验更稳。标准做法是:根 qdisc 用 htb,然后在叶子类上挂 fq_codel。

一步步搭建出口限速

先看网卡名字,通常是小 VPS 上的 eth0 或 ens3。下面的命令假设网卡是 eth0、物理出口带宽是 5 Mbps。为了让内核有调度余量,根速率一般设得比物理带宽略低一点,比如 4800 Kbit(4.8 Mbps),否则队列来不及处理,限速反而不准。

# 1) 清掉可能存在的旧规则,避免叠加
tc qdisc del dev eth0 root 2>/dev/null

# 2) 建根队列 htbd,根速率设为物理带宽的 ~96%
tc qdisc add dev eth0 root handle 1: htb default 30

# 3) 根类:给出总天花板
tc class add dev eth0 parent 1: classid 1:1 htb rate 4800kbit ceil 4800kbit

# 4) 交互类(网站/SSH):保证 1Mbps,最多借到 4Mbps
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 1000kbit ceil 4000kbit prio 1

# 5) 备份类:保证 500kbps,最多 2Mbps
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 500kbit ceil 2000kbit prio 2

# 6) 默认类(其他所有流量)
tc class add dev eth0 parent 1:1 classid 1:30 htb rate 500kbit ceil 1000kbit prio 3

# 7) 给每个叶子类挂 fq_codel,治理延迟
tc qdisc add dev eth0 parent 1:10 fq_codel
tc qdisc add dev eth0 parent 1:20 fq_codel
tc qdisc add dev eth0 parent 1:30 fq_codel

到这里带宽已经分好了,但流量还全在 1:30(默认类)里,因为还没有分类规则。接下来用 tc filter 把不同类型的流量打到对应的类上。最常用的是按端口分类,比如把 SSH(22)和网站端口(80/443)归到交互类,把备份常用的 rsync(873)归到备份类。

# SSH 22 端口 -> 交互类 1:10
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 \
  match ip dport 22 0xffff flowid 1:10

# 网站端口 80 -> 交互类
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 \
  match ip dport 80 0xffff flowid 1:10

# 网站端口 443 -> 交互类
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 \
  match ip dport 443 0xffff flowid 1:10

# rsync 873 -> 备份类 1:20
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 \
  match ip dport 873 0xffff flowid 1:20

更实用的做法:按目的 IP 或网段限速

按端口分类有个前提:你知道备份进程用哪个端口。但很多站长是用 rsync -e ssh 或 restic 走 SSH 的,那就没法靠端口区分了。这时更可靠的是按目的 IP分类——因为你备份的目标服务器 IP 是固定的。比如备份仓库在 203.0.113.10,就可以给发往这个 IP 的所有包限速:

# 发往备份仓库 IP 的流量 -> 备份类 1:20
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 \
  match ip dst 203.0.113.10/32 flowid 1:20

反过来,如果你希望「除了访问自己网站的人以外,其他所有出站流量都不能挤占带宽」,可以先给目标 IP 网段做交互类,其余的落到默认类。分类的优先级(prio)越小越先匹配,所以要把最具体的规则放在最前面。

另一个高频需求是限制某个用户或某个 cgroup 的出口带宽。tc 本身只认数据包,不认识进程;要用 net_cls cgroup 给进程打上 classid 标记,再用 tc filter ... cgroup 匹配。这条路稍复杂,个人站在没有多租户需求时可以跳过,直接用 IP/端口分类就够了。

常用查看与排错命令

规则建好之后,第一件事是确认层级对不对。下面三条命令分别看队列规则、类和过滤器:

tc qdisc show dev eth0
tc class show dev eth0
tc filter show dev eth0 parent 1:

tc -s class show dev eth0 会额外打印每个类的统计:发包数、字节数、被丢弃数、被借用的速率等。重点看 dropped 和 overlimits。如果某个类的 dropped 很高,说明这个类经常撞到自己的 ceil,被硬丢了包——这时要么调高 ceil,要么这个业务本身就不该被限得这么死。如果所有类的 borrowed/rate 都是 0,说明根本没匹配到流量,问题多半出在 filter 没生效或者网卡方向搞反了。

还有一个坑:限速不生效一定要先确认网卡名和方向。在小 VPS 上,有些虚拟网卡(比如 OpenVZ 的 venet)对 tc 支持有限,甚至根本不支持队列规则。用 ip link show 看网卡类型,如果发现加了规则但 tc -s 里计数永远是 0,基本就是虚拟化层不支持。KVM/Xen 的 virtio 网卡通常没问题。

持久化:重启后规则别丢

tc 规则在重启后会全部消失。个人站最容易踩的坑就是「调试时跑通了,重启后限速没了」。有两种持久化方式:一是写进 /etc/network/interfaces 的 post-up(Debian 老式网络),但更通用、更可控的是写一个 systemd oneshot 服务,开机执行一段脚本。脚本里就是上面那串 tc 命令,注意保留 tc qdisc del ... 2>/dev/null 开头,保证重复执行不会叠加。

[Unit]
Description=Traffic shaping via tc
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/tc-shape.sh

[Install]
WantedBy=multi-user.target

把脚本设成可执行,服务 systemctl enable --now 之后,每次开机就会自动重建规则。改参数时先 systemctl restart 对应的服务,或者直接手工跑一遍脚本,再看 tc -s 确认。

几个容易翻车的细节

第一,根速率不要等于物理带宽。很多人直接把根 rate 设成标称的 5 Mbit,结果因为内核调度和封装开销(以太网头、IP 头、TCP 头加起来每包好几十字节),实际吞吐反而达不到预期,还容易丢包。设成物理带宽的 90%~96% 更稳。

第二,HTB 的单位别写错。tc 里 kbit 是千比特每秒,kbps 也常被 tc 理解为千比特每秒,但 KB、mbps 这些非标准写法容易出问题。统一用 kbit / mbit,单位换算时记住 1 Mbps = 1000 kbit(tc 默认十进制)。

第三,限速是双向的,热点要分开处理。你限制了上行,但没限制下行;如果下行也被占满(比如你在服务器上下载大文件),访客同样会变慢。下行的限速要作用在「你的上游网关」,自己的 VPS 管不到。折中做法是给下载类任务用 wget --limit-rate 或 rsync --bwlimit 在应用层限速,比在内核层硬怼更简单。

第四,ssh 别把自己锁死。如果你把 SSH 也归到限速类,且刚好在传大文件,SSH 可能会卡到没法操作。稳妥做法是给 SSH 一个较高的 prio 和足够的 ceil,或者干脆给管理 IP 单独放行、不限速。tc 支持按源 IP 匹配,把自己的办公 IP 直接指向根类 1:1 就能「免限速」。

小结

tc + HTB 给个人站长带来的最大价值不是「省带宽」,而是把「带宽」这件事从不可控变成可预期。你不再需要担心某个定时任务在半夜把出口堵死、导致网站白天打开缓慢;也不用为了让备份跑完而临时停站。核心操作其实只有四步:del 旧规则、建 htb 根、建几个子类、加 filter 分类。真正需要花心思的是「怎么分」——哪类流量该保底、哪类该封顶、哪类该优先。分好之后,用 tc -s class show 观察 dropped 和 rate,再微调参数,一台便宜的小 VPS 也能跑出稳定的体验。

Last modification:October 12th, 2026 at 12:24 pm

Leave a Comment