序
Linux 早在内核 2.2 版本就已经引入了网络 QoS 的机制,并且网络资源的隔离功能只是其所实现功能的一部分而已。无论如何, cgroup 并没有再重新搞一套网络资源隔离的实现,而是直接使用了 Linux 的 iproute2 的 traffic control ( tc )功能。详情请参阅LARTC ( Linux Advanced Routing & Traffic Control )文档翻译成了中文版。
队列规则
tc 命令引入了一系列概念,其中我们最需要先理解的就是队列规则。它的英文名字叫做 queueing discipline ,在 tc 命令中也叫 qdisc ,或者直接简写为 qd 。我们先来看看它,有个感性的认识。
从以上输出大家应该可以判断出来,这个所谓的 qdisc 是针对网卡的,每有一个网卡就会有一个 qdisc 。而且如果你用过 ip 命令并且比较细心的话,应该早就注意到 ip ad sh 的时候也会出现相关的信息:
虽然看上去有些高深莫测,但是 qdisc 其实是个挺简单的概念,它就是它字面的意思:队列规则,或者叫做排队规则。我们都知道,网络数据都是被封装成一个一个的数据包进行传输的。如果网卡相当于数据包要出发的大门的话,那么 qdisc 无非就是规定了这些包在出发前如果需要排队的话该怎么排。我们先拿这个叫做 pfifo_fast 的队列规则来举例子描述一下吧,这个 qdisc 实现了一个以数据包( package )为单位的 fifo 队列,实际上可以认为是实现了三个队列(叫做 bands ),给每个队列定了一个优先级,以实现带优先级的排队规则。我们举个现实中的例子再来说明一下,大家都应该有去公交车站排队的经验吧?(神马?作为中国人你从来不排队?)无论怎样,我们假定你是排队的。每来一次公交车,就相当于网卡处理一次队列中的数据包,而每个人就是一个数据包。那么我们一般人到了公交站,如果发现前面已经排了一队人,此时根据 fifo ( first in first out )的规则,我们会排在队列尾部。如果来车了,就从队列头的人先上车,车满就走,没上完的人继续等待。但是我们也知道,如果此时来了个孕妇或者大爷大娘等一些按照我们社会美德要求应该让他们优先的乘客的话,这些人应该有权利优先上车。那么怎么办呢?我们公交站台的解决办法一般是直接让他们去队列头插队就好,但是如果空间允许的话,我们可以考虑多建立一个队列。让这些可以优先上车的人排一个队,正常人排一个队,车来了先上优先级比较高的那个队列中的人,他们都上完了再让一般队列中上人车。这样就实现了一个简单的队列规则,大家根据自己的情况去选择排队就好了。
pfifo_fast 实现了一个类似上述描述的队列规则,区别是它实现了 3 个优先级的队列( bands ),每个数据包来了都根据自己的情况选择一个 band 进行排队,每个 band 都是 fifo 方式处理数据包。它总是先处理优先级最高的 band ,直到没有数据包了再处理下一个优先级的 band ,直到三个都处理完,或者本次处理不完,继续等着下次处理。那么数据包按什么规则进行选择自己该进入哪个 band 呢?这就是后面显示的priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1的含义,这个字段描述了一个 priomap ,可以理解为优先级位图,后面的 16 个不同的位,表示相关制如果为真时的数据包应该进入哪个队列,一共有 0 、 1 、 2 三个队列。而这个 16 位的位图标记,针对的就是我们 IP 报头中的 TOS 字段。根据 IP 协议的定义我们知道, TOS 字段 8 位中的 4 位分别是用来标示最小延时、最大吞吐量、最大可靠性和最小消费四种数据包类型的, IP 协议原则上会根据这些标示的不同以不同的 QOS 对上层交付不同的服务质量。这些不同的搭配理论上一躬有 16 种,这就是 priomap 所映射的优先级的概念。
如果你对 TOS 的概念还不熟悉,请自行补充网络相关基础知识。推荐的教材是《 TCP / IP 详解卷 1 》。pfifo_fast 队列处理过程如图所示:
pfifo_fast 一般情况下是内核对网卡默认选择的 qdisc ,它虽然提供了简单的优先级分类的支持,但是并没有提供可供修改的参数,就是说默认的优先级分类设置不能更改,也没有提供相关限速的功能。这个队列规则在一般情况下工作的都很稳定,但是最近 Linux 已经开始放弃使用这个 qd 作为默认的队列规则而改用一种叫做 fq_codel 的 qdisc 了。主要原因是,由于移动互联网的广泛应用,一种叫做 Bufferbloat 的现象影响越来越大了。
Bufferbloat
Bufferbloat 现象最初是用来形容在一个分组交换网络上,路由器为防止丢包,往往 buffer 缓冲区都会实现的很大,但是这种过大的 fifo 缓冲区可能导致数据包 buffer 中等待时间过长而导致很多问题(后面会有分析)。再加上网络上 TCP 的拥塞控制算法的影响,以及很多商业操作系统甚至并不实现拥塞控制,导致数据传输质量抖动很大(全局同步),甚至于达到服务不可用的状态。
后来我们发现, Bufferbloat 这种现象比较广泛的存在在各种系统中,只要系统中使用了类似队列、缓存等机制的时候,就在某些极端状态下出现这种类似雪崩的现象。我们简要描述一下这个状态。我们先简单构建一个试用 buffer 的场景,如图所示:
根据图的描述,我们假定这个简单的 fifo 就是我们要的 buffer 系统,它在两个处理过程之间充当缓冲区的作用。每个请求从队列的上面进入排队,然后依次被下面的处理程序处理。大家应该知道 buffer 的作用:一个缓冲器的作用主要是弥补两个处理系统之间的速度差异,能够在一定程度的请求速度抖动的时候缓解处理速度慢而导致的请求失败。假设,后段处理请求的速度为 1000 个 /s ,每个请求平均长度为 100byte ,队列长队为 1Mbyte ,此时,如果请求突然增加到了 2000 个 /s ,那么这个压力直接压给后端是处理不过来的,每秒钟就要丢弃 1000 个包。所以我们使用一个 buffer ,可以让这一秒钟来的请求先处理 1000 个,然后有 1000 个排在队列中,下一秒处理。只要来的请求的抖动范围还算正常,我们的系统将会工作的良好,没有失败的请求。
对于一般的系统,我们发送的请求都是有延时要求的,鉴于我们的系统每秒钟可以处理 1000 个请求,所以每个请求的处理时间平均为 1ms 。而我们的系统基于目前的处理时间,对外提供了 100ms 的延时 SLA ,就是说,后端系统保证每个请求的处理时间是 100ms 以内,这已经很大了,是正常情况的 100 倍。于是前端的请求方,会根据后端给出的 SLA 在程序中设定一个超时时间,在这个例子中就应该是 100ms ,这可能意味着,程度调用后端系统,如果等待 100ms 还没有结果,那么将重试一次或者几次不等,之后应该会返回失败。场景就是这样一个场景,那么我们来看看究竟什么是 bufferbloat ?
假定现在因为业务问题,比如上线了一个秒杀的抢购活动,导致从前端发来的请求一瞬间远远大于后端的处理能力。比如,一秒钟内产生了 10000 次请求,这一万次请求都会立即进入队列中等待后端处理。因为后端的处理速度是 1000 次每秒,所以可以想像,当前在队列中的最后一个数据包至少要等待 9 秒钟才能处理到。实际上根本处理不到这最后一个请求,由于我们设置了 100ms 的超时时间,那么调用方将很快因为发现 100ms 中没有返回而重试一次,于是又来了将近 10000 个请求。这些请求都积压在了队列中,还没交给后端进行处理,如果交给了后端处理,后端肯定会因为压力变大处理变慢,而导致处理事件超过 100ms 的 SLA ,会在超时之后告诉前端本次请求失败(如果是这样实现的话),而现在由于队列的存在,并大量的积压请求,导致调用方不能明确的得知失败。所以一般都是等待至少一次超时重试一次再失败,当然也有很多情况会重试个 4 , 5 次也说不定。
无论如何,这突发的 10000 个请求的流量来了之后,如果平均每个请求 100 字节,这 1M 的缓冲区就已经满了,后续再有任何请求来,都会排在队列末尾,一直等到前面的请求处理完再处理这个请求,而此时因为整体处理时间很慢,要将此队列中的全部请求处理完需要 9 秒钟,无论如何,这个请求都已经超时失败了。这个时候后端服务一直满载的处理队列中的请求,而前端还不断有新请求源源不断的放进队列,但是由于超时,前端所有请求都是返回失败,后端所处理的请求也都是等待时间超过 100ms 的无效的请求,即使成功返回结果给前端,前端也不会要了。效果就是后端很忙,而整体服务却是不可用的。此时哪怕请求平均速度恢复到 1000 个每秒,服务也无法恢复。这就是一个典型的 bufferbloat 场景。
于是我们可以考虑一下这个场景会发生在什么地方?比如 buffer 比较大的路由器,由于 tcp 的流量控制和重试机制导致网络质量的抖动;比如一个后端的数据库系统为了能够承载更大的吞吐量而添加了队列系统;比如 io 调度;比如网卡调度;只要是大 buffer 的场景都会可能产生类似的问题。那么该如何解决这个问题呢?于是主动队列管理算法应运而出了。
CoDel 算法
CoDel 算法是另一种 AQM 算法,其全称是 Controlled Delay 算法。是由 Van Jacobson 和 Kathleen Nichols 在 2012 年实现的。具体描述参见Controlling Queue Delay。 CoDel 采用了另外一种角度来观察队列满载的问题,其出发点并不是对队列长度进行控制,而是对队列中的数据包的驻留时间进行控制。事实上如果我们将管理方式由队列长度控制变成等待时间控制的时候, bufferbloat 就可以彻底解决了,这也是更先进的 AQM 算法所用的方式。我们仔细观察 bufferbloat 问题,会发现,引起这个问题的重要原因就是数据包在队列中的驻留时间过长,超过了有效的处理时间( SLA 定义的时间或者重试时间),导致处理到的数据包都已经超时。
首先我们根据我们的业务设计,确定出请求在队列中正常情况应该驻留多久。我们还是假定这样一种场景,根上面 bufferbloat 中描述的例子差不多:后端处理速度是 1000 次每秒,就是 1ms 可以处理一个请求,而队列平均长度一般为 5 ,就是说一个新请求进入队列之后,发现前面还有 5 个请求在等待,那么这个新请求的处理时间大约为 6ms (在队列中等待 5ms )。那么请求在队列中的驻留时间正常情况下基本为 5ms 。而我们服务的 SLA 确定的时间是 100ms (由诸如服务超时时间或者所在网络的最大 RTT 时间等条件确定),就是说,服务应确保在 100ms 内给出反馈,这个时间叫做 interval time ,如果超过这个时间应该返回失败。针对这样的情况,我们可以根据请求驻留时间的情况来描述一个动态长度的队列,当一个请求入队之后,对其驻留时间( sojourn time )进行追踪,以正常的情况作为其目标驻留时间( target time ),在这个例子中是 5ms ,就是说一般情况下,我们期望请求在队列中的驻留时间不高于 5ms 。由于业务的超时时间或者说我们提供的 SLA 处理时间是 100ms ,所以,在这个队列中驻留超过 100ms 的请求都应该丢弃(从队列头开始),因为即使处理完成它们也没有意义了。丢弃将持续到队列中的请求等待时间回到理想的 target time 为止,并且队列长度整体不大于队列容量上限。这样就根据驻留时间维持了一个动态长度的队列,这个队列中的所有请求理论上都应该等待 100ms 以内,要么被正常处理掉,要么被丢弃。这就是 CoDel 算法的基本思路。
为了有助于大家理解,我们再详细一点描述一下这个算法的处理过程:
CoDel 算法对队列状态维护一个状态机,进行队列 dequeue 处理的时候,先判检查队列头请求的驻留时间( sojourn time )是否大于 target time ,如果不大于 target time ,就直接 dequeue ;如果大于( target time )的请求维持了 interval time 这么长的时间,则队列应该进入 dropping 状态开始丢包。这种丢包状态将可能维持一段时间,这段时间的长度将根据情况而定(驻留时间一直处在 target 以上,并且下一个包丢弃的时间采用逆平方根运算( inverse-square-root ),公式为:
t (第一次取 now ,以后取上次的值) + interval / sqrt(count))
count 的取值为丢弃包的个数,如果 count 大于 2 则 count = count - 2 ,其他情况 count 取值为 1 。直到驻留时间小于 target time ,就退出 dropping 状态。
算法的伪代码描述参见这里。
我们之所以要如此详细的描述 bufferbloat 问题以及其解决方案,尤其是 CoDel 算法,原因是其不仅仅被用在网络的分组交换和路由的处理上。除了 TC 的队列规则外, CoDel 当前还被用在了内核 TCP 协议栈的拥塞控制中,并且 rabbitmq 也已经把这个算法应用于消息队列的延时控制了,参见。这个算法在数据中心的应用场景下,是一个非常好的解决队列阻塞的方案。
了解了以上知识之后,我们来看一下再 Linux 上如何配置一个 CoDel 的队列规则,我们刚才已经将队列规则改为 RED 了,此时如果要将其改为 CoDel ,需要先删除 RED 的队列规则,再添加新的队列规则:
tc 的-s 参数相信你已经明白什么意思了。来说一下 codel 队列规则的相关参数:
limit:队列长度上限,如果超过这个长度,新来的数据包将被直接丢弃。单位为字节数,默认值为 1000.
target && interval:这两个参数相信大家已经明白是什么意思了,根据自己的场景进行配置就好了。
ecn && noecn:这个参数的含义根 RED 中的一样,默认是开启的 ecn 方式通知源端,不丢包。
大家也可以直接使用 codel 规则的默认参数,就是其他参数都省略即可。我们来看看什么效果:
使用 cgroup 限制网络流量
最后,我们要来看看如何在 cgroup 的场景下对网络资源进行隔离了。实际上跟我们上面讲的 HTB 的例子类似,区别是,上面的例子是通过端口分类,而现在需要通过 cgroup 进行分类。我们还是通过一个例子来说明一下场景,并实现其功能:我们假定现在有两个 cgroup ,一个叫 jerry ,另一个叫 zorro 。我们现在需要给 jerry 组中运行的网络程序限制带宽为 10mbit , zorro 组的网路资源占用为 20mbit ,总带宽为 100mbit ,并且不允许借用( ceil )网络资源。那么配置思路是这样:
我们的配置环境是一台 centos7 的虚拟机,首先,我们在这个服务器上运行一个 apache 的 http 服务,并发布了一个 1G 的数据文件作为测试文件,并在不限速的情况下对齐进行下载速度测试,结果为 100MBps ,注意这里的速度是 byte 而不是 bit :
之后我们在 centos7(192.168.139.136)上实现三个分类,一个带宽限制 10m 给 jerry ,另一个 20m 给 zorro ,还有一个为 30m 用作 default ,总带宽 100m ,剩余的资源给以后可能新加入的 cgroup 来分配,于是先建立相关的规则和分类:
建立完分类之后,由于默认情况都要走 1:100 的分类,所以限速应该是 30mbit ,验证一下:
当前速度为 3452kB 左右,大概为 30mbit ,符合预期。之后将我们的 http 服务放到 zorro 组中看看效果,当然是首先建立相关 cgroup 以及相关配置:
建立完毕之后分别配置相关的 cgroup ,将对应 cgroup 产生的数据包对应到相应的分类中,配置方法:
这里的 tc 命令是对 filter 进行操作,这里我们使用了 cgroup 过滤器,来实现将 cgroup 的数据包送到 1:0 分类中,细节不再解释。对于 net_cls.classid 文件,我们一般 echo 的是一个 0xAAAABBBB 的值, AAAA 对应 class 中:前面的数字,而 BBBB 对应后面的数字,如: 0x00010100 就表示这个组的数据包将被分类到 1:100 中,限速为 30mbit ,以此类推。之后我们把 http 服务放倒 jerry 组中看看效果:
测试效果:
确实限速在了 10mbitps 。成功达到效果,再来看看放倒 zorro 组下:
再次测试效果:
限速 20mbps 成功。如果想要修改对于一个分类的限速,使用如下命令即可:
tc cl change dev eno16777736 parent 1: classid 1:100 htb rate 100mbit
关于命令参数的详细解释,这里不做过多说明了。大家可以自行找帮助。
最后
实际上 Linux 的 Cgroup 除了 CPU 、内存、 IO 和网络的资源管理以外,还有一些其它的配置,比如针对设备文件的访问控制和 freezer 机制等功能,但是这些功能都相对比较简单,个人认为没必要过多介绍了,大家要用的时候自己找帮助即可。
最后的最后,还是奉送一张 Linux 网络相关的数据包处理流程图,从这张图上大家可以清晰的看到 qdisc 的作用位置和其根 iptables 的作用关系。原图链接
原文链接:https://www.v2ex.com/t/255496