tc | 流量控制 (1/2)

2019 12 24, Tue

Linux下有一系列关于网络的工具。用来处理网络数据包,比如说Linux内核本身、iptables、tcpdump、ping、nmap/ncat……除此之外,有openflow、dpdk这些虚拟网络解决方案。

当然今天说的相比dpdk、openflow这些特别复杂的,就比较简单一些。

Backgrounds

Bandwidth shaping

有时候我们需要限制两个主机件的所有流量,比如说机房到云服务商,比如说腾讯云说是限制带宽但是实际上有时候会跑出来一个峰值带宽,这个带宽会触发云服务商的DDoS防御。云服务商会跟你说要买他们的BGP防御包,要不然遇到一次封一次带宽让你们服务down个几个小时的。

不过我们是正经的技术人,自己的服务自己负责,当然要拒绝这种强卖啊。

或者有时候我们会需要限制一些流量。比如说我开了一个千度网盘,我想要限制用户的下载速度为30KB/s。

Priority

机房内负载看情况,有时候会很重。尤其是网盘一类的。我们一方面又想保证到数据库的数据包可以优先到达,又不想增加成本增加一个网络还要维护它,跑不满又是一笔多余的支出。怎么办呢?

Testing

有的时候我们需要模拟一些特殊的网络情况,比如说上面提到的限制带宽的问题,或者我们需要给数据包加上一定量的延迟,或者我们需要随机丢掉几个包。

TC

tc是iproute2带的流量控制的工具,用法多种多样五花八门,没有做不到只有想不到。

一开始我只是在自己的路由上,用现成的cookbook里面的命令限制了特定网段的速度,来防止蹭Wifi的问题。然后由于最近我们机房内偶发的,有数据库因为其他流量过大超时的现象,所以稍微深入摸了一摸tc具体的都有什么。下面是我的理解,加上翻译了一丢丢文档1

iproute2

iproute是Linux内核加入netlink socket后,意在替换老旧的ifconfig等工具,除了tc以外还有arpd、ip、ss等命令,可以控制路由、流量控制等Linux特性。

基本概念

下面的列举都不用记,大概眼熟,知道什么是什么,不把tbf当成filter就行。

qdisc: scheduler

qdisc is short for ‘queueing discipline’ and it is elementary to understanding traffic control. Whenever the kernel needs to send a packet to an interface, it is enqueued to the qdisc configured for that interface. Immediately afterwards, the kernel tries to get as many packets as possible from the qdisc, for giving them to the network adaptor driver.

qdisc(queueing discipline,队列规则)在tc中代表一个调度器和队列,队列有的时候也叫buffer。理解qdisc是理解tc的基石。

内核无论什么时候要发包了,都会把数据包放入NIC对应的队列中,然后马上从队列里面去取。这个过程都是由qdisc来控制。

      +---------+
in -> |  qdisc  | -> out
      +---------+

最简单的qdisc就是pfifo,单纯的先入先出,偶尔也会因为网卡被塞满短时间不能处理更多包而囤一些数据包在内存里面。

分为两类,带有class的和不带class的,带有class的有一些额外的调度规则。常用的qdisc有:

  • classless:
    • [p|b]fifo:单纯的先入先出,p和b表示通过packet来限制数量还是byte
    • pfifo_fast:这个略微复杂一些,是’Advanced Router’ enabled kernels 默认的qdisc,会遵循ToS来给数据包打上优先级标志
    • fq:fair queue,暂时还没懂tcp pacing是什么概念
    • tbf:token bucket filter,这个是基于令牌桶的流量限制。非常适合限制流量
  • classful
    • cbq:基于class的排队算法
    • prio:基于优先级的排队算法

更多的去看man page

filter & class

A filter is used by a classful qdisc to determine in which class a packet will be enqueued

根据man page,filter是qdisc用来把不同的数据包分成不同的class的规则,而class是classful qdisc用来区分对待数据包的分类。

常用的filter有

  • bpf:基于eBPF的filter
  • cgroup
  • flow,flower(我还没用过,用到了再来讲)
  • fw:基于iptables的fwmark
  • route:基于路由表
  • tcindex
  • matchall:所有

还要一个u32,不是很清楚是什么filter

classless qdisc

这类比较简单,即可以单独用在网口上,也可以作为classful qdisc的leaf qdisc强强联手打出combo。

我们用classless qdisc来入门。

开始以前我们需要用命令

$ ip a
......
1: enp3s0: <UP,LOWER_UP> mtu 1500 qdisc mq state UNKNOWN mode DEFAULT group default qlen 1000 link/[519]

来获得我们网卡的名字。

比如说我的就是enp3s0。

fifo qdisc

最简单的先入先出规则,但是不是默认的qdisc。

数据包到了队列里面,就尽可能早的送出去。只是按照packet或者是byte维持队列大小,不能让队列把内存吃光。

      +--------------------------+
in -> |  packet, packet, ......  | -> out
      +--------------------------+
      |<- limit in packet/byte ->|

设置fifo qdisc相对很简单:

tc qdisc add dev enp3s0 pfifo limit 30

如果想要用byte做单位,就换用bfifo就好。

下面也有很多别的qdisc用了类似的fifo buffer。

pfifo_fast

在网卡没有特殊设置的情况下,是默认的qdisc。没有什么配置项。

相比较而言,pfifo_fast有三个队列对应不同的优先级(最高优先级是0,最低是2),还有自己的priomap,会遵循type of service把包分为不同优先级。总是先出高优先级的包,然后再出低优先级的。

Data arrived and was classified into three queues according to ToS.
      +--------------------------+
in -> |  packet, packet, ......  |(highest priority)
      +--------------------------+
in -> |  packet, packet, ......  |(higher)
      +--------------------------+
in -> |  packet, packet, ......  |(normal)
      +--------------------------+

Packet of higher priority come out first.
      +--------------------------+
      |  packet, packet, ......  |-> out
      +--------------------------+
      |  packet, packet, ......  |
      +--------------------------+
      |  packet, packet, ......  |
      +--------------------------+
.....
      +--------------------------+
      |         (empty)          |
      +--------------------------+
      |  packet, packet, ......  | -> out
      +--------------------------+
      |  packet, packet, ......  |
      +--------------------------+
.....
      +--------------------------+
      |         (empty)          |
      +--------------------------+
      |         (empty)          |
      +--------------------------+
      |  packet, packet, ......  | -> out
      +--------------------------+

SFQ/ESFQ

概率上把带宽均摊给各个流(比如说TCP流)。

      +--------+      +-------+
in -> |  flow  |      |       |
      +--------+   => | round |
in -> |  flow  |      |       | -> out
      +--------+   => | robin |
in -> |  flow  |      |       |
      +--------+      +-------+
    ......

TBF

利用一种基于令牌桶的限速的方法。非常适合HTTP这种有峰值流量的情形。

配置项主要是

  • 桶大小
  • 令牌生成速率
  • 最大令牌消耗速率

最傻的直接计算流量的方法

我做学生的时候的思路:维持另一个队列,按照大小或者时间计算一段窗口内的流量,超过就限速。即费时间又费空间还可能出bug。

广泛用于rate limit的token bucket

换一个思路。

我们假设有个桶。桶里面可以放令牌,我们只在桶里面有令牌时放人,放一个人取走一个令牌,桶空了就等着。而桶内会定时重新生成令牌,如果数量超过容量了,就只留容量这么多的令牌。取令牌的速度也可以有限制,不限制的时候可以瞬间消耗掉所有令牌,限制的时候取令牌也有速度限制。这样就可以完美限速了。

除了桶我们还有一个queue,用来在缺token的情况下存下没有发出的数据包。

every packet comsumes a token

          +--------------------------+
gen token |   token, token, .....    | -> token
          +--------------------------+

          +--------------------------+
in   ->   |  packet, packet, ......  | -> out
          +--------------------------+
          
          
once the token bucket is empty, qdisc stops to send packet, until a token is generated

          +--------------------------+
gen token |         (empty)          |
          +--------------------------+

          +--------------------------+
in   ->   |  packet, packet, ......  |
          +--------------------------+

那么我们假设有这么两种情况:

  1. 数据包到达时没有令牌 那么qdisc就要等待token,来一个token放走一个数据包。放走数据包的速度就是生成token的速度。完美限速。
  2. 数据包到达的时候有令牌 尽可能多的发出packet,耗尽token后等待生成token。

所以tbf就适合于我们希望在需要时爆发发送一定量的数据,并且在之后平缓限速的情况。无论爆发时带宽多高,总流量是一定的。

Random Early Drop2

数据包少的时候正常FIFO,超过一个阈值的时候随机Drop一些,超过一个更高的阈值时把超过的部分彻底drop掉。基于可能性来让带宽维持在一定范围内。


when minimal threshold > the size of queue, it behaves like a normal FIFO queue.
      +------------------------+-----------+
in -> |  packet, packet, ......|  (empty)  | -> out
      +------------------------+-----------+
              threshold min -> |           | <- threshold max
              
when minimal threshold <= the size of queue < maximal threshold, it randomly drops some packet.

      +------------------------------------------+
in -> | calculate possibility to drop the packet |
      +------------------------------------------+
                                      ↓
      +------------------------+--------------+
      |  packet, packet, ......| packet, ...  | -> out
      +------------------------+--------------+
              threshold min -> |              | <- threshold max
              
when the size of queue >= maximal threshold, it just send packets and never buffer packets until the size of queue becomes smaller than maximal threshold

      +----------------------+
in -> | drop all the packets |
      +----------------------+
                                      
      +------------------------+----------------+
      |  packet, packet, ......| packet, packet | -> out
      +------------------------+----------------+
              threshold min -> |                | <- threshold max

不过说实话这种方法因为本身带有随机属性,所以除非你非常清楚自己做什么,尽量不要用它。

选择合适的qdisc

  • 限速: TBF
  • 均分流量:SFQ
  • 懒:qfifo_fast

除此之外

  • 入流量:ingress policer3 暂时不怎么懂,以后再更新
  • 如果你要在路由上进行流量控制,一定要加在出流量的网口上哦。
  • 可以的时候增加节点、重构网络结构或者把用户导向其他节点,效果比tc好很多。

classful qdisc

用来创建更复杂的规则。且听下回分解。