Linux性能优化12:网络IO的调度模型

[介绍]

这一篇总结Linux网络IO的调度模型。还是那句话,这里仅仅是建一个便于讨论问题的初步模型,更多的信息我们在初稿出来后,有机会就慢慢调整。



[基本模型]

还是老规矩,我们从最简单的模型谈起。我们把重点放在队列和线程的模型上。

先看发送,用户程序用socket的send之类的函数给协议栈写入数据,协议栈使用用户线程来驱动第一步的行为:分配skb,把数据拷贝进去,经过协议栈的处理,直到到达网卡驱动,这之前都不需要队列。但到了网卡驱动就不行了,因为我们并不能确认网卡现在就绪了。所以到这里需要一个等待队列(具体队列的形态,我们在qdisc的时候再讨论)。

第二个执行流是网卡的中断,当网卡完成了上一波数据的发送,它给CPU(驱动)发来一个中断,中断raise一个软中断完成发送的动作。

这段执行有两个线程(我这里把具有独立执行能力的软中断也看作是一个线程),两个队列:网卡硬件上的队列和协议栈的发送队列。

接收则反过来,网卡收到足够的消息了,给CPU发一个中断,CPU分配skb,接收报文(实际上现代的网卡通常是把skb预分配给硬件,让硬件自己填skb,收的时候只是替换一批空的skb,并把收好的包往上送。但这两者在调度上原理一样,所以我们这里认为是一回事的),然后把skb送入协议栈(注意,这里仍使用softirq的上下文),最后到达socket的队列,通知用户态的线程从队列中获得数据)

这里仍是两个队列两个线程。

其实协议栈常常还有第三种执行流,就是把Linux作为一个路由器或者交换机,做转发,但对于大型商业应用,我们不靠CPU来做转发,这个部分的压力分析没有什么价值,我这里就不讨论了。



[NAPI]

现代的Linux网卡驱动通常使用napi来实现上面的流程。NAPI提供一个相对统一的处理机制,但其实不怎么改变前面提到的整个处理过程。

NAPI的主要作用是应对越来越快的网络IO的需要:网络变快以后,从网络送上来的包可能会超过CPU的处理能力,如果让接收程序持续占据CPU,CPU就完全没有时间处理上层的协议了。所以NAPI每次接收(或者发送)不允许超过特定的budget,保证有一定时间是可以让给上层协议的。

使用NAPI后,网卡驱动的收发IRQ不再使用自己的收发处理,而是调用napi_schedule(),让napi_schedule()激活softirq,在里面决定每次poll多少数据,以及在polling超限的时候,等待多长时间再进行下一波调用。(每次polling有tracepoint的,trace_napi_poll,我们可以跟踪这个点来判断状况)。

NAPI比较有趣的地方是,很多网卡的实现,无论是发还是收,其实用的都是NET_RX_SOFTIRQ。这个是我们分析的时候要注意的。



[Offloading]

网卡的Offload特性,比如GRO等,能在很大程度上把很多CPU协议栈必须完成的工作Offload到网卡上,但那个和调度无关,这里忽略。



[多队列]

有前面看CPU调度和存储调度的经验,我们应该很容易就看到网卡这个模型的问题了:只有4个线程,基本上无法充分利用多核的能力。所以现代网卡通常支持多队列。用ethtool -l可以查看网卡的多队列支持情况,下面是我们的网卡的多队列支持输出(每个网卡多少个队列可以通过BIOS配置):

网卡和协议栈会把数据通过特定的Hash算法分解到不同的队列上,从而实现性能的提升。发送方向上,协议栈通过设置skb的queue_map参数为一个包选定使用的queue,网卡驱动也可以根据需要调整已经被选择的队列。接收方向上,网卡一侧的算法称为RSS(Receive Side Scaling),它通过源、目标的IP地址和端口组成等进行Hash,算的结果通过一个转发表调度到不同的queue上,我们可以通过ethtool -x/X来查看或者就该这个转发表:

网卡的多队列的模型其实挺简单的,最后就是看我们怎么分布不同网卡和内存的距离,保证对一个的业务在靠近的Numa Node上就好了。



[qdisc]

RSS主要是针对接收方向的,发送方向的情况会更复杂一些,因为发送方向上我们需要做QoS(接收方向上你没法做,因为你没法随便调整送过来的消息的顺序)。Linux网络的QoS特性称为TC(Traffic Control,实际上TC比一般意义的QoS强大得多),它工作在协议栈和网卡驱动之间。当协议栈最终决定把一个skb发到设备上的时候,它调用dev_queue_xmit()启动调度,这时进入的不是网卡的发送函数,而是qdisc子系统(它是netdev的一部分),qdisc可以使用不同的算法把消息分到不同的队列(注意,这个队列不是网卡的队列,而是qdisc的队列,为了区分,我们后面简称disc_q,每个网卡的队列有至少一个disc_q对应)上,然后用napi的发送函数来触发qdisc_run()(这个函数会在netif_tx_wake_queue()等函数中自动被调用)来实现真正的驱动中的发送回调。

qdisc支持的调度算法极多,读者们自己搜一下register_qdisc()这个函数的调用就可以领略一下。要了解所有这些算法,可以参考man 8 tc (或者tc-<算法名>)。

我们这里简单看看默认的算法pfifo_fast体会一下:

FIFO很好理解,就是先入先出(有一个独立的算法叫pfifo),pfifo的队列长度可以用tc手工设置,pfifo的队列长度由驱动直接指明(netdev->tx_queue_len),如果超过长度没有发出去,后续的包就会丢弃。pfifo_fast和pfifo不同的地方是,pfifo_fast可以根据报文的ToS域把报文分发到三个队列,然后按优先级出队列(具体优先级的算法可以参考man 8 tc-prio)。这在队列没有发生积累的时候等于没有用,但在队列有积累的时候,高优先级的包就会优先得到保证。

qdisc可以玩出延迟(比如通过netem强行delay每个包的发出,随机丢包等),限流,控制优先级等很多花样。它可以通过class进行叠加:

(这个配置把sfq,tbf,sfq三个算法叠加在prio算法中,实现不同分类的包用不同的方式调度)

这也给调优带来很多的变数,这些我们只能在具体的环境中做ftrace/perf才能跟踪出来了(幸运的是,网络子系统的tracepoint还是很完善的)。




[ODP]

ODP(opendataplane.org)是ARM体系结构的协同社区Linaro推出的网络处理构架,这个构架其实和我们讨论到的Linux系统调优的关系不大。因为它几乎和Linux一点关系没有。ODP一般用于固定使用几个核用于数据处理的场景。比如你做一个路由器,管理,路由协议的部分使用Linux的协议栈处理,但转发的部分用Linux这么复杂的框架进行处理就不合适了。这种情况你可以写一个ODP的应用程序,独占其中几个核,这个应用程序直接从网卡上把包拉出来,然后处理,然后转发出去。这个就称为“数据面”处理。ODP设计上就预期和DPDK的网卡兼容,它对网卡的包的格式没有强制要求,它提供一整套编程接口(内存分配,定时器,任务管理等),整个平台工作在用户态,默认的业务模型不是基于中断的,而是基于polling的(反正这个核除了做转发也不做别的事情),调度方法就是多个任务(线程)调度“Queue-分发-下一个Queue”这样的模型,所以一般应用的调优手段和优化原则可以直接用于ODP。我们这里也没有什么可以特别补充的了。

编辑于 2016-10-17

文章被以下专栏收录