Linux Traffic Control (TC) 子系统是Linux操作系统中用于对从网络设备驱动进出的流量进行分配,整形,调度以及其他修改操作的子系统,借助对数据包比较直接的处理,可以实现流量控制,过滤,行为模拟和带宽限制等功能。
1) Linux Traffic Control的核心原理
对于网络数据报文,网络设备驱动通过将二层的以太网数据报文按照Linux内核定义的网络设备驱动规范,以sk_buff结构体的方式进行接收或者发送,即通常我们所描述的报文的最小单元skb。
● 内核通过将网络设备缓冲区环形队列中的skb取出,并按照以太网层,网络层,传输层的顺序处理后,将报文数据放置到对应的Socket缓冲区中,通知用户程序进行读取,从而完成收包。
● 内核为Socket缓冲区的待发送数据封装为skb,经过传输层,网络层和以太网层依次填充对应的报头后,调用网络设备驱动的方法将skb发送到网络上,从而完成发包。
TC子系统通过工作在网络设备驱动操作和内核真正进行每一层的收包与发包动作之间,按照不同的模式对数据包进行处理,实现复杂的功能。
TC子系统比较常见的作用是对需要发送的数据包进行操作,由于作为收包一侧的Linux内核,无法控制所有发送方的行为,因此TC子系统主要的功能实现都是围绕着发送方向,以下介绍也都是基于发送方向的TC子系统进行。
TC子系统的关键概念
TC子系统与netfilter框架工作在内核网络数据处理流程的不同位置,相比于netfilter,TC子系统工作的实际更加靠近网络设备,因此,在TC子系统的设计中,是与网络设备密不可分,在TC子系统中,有三个关键的概念用于对TC子系统的工作流程进行描述:
● Qdisc是queueing discipline的简写,与我们理解的网卡的队列(queue)不同,qdisc是sk_buff报文进行排队等待处理的队列,不同类型的qdisc有着不同的排队规则,TC子系统会为每个网卡默认创建一个根队列,在跟队列的基础上,可以通过Class来创建不同的子队列,用于对流量进行分类处理。
● Class,如下图所示,class将流量进行分类,不同分类的流量会进入不同的qdisc进行处理。
● Filter,如下图所示,filter通过指定匹配的规则来实现将流量进行分类的作用,filter与class配合之后就可以将流量按照特征,采用不同的qdisc进行处理。
不同的qdisc之间的主要差别就是他们对排队的数据包进行调度的算法的区别,你可以通过一下命令查看网卡的qdisc信息:
# 查看eth0的class为2的流量的默认qdisc,其中handle指代qdisc id, parent指代class id tc qdisc show dev eth0 handle 0 parent 2
常见的qdisc包含以下几种:
● mq(Multi Queue),即默认有多个qdisc。
● fq_codel(Fair Queuing Controlled Delay),一种公平和随机分配流量带宽的算法,会根据数据包的大小,五元组等信息,尽量公平得分配不同的流之间的带宽。
● pfifo_fast,一种不分类的常见先进先出的队列,但是默认有三个不同的band,可以支持简单的tos优先级。
● netem,network emulator队列,常见的依赖TC子系统进行延迟,乱序和丢包模拟,都是通过netem来实现。
● clsact,这是TC子系统专门为了支持eBPF功能而提供的一种qdisc队列,在通过class分配到这个qdisc之后,流量会触发已经挂载到TC子系统上的对应的eBPF程序的处理流程。
● htb,一种通过令牌桶的算法对流量进行带宽控制的常用qdisc,用于在单个往卡上对不同用户,场景的流量进行独立的带宽限流。
报文在TC子系统的处理
在egress方向,当以太网层完成数据报文skb的报头封装后,一个skb就可以直接调用网卡的方法进行发送了,而在Linux内核中,当以太网层完成封装并调用__dev_queue_xmit时,会将skb放入他所在网络设备的TC队列中:
static inline int c(struct sk_buff *skb, struct Qdisc *q, struct net_device *dev, struct netdev_queue *txq) { if (q->flags & TCQ_F_NOLOCK) { if (unlikely(test_bit(__QDISC_STATE_DEACTIVATED, &q->state))) { __qdisc_drop(skb, &to_free); rc = NET_XMIT_DROP; } else { // 对于大多数数据包,都会从这里进入qdisc进行排队 rc = q->enqueue(skb, q, &to_free) & NET_XMIT_MASK; qdisc_run(q); } if (unlikely(to_free)) kfree_skb_list(to_free); return rc; } }
在入队动作发生后,内核一般都会直接进行一次qdisc的发包操作,将队列进行排序并按照规则发送符合条件的数据包:
void __qdisc_run(struct Qdisc *q) { int quota = dev_tx_weight; int packets; // 每次restart都会发送数据包,直到发送完成,这并不意味着所有数据都发送完了,只是这次发送完成了 while (qdisc_restart(q, &packets)) { quota -= packets; if (quota <= 0 || need_resched()) { __netif_schedule(q); break; } } }
qdisc每次被触发执行,都会将已经进入qdisc的数据进行入队操作,同时选择符合发送条件的数据包进行出队动作,也就是调用网卡的操作方法进行数据的发送,以pfifo_fast为例:
static int pfifo_fast_enqueue(struct sk_buff *skb, struct Qdisc *qdisc) { // 检测 Qdisc 队列数据包数量是否达到 dev 预定的最大值 if (skb_queue_len(&qdisc->q) < qdisc_dev(qdisc)->tx_queue_len) { // 确定数据包需要进入哪个通道 int band = prio2band[skb->priority & TC_PRIO_MAX]; struct pfifo_fast_priv *priv = qdisc_priv(qdisc); // 获取通道列表的head struct sk_buff_head *list = band2list(priv, band); priv->bitmap |= (1 << band); qdisc->q.qlen++; // 添加到通道队尾 return __qdisc_enqueue_tail(skb, qdisc, list); } return qdisc_drop(skb, qdisc); } static struct sk_buff *pfifo_fast_dequeue(struct Qdisc *qdisc) { struct pfifo_fast_priv *priv = qdisc_priv(qdisc); int band = bitmap2band[priv->bitmap]; if (likely(band >= 0)) { struct sk_buff_head *list = band2list(priv, band); struct sk_buff *skb = __qdisc_dequeue_head(qdisc, list); qdisc->q.qlen--; if (skb_queue_empty(list)) priv->bitmap &= ~(1 << band); return skb; } return NULL; }
qdisc流量控制由于设计非常复杂,所以很难简单概括其特性,通常在排查网络问题的过程中,我们需要了解的就是常见的qdisc的算法的大致工作原理,以及查看qdisc统计信息。
更多精彩内容,欢迎观看:
《云原生网络数据面可观测性最佳实践》—— 一、容器网络内核原理——3.tc子系统(下):https://developer.aliyun.com/article/1221713?groupCode=supportservice