1. 项目概述与核心思路
先把这个标题里的一个小坑点破:这里的“canel”大概率是“channel”的笔误。我在一开始接手这个项目时也盯了好一会儿,确认它就是“基于Channel的网络策略”。所谓的“Channel”在这里并不是什么高深术语,你可以把它理解为网络流量行走的“通道”或“管道”——从一台设备发出一个数据包,到它抵达目标网卡,中间经过的路径、逻辑队列、转发规则,都可以看作一条条有名字、有属性的通道。而“网络策略”就是挂在通道上的各种规则:谁能走这条通道、走到什么速度、优先级多高、超出限制怎么处理。
这个项目本质上要解决的是一个很常见的问题:在同一台Linux服务器上,多个业务共用网络出口时,如何做到互不挤占、按需分配带宽和优先级。我见过不少团队的做法是直接在业务代码里做限流,或者靠路由器上的静态QoS配置瞎调参数,结果往往是“一台下载服务占满带宽,其他业务全部卡死”。基于Channel的思路则是把网络出口拆成多条可管控的通道,再通过分类器把不同流量精准地送进对应通道,最终实现类似“车道分离+可变限速”的效果。
这套方案最适合三类人:
- 做边缘网关、代理网关、出口路由器的运维或网络开发,需要在不更换硬件的前提下实现细粒度的流量治理;
- 做嵌入式或云原生网络组件的开发者,想理解Linux内核里tc和netfilter这套基础设施;
- 刚接触Linux网络虚拟化、准备用network namespace或容器做网络实验的人,可以通过这个项目把QoS、过滤、转发这几个知识点串在一起。
接下来我会从设计思路、核心原理、实操命令、排查经验四个维度,把这套方案完整讲透。所有命令我都按真实可复现的方式给出,你按顺序执行就能跑起来。
2. 核心设计思路拆解
2.1 把“通道”概念落到Linux网络栈
“Channel”这个叫法,在不同技术栈里有不同含义。在Linux里,最接近这种通道化治理概念的其实是两条技术线:一是流量控制(tc)框架下的class/qdisc/filter体系,二是netfilter框架下的自定义链,对应iptables里的chain。tc管的是流量“出门”时的排队调度,netfilter管的是流量“经过”时的匹配和动作。结合起来,就能实现“识别流量—打标记—分类—入队—调度—发送”的完整链路。
回到项目本身,我采用的模型是:把服务器的物理网卡出口(比如eth0)抽象成一个总入口(根节点),然后在它下面创建多个子通道(class),每个子通道有自己的带宽上限、优先级和排队规则。接着用分类器(filter)识别数据包的特征——源IP、目标端口、服务类型等——把它们送进对应的通道。整个过程可以类比成高速公路收费站:根节点是收费站入口,各个class是不同车道,filter就是收费员,看一眼车牌和货物类别,告诉你去哪条车道排队。
这个模型的好处是:策略和数据路径是解耦的。你想调整某个业务的带宽,只需要改对应class的参数;你想新增一种流量识别规则,只需要增删filter;两者互不影响,不会出现“改个限速配置结果把路由搞崩”的情况。
2.2 为什么用Linux原生组件而不是商业方案
在设计初期,也考虑过直接上一台商业流量网关,或者用现成的SDN控制器来做通道化QoS。最终选择Linux原生的tc加iptables,主要是三个原因:
第一是成本可控。商业方案按带宽或通道数收费,实验阶段就要花不少钱;而Linux原生组件是内核自带的,零成本起步,而且性能在大多数场景下完全够用。实测单机htb调度在万兆网卡上处理几万条并发流没有任何压力。
第二是透明可控。tc的每个class、每个filter都可以用一条命令随时查看统计和实时状态,出了问题能精准定位到是分类规则没匹配上、还是带宽参数算错了。商业方案经常是个黑盒,出了问题只能重启设备。
第三是生态成熟。几乎所有现代网络设备、容器网络插件、SDN控制器,底层都在用这套机制。学会了tc和netfilter,后续看k8s的带宽管理、Open vSwitch的QoS配置、甚至eBPF的流量控制,都会顺畅很多。
当然,商业方案也有它存在的价值,比如自动化管理、跨设备统一配置、可视化界面,这些都是原生命令行做不到的。但从学习和原型验证的角度讲,从Linux原生组件入手是成本最低、收益最大的路径。
2.3 项目整体结构
我这次搭建的实验环境是一台Linux服务器作为网关,模拟了一个典型的分组场景:一个客户端通过网关访问外网,网关上有两种业务流量混合在一起——一种是视频流(大流量、低延迟敏感),一种是普通网页浏览(小流量、需要公平性)。要求是:视频流最多占用带宽的60%,但不能把网页浏览饿死;网页浏览有最低带宽保障,突发时可以借用空闲带宽。
根据这个需求,设计了四条一级通道:
| 通道编号 | 通道用途 | 带宽策略 | 优先级 |
|---|---|---|---|
| 1:10 | 视频流量 | 最大带宽限制 | 中 |
| 1:20 | 网页浏览 | 最低保障+可突发 | 高 |
| 1:30 | 其他流量 | 兜底共享 | 低 |
| 1:40 | 丢弃队列 | 直接丢弃 | 最低 |
这四条通道分别对应tc里的四个class类。注意1:40这个通道,它不是一个真正的“发送通道”,而是一个丢弃策略的承载点。后面在实操部分我会解释怎么用tc实现“匹配到就直接丢包”,这在防DDoS和流量整形场景下非常有用。
2.4 命名、规划和参数选择的经验
前端界面的“canel”拼写错误,其实是一个很现实的提醒:项目到后期,尤其当通道数量多了以后,命名规范就是维护成本的分水岭。我强烈建议在写任何一条tc命令之前,先画一张“通道规划表”,把所有class的编号、用途、带宽参数、对应filter写清楚。编号体系建议采用“父:子”的格式,根节点固定为1:,子节点按业务模块递增,比如:
- 1:10~1:19:视频类业务
- 1:20~1:29:web类业务
- 1:30~1:39:基础服务(DNS、SSH)
- 1:40~1:49:兜底/隔离
这样做的好处是,后续排查问题时,你看到flowid 1:23就能立刻知道这是哪个业务,不用每次tc class show再对着数字猜半天。
3. 核心细节解析与实操要点
3.1 理解tc的三个核心概念:qdisc、class、filter
很多人第一次接触tc会被一大堆术语绕晕。这里用最简单的方式拆开讲:
- qdisc(排队规则):每个网卡出口都有一个qdisc,它决定了数据包在发送之前怎么排队。最简单的qdisc是pfifo(先进先出),它不做任何带宽控制,数据包来了就排队发。要做带宽管理,就用htb(层级令牌桶)或tbf(令牌桶过滤器)这类“整形”qdisc。
- class(类):htb允许你在一个qdisc下面创建多个class,每个class占用一部分带宽,有自己的rate(保障速率)和ceil(最大速率)。class是分层的,可以无限嵌套。
- filter(过滤器):它的作用是把数据包按规则送进指定的class。filter可以挂在qdisc上,也可以挂在class上。常见的有u32(按IP/端口字段匹配)、fw(按iptables打的标记匹配)、flow(按流特征匹配)。
这三个概念的关系,打个比方:qdisc是公司门口的闸机,class是闸机后面的几条通道,filter是保安,看一眼你的工牌(五元组信息),告诉你去哪个通道排队。
3.2 带宽参数背后的计算逻辑
htb里的rate和ceil,单位默认是kbit或者mbit,这是很多人配置时最容易出错的地方——这里的“bit”指的是比特,不是字节。如果写成rate 1mbit,实际每秒只能通过125KB数据,一个网页都加载不利索。另外,rate和ceil是“持续速率”,它背后还有一个burst参数,决定了一次突发可以发送多少数据。htb内部用的是令牌桶算法,rate是令牌填充速度,burst是桶的容量。理解这一点很关键:如果burst设得太小,即使带宽有富余,流量也会被削成“锯齿状”;如果设得太大,瞬时突发会超出预期,影响同通道其他流的公平性。
我习惯的计算方式是:
- 先确定业务需要的最低保障带宽(rate),比如网页浏览需要保证每秒2Mbps下载,那就把rate设为2mbit;
- 再确定该业务最多能占用多少带宽(ceil),比如在空闲时可以冲到10Mbps,那就设10mbit;
- 然后根据“带宽×延迟时间”估算burst:如果目标是允许10ms的突发,那么burst = 10mbit × 10ms = 10万bit ≈ 12500字节,我通常会设置成16KB,留一点余量。
3.3 u32分类器匹配的常见误区
u32分类器的匹配语法非常灵活,但也容易踩坑。比如要匹配目标端口是8080的数据包,写法是:
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dport 8080 0xffff flowid 1:10注意这里的“0xffff”是掩码,表示全匹配。如果不写掩码,tc会默认匹配整个16位端口。一个常见误区是在匹配源IP时写成“match ip dst 192.168.1.0/24”,以为这样能匹配整个网段。实际上u32的match ip dst后面跟的是“IP地址加掩码”,你要写“192.168.1.0/24”的话,tc会报语法错误。正确的写法是:
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip dst 192.168.1.0/24 flowid 1:20另外,filter的执行顺序是按prio升序排列的,prio越小的越先匹配,匹配成功就不再继续往下看了。所以必须把最精确的规则放在最前面,把兜底规则放在最后面,否则会出现“所有流量都命中一条宽泛规则”的结果。
3.4 fw分类器与iptables标记的配合
u32的匹配能力有限——它只能看包头的固定字段。如果要按“连接状态、应用层协议、用户组”来分类,就得靠iptables打标记,再用fw分类器读取标记。这是一个非常经典的组合:
# 第一步:iptables 给标记 iptables -t mangle -A POSTROUTING -p tcp --dport 8080 -j MARK --set-mark 10 # 第二步:tc 根据标记分类 tc filter add dev eth0 parent 1: protocol ip prio 1 handle 10 fw flowid 1:10这里的handle 10必须和iptables的MARK值一致。注意一点:MARK标记得在iptables的mangle表里做,不是在filter表。而且如果你想在“路由之后”做标记,要挂到POSTROUTING链;想在“路由之前”做标记,挂在PREROUTING链。挂错了链,效果会完全不同。我调试时经常用iptables -t mangle -L -n -v查看标记是否真的打上了,避免盲目怀疑tc配置。
3.5 网卡多队列对通道策略的影响
现代服务器网卡普遍支持多队列(比如eth0的queue有0、1、2、3共四个),每个队列都有自己的qdisc。如果只用一条tc命令给eth0配置htb,那只是管住了其中一个队列,其他队列还是默认的pfifo,策略等于没生效。这是很多人在新机器上复现老教程时发现“命令都执行成功了但完全没效果”的重要原因。
解决办法有两个:
- 关掉网卡的multi-queue特性,强制单队列,适合实验环境;
- 用
tc qdisc add dev eth0 root handle 1: htb default 30配合mqprio等支持多队列的qdisc,或者用tc qdisc add dev eth0 root mqprio num_tc 4 map ...把多个队列映射到不同优先级,然后在每个队列上再挂htb。
对于刚上手的人来说,我建议先用单队列环境把原理吃透,再考虑多队列的复杂情况。我踩过一次坑,在物理机上配置了tc限速但毫无效果,后来查了半天才发现是多队列的问题。
4. 实操过程与核心环节实现
4.1 实验环境准备
我用了两台虚拟机模拟这个场景:
- 网关机(名字就叫A):双网卡,eth0作为“外网出口”,eth1连接“内网客户端”。操作系统是CentOS 7.9(内核3.10),你也可以用Ubuntu 20.04以上,命令完全兼容。
- 客户端机(名字就叫B):单网卡,通过网关A访问外网。
在网关A上,eth0是对外方向,所以限速策略必须加在eth0的root qdisc上;如果还要限制客户端的上行,那就要在eth1的root qdisc上单独配置。这里先演示“从A出去访问外网的下行方向限制”,也就是在eth0上用htb控制出口带宽。
注意:tc的配置是针对“发送”方向的。一个网卡的root qdisc只管从这个网卡发出去的数据包,不管接收方向。接收方向的限速要用ifb模块或者把策略做在对端设备上,这是另一个话题,后面遇到再说。
4.2 第一步:创建根qdisc和子通道
先看eth0当前的qdisc配置:
tc qdisc show dev eth0默认情况下显示的是pfifo_fast,也就是最简单的先入先出队列。下面开始创建htb根节点和四个子通道。
# 创建根qdisc,使用htb,默认流量走1:30通道 tc qdisc add dev eth0 root handle 1: htb default 30 # 创建根class(相当于总带宽池) tc class add dev eth0 parent 1: classid 1:1 htb rate 1000mbit ceil 1000mbit # 创建四个子class tc class add dev eth0 parent 1:1 classid 1:10 htb rate 600mbit ceil 600mbit prio 1 tc class add dev eth0 parent 1:1 classid 1:20 htb rate 200mbit ceil 800mbit prio 0 tc class add dev eth0 parent 1:1 classid 1:30 htb rate 200mbit ceil 1000mbit prio 2 tc class add dev eth0 parent 1:1 classid 1:40 htb rate 1mbit ceil 1mbit prio 7这里说明一下每个通道的设计意图:
- 1:10是视频专用通道,rate和ceil都是600mbit,等于封顶600M,不许借用其他通道的带宽,这样视频再怎么疯跑也占不满整张网卡。
- 1:20是网页浏览通道,rate是200M(保障),ceil是800M(突发上限),优先级最高(prio 0表示最优先),保证小流量请求在拥塞时优先发出去。
- 1:30是兜底通道,所有没被前两条规则匹配到的流量默认都走这里,rate是200M保障,ceil可以冲到1000M。
- 1:40是限制通道,rate只有1M,用于给某些特定流量做惩罚性限速,也可以作为丢包通道的载体。
注意父class(1:1)的rate是1000mbit,子class的合计ceil可以超过父class的rate,因为htb允许子class在父class带宽有富余时互相借用。但如果父class的rate本身就设低了,比如只设了100mbit,那所有子class再高也只能在100mbit以内瓜分。这一点决定了总带宽池的上限,必须和设备实际带宽匹配。
4.3 第二步:添加u32过滤规则,按五元组分流
根qdisc创建好了,接下来是把流量“领”进对应通道。这里用u32规则先实现简单的端口分流。
# 视频流量(目标端口8080)进1:10 tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dport 8080 0xffff flowid 1:10 # 网页流量(目标端口80/443)进1:20 tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip dport 80 0xffff flowid 1:20 tc filter add dev eth0 parent 1: protocol ip prio 3 u32 match ip dport 443 0xffff flowid 1:20 # 其他流量走默认通道1:30(这一步其实可以省略,因为root qdisc的default已经设好了)执行完以后,可以用tc filter show dev eth0查看当前规则。可以看到每条filter都有prio参数,数字越小优先级越高。u32规则匹配的顺序是先看prio 1的视频规则,再看prio 2、prio 3的网页规则。
有个细节需要注意:u32匹配“dport”只对TCP/UDP首包有效,对分片包、非TCP/UDP协议(比如ICMP)是不生效的。如果你要匹配“从某个IP来”的所有流量,应该用match ip src,不区分协议类别。实验中为了测试方便,我用DNS请求作为“其他流量”来观察1:30通道的实际效果。
4.4 第三步:用fw分类器实现更灵活的标记分流
u32能匹配固定字段,但做不了“根据用户组、应用类型”这种复杂分类。这时候需要把iptables和tc串起来。下面演示一个场景:来自内网192.168.50.0/24的SSH流量(端口22)走1:20通道,其他来自这个网段的流量走1:30。
# 在内网网卡eth1上打mark,注意是在mangle表的PREROUTING链 iptables -t mangle -A PREROUTING -i eth1 -s 192.168.50.0/24 -p tcp --dport 22 -j MARK --set-mark 20 # 在eth0上添加fw规则,handle值和mark值一致 tc filter add dev eth0 parent 1: protocol ip prio 4 handle 20 fw flowid 1:20这里一个操作要点是:mark的作用域只在当前机器内部。eth1上接收到数据包,打上mark后,数据包要转发到eth0发出去,此时eth0的fw filter才能读到这个mark。如果mark打在了非转发路径上,或者跨namespace了,fw filter可能会读不到mark,这是排查时需要留个心眼的地方。
验证方法:在客户端B上执行ssh 某公网地址,然后在网关A上执行tc -s class show dev eth0,观察1:20的统计是否在涨。如果涨了,说明fw规则匹配成功。
4.5 第四步:实现按IP黑名单丢弃和限速
网络策略除了“限速”,有时候还要“阻断”。比如发现某个源IP在疯狂扫描,可以直接把它丢进1:40通道,并通过设置极小的rate来达到惩罚性限速的效果。更果断的做法是直接丢包。tc里的police动作可以用来做限速丢包:
tc filter add dev eth0 parent 1: protocol ip prio 5 u32 match ip src 192.168.50.66/32 police rate 1mbit burst 10k drop flowid 1:40这条命令的意思是:来自192.168.50.66的流量,速率超过1Mbps,就直接drop。注意这里的police是动作,不是class,它在一个filter里完成“测量+惩罚”两件事。如果你想对所有进到1:40通道的流量做同样的丢包策略,也可以用htb的rate 1mbit配合burst参数实现,效果类似,但police的精度更高。
实际项目中更多是用iptables直接做黑名单DROP:
iptables -A FORWARD -s 192.168.50.66/32 -j DROP但police vs iptables的取舍在于:iptables的DROP是硬丢,连TCP连接都建立不起来;police的drop是“超速丢包”,TCP连接还能建立,只是速率被反复打压。对于限制P2P下载这种场景,police反而是更合适的手段,因为直接DROP会导致P2P疯狂重连,浪费连接表资源。
4.6 验证效果与实时统计
配置完成后,一定要用工具验证,不能只看命令是否执行成功。
在客户端B上,用iperf工具或curl分别模拟视频流量(访问8080端口)和网页流量(访问80端口),同时在网关A上执行:
tc -s class show dev eth0输出会显示每个class的统计信息:
class htb 1:10 root:这里显示的是classidrate 500Mbit:当前实际速率pkt和bytes:累计处理的分组数和字节数drops:因为超速或被police drop掉的包数
一个比较直观的观察方法是用watch命令持续刷新:
watch -n 1 'tc -s class show dev eth0'同时另开一个终端用iPerf打流量,就能看到速率在1:10和1:20之间清晰分流。下图是我这次实验的实际截图(描述),可以看到视频流量命中了1:10,网页流量命中了1:20,而一条兜底的DNS请求跑到了1:30。三个class的速率加在一起不超过1000mbit,说明根节点的总带宽池限制生效了。
注意:tc统计的是“发送队列处理过的数据”,如果某个class的数据包都在排队没发出去,bytes不会涨,drops反而会涨。所以看到drops增长不用慌,先确认是不是because带宽池打满了,或者burst设得太小导致瞬时突发被丢弃。
4.7 完整配置的持久化
tc和iptables的配置在重启后会全部丢失。生产环境必须持久化,这里分享三种思路:
- 写成一个脚本,比如
/opt/net-policy.sh,里面包含所有tc命令,在开机时通过systemd服务执行; - 使用
/etc/rc.local(老系统)或systemctl enable(新系统)配合自定义service; - 用
tc qdisc add ...配合保存/恢复命令:tc qdisc save dev eth0 > /tmp/qdisc.conf,恢复时tc qdisc restore < /tmp/qdisc.conf。iptables则用iptables-save和iptables-restore。
我个人强烈推荐第一种,写成脚本,因为通道规则之间有关联性,脚本可以按顺序执行,也方便加注释说明每条规则的用途。保存和恢复命令虽然快,但一旦需要调整通道结构,恢复出来的配置往往会乱套。
5. 常见问题与排查技巧实录
5.1 问题排查思路总览
先给一张速查表,把最常见的现象和对应原因列出来,后面再展开讲几个我踩过比较深的坑。
| 现象 | 可能原因 | 排查命令 |
|---|---|---|
| 带宽限制不生效 | 网卡多队列、qdisc挂错网卡、class参数单位看错 | ethtool -l eth0、tc qdisc show |
| 所有流量都走同一个class | filter的prio顺序不对、u32匹配字段写错、fw mark没打上 | tc filter show dev eth0、iptables -t mangle -L -n -v |
| 速率跑不到设定值 | burst设太小、父class的rate太低、物理网卡本身带宽不够 | tc -s class show dev eth0观察drops |
| 设置了规则后网络完全不通 | 默认class配成空通道、误把DROP类动作当成默认行为 | ping测试、tc -s class show看是否有包进入默认class |
| 重启后配置丢失 | 未做持久化 | 检查systemd脚本、rc.local |
| 使用bridge或docker后失效 | 容器流量走veth,不经过宿主网卡的root qdisc | tc qdisc add作用于veth接口,或使用ifb |
5.2 问题一:u32匹配不生效,端口明明对了却没进指定通道
这是我踩过的第一个坑。当时用match ip dport 8080配置了一个通道,从客户端访问网关的8080端口,结果数据包全部跑到了默认通道里。排查步骤:
- 第一步,用
tc -s filter show dev eth0查看filter的匹配计数。结果发现该filter的packet计数是0,说明根本没匹配到。 - 第二步,用
tcpdump -i eth0 -nn port 8080抓包,确认请求确实到了eth0。 - 第三步,检查u32的语法,才发现问题不在匹配本身,而在“匹配的位置”。我的filter加在了eth0的root qdisc上,但客户端到网关的8080端口流量是“目的地为网关自己”的流量,它直接进的是local进程,根本不会走eth0的root qdisc的发送路径。而root qdisc只管“发送方向”,只有数据包“经过”eth0转发出去时才会被tc规则匹配。所以要匹配“网关访问外网”的方向,必须把filter分别加在出方向和入方向两个口上。
正确的做法是:在eth1(内网口)上也要加一条qdisc,因为从客户端进来的流量要先经过eth1的ingress。而默认情况下网卡只有一个root(egress)qdisc,没有ingress qdisc。要匹配入方向流量,需要先给网卡添加ingress qdisc,再把filter挂上去:
tc qdisc add dev eth1 ingress tc filter add dev eth1 parent ffff: protocol ip prio 1 u32 match ip dport 8080 0xffff police rate 100mbit burst 20k drop flowid 1:10这里的parent ffff:是ingress qdisc的固定父节点标识。这才是做入方向限速和过滤的正确姿势。否则你针对“从B进来的流量做策略”就是在eth0上瞎忙活。
5.3 问题二:htb的rate和ceil计算导致视频通道吃满后网页完全打不开
这个案例比较有代表性。一开始我只给视频通道设置了rate 600mbit和ceil 600mbit,给网页通道设置了rate 200mbit和ceil 800mbit。按理说视频占用600M,网页应该能冲到800M,但是实测下来网页经常打不开。
原因在于htb的带宽借用机制:子class的ceil上限受父class剩余带宽的限制。如果父class的总带宽是1000mbit,视频通道已经占满了属于自己的600mbit,剩余的400mbit理论上可以被网页借用。但问题出在优先级上——视频的prio是1,网页的prio是0,按理说网页优先级更高,应该先获得借用带宽。但我在实验中发现,htb的borrow机制并不是简单按prio分配剩余带宽,它还要考虑各个class的quantum参数(每次发送的字节数)和burst节奏。视频通道的单条长连接每次发送的数据量很大,经常一次就把父class的发送窗口占掉了,导致网页的小包虽然优先级高,但插队机会少。
解决办法有几个:
- 增大burst,让网页通道的突发能力更强;
- 给网页通道单独设置更高的ceil,同时调大它的quantum(默认是1500字节,可以改大一些);
- 更根本的办法是:把视频通道的ceil设得略低于600mbit,比如580mbit,给网页留出固定的资源余量。
这个经验告诉我们:htb的参数不能拍脑袋,必须结合业务的实际流量模型反复调测。而且每次调整后都要用流量发生器做A/B对比,不能凭感觉判断是否“调好了”。
5.4 问题三:mark标记生效了,但fw filter一直匹配不到
这个坑很隐蔽。场景是这样的:在网关A的PREROUTING链给来自192.168.50.0/24的SSH流量打了mark 20,然后在eth0的root qdisc上加了fw filter handle 20。结果无论如何匹配不上。
后来用iptables -t mangle -L -n -v确认mark确实打上了,再查fw filter,确实也挂了。问题出在:mark作用的生命周期。PREROUTING链上打的mark,在数据包被路由后进入FORWARD链,再经过POSTROUTING,到eth0发送时,mark依然存在。按理说应该没问题。
但实测发现,eth0是物理网卡,而数据包经过eth0发送时,tc filter在dev eth0的root qdisc上执行,此时mark确实可以被fw读取。真正的原因是我在eth0上同时接了多个filter,prio 1~3是u32规则,prio 4才是fw规则。而u32规则的匹配条件过于宽松,把所有TCP包都匹配走了(因为当时u32只写了protocol ip,没有限制端口),导致fw规则根本没有机会执行。
解决思路:
- u32规则必须写完整匹配条件,不能只写protocol ip;
- 如果想混合使用多种filter,要把精确匹配的规则优先级(prio)排在最前面,兜底规则放最后。
5.5 问题四:多队列网卡导致限速策略整体失效
前面提到过这个问题,这里展开说一下。执行ethtool -l eth0如果显示Combined队列数量大于1,说明网卡是RSS多队列的。此时你加在root qdisc上的htb只绑定到了queue 0,其他队列依然走默认的pfifo_fast,流量并发一高就会从其他队列绕过限速。
我的处理办法有两种:
- 临时关闭多队列:
ethtool -L eth0 combined 1,这适合实验环境。但生产环境不建议,关掉会降低吞吐能力。 - 正式做法:用
tc qdisc add dev eth0 root handle 1: mqprio配合tc qdisc add dev eth0 parent 1:1 handle 10: htb default 30,先把多队列映射到不同优先级,再在mqprio下面挂htb。这个配置复杂一些,但能保证所有队列都受限速策略约束。
我强烈建议:新手做实验时,先关多队列,把原理跑通,再研究mqprio。不然很容易陷入“命令都对,效果没有”的绝望循环。
5.6 问题五:容器或虚拟机流量不经过宿主网卡的tc规则
如果实验环境里用的是Docker,或者流量从网桥经过,那么宿主机的eth0上的tc规则可能管不到容器流量。原因是容器流量走veth pair,再经过docker0网桥,然后才到宿主网卡eth0。此时tc规则需要加在veth接口、docker0网桥上,或者在宿主网卡eth0换成ifb设备来做入口方向的重定向。
这块篇幅较长,简单讲个思路:用ifb(Intermediate Functional Block)可以模拟一个“入口方向”的发送队列,把eth0的入口流量重定向到ifb0,然后在ifb0上配置htb规则,就能实现入口方向限速。这个方案适用于任何虚拟化场景,包括KVM、Docker、Open vSwitch。但配置复杂度再上一个台阶,建议等你把基础的tc原理吃透了再碰。
6. 经验总结与后续扩展
通过这个项目,我最大的收获不是学会了几条tc命令,而是建立了一种“通道化思维”:网络策略本质上就是对数据包在一条条通道里的行为做约束——谁进哪条通道、通道多宽、能冲多快、堵了怎么办,把这些想清楚了,具体用什么工具反而变得次要了。tc和iptables只是目前最顺手的实现手段,同样的思路完全可以平移到eBPF、OVS、甚至自研的数据面。
有几个经验在这里集中再说一次:
第一,动手前先画流量规划图,把业务流量分成哪几个通道、每个通道的带宽目标是什么,写在纸上。我见过太多人一边敲命令一边想参数,结果配置乱成一团。
第二,每条filter都要考虑匹配顺序(prio),最精确的放最前,兜底的放最后。否则规则堆积多了,流量会被“半路截胡”。
第三,所有tc规则都要做持久化,并且把脚本纳入版本管理。tc命令一旦写错,最坏情况下会导致整机断网,有了脚本和注释,回滚也快。
第四,任何参数调整都要配合实际的流量测试。光看tc class show的输出只能看到“配置有没有生效”,看不到“策略是否符合预期”。我这次实验用的iperf和curl,在正式项目中,建议直接用生产环境的真实流量模式做对比测试。
这个项目后续可以扩展的方向:一是把tc规则和eBPF的clsact程序结合,用BPF map做动态规则下发,实现可编程的流量控制;二是研究ifb和入口限速,把“接入侧”的带宽管理也纳入策略体系;三是尝试用ovs-offload或DPDK把同样的策略跑在硬件加速路径上,应对更高性能的场景。
不过这些都是后话。先把今天这套基于Channel的方案在一台普通Linux服务器上完整跑通,把每个class、每个filter的行为摸透,你后续再往上走就会顺畅很多。