网络流量限速是一个经久不衰的话题,Linux 内核中已经实现了若干种流量限速的方式。
最简单的方式是通过定期采集速率,在超过指定的速率后直接丢包,但这种方案效果不佳,不能精准地将流量控制在指定的速率。
更成熟的方案都是把需要延迟发送的数据包缓冲在队列中,在合适的时间再进行发送,因此 Linux 内核中的 Traffic Control(简称 TC)层就成了实现透明的网络流量限速的最佳位置。
由于历史原因,TC 只有在出口方向上实现了有队列的 Qdisc (Queue discipline),所以我们在这里提到的限速方案也都是在出口方向上实现的。入口方向上的限速比较困难,除了 TC 层缺少队列之外,还很难对流量的源头进行限速,因此我们在这里不予讨论。
1. 传统限速方案
传统的 TC 限速,是通过给网络设备添加单一的具有限速功能的 Qdisc (比如 HTB、TBF 等)来完成的。这些方案有一个共同的缺点,即依赖一把设备全局的 Qdisc spinlock 来进行同步。
这把锁很难进行优化,主要有两个原因:
数据包的入队、出队都是写操作,且都不是原子操作,因此需要使用锁来同步;
这些 Qdisc 实现的都是设备全局的限速,其中一部分(如 HTB Qdisc)还允许不同流量类型 (class) 之间互相借用带宽,因此更需要一把全局锁来进行统一协调。
上述传统方案在发送流量较大的时候会碰到这个全局锁的性能瓶颈。
2. mq Qdisc 方案
针对传统方案的弊端,Linux 内核提供了一个「拆散」这把全局 spinlock 的软件方案:mq Qdisc。mq Qdisc 是一个很特殊的 Qdisc,它为网络设备的每一个硬件队列分别创建一个软件 Qdisc,再通过一个 ->attach()
操作将它们挂载到各个硬件队列上,如图所示:
这样一来,每一个硬件队列上的 Qdisc(上图中「child Qdisc」)都有各自的 spinlock,一个锁被「拆」成了多个锁,从而改善了设备全局 spinlock 带来的性能问题。
需注意的是,mq Qdisc 本身并不实现任何限速机制,仅仅提供了一个框架,须和其它具有限速功能的 child Qdisc(HTB、SFQ 等)配合使用。
这样做有一个缺点:现在设备上的一个 Qdisc 被「拆」成了多个 child Qdisc,我们就只能分别对这些 child Qdisc 配置限速规则,从而失去了传统方案里对设备流量的全局控制。
3. HTB 硬件 offload 方案
针对传统 HTB 方案的缺点,Mellanox 网卡推出了一个通过硬件来实现限速的方案,给 HTB Qdisc 添加了「offload 模式」,模拟 mq Qdisc 的 ->attach()
操作:

每个硬件队列分别对应了一个 HTB 树形结构中最下层的流量类型 (leaf class)。传统 HTB Qdisc 里的分类逻辑现在被移到了 clsact Qdisc 里,例如:
$ tc qdisc add dev $DEV cls