最近接触了linux tc,做一些记录。参考资料中有两篇文章值得看:
第一篇是携程大佬翻译的经典文章,第二篇是字节大佬顺带提了一些 tc 历史的文章。
一、背景
TC 在 Linux Kernel 2.2版本开始提出,并在2.4版本(2001年)完成。
Linux TC的诞生是为了实现QoS,它在netdev设备的入方向和出方向增加了挂载点,进而控制网络流量的速度,延时,优先级等。
Linux TC在整个Linux Kernel Datapath中的位置如下图所示:
https://people.netfilter.org/pablo/netdev0.1/papers/Linux-Traffic-Control-Classifier-Action-Subsystem-Architecture.pdf
二、linux收发包整体框架
从客户端和服务端整体框架层面来看数据收发流程:
- 用户态(User Space)程序 Client 向另一台主机上的 Server 发送数据,需要通过调用内核态(Kernel Space)提供给用户态的 Socket 抽象层接口发送数据;
- Socket 抽象层接口收到用户态数据后,向下交给传输层接口(TCP 或 UDP);
- 传输层负责创建 sk_buff,并将用户数据(应用层数据)填充到缓冲区,做合法性检查后,添加传输层头部,并通过网络层注册的接口将数据包交给网络层处理;
- 网络层收到传输层数据包后,会查询路由表,决定数据包去向,如果是需要发出的数据包,会填充网络层头部,并交到内核虚拟网络接口设备的发送队列中;
- 虚拟网络接口从发送队列获取数据,调用对应网卡驱动发送数据;
Server 端接收数据时,按照相反的过程从网卡驱动中将数据包一层层上交,直到通过 Socket 抽象层接口将用户数据上交到用户态 Server 进程处理。
三、概念
报文分组从输入网卡接收进来,经过路由的查找, 以确定是发给本机的,还是需要转发的。如果是发给本机的,就直接向上递交给上层的协议,比如TCP,如果是转发的, 则会从输出网卡发出。
网络流量的控制通常发生在输出网卡处,我们可以通过改变发送次序来控制传输速率。一般说来, 由于我们无法控制自己网络之外的设备, 入口处的流量控制相对较难。
流量控制的一个基本概念是队列,每个网卡都与一个队列相联系,每个队列对应一个QDisc(排队规则), 每当内核需要将报文分组从网卡发送出去, 都会首先将该报文分组添加到该网卡所配置的QDisc中, 由该QDisc决定报文分组的发送顺序,可以说,所有的流量控制都发生在队列中。
为实现复杂QoS,队列需要使用不同的过滤器(Filter)来把报文分组分成不同的类别(Class),因此类别(class)和过滤器(Filter)也是流量控制的另外两个重要的基本概念。
-
qdisc(排队规则)
QDisc(排队规则)是queueing discipline的简写,它是理解流量控制(traffic control)的基础。无论何时,内核如果需要通过某个网络接口发送数据包,它都需要按照为这个接口配置的qdisc(排队规则)把数据包加入队列。然后,内核会尽可能多地从qdisc里面取出数据包,把它们交给网络适配器驱动模块。最简单的QDisc是pfifo它不对进入的数据包做任何的处理,数据包采用先入先出的方式通过队列。不过,它会保存网络接口一时无法处理的数据包。
其中qdisc又分为 不分类qdisc 和可分类qdisc:
-
CLASSLESS QDisc
不可分类 QDisc 只能附属于设备的根。用法如下:
tc qdisc add dev DEV root QDISC QDISC-PARAMETERS
tc qdisc del dev DEV root
一个网络接口上如果没有设置QDisc,pfifo_fast就作为缺省的QDisc。
- pfifo_fast就是系统的标准QDISC。它的队列包括三个波段(band)。在每个波段里面,使用先进先出规则。而三个波段(band)的优先级也不相同,band 0的优先级最高,band 2的最低。如果band0里面有数据包,系统就不会处理band 1里面的数据包,band 1和band 2之间也是一样。数据包是按照服务类型(Type of Service,TOS)被分配多三个波段(band)里面的。
- red是Random Early Detection(随机早期探测)的简写。如果使用这种QDISC,当带宽的占用接近于规定的带宽时,系统会随机地丢弃一些数据包。它非常适合高带宽应用。
- sfq是Stochastic Fairness Queueing的简写。它按照会话(session--对应于每个TCP连接或者UDP流)为流量进行排序,然后循环发送每个会话的数据包。
- tbf是Token Bucket Filter的简写,适合于把流速降低到某个值。
-
CLASSFUL QDISC
可分类的qdisc包括:
- CBQ是Class Based Queueing(基于类别排队)的缩写。它实现了一个丰富的连接共享类别结构,既有限制(shaping)带宽的能力,也具有带宽优先级管理的能力。带宽限制是通过计算连接的空闲时间完成的。空闲时间的计算标准是数据包离队事件的频率和下层连接(数据链路层)的带宽。
- HTB是Hierarchy Token Bucket的缩写。通过在实践基础上的改进,它实现了一个丰富的连接共享类别体系。使用HTB可以很容易地保证每个类别的带宽,它也允许特定的类可以突破带宽上限,占用别的类的带宽。HTB可以通过TBF(Token Bucket Filter)实现带宽限制,也能够划分类别的优先级。
- PRIO QDisc不能限制带宽,因为属于不同类别的数据包是顺序离队的。使用PRIO QDisc可以很容易对流量进行优先级管理,只有属于高优先级类别的数据包全部发送完毕,才会发送属于低优先级类别的数据包。为了方便管理,需要使用iptables或者ipchains处理数据包的服务类型(Type Of Service,ToS)。
-
class(类别)
某些QDisc(排队规则)可以包含一些类别,不同的类别中可以包含更深入的QDisc(排队规则),通过这些细分的QDisc还可以为进入的队列的数据包排队。通过设置各种类别数据包的离队次序,QDisc可以为设置网络数据流量的优先级。
-
filter(过滤器)
Filter(过滤器)用于为数据包分类,决定它们按照何种QDisc进入队列。无论何时数据包进入一个划分子类的类别中,都需要进行分类。分类的方法可以有多种,使用fileter(过滤器)就是其中之一。使用filter(过滤器)分类时,内核会调用附属于这个类(class)的所有过滤器,直到返回一个判决。如果没有判决返回,就作进一步的处理,而处理方式和QDISC有关。需要注意的是,filter(过滤器)是在QDisc内部,它们不能作为主体。
四、SFQ
tc有很多内容,如果未来有场景需要深入学习,再开新文章记录下。这里只转载了sfq的用法。再次推荐文初列的两篇文章。
4.1 SFQ(Stochastic Fairness Queueing,随机公平排队)
日常使用中,如果只想尽可能公平地响应每个请求,sfq 可解愁。本小节转自文初的文章一。
随机公平排队(SFQ)是公平排队算法族的一个简单实现。相比其他算法,SFQ 精准性要差一些,但它所需的计算量也更少,而结果几乎是完全公平的(almost perfectly fair)。
SFQ 中的核心是 conversion(会话)或 flow(流),大部分情况下都对应一个 TCP session 或 UDP stream。每个 conversion 对应一个 FIFO queue,然后将流量分到不 同 queue。发送数据时,按照 round robin 方式,每个 session 轮流发送。
这种机制会产生非常公平的结果,不会因为单个 conversion 太大而把其他 conversion 的带宽都 挤占掉。SFQ 被称为“随机的”(stochastic)是因为它其实并没有为每个 session 分配一个 queue,而是用算法将流量哈希到了一组有限的 queue。
但这里会出现另一个问题:多个 session 会可能会哈希到同一个 bucket(哈希槽), 进而导致每个 session 的 quota 变小,达不到预期的整流带宽(或速度)。为避免这个 问题过于明显,SFQ 会不断变换它使用的哈希算法,最终任何两个会话冲突的持续时间 都不会很长,只会有几秒钟。
SFQ 只有在实际出向带宽已经非常饱和的情况下才有效,这一点非常重要!否则, Linux 机器上就不存在 queue,因此也就没用效果。稍后会看到如何将 SFQ 与其他 qdisc 相结合来实现一般情况下的公平排队。
说的更明确一点:没用配套的整流配置的话,单纯在(连接 modem 的)以太网接口上配 置SFQ 是毫无意义的。
SFQ 大部分情况下默认参数就够了:
-
perturb
每隔多少秒
就重新配置哈希算法。如果这个参数没设,哈希算法就永远不会重新配置。 建议显式设置这个参数,不要为空。10s
可能是个不错的选择。
-
quantum
在轮到下一个 queue 发送之前,当前 queue 允许出队(dequeue)的最大字节数。默认是 一个 MTU。不建议设置为小于 MTU 的值。
-
limit
SFQ 能缓存的最大包数(超过这个阈值将导致丢包)。
如果你有一个带宽已经饱和的网络设备,那下面的配置有助于提高公平性:
$ tc qdisc add dev ppp0 root sfq perturb 10
$ tc -s -d qdisc ls
qdisc sfq 800c: dev ppp0 quantum 1514b limit 128p flows 128/1024 perturb 10sec
Sent 4812 bytes 62 pkts (dropped 0, overlimits 0)
解释:
800c:
:自动分配的 handle number(句柄编号)
limit 128p
:最大缓存 128 个包
flows 128/1024
:这个 sfq 有 1024 个哈希槽(hash buckets),其中 128 个当前有 数据待发送。
perturb 10sec
:每隔 10s 换一次哈希算法。
五、常用命令
#查看现有的队列
tc -s qdisc ls dev eth0
#查看现有的分类
tc -s class ls dev eth0
# 清理原有的队列类型
tc qdisc del dev eth0 root
5.1 针对端口进行限速
#查看现有的队列
tc -s qdisc ls dev eth0
#查看现有的分类
tc -s class ls dev eth0
#创建队列
tc qdisc add dev eth0 root handle 1:0 htb default 1
#添加一个tbf队列,绑定到eth0上,命名为1:0 ,默认归类为1
#handle:为队列命名或指定某队列
#创建分类
tc class add dev eth0 parent 1:0 classid 1:1 htb rate 10Mbit burst 15k
#为eth0下的root队列1:0添加一个分类并命名为1:1,类型为htb,带宽为10M
#rate: 是一个类保证得到的带宽值.如果有不只一个类,请保证所有子类总和是小于或等于父类.
#ceil: ceil是一个类最大能得到的带宽值.
#创建一个子分类
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 10Mbit ceil 10Mbit burst 15k
#为1:1类规则添加一个名为1:10的类,类型为htb,带宽为10M
#为了避免一个会话永占带宽,添加随即公平队列sfq.
tc qdisc add dev eth0 parent 1:10 handle 10: sfq perturb 10
#perturb:是多少秒后重新配置一次散列算法,默认为10秒
#sfq,他可以防止一个段内的一个ip占用整个带宽
#使用u32创建过滤器
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip sport 22 flowid 1:10
#删除队列
tc qdisc del dev eth0 root
配置完成后加入本地启动文件:
/etc/rc.local
5.2 针对不同的ip进行限速
#!/bin/bash
#清空原有规则
tc qdisc del dev eth0 root
#创建根序列
tc qdisc add dev eth0 root handle 1: htb default 1
#创建一个主分类绑定所有带宽资源(20M)
tc class add dev eth0 parent 1:0 classid 1:1 htb rate 20Mbit burst 15k
#创建子分类
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 20Mbit ceil 10Mbit burst 15k
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 20Mbit ceil 20Mbit burst 15k
#避免一个ip霸占带宽资源(git1有讲到)
tc qdisc add dev eth0 parent 1:10 handle 10: sfq perturb 10
tc qdisc add dev eth0 parent 1:20 handle 20: sfq perturb 10
#创建过滤器
#对所有ip限速
tc filter add dev eth0 protocol ip parent 1:0 prio 2 u32 match ip dst 0.0.0.0/0 flowid 1:10
#对内网ip放行
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dst 12.0.0.0/8 flowid 1:20
六、术语
-
Queueing Discipline (qdisc,排队规则)
管理设备队列(queues of devices)的算法,可以是管理入向(incoing/ingress )队列,也可以是管理出向队列(outgoing/egress)。
-
root qdisc(根排队规则)
attach 到网络设备的那个 qdisc。
-
Classless qdisc(无类别排队规则)
对所有包一视同仁,同等对待。
-
Classful qdisc(有类别排队规则)
一个 classful qdisc 会包含多个类别(classes)。每个类别(class)可以进一步包 含其他 qdisc,可以是 classful qdisc,也可以是 classless qdisc。
严格按定义来说,pfifo_fast
属于有类别排队规则(classful),因为它内部包 含了三个 band,而这些 band 实际上是 class。但从用户配置的视角来说,它是 classless 的,因为这三个内部 class 用户是无法通过 tc 命令配置的。
-
Classes(类别)
每个 classful qdisc 可能会包含几个 class,这些都是 qdisc 内部可见的。对于每 个 class,也是可以再向其添加其他 class 的。因此,一个 class 的 parent 可以 是一个 qdisc,也可以是另一个 class。
Leaf class 是没有 child class 的 class。这种 class 中 attach 了一个 qdisc ,负责该 class 的数据发送。
创建一个 class 时会自动 attach 一个 fifo qdisc。而当向这个 class 添加 child class 时,这个 fifo qdisc 会被自动删除。对于 leaf class,可以用一个更合适的 qdisc 来替换掉这个fifo qdisc。你甚至能用一个 classful qdisc 来替换这个 fifo qdisc,这样就可以添加其他 class了。
-
Classifier(分类器)
每个 classful qdisc 需要判断每个包应该放到哪个 class。这是通过分类器完成的。
-
Filter(过滤器)
分类过程(Classification)可以通过过滤器(filters)完成。过滤器包含许多的判 断条件,匹配到条件之后就算 filter 匹配成功了。
-
Scheduling(调度)
在分类器的协助下,一个 qdisc 可以判断某些包是不是要先于其他包发送出去,这 个过程称为调度,可以通过例如前面提到的 pfifo_fast
qdisc 完成。调度也被 称为重排序(reordering),但后者容易引起混淆。
-
Shaping(整形)
在包发送出去之前进行延迟处理,以达到预设的最大发送速率的过程。整形是在 egress 做的(前面提到了,ingress 方向的不叫 shaping,叫 policing,译者注)。 不严格地说,丢弃包来降低流量的过程有时也称为整形。
-
Policing(执行策略,决定是否丢弃包)
延迟或丢弃(delaying or dropping)包来达到预设带宽的过程。 在 Linux 上, policing 只能对包进行丢弃,不能延迟 —— 没有“入向队列”(”ingress queue”)。
-
Work-Conserving qdisc(随到随发 qdisc)
work-conserving qdisc 只要有包可发送就立即发送。换句话说,只要网卡处于可 发送状态(对于 egress qdisc 来说),它永远不会延迟包的发送。
-
non-Work-Conserving qdisc(非随到随发 qdisc)
某些 qdisc,例如 TBF,可能会延迟一段时间再将一个包发送出去,以达到期望的带宽 。这意味着它们有时即使有能力发送,也不会发送。
七、参考资料