高级流量控制
Linux 内核的网络堆栈具有网络流量控制和整形功能。iproute2 软件包安装了 tc
命令,可以通过命令行控制这些功能。
本文的目标是展示如何通过使用队列规则来整形流量。例如,如果您曾经不得不禁止网络上的下载或 torrent,并不是因为您反对这些服务,而是因为用户“滥用”了带宽,那么您可以使用队列规则来允许这类流量,同时确保一个用户不会拖慢整个网络的速度。
这是一篇高级文章;您应该具备一定的网络设备、iptables 等知识。
前提条件
重要的是禁用您的网络适配器上的 TCP 分段卸载,否则它会绕过流量整形器以节省 CPU。[1]
# ethtool -K eth0 tso off
队列
队列控制数据发送的方式;接收数据更具反应性,网络导向的控制更少。然而,由于 TCP/IP 数据包使用慢启动发送,系统开始缓慢发送数据包,并不断加速发送,直到数据包开始被拒绝 - 因此可以通过在数据包到达路由器并在转发之前丢弃它们来控制局域网上接收的流量量。还有更多相关的细节,但它们没有直接触及队列逻辑。
为了完全控制流量的形状,我们需要成为链条中最慢的环节。也就是说,如果连接的最大下载速度为 500k,如果您不将输出限制为 450k 或更低,那么将由调制解调器而不是我们来整形流量。
每个网络设备都有一个根,可以在其中设置队列规则。此根默认具有 fq_codel 队列规则。(更多信息见下文)
有两种类型的规则:有类别和无类别。
有类别队列规则允许您创建类,这些类就像树上的分支。然后,您可以设置规则将数据包过滤到每个类中。每个类本身都可以分配其他有类别或无类别队列规则。
无类别队列规则不允许在其上添加更多队列规则。
在开始配置队列规则之前,首先我们需要从根目录中删除任何现有的队列规则。这将从 eth0 设备中删除任何队列规则
# tc qdisc del root dev eth0
无类别队列规则
这些队列通过重新排序、减速或丢弃数据包来执行基本的流量管理。这些队列规则不允许创建类。
fifo_fast
这是 systemd 217 之前的默认队列规则。在未应用自定义队列规则配置的每个网络设备中,fifo_fast 是在根目录上设置的队列规则。fifo 表示先进先出,也就是说,先进来的数据包将是第一个被发送的数据包。这样,没有数据包获得特殊待遇。
令牌桶过滤器 (TBF)
此队列规则允许字节通过,只要不超过一定的速率限制。
它的工作原理是创建一个虚拟桶,然后以一定的速度放入令牌,填充该桶。每个数据包从桶中取出一个虚拟令牌,并使用它来获得通过的许可。如果到达的数据包过多,桶将没有剩余的令牌,剩余的数据包将等待一段时间以获取新的令牌。如果令牌到达速度不够快,数据包将被丢弃。在相反的情况下(发送的数据包太少),令牌可以用于允许发生一些突发(上传峰值)。
这意味着此队列规则对于减慢接口速度很有用。
例子
上传可能会填满调制解调器的队列,因此,当您上传一个巨大的文件时,交互性会被破坏。
# tc qdisc add dev ppp0 root tbf rate 220kbit latency 50ms burst 1540
请注意,上面的上传速度应更改为您的上传速度减去一小部分百分比(以成为链条中最慢的环节)。此配置为 ppp0
设备设置了 TBF,将上传速度限制为 220k,为数据包设置了 50 毫秒的延迟,然后才被丢弃,突发为 1540。它的工作原理是将队列保留在 Linux 机器上(可以在其中整形),而不是调制解调器上。
随机公平队列 (SFQ)
这是一个轮询调度队列规则。每个会话都设置在一个 fifo 队列中,在每一轮中,每个会话都有可能发送数据。这就是为什么它被称为“公平”。它也被称为“随机”是因为它实际上并没有为每个会话创建一个队列,而是使用了一种哈希算法。对于哈希,同一个桶上可能会有多个会话。为了解决这个问题,SFQ 经常更改其哈希算法,以防止这种情况变得明显。
例子
此配置在 eth0 设备上的根目录上设置 SFQ,并将其配置为每 10 秒扰乱(更改)其哈希算法。
# tc qdisc add dev eth0 root sfq perturb 10
CoDel 和公平队列 CoDel
自 systemd 217 以来,fq_codel 是默认设置。CoDel (受控延迟) 试图通过区分快速清空的良好队列和保持饱和和缓慢的坏队列来限制缓冲区膨胀并最大限度地减少饱和网络链路中的延迟。公平队列 Codel 利用公平队列更轻松地在 Codel 流之间分配可用带宽。配置选项有意受到限制,因为该算法旨在与动态网络一起工作,并且 bufferbloat wiki 关于 Codel 的维基 上讨论了一些需要考虑的极端情况,包括超大型交换机和亚兆位连接的问题。
更多信息可通过 tc-codel(8) 和 tc-fq_codel(8) 获取。
有类别队列规则
如果您有不同类型的流量应区别对待,则有类别队列规则非常有用。有类别队列规则允许您拥有分支。分支称为类。
设置有类别队列规则需要您命名每个类。要命名一个类,请使用 classid
参数。parent
参数,顾名思义,指向类的父级。
所有名称都应设置为 x:y
,其中 x
是根的名称,y
是类的名称。通常,根称为 1:
,其子级类似于 1:10
分层令牌桶 (HTB)
HTB 非常适合您希望为不同目的划分固定带宽的设置,为每个目的提供保证的带宽,并有可能指定可以借用多少带宽。这是一个带有注释的示例,解释了每一行的作用
# This line sets a HTB qdisc on the root of eth0, and it specifies that the class 1:30 is used by default. It sets the name of the root as 1:, for future references. tc qdisc add dev eth0 root handle 1: htb default 30 # This creates a class called 1:1, which is direct descendant of root (the parent is 1:), this class gets assigned also an HTB qdisc, and then it sets a max rate of 6mbits, with a burst of 15k tc class add dev eth0 parent 1: classid 1:1 htb rate 6mbit burst 15k # The previous class has this branches: # Class 1:10, which has a rate of 5mbit tc class add dev eth0 parent 1:1 classid 1:10 htb rate 5mbit burst 15k # Class 1:20, which has a rate of 3mbit tc class add dev eth0 parent 1:1 classid 1:20 htb rate 3mbit ceil 6mbit burst 15k # Class 1:30, which has a rate of 1kbit. This one is the default class. tc class add dev eth0 parent 1:1 classid 1:30 htb rate 1kbit ceil 6mbit burst 15k # Martin Devera, author of HTB, then recommends SFQ for beneath these classes: 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 tc qdisc add dev eth0 parent 1:30 handle 30: sfq perturb 10
过滤器
一旦在根目录上设置了有类别队列规则(其中可能包含具有更多有类别队列规则的类),就有必要使用过滤器来指示哪些数据包应由哪个类处理。
在仅限无类别的环境中,过滤器不是必需的。
您可以使用 tc 或 tc + iptables 的组合来过滤数据包。
仅使用 tc
这是一个解释过滤器的示例
# This command adds a filter to the qdisc 1: of dev eth0, set the # priority of the filter to 1, matches packets with a # destination port 22, and make the class 1:10 process the # packets that match. tc filter add dev eth0 protocol ip parent 1: prio 1 u32 match ip dport 22 0xffff flowid 1:10 # This filter is attached to the qdisc 1: of dev eth0, has a # priority of 2, and matches the ip address 4.3.2.1 exactly, and # matches packets with a source port of 80, then makes class # 1:11 process the packets that match tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip src 4.3.2.1/32 match ip sport 80 0xffff flowid 1:11
使用 tc + iptables
Iptables 有一种称为 fwmark 的方法,可用于跨接口标记数据包。
首先,这使得标记为 6 的数据包由 1:30 类处理
# tc filter add dev eth0 protocol ip parent 1: prio 1 handle 6 fw flowid 1:30
这将使用 iptables 设置标记 6
# iptables -A PREROUTING -t mangle -i eth0 -j MARK --set-mark 6
然后,您可以正常使用 iptables 来匹配数据包,然后使用 fwmark 标记它们。
使用 SNAT 进行入口流量整形的示例
入口流量上的队列规则仅提供策略,不提供整形。为了整形入口,必须使用 IFB(中间功能块)设备。但是,如果使用 SNAT 或 MASQUERADE,则会出现另一个问题,因为所有传入流量都具有相同的目标地址。队列规则在反向 NAT 转换之前拦截外部接口上的传入流量,因此它只能看到路由器的 IP 作为数据包的目标。
以下解决方案在 OpenWRT 上实施,可以应用于 Arch Linux:首先,传出数据包用 MARK 标记,相应的连接(和相关连接)用 CONNMARK 标记。在传入数据包上,入口 u32 过滤器将流量重定向到 IFB(action mirred),并且还从 CONNTRACK(action connmark)检索数据包的标记,从而提供有关 NAT 后面的哪个 IP 发起流量的信息)。
自 linux-3.19 和 iproute2 4.1 以来,此功能已集成到内核中。
以下是一个小脚本,入口处只有 2 个 HTB 类来演示它。流量默认为类 3:30。从 192.168.1.50(NAT 后)到互联网的传出流量标记为 “3”,因此从互联网到 192.168.1.50 的传入数据包也标记为 “3”,并在 3:33 上分类。
#!/bin/sh -x # Maximum allowed downlink. Set to 90% of the achievable downlink in kbits/s DOWNLINK=1800 # Interface facing the Internet EXTDEV=enp0s3 # Load IFB, all other modules all loaded automatically modprobe ifb ip link set dev ifb0 down # Clear old queuing disciplines (qdisc) on the interfaces and the MANGLE table tc qdisc del dev $EXTDEV root 2> /dev/null > /dev/null tc qdisc del dev $EXTDEV ingress 2> /dev/null > /dev/null tc qdisc del dev ifb0 root 2> /dev/null > /dev/null tc qdisc del dev ifb0 ingress 2> /dev/null > /dev/null iptables -t mangle -F iptables -t mangle -X QOS # appending "stop" (without quotes) after the name of the script stops here. if [ "$1" = "stop" ] then echo "Shaping removed on $EXTDEV." exit fi ip link set dev ifb0 up # HTB classes on IFB with rate limiting tc qdisc add dev ifb0 root handle 3: htb default 30 tc class add dev ifb0 parent 3: classid 3:3 htb rate ${DOWNLINK}kbit tc class add dev ifb0 parent 3:3 classid 3:30 htb rate 400kbit ceil ${DOWNLINK}kbit tc class add dev ifb0 parent 3:3 classid 3:33 htb rate 1400kbit ceil ${DOWNLINK}kbit # Packets marked with "3" on IFB flow through class 3:33 tc filter add dev ifb0 parent 3:0 protocol ip handle 3 fw flowid 3:33 # Outgoing traffic from 192.168.1.50 is marked with "3" iptables -t mangle -N QOS iptables -t mangle -A FORWARD -o $EXTDEV -j QOS iptables -t mangle -A OUTPUT -o $EXTDEV -j QOS iptables -t mangle -A QOS -j CONNMARK --restore-mark iptables -t mangle -A QOS -s 192.168.1.50 -m mark --mark 0 -j MARK --set-mark 3 iptables -t mangle -A QOS -j CONNMARK --save-mark # Forward all ingress traffic on internet interface to the IFB device tc qdisc add dev $EXTDEV ingress handle ffff: tc filter add dev $EXTDEV parent ffff: protocol ip \ u32 match u32 0 0 \ action connmark \ action mirred egress redirect dev ifb0 \ flowid ffff:1 exit 0