站点介绍
本文从底层到上层介绍了Linux网络性能优化策略”
00
从0开始是码农的基本素养
网卡功能配置
一般来说,完成同一个功能,硬件的性能要远超软件。随着硬件的发展,支持的功能也越来越多。因此,我们要尽量将功能offload到硬件上
使用ethtool -k 查看网卡支持的功能列表以及当前状态。下面是笔者一台虚机的输出。
注:不同型号网卡的输出不同,不同内核版本输出也会略有区别。
一般情况下,需要使能以下功能:
可以使用ethtool -K 启用指定功能。
【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)
网卡ring buffer配置
网卡驱动的默认ring buffer一般都不大,以笔者的虚机为例。
接收和发送的大小都是256字节,这个明显偏小。在遇到burst流量时,可能导致网卡的接收ring buffer满而丢包。
在高性能大流量的服务器或者转发设备上,一般都要配置为2048甚至更高。并且,笔者要是没有记错的话,在intel网卡驱动中,推荐发送buffer的大小设置为接收buffer的两倍 —— 原因没有特别说明。
使用ethtool -G设置网卡ring buffer的大小,笔者一般设置为2048和4096。如果是转发设备,有可能会设置的更大一些。
中断设置
现在的网卡绝大部分都是多队列网卡,每个队列都有独立的中断。为了提高并发处理能力,我们要将不同中断分发到不同CPU核心上。
通过cat /proc/interrupts来查看硬中断的状态。
上图笔者虚机的网卡中断还是比较均匀分布在不同CPU核心上。
查看对应中断的CPU亲和性
不同接收/发送队列对应的中断,被分配到CPU0~7上。
而默认情况,一般对应中断的smp_affinity会被设置为ff,即该中断可以被分发到所有核心上。这时候,看上去所有队列中断都可以被分发到任意核心,理论上似乎可以做得比上面指定核心更好。然而实际效果往往不是如此。这取决于硬件和OS的实现,在笔者的经历中,还没有遇到smp_affinity设置为ff后,硬中断负载很均衡的情况。一般都是被分发到指定几个核心上,而其它核心只收到很少的一部分中断。
所以,一般情况下,我们都是把网卡的不同接收队列按顺序分配给不同CPU。这时候,一个问题出来了。网卡是如何决定把报文放到哪个队列上呢?
网卡RSS设置
网卡也是通过hash运算来决定把报文放在哪个接收队列中。虽然我们无法改变hash算法,但我们可以设置hash的key,也就是通过报文的什么字段来计算,从而影响最后的结果。
使用ethtool --show-tuple来查看指定协议
不同网卡的RSS能力不同,支持的协议,可以设置的字段也都不同。但比较奇怪的是,UDP协议的默认key,与TCP不同,只是源IP+目的IP。这样在做UDP的性能测试时,就要格外注意,使用同一台设备作为客户端,产生的UDP报文只会被分发到一个队列中,导致服务端只有一个CPU处理中断,会影响测试结果。
因此,一般我们都要通过ethtool --config-tuple来更改UDP RSS的key,使其与TCP类似。
01
—
下面开始进入软件领域的优化策略。
NAPI机制
现代的Linux网络设备驱动一般都是支持NAPI机制的,其整合了中断和轮询,一次中断,可以对设备进行多次轮询。这样可以同时具有中断和轮询的优点。—— 当然,对于纯转发设备来说,可以直接采用轮询。那么,一次中断,究竟要轮询多少次呢?可以通过/proc/sys/net/core/netdev_budget进行配置,默认是300。这个是所有设备共享的。当进行接收软中断处理时,有可能已经有多个网络设备触发了中断,追加在NAPI list上。那么这些设备共享的budget是300,而每个设备一次NAPI的调用,最多轮询次数,一般是直接写在驱动中,通常为64。这里,无论是64还是300,都是指最多的轮询次数,如果硬件中没有准备好的报文,即使没有达到budget的数量,也会退出。如果设备一直有报文,那么接收软中断就会一直收取报文,直到budget的数量。
当软中断占用CPU较多时,会导致在这个CPU上的应用得不到调度。所以这个budget值,要根据业务来选择合适的值。因为不同协议报文的处理耗时是不同的,并且通过设置budget数量来控制接收软中断的占用CPU的时间,并不直观。因此在新版本的内核中,引入了一个新的参数netdev_budget_usecs,用于控制接收软中断最大占用CPU时间。
RPS和RFS
在没有多队列网卡的时代,网卡只能产生一个中断,发送给一个CPU。这时,如何来利用多核提高并行处理的能力呢?RPS就是为了解决这个问题而诞生的。RPS与网卡RSS类似,只不过是CPU根据报文协议计算了一个hash值,然后利用这个hash值选择一个CPU,将报文存入该CPU的接收队列。并发送一个IPI中断给目的CPU,通知其进行处理。这样的话,即使只有一个CPU收到中断,也可以RPS再次把报文分发到多个CPU上。
通过写入文件/sys/class/net/ethx/queues/rx-0/rps_cpus,来设置该网卡接收队列可以分发到哪些CPU。
RFS与RPS类似,仅是中间的字母不同,前者是Flow,后者是Packet。这也说明了其实现原理。RPS是完全根据当前报文特征进行分发的,而RFS则考虑到了flow —— 这里的flow并不是一个简单的流,而是考虑“应用”的行为,即上次是哪个CPU核心处理的这个flow的报文,该CPU就是目的CPU。
现在因为有了多队列网卡,且可以设置自定义的ntuple,来影响hash算法,所以RPS已经没有了多少用武之地。
那么RFS是否也要进入历史的尘埃中呢?我个人认为是否定的。试想,下面这个场景,在一个8核的服务器上,部署了一个服务S,其6个工作线程占用CPU0~5,剩余的CPU6~7负责处理其它业务。因为CPU核心为8个,网卡队列一般也会设置为8个。假设这8个队列分别对应为CPU0~7,这时候问题来了。服务S的业务报文被网卡收到,经过RSS计算之后,被放置在队列6中,对应的中断也发给了CPU6,可CPU6上并没有运行服务S的线程,数据报文被追加到6个工作线程中的socket接收buffer中。这一方面会与正在运行的工作线程的读取操作可能产生竞争关系,另一方面当对应的工作线程读取该报文时,该报文数据还需要重新读取到对应CPU的cache中。而RSS可以解决这个问题,当工作线程处理socket的报文时,内核会记录这个报文是由某个CPU处理的,将这个映射关系保存到一个flow表中。这样即使是CPU6收到中断,其根据报文协议特征,找到flow表中对应项,于是该报文应该由CPU3处理。这时,就会把报文存放到CPU3的backlog队列中,避免了出现上面文中的问题。
XPS
RPS和RFS是用于建立接收队列与处理CPU的关系,而XPS不仅可以用于建立发送队列和处理CPU的关系,还可以建立接收队列与发送队列的关系。前者是当发送完成时,其工作由指定CPU完成,而后者是通过接收队列选择发送队列。
netfilter和nf_conntrack
netfilter即iptables工具在内核中的实现,其性能一般,尤其是当规则数目较多或者使用了扩展匹配条件时。这个根据大家的情况,斟酌使用。而nf_conntrack为netfilter作为状态防火墙所需的连接跟踪功能。在Linux早期版本,其会话表使用的是一把全局大锁,对性能伤害较大。在生产环境下,一般都不推荐加载这个模块,也就不能使用状态防火墙,NAT,synproxy等。
early_demux开关
对linux内核比较熟悉的同学都知道,linux收到报文后,会通过查找路由表,来判断报文是发给本机还是转发的。如果确定是发往本机的,还要根据4层协议来查找是发给哪个socket的。这里牵涉到了两次查找,而对于为establish状态的TCP和某些UDP来说,已经完成了“连接”,其路由可以视为“不可变”的,因此可以缓存“连接”的路由信息。当打开了/proc/sys/net/ipv4/tcp_early_demux或udp_early_demux后,上面的两次查找可能合并为一次。内核收到报文后,如果该4层协议使能了early_demux,就提前进行socket查找,如果找到,就直接使用socket中缓存的路由结果。该开关对于转发设备来说,无需开启 —— 因为转发设备以转发为主,没有本机的服务程序。
使能busy_poll
busy_poll最早命名为Low Latency Sockets,是为了改善内核处理报文的延时问题。其主要思想就是在做socket系统调用时,如read操作时,在指定时间内由socket层直接调用驱动层方法去poll读取报文,大概可以提升几倍的PPS处理能力。
busy_poll有两个系统层面的配置,第一个是/proc/sys/net/core/busy_poll,其设置的是select和poll系统调用时执行busy poll的超时时间,单位为us。第二个是/proc/sys/net/core/busy_read,其设置读取操作时的busy_poll的超时时间,单位也是us。
从测试结果上看,busy_poll的效果很明显,但其也有局限性。只有当每个网卡的接收队列有且只有一个应用会读取时,才能提高性能。如果有多个应用同时都在对一个接收队列执行busy poll时,就需要引入调度器进行裁决,白白增加消耗。
接收buffer大小
Linux socket接收buffer有两个配置,一个是默认大小,一个是最大大小。既可以使用sysctl -a | grep rmem_default/max得到,也可以通过读取/proc/sys/net/core/rmem_default/max获得。一般来说,Linux这两个配置的默认值,对于服务程序来说,有些偏小(大概是几百k)。那么我们可以通过sysctl或者直接写入上面的proc文件来增大接收buffer的默认大小和最大值,来避免突发流量应用来不及处理导致接收buffer满而丢包的情况。
TCP配置参数
/proc/sys/net/ipv4/tcp_abort_overflow: 控制TCP连接建立但是backlog队列满时的行为。默认一般为0,行为是会重传syn+ack,这样对端会重传ack。值为1时,会直接发送RST。前者是比较温和的处理,但是不容易暴露backlog满的问题,可以视自己的业务设置适合的值。
/proc/sys/net/ipv4/tcp_allowed_congestion_control:显示当前系统支持的TCP流控算法
/proc/sys/net/ipv4/tcp_congestion_control:配置当前系统使用的TCP流控算法,需要为上面显示的算法。
/proc/sys/net/ipv4/tcp_app_win:用于调整缓存大小在应用层和TCP窗口的分配。
/proc/sys/net/ipv4/tcp_dsack:是否开启Duplicate SACK。
/proc/sys/net/ipv4/tcp_fast_open:是否开启TCP Fast Open扩展。该扩展可以提高长距离通信的响应时间。
/proc/sys/net/ipv4/tcp_fin_timeout: 用于控制本端主动关闭后,等待对端FIN包的超时时间,用于避免DOS攻击,单位为秒。
/proc/sys/net/ipv4/tcp_init_cwnd: 初始拥塞窗口大小。可以根据需要,设置较大的值,提高传输效率。
/proc/sys/net/ipv4/tcp_keepalive_intvl:keepalive报文的发送间隔。
/proc/sys/net/ipv4/tcp_keepalive_probes: 未收到keepalive报文回应,最大发送keepalive数量。
/proc/sys/net/ipv4/tcp_keepalive_time:TCP连接发送keepalive的空闲时间。
/proc/sys/net/ipv4/tcp_max_syn_backlog:TCP三次握手未收到client端ack的队列长度。对于服务端,需要调整为较大的值。
/proc/sys/net/ipv4/tcp_max_tw_buckets:TCP处于TIME_WAIT状态的socket数量,用于防御简单的DOS攻击。超过该数量后,socket会直接关闭。
/proc/sys/net/ipv4/tcp_sack:设置是否启用SACK,默认启用。
/proc/sys/net/ipv4/tcp_syncookies:用于防止syn flood攻击,当syn backlog队列满后,会使用syncookie对client进行验证。
/proc/sys/net/ipv4/tcp_window_scaling:设置是否启用TCP window scale扩展功能。可以通告对方更大的接收窗口,提高传输效率。默认启用。
常用的socket option
SO_KEEPALIVE:是否使能KEEPALIVE。
SO_LINGER:设置socket “优雅关闭”(我个人起的名字)的超时时间。使能LINGER选项时,当调用close或者shutdown时,如果套接字的发送缓存中有数据,不会立刻返回而是等待报文发送出去或者直到LINGER的超时时间。这里有一个特殊情况,使能了LINGER,但是LINGER时间为0,会怎么样呢?会直接发送RST到对端。
SO_RCVBUFF:设置套接字的接收缓存大小。
SO_RCVTIMEO:设置接收数据的超时时间,对于服务程序来说,一般都是无阻塞,即设置为0。
SO_REUSEADDR:是否验证绑定的地址和端口冲突。比如已经使用ANY_ADDR绑定了某端口,则后面不能使用任何一个local地址再绑定同一个端口了。对于服务程序来说,推荐打开,避免程序重启时bind地址失败。
SO_REUSEPORT:允许绑定完全相同的地址和端口,更重要的是当内核收到的报文可以匹配到多个相同地址和端口的套接字时,内核会自动在这几个套接字之间做到负载均衡。
其它系统参数
最大文件描述符数量:对于TCP服务程序来说,每个连接都要占用一个文件描述符,因此默认的最大文件描述符个数远远不够。我们需要同时增大系统和进程的最大描述符限制。前者可以使用/proc/sys/fs/file-max
可以使用/proc/sys/fs/file-max或者sysctl -n fs.file-max=xxxxxx设置。而后者可以使用ulimit -n,也可以调用setrlimit设置。
绑定CPU:服务程序的每个线程绑定到指定CPU上。可以使用taskset或者cgroup命令,将指定服务线程绑定到指定CPU或者CPU集合上。也可以调用pthread_setaffinity_np来实现。通过将指定线程绑定到CPU,一方面可以保证cache的热度(高命中),另一方面也可以做到符合业务的CPU负载分配。
03
—
前面主要是通过调整内核参数来优化Linux的网络性能,但对于应用层的服务程序来说,还是有几个绕不开的问题,比如进出内核的数据拷贝等。于是就诞生了bypass内核的方案,如dpdk,netmap,pfring等,其中以dpdk应用最广。相对于内核其有三个优势:1. 避免了进出内核的数据拷贝;2. 使用大页,提高了TLB的命中率;3. 默认使用poll的方式,提高了网络性能。
不过这些收发包工具,还无法做到内核那样包含完整的协议栈和网络工具。—— 当然,现在DPDK已经拥有很多库和工具了。
对于网络转发设备来说,基本上只处理二三层的报文,对协议栈要求不高。对于服务程序来说,则需要比较完备的协议栈。目前已有DPDK+mtcp、DPDK+fstack和DPDK+Nginx方案。
因为本文主要聚焦于linux的网络性能提升,bypass的方案仅做一个介绍而已。
评论列表 (0条)