最近读了一篇关于TCP性能优化的文章【1】。它里面讲的问题,是一个普遍性的问题:一个TCP链接的收包和发包流程,经常在不同CPU核上运行,性能开销很大。对很多系统来说,解决掉这个问题,性能提升可以达到20%以上。

这篇论文的摘要

问题描述

随着网络速度提高,网卡接收报文的速度超过了单个CPU核的处理能力。为了解决这个问题,现在的很多网卡芯片都支持RSS(Receive Side Scaling)特性,也就是,把接收到的TCP报文送给多个CPU核处理。

为了保证同一条TCP链接的报文被送到相同的CPU核上,网卡会用五元组(local_addr, remote_addr, local_port, remote_port, proto)来计算一个hash值,然后根据这个hash值,把报文送入不同的接收队列,不同队列由不同CPU核负责处理。具体实现方法是,不同的接收队列,对应不同的中断号码,这些中断号码,会被路由到不同的CPU核上去。网络报文到达之后,通过中断通知CPU核,然后Linux内核会唤醒这个核上的软中断处理流程,在软中断中运行TCP接收报文的代码路径。然后唤醒上层应用线程,调用recvmsg()把报文从内核拷贝出去。也就是说,内核TCP的接收处理路径,是由网卡决定的,由网卡的RSS的Hash算法,以及网卡的接收队列中断和CPU核之间绑定关系决定的。

Linux内核的TCP发送处理路径,是与应用线程在同一个CPU核上。如果不做特殊处理,同一条TCP链接的接收流程和发送流程大概率是在不同CPU核上处理的。

由于接收流程和发送流程都需要访问TCP链接的数据结构,它们在不同CPU核上运行,就需要锁来保证互斥访问,会产生锁竞争。这些TCP数据结构频繁在不同CPU核的L1/L2 Cache之间迁移。锁竞争和Cache中数据的频繁迁移会产生很大性能开销。设法让同一条TCP的接收和发送流程在同一个CPU核上运行,成为优化TCP性能的一个重要方向。

NIC接收发送

现有方案

流定向,Flow Direct,简写为FD,或者FDir。有些网卡内部集成了一个路由表,软件可以配置这个路由表来决定某个特定TCP链接的报文,被送到哪个接收队列中。一般来说,这个路由表中可配置的表项数量有限,也不是所有高速网卡都支持。

Intel某些型号网卡支持Application Targeting Routing (ATR),网卡自动配置FDir路由表,把TCP接收队列调整到发送流程所在的CPU核上,也不是所有网卡都支持这个特性。

除了上面这些网卡硬件实现的特性之外,Linux内核软件也实现了一些类似的特性。RPS是软件实现的RSS,由一个CPU核来处理网卡接收报文,然后把报文分发到多个CPU核上去。RFS可以把报文分发到上层应用所在的核。

本文开头提到的《Improving Network Connection Locality on Multicore System》【1】,提出了一个新方法。应用可以在多个线程中同时调用accept(),Linux内核选择这个新TCP链接的接收流程所在的CPU核返回accept()。这样,处理这个TCP链接的应用线程所在的CPU核,跟TCP链接的报文接收流程处理CPU核,很大概率是相同的。也就解决了这个问题。这个方案只对服务端起作用,对客户端不起作用。似乎最新的Linux内核代码也没包括这部分代码。

我的方法

我也曾经发明过一种土方法,通过主动选择local_port,让网卡把收到的TCP消息送到应用线程所在的CPU核,从而避免TCP接收和发送在不同核上处理。

在TCP客户端调用connect()发起链接之前,先为这条链接选定一个本地端口号(local_port),并调用bind让这条TCP链接使用这个选定的本地端口号,避免让Linux内核在connect的自动选择一个随机的端口号。应用通过选定本地端口号,可以控制这个TCP链接的接收流程在特定的CPU核上运行,这样就可以控制TCP的接收和发送流程在相同的CPU核上运行。参与RSS的Hash值计算的五元组(local_addr, remote_addr, local_port, remote_port, proto)中的有四个都是固定的,只有local_port是一个可变的。控制local_port的值,可以让Hash函数算出期望的结果。用这个方法,如果这条TCP链接是一个集群内部的链接,可以同时选择本地CPU核和远端的CPU核。

在zStorage里面,解决很多类似的小问题。zStorage的高性能,就是靠很多个类似微创新积累起来才达到的。

(结束)

【1】https://people.csail.mit.edu/nickolai/papers/pesterev-multiaccept.pdf