提升 Linux 网络性能,应付 100 GB的网卡
贾斯玻.布鲁勒在2015年澳大利亚Linux研讨会(LCA)的有关内核的小型研讨会上提到:100GB的网卡即将来临( 见幻灯片,PDF格式的)。对Linux内核来说,要以最大的速度驱动这样的适配器将是巨大的挑战。应对这一挑战是目前和未来一段时间内工作的重心。好消息是Linux网络通信速度已经有了很大的提高-不过还有一些问题有待解决。
挑战
由于网络适配器的速度越来越快,那么发送数据包的时间间隔(也就是内核处理一个包的时间)就会越来越短。就当前正在使用的10GB适配器而言,发送两个1538字节数据包的时间间隔是1230纳秒,40GB的网络通信会把这个时间间隔大幅缩减到307纳秒。很自然,100GB的网络适配器会急剧缩减这个时间间隔,即缩减一个包的处理时间大约为120纳秒。此时,网卡每秒就要处理八百一十五万个数据包。这样就不会有太多时间来进行数据包的处理了。
那么,如果像大多数人一样没有100GB的网络适配器把玩一下,你该怎么做呢? 你可以替代性地使用10GB的网络适配器发送很小的帧。以太网所能发送的最小帧长度是84字节;贾斯玻说使用10G网络适配器发送最小尺寸的以太网帧的时间间隔是67.2纳秒。能够处理最小尺寸以太网帧负载的系统就适宜于处理100GB的网络通信,不过目前还未实现。而且对这种负载的处理也非常困难:对一个3GHz的CPU来说,处理一个包只需要200个CPU周期。贾斯玻还提醒说:这点时间本身就不算多。
以往,内核就没有在处理密集型网络通信的工作负载方面做太多工作。这也使得大量次要的网络通信实现完全不包含在内核网络协议栈中。如此的系统需求就表明内核没有充分利用上硬件;由单个CPU执行的次要实现就可以以最大速度驱动适配器,这就是内核主线开发犯难的地方。
贾斯玻说现在的问题是:内核开发人员过去曾经集中力量添加多核支持。在此情形下,他们就不会关心单核运行效率的衰退情况。由此而产生的结果就是:现今的网络通信协议栈可以很好地处理许多种工作负载,然而对于哪些对时延特别敏感的工作负载则无能为力了。现今的内核每秒每核心只能处理一百万到两百万之间个数据包,而某些不使用内核的方法每秒每核心可处理的数据包数可达一千五百万个。
预算所需时间
如果你打算解决这个问题,那么你就需要仔细地看看处理一个包的每一个步骤所花费的时间。例如,贾斯玻提到的3GHz的处理器上出现一次高速缓冲未命中需要32纳秒才找到对应的数据。因此,只要有两次未命中就可以占完处理一个数据包所需要的时间。假如套接字缓冲(“SKB”)在64位系统上占有四个高速缓冲行,而且许多套接字缓冲(“SKB”)都是在包处理期间写入的,那么问题的第一部分就非常明显了-四次高速缓冲未命中需要的时间将会比可使用的时间更长。
除上述外,X86架构上的锁的原子级别的操作大概需要费时8.25ns. 也就是说,在实践中一次最短的自旋锁的上锁/解锁的过程就要耗时16ns还多一点。 所以尽管有大量的锁操作,但是在这方面却没有多少空间可以降低延时。
接下来是与系统进行一次通信的时间消耗。在安装有SELinux及审计功能(auditing enabled)的系统上,一次系统的调用就要耗时75ns — 超过了帧处理的整个时间。 禁用审计和SELinux可以减少时间,使系统通信降到42ns以下。这看上去好多了,但是还是很大的时间消耗。有许多方法可以在处理多个包的时候降低时间消耗,包括调用sendmmsg(),recvmmsg(),sendfile()和splice()这样的函数。但是在实际中,有开发者觉得这些函数的表现并不如预期的那么好,但是他们又找不出原因。Chirstoph Lameter站在旁观者的角度指出来说,那些对延时敏感的用户可以倾向于使用InfiniBand架构的“IB verbs”机制。
贾斯玻询问:如果上面所要花费的时间已经确定,那么不需要网络通信的方案是如何获得更高的性能呢?关键在于批量进行操作处理和资源的预分配和预获取。这样的操作始终在CPU范围内运行,而且不需要上锁。这些因素对于缩减包的元数据和减少系统调用的次数来说也非常重要。要想在速度上再提高一点,哪些可充分利用高速缓冲的数据结构可以帮一些忙。在上面所述的所有技术中,批量进行操作处理是最最重要的。一个包所需要的处理时间可能无法接受,但如果是一次性执行多个包的话,你就很容易接受了。一个包加锁需要16纳秒让人很受伤,如果一次性加锁16个包,那么每个包所需要的处理时间就缩减到1纳秒了。
对批量操作的改进
因此,毫不奇怪,贾斯玻的工作过去曾经集中精力改进网络层的批量操作。其中包括 TCP 层的批量传输工作,这里十月份的文章中有所介绍;有关它是如何运行的详细情况请参阅上面链接的那篇文章。简要的来说,机制是这样的:通知网络驱动还有更多的数据包等着传输,让驱动延迟花费时间较多的操作,直到所有的包都填满队列。如果把这样的修改放在适当的地方,那么在同样小的数据包一个接着一个传输的情况下,他编写的系统每秒至少能传输一千四百八十万个数据包。
他说做到这些技巧在于给网络通信层添加了进行批量操作处理的应用程序接口(API),同时不会增加系统延迟时间。通常,延迟和吞吐量之间必须要进行某种权衡;然而,在这里,延迟和吞吐量都有所改进。特别难于处理的技巧是对传输延迟的推测-这就好比赌博后续的包马上就到来。这样的技巧常常用在改进基准测试的结果上,在现实世界中的工作负载上极少用到。
批量操作可以并且应当在通信协议栈的的多个层实现。例如,排队子系统(“qdisc”)就是非常适合进行批量操作;毕竟,排队就意味着延迟。目前,在最理想的情况下,排队子系统(“qdisc”) 编码部分处理一个包需要六个锁操作-仅锁操作就需要48纳秒。而对一个包进行排队处理的全部时间是58-68纳秒,可见大量的时间都用在锁操作上了。贾斯玻已经添加了批量操作,这样就可以把所消耗的时间分散在对多个包的处理上了,不过,这段代码实只在对包进行排队的情况下才运行。
排队子系统(“qdisc”)代码名义上的最快运行路径只有在不进行排队的情况下才运行;此时,这些包通常直接传送给网络接口,根本就不需要进行排队处理。不过目前,处理这些包依然需要完完整整地进行6次锁操作。他说,改进这样的处理过程是可能的。无锁的排队子系统几乎不需要花费时间对包进行排队处理,贾斯玻对现有的实现进行了测试,确定了哪些地方可以优化,他说,至少剔除48纳秒的锁操作就值得一试。
他说,传输的性能现在看起来已经够好了,不过接收的处理过程仍然可以进行改进。一个调整的非常好的接收系统每秒可接收的最大包数目大约是六百五十万个-不过这种情况只在接收到包后即刻就丢弃的情况下才发生。优化接收过程的一些工作一直在进行着,最大处理速度可提升到每秒处理九百多万个数据包。然而,对优化后的基准测试也暴露出了问题,它没有给出与内存管理子系统交互所花费的时间。
内存管理
有证据表明与内存管理系统的交互很花时间。网络通信栈的接收过程似乎采用了这样的行为:使用slab分配器时其性能不是最佳。接收代码每次可给高达64个包进行内存空间分配,而传输过程的批处理操作中可释放内存的包数目达256个。这样的模式似乎特意把SLUB分配器用到了相对处理较慢的代码段上了。贾斯玻对其进行了一些小型基准测试,他发现在kmam_cache_free()后只有调用kemem_cache_alloc()一次需要大约19纳秒。然而,当完成256次分配和释放时,所需要的时间就会增加到40纳秒。实际的网络通信中,内存分配和释放是与其他操作同时进行的,这样内存分配和释放所花费的时间就会更多,高达77纳秒-超过预计分配给它的时间。
因此,贾斯玻得出这样的结论:要么必须对内存管理代码部分进行改进,要么必须通过某钟方式完全绕过这段代码。为了看看后一种方法是否可行,他实现了qmempool子系统;这个子系统可以以无锁的方式批量进行内存分配和释放。通过使用qmempool,他在简单测试中节省了12纳秒,而在包转发测试中则节省高达40纳秒。qmempool为了使自身运行更快采用了许多技术,然而杀手级技术是批量处理操作。
贾斯玻平静地说:qmempool的实现是一种激励。他想去表明哪些地方可以进行修改,同时鼓励内存管理系统的开发人员在这方面做些工作。内存管理团队的回应将在下一次谈话中介绍,我们将单独对其进行报导。