Traffic Control 技术主要用于提供以下 2 种网络服务类型:
Traffic Control 技术的本质就是在网络设备(包括:主机网卡、交换机、路由器等)的 Ingress 和 Egress 处使用一系列的 Queues(队列)来对数据报文进行排队,继而控制它们的发送顺序和速率。同时,还可以针对不同的 Queue 施加相应的 Policy(策略)。
根据数据报文的类型不同,通常还可以分为以下 2 种:
在本文中,我们主要讨论 Linux Kernel 中实现的 L3 Packets Traffic Control 及其对应的 tc CLI。
最基本的 Traffic Control 实现原理应该分为 3 个不同的层面:
流量标记(Marker):Traffic Marker 可以由应用程序或者网络设备来充当,并根据需要对 Packets 进行标记,例如:修改 IPv4 Header 中的 ToS 字段。以此辅助更加灵活且个性化的流量分类需求。
流量策略(Policier):在完成了流量的分类和标记之后,Traffic Policier 可以根据不同的 Policies 对相应的 Traffic 进行 Controlling 和 Scheduling。
更具体而言,下图所示是一个常见的从 Ingress 到 Egress 的 Traffic Control 实现模型。
报文的标记与分类:
拥塞避免:Classifier 在将 Packets 写入 Queues 的过程中需要注意避免队列拥塞。可以通过实时监控网络资源(e.g. 队列或内存缓冲区)的使用情况来实现。如果拥塞水位超过阈值,则 Classifier 应该主动丢弃报文。
拥塞管理:通常而言,Traffic Control 只能限制 Egress 的 Packets,而不能限制 Ingress 的 Packets,所以直接影响 Egress 的拥塞管理是 Traffic Control 的核心,在满足 Bandwidth Mgmt、QoS 等网络服务的前提下,还需要保障网络不会处于拥塞的危险中。主要通过以下手段来实现:
Queue 是实现 TC 的关键,在长久的发展过程中,针对不同的业务需求,也诞生了多种类型的 Queues 和算法。
最简单的是 FIFO(先入先出)队列,也是早期 Linux Kernel 默认使用的发包队列类型。当 IP Packet 从 TCP/IP Stack 传递到 Network Device Layer 之后,就进入到 TC Egress FIFO Queue,并以 NIC 所能支持的最大速率发送出去。
PFIFO_FAST 队列是 FIFO 队列的改进版本,作为新版 Kernel 的默认队列类型。在 FIFO 队列的基础上,支持识别 IPv4 Header 中的 ToS(Type of Service,服务类型)字段,继而进入到不同的优先级 Queues 中,以此来实现对不同服务类型的 QoS 保障。
SFQ 公平队列,顾名思义,它对所有 IP Packets 一视同仁,常用于防止某一个 Client 或 Flow 占用过多的带宽。
令牌桶队列(Token Bucket)实现了令牌桶算法,该算法的设计比较简单:
可见,网络流量比较恒定的场景中适合使用较小的令牌桶,而经常有突发流量的网络则适合使用大的令牌桶。
常见的令牌桶算法和队列主要有 2 种:
下图所示,TC 位于 L3 Sub-system 的外边界。IP Packets 进入 L3 Sub-system 之前需要先经过 TC Ingress(入方向),IP Packets 从 L3 Sub-system 出来后也会经过 TC Egress(出方向)。下面再逐一介绍 Kernel Traffic Control 的组部分。
Qdisc(Queue describer,队列描述),在 Linux 中的每个 Network Interface 设备都可以关联一个 Qdisc。
当 Kernel 发送 IP Packets 时,首先在 L3 sub-system 完成 Routing 确定一个 Next Hop Interface,然后再把 Packets 入队到 Interface 关联的 Qdisc 队列中。最终由 Qdisc 来决定 Packet 的发送顺序和速率。
每个 Qdisc 都具有一个队列类型,例如前文中介绍过的 FIFO、SFQ、令牌桶队列等。从实现原理上,Qdisc 可分为两大类型:
Classless Qdiscs(无类别队列):实现相对简单,因为无需对 Packets 进行分类。包括:PFIFO_FAST(先进现出,默认队列)、SFQ(随机公平队列)、TBF(令牌桶过滤器)、ID(前向随机丢包)等队列类型。Classless Qdiscs 只支持接受数据包、重新编排数据包、延迟或丢弃数据包,可以对整个网卡接口的流量进行整形,但不会细分各种情况。常用于简单的排序、限速和丢包场景。
Classful Qdiscs(分类队列):实现复杂,必须要关联到 Class 和 Filter 这两个高级分类概念上。包括:HTB(Hierarchical Token Bucket,分层令牌桶)队列等。当数据包进入一个 Classful Qdiscs 后,就会被 Filter 分类到某一个 Class 中。每个 Class 又可以具有多个 Sub-classes 和 Filters。直到不再分类为止,数据包才最终确定进入一个队列中。
值得注意的是,每个 Network Interface 都可以同时具有 TC Ingress 和 Egress,但实际上 Ingress 只能使用 Classless Qdiscs,其被当作一个用于附加 Policer 来控制入站流量的对象。而 Egress 则可以使用 Classful Qdiscs,是 Traffic Control 的关键位置。
Class 作用于 Classful Qdiscs(e.g. HTB),被设计成树状结构,虽然一个 Class 仅可以关联一个 Qdiscs。但是一个 Class 可以具有任意个 Sub-Classes。所以理论上 Class 可以无限扩展,这样的设计使得 Linux 流量控制系统具有了极大的可扩展性和灵活性。
在一棵 Class Tree 中,可能会具有多种 Class 类型:
叶子分类都拥有一个负责发送该类数据的 Qdiscs,而且这个 Qdisc 是可以被再次分类的。但需要注意的是,一个 Qdisc 只能包含与其类型相同的 Class。例如:HTB Qdisc 只能包含 HTB Class,CBQ Qdisc 不能包含 HTB Class。
Filter 由 Classifier(分类器)和 Policer(策略器)组成,可以关联到 Qdisc 也可以关联到 Inner Class。每一个 Class 都有一个 ID,filter 将根据这个 ID 与指定的 Class 相关联。
所有 Egress 的 Packets 首先会通过 Interface 的 Root Qdisc,接着被与 Root Qdisc 关联的 Filter 对 Packets 进行分类,进入子分类,然后由子分类下的 Filter 继续进行分类。所以,如果需要在 Leaf Class 上再实现分类,那就必须将 Filter 与 Leaf Class 的 Qdisc 关联起来,而不能直接与 Leaf Qdisc 相关联。
Policer 只能配合 Filter 使用。Policer 只有两种操作,当流量高于用户指定值时执行一种操作,反之执行另一种操作。
Policing 和 Shaping 都是 Linux 流量控制中的基本组件,两者都可以对带宽进行限制。但不同的是,Shaping 能保存并延迟发送数据包,而 Policing 只会直接丢弃数据包。所以,想要丢弃数据包只能依靠 Policer。
使用 TC 进行流量控制通常需要 4 个步骤:
$ tc qdisc add dev eth0 root handle 1: htb default 11
$ tc class add dev eth0 parent 1: classid 1:11 htb rate 40mbit ceil 40mbit
$ tc class add dev eth0 parent 1: classid 1:12 htb rate 10mbit ceil 10mbit
$ tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 80 0xffff flowid 1:11
$ tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 25 0xffff flowid 1:12
$ tc qdisc del dev eth0 root
$ tc qdisc add dev eth0 root handle 1: htb
$ tc class add dev eth0 parent 1: classid 1:1 htb rate 20mbit ceil 20mbit
$ tc class add dev eth0 parent 1:1 classid 1:10 htb rate 10mbit ceil 10mbit
$ tc qdisc add dev eth0 parent 1:10 sfq perturb 10
# 让 172.20.6.0/24 走默认队列,为了让这个 IP 的数据流不受限制
$ tc filter add dev eth0 protocol ip parent 1: prio 2 u32 match ip dst 172.20.6.0/24 flowid 1:1
# 默认让所有的流量都从特定队列通过,带宽受到限制
$ tc filter add dev eth0 protocol ip parent 1: prio 50 u32 match ip dst 0.0.0.0/0 flowid 1:10
$ modprobe ifb
$ ip link set dev ifb0 up
$ tc qdisc add dev eth0 handle ffff: ingress
$ tc filter add dev eth0 parent ffff: protocol ip u32 match u32 0 0 action mirred egress redirect dev ifb0
$ tc qdisc add dev ifb0 root handle 1: htb default 10
$ tc class add dev ifb0 parent 1: classid 1:1 htb rate 10mbit
$ tc class add dev ifb0 parent 1:1 classid 1:10 htb rate 10mbit ceil 10mbit
$ tc qdisc add dev ifb0 root handle 1: htb default 20
$ tc class add dev ifb0 parent 1: classid 1:1 htb rate 10000mbit
$ tc class add dev ifb0 parent 1:1 classid 1:10 htb rate 2000mbit
$ tc class add dev ifb0 parent 1:1 classid 1:20 htb rate 1000mbit
$ tc class add dev ifb0 parent 1:1 classid 1:30 htb rate 500mbit
$ tc filter add dev ifb0 protocol ip parent 1:0 prio 1 u32 match ip src 129.9.123.85 flowid 1:10
$ tc filter add dev ifb0 protocol ip parent 1:0 prio 1 u32 match ip src 129.9.123.89 flowid 1:20
$ tc filter add dev ifb0 protocol ip parent 1:0 prio 1 u32 match ip src 129.9.123.88 flowid 1:20
# add qdisc
$ tc qdisc add dev eth0 root handle 1: htb default 2 r2q 100
# add default class
$ tc class add dev eth0 parent 1:0 classid 1:1 htb rate 1000mbit ceil 1000mbit
$ tc class add dev eth0 parent 1:1 classid 1:2 htb prio 5 rate 1000mbit ceil 1000mbit
$ tc qdisc add dev eth0 parent 1:2 handle 2: pfifo limit 500
# add default filter
$ tc filter add dev eth0 parent 1:0 prio 5 protocol ip u32
$ tc filter add dev eth0 parent 1:0 prio 5 handle 3: protocol ip u32 divisor 256
$ tc filter add dev eth0 parent 1:0 prio 5 protocol ip u32 ht 800:: match ip src 192.168.0.0/16 hashkey mask 0x000000ff at 12 link 3:
# add egress rules for 192.168.0.9
$ tc class add dev eth0 parent 1:1 classid 1:9 htb prio 5 rate 3mbit ceil 3mbit
$ tc qdisc add dev eth0 parent 1:9 handle 9: pfifo limit 500
$ tc filter add dev eth0 parent 1: protocol ip prio 5 u32 ht 3:9: match ip src "192.168.0.9" flowid 1:9