tc
(traffic control
)是一个应用层工具,它与内核的tc
模块共同协作,可以对内核进行流量控制,通过tc
可以实现限速、数据包过滤等功能,接下来简单的介绍下tc
的关键概念。
qdisc class filter
数据包的发送路径上,当数据包穿过网络协议栈到达网络设备层时,网络设备层并不会直接将数据包送往驱动层,网络设备层会先将数据包放入网络设备的队列中(enqueue
),随后再尝试从队列取出数据包(dequeue
)再送往驱动。
在enqueue
/dequeue
这样一个过程中,内核可以执行一些规则来控制包的走向,这里说到的规则,在tc
的世界里被叫做qdisc
,按照官方的解释,qdisc
(queueing discipline
)叫做队列规则,其实qdisc
不止体现了规则的作用,它也能够缓存数据包,enqueue
进网卡输出队列的数据包其实是缓存在qdisc
上的。
enqueue
到qdisc
的数据包有可能会马上被dequeue
然后送往驱动,也有可能暂时存放于qdisc
,待到合适的时机再dequeue
,数据包最终的结局全都取决于qdisc
的逻辑。
比如用下面的命令给ens33
网卡添加一个tbf
类型的qdisc
来给ens33
网卡限速:
tc qdisc add dev ens33 handle 10: root tbf rate 10mbps burst 1m limit 1m
enqueue
时,若tbf qdisc
内部的数据包缓存队列还未满,则数据包会被添加到tbf qdisc
内部的缓存队列上,若tbf qdisc
内部的缓存队列满了,则enqueue
的数据包会被丢弃。
dequeue
时,tbf qdisc
则会根据当前的发送速率来决定是否应该把缓存队列上的数据包取出来,速率达到了限定值,就不会把数据包从缓存队列上取出来。
不同类型的qdisc
有不同的逻辑,想要了解有哪些qdisc
以及它们的作用,可以参考tc
的帮助手册 1
qdisc
的大体可以分为classless
和classfull
两类,它们的区别在于:classless qdisc
内部最多只能有一个class
,classfull qdisc
内部可以创建多个class
,那class
又是用来做什么的呢?
可以将class
理解为qdisc
的子规则,enqueue
进qdisc
的流量会被细分到class
上,不同的class
在dequeue
时会执行不同的规则。
比如下面的命令,创建了一个htb qdisc
,并给htb qdisc
添加了两个class
,enqueue
到htb qdisc
的流量将会enqueue
到两个class
上,而dequeue
时,class 10:1
的输出速率会被限制到10MB/s,class 10:2
的输出速率会被限制到20MB/s。
tc qdisc add dev ens33 handle 10: root htb
tc class add dev ens33 classid 10:1 root htb rate 10mbps
tc class add dev ens33 classid 10:2 root htb rate 20mbps
对于那些包含class
的qdisc
,在enqueue
/dequeue
时,实际上就是从它们内部的class
去enqueue
/dequeue
。
对于classless qdisc
而言,它们内部最多只有一个class
,enqueue
/dequeue
的目标class
是确定的,但classfull qdisc
有多个class
,那enqueue
/dequeue
时,如何确定从哪个class
去操作呢?
enqueue
时,输入classfull qdisc
的数据包具体会被enqueue
到哪个class
上取决于filter
,filter
是classfull qdisc
的过滤规则,它会根据输入classfull qdisc
数据包的特性(比如报文头部的某些字段)来决定数据包的应该enqueue
到哪个class
上,大致如下图所示:
dequeue
时,则没有统一的准则来选择class
,不同类型的classfull qdisc
选择class
的方式是不同的(比如prio qdisc
,它给每个class
都赋予了一个优先级,dequeue
时它会选择优先级最高且有报文缓存的class
来进行操作)。
在使用classfull qdisc
,filter
是必不可少的,filter
需要我们手动添加,对于上面htb qdisc
的例子,假如使用了如下的命令来添加filter
,那么进入htb qdisc
的数据包若发往192.168.8.0/24,则它会被enqueue
到class 10:1
上, 若数据包发往192.168.4.0/24,则它会被enqueue
到class 10:2
上,没有被filter
匹配上的数据包去向如何则完全取决于qdisc
的逻辑,htb qdisc
会选择丢弃。
tc filter add dev ens33 parent 10: protocol ip prio 99 u32 classid 10:1 match ip dst 192.168.8.0/24
tc filter add dev ens33 parent 10: protocol ip prio 99 u32 classid 10:2 match ip dst 192.168.4.0/24
filter
也有很多种类型,不同类型的filter
采用的匹配策略是不一样的,想了解更多的filter
类型以及用法,可以参考tc
的帮助手册1。(上述两个命令使用的是u32
,u32 filter
可以对报文的各个字段进行匹配,我们匹配的是目的ip)
逻辑结构
在使用classfull qdisc
时,通常会给它添加多个class
,这些class
与qdisc
会形成树状的逻辑结构,如下所示:
有的qdisc
还支持在class
下端添加class
,比如htb qdisc
,随着class
的添加,最终可以形成多层的树状结构,如下所示:
从树状结构可以看到每个class
只会有一个parent
,可能有多个child
,enqueue
时,流量从parent
流向child
。
树状结构中的class
有两种,一种是inner class
,另一种是leaf class
,树状结构中那些叶子节点就是leaf class
(10:3 10:4 10:5 10:6 10:7),枝干节点就是inner class
(10:1 10:2)。
leaf qdisc
每个leaf class
都有一个leaf qdisc
,如下所示:
其实class
自身是不会缓存数据包的,它通过leaf qdisc
来缓存数据包,enqueue
到class
的数据包会enqueue
到它的leaf qdisc
上去。
创建class
时leaf qdisc
便会一同被创建,leaf qdisc
默认的类型为fifo,fifo qdisc
是逻辑最简单的一类qdisc
,enqueue
时,它将数据包缓存下来,dequeue
时,它按照enqueue
的顺序取出数据包,所以默认的leaf qdisc
仅起到缓数据包的作用,没有其它复杂的逻辑。
通常情况下,通过tc qdisc
命令创建qdisc
后,通过tc qdisc show
命令是可以看到我们创建的qdisc
的,但默认的leaf qdisc
是无法使用tc qdisc show
命令看到的,它们在背后默默的干活。
对于那些leaf qdisc,我们可以使用tc命令将它们替换为其它类型的qdisc
,进而组合出更为复杂的流控规则。
比如说把上图的class 10:3
的leaf qdisc
替换为sfq qdisc
(sfq qdisc
的作用是保证输出流的公平性),那么enqueue
到class 10:3
的数据包就还要执行sfq qdisc
的enqueue
逻辑,dequeue
时也会先执行sfq qdisc
的dequeue
逻辑,再执行htb class
的限速逻辑。
在一个class
下端添加另一个class
后,它就会由leaf class
转换为inner class
后,它的leaf qdisc
也会被释放,比如上图的class 10:1
和class 10:2
,它们的leaf qdisc
已经不存在了,而且inner class
本身不会缓存数据包。
有的classless qdisc
内部也有一个class
,比如tbf qdisc
,这种class
也是有leaf qdisc
的,也能替换成其它类型的qdisc
。但有的classless qdisc
内部没有qdisc
,比如sfq qdisc
,fifo qdisc
,它们就无法在下端级联其它类型的qdisc
。
classify
在enqueue
的过程中,classfull qdisc
会使用数据包携带的信息来从它众多的class
中选出一个class
用于enqueue
, 这个过程在tc的世界里叫做classify
。
正如前面提到的,classify
的一种方式就是通过filter
来实现的,enqueue
的过程中,qdisc
会遍历自身的filter
并使用数据包携带的信息与filter
进行匹配,匹配成功后数据包会被enqueue
到filter
指定的class
上去。
除了filter
,其实还有两种方式来分发数据包:
- 根据skb携带的
priority
字段来确定class
,这使得我们可以通过配置socket option(SO_PRIORITY)的来指定class
- 有的
qdisc
会基于TOS
来确定class
ingress qdisc
在数据包的发送路径上,可以创建qdisc
来控制数据包的输出行为,同样的在数据包的输入路径上,也可以创建qdisc
来控制数据包的输入行为,但输入路径上qdisc
的工作模式与发送路径上稍有不同,它不再将数据包enqueue
/dequeue
,自然也不会在enqueue
/dequeue
过程种去执行某些操作,输入路径上的qdisc
会通过filter
来完成工作(限速、过滤包等)。
在输入路径上,只能创建ingress
和clsact
这两类qdisc
,其它的类型的qdisc
不能创建,ingress
和clsact
都是classless qdisc
,它们自身没有任何关于限速的逻辑,它们依赖于filter
来完成工作,ingress
和clsact
在输入路径上的工作原理是相同的,接下来的描述中以ingress
为例。
网络设备层在将数据包输入网络协议栈之前,会判断是否存在ingress qdisc
,若存在,则调用ingress qdisc
的过滤规则(filter
)。前面提到过,filter
可以把数据包导向不同的class
,但filter
的功能远不止如此,它还能将数据包导向action
,action
可以执行一系列特殊的行为(包括限速、重定向包、NAT等)。
比如,执行如下两条命令后,就能给ens33
添加一个ingress qdisc
,并给ingress qdisc
添加一个filter
,filter
创建过程中会创建一个action
。数据包输入ingress qdisc
后,filter
会将来自192.168.6.0/24的数据包导向了action police
,action police
对送往自己的数据包进行限速。
tc qdisc add dev ens33 ingress
tc filter add dev ens33 parent ffff: protocol ip u32 match ip src 192.168.6.0/24 action police rate 2mbps burst 1m conform-exceed drop/ok
对于上面的例子,最终产生的结构如下图所示:
其实输入路径上的qdisc
工作过程就是通过filter
来匹配数据包,匹配上了就去执行filter
指定的action
,没匹配上就继续下一条匹配filter
。action
有很多类型,可以实现各式各样的功能,除了police
这类action
,还有mirred
、nat
、gact
等等,关于它们的作用和参数可以参考 2 中的 SEE ALSO
部分。
另外,action
是可以独立创建的,它不依赖于filter
的创建,比如下面的三条命令和上面的例子效果是一样的:
tc qdisc add dev ens33 ingress
// 创建一个index为4的police action
tc action add action police index 4 rate 2mbps burst 1m conform-exceed drop/ok
// filter将数据包导向index为4的police action
tc filter add dev ens33 parent ffff: protocol ip u32 match ip src 192.168.6.0/24 action police index 4
上面的例子中,filter
通过index
就能找到目的action
。不同的类型action
是独立管理index
的,比如说创建了index
为4的police action
,仍然可以创建index
为4的mirred action
。