hi,大家好,今天给大家分享一篇后台服务器性能优化网络性能优化的文章,希望大家对Linux网络有更深入的了解。曾几何时,一切都是那么简单。网卡速度慢,只有一个队列。当有数据包到达时,网卡通过DMA复制数据包并发送中断,Linux内核收集这些数据包并完成中断处理。随着网卡变得更快,基于中断的模型可能会由于大量传入数据包而导致IRQ风暴。这将消耗大部分CPU功率并冻结系统。为了解决这个问题,NAPI(Interrupt+Polling)被提出。当内核收到来自网卡的中断时,它开始轮询设备并尽快收集队列中的数据包。NAPI与当今常见的1Gbps网卡配合良好。但是,对于10Gbps、20Gbps甚至40Gbps的网卡,NAPI可能还不够用。如果我们仍然使用一个CPU和一个队列来接收数据包,那么卡将需要更快的CPU。幸运的是,现在多核CPU很流行,那么为什么不并行处理数据包呢?RSS:接收方缩放(RSS)是处理具有多个RX/TX队列的数据包的机制。当具有RSS的网卡接收到数据包时,它会对数据包应用过滤器并将数据包分派到RX队列。过滤器通常是哈希函数,可以使用“ethtool-X”进行配置。如果你想在前3个队列中平均分配流量:#ethtool-Xeth0equal3或者,如果你发现一个神奇的哈希键特别有用:#ethtool-Xeth0hkey对于低延迟网络,除了过滤器,CPU亲和力也很重要。最好的设置是将一个CPU专用于一个队列。首先通过检查/proc/interrupt找出IRQ号,然后将CPU位掩码设置为/proc/irq//smp_affinity以分配专用CPU。为避免设置被覆盖,必须禁用守护程序irqbalance。请注意,根据内核文档,超线程对中断处理没有任何好处,因此最好将队列数与物理CPU内核数相匹配。RPS:ReceivepacketcontrolRSS提供了一个硬件队列,在Linux内核中实现了一种称为ReceivePacketSteering(RPS)的软件队列机制。当驱动程序接收到一个数据包时,它会将数据包包装在一个套接字缓冲区(sk_buff)中,其中包含数据包的u32哈希值。哈希是根据源IP、源端口、目的IP和目的端口,由网卡或__skb_set_sw_hash()计算得到的所谓的四层哈希(l4hash)。由于同一TCP/UDP连接(流)的每个数据包共享相同的哈希值,因此使用相同的CPU来处理它们是合理的。RPS的基本思想是根据每个队列的rps_map将相同流的数据包发送到特定的CPU。这是rps_map的结构:映射根据CPU位掩码动态更改为/sys/class/net//queues/rx-/rps_cpus。例如,我们希望队列使用前3个CPU。在8CPU的系统中,我们先构造一个位掩码,00000111,到0x7,然后#echo7>/sys/class/net/eth0/queues/rx-0/rps_cpus这样就会确保从eth0中的队列0收到的数据包进入CPU1~3。驱动程序在sk_buff中包装一个数据包后,它将转到netif_rx_internal()或netif_receive_skb_internal(),然后转到get_rps_cpu()structrps_map{unsignedintlen;structrcu_headrcu;u16cpus[0];};将被调用以将哈希映射到rps_map中的一个条目,即CPUid。得到CPUid后,enqueue_to_backlog()将sk_buff放入特定的CPU队列中进行进一步处理。每个CPU的队列在每个CPU变量softnet_data中分配。使用RPS的优点是可以在CPU之间分担数据包处理的负载。但是,如果RSS可用,则可能没有必要,因为NIC已经按队列/CPU对数据包进行排序。但是,如果队列中有更多CPU,RPS仍然可以运行。在这种情况下,每个队列可以与多个CPU关联并在它们之间分发数据包。RFS:ReceiveFlowSteering虽然RPS是根据流分发数据包,但它并没有考虑到用户空间的应用。一个应用程序可能在CPUA上运行,而内核将数据包放入CPUB上的队列中。由于CPUA只能使用自己的缓存,因此缓存在CPUB中的数据包变得无用。ReceiveFlowSteering(RFS)作为RPS的应用进一步扩展。RFS维护一个全局流到CPU表,而不是每个队列的哈希到CPU映射,rps_sock_flow_table:此掩码用于将哈希值映射到索引到所述表中。由于表大小将四舍五入为2的幂,因此掩码设置为table_size-1。u32ents[0];};并且很容易找到索引:一个带有散列和scok_table->mask的sk_buff。这个条目通过rps_cpu_mask分为flowid和CPUid。低位用于CPUid,而高位用于流id。当应用程序对socket进行操作(inet_recvmsg()、inet_sendmsg()、inet_sendpage()、tcp_splice_read())时,会调用sock_rps_record_flow()更新sock流表。当数据包到达时,调用get_rps_cpu()以确定使用哪个CPU队列。下面是get_rps_cpu()如何确定数据包的CPU找到条目的索引并检查哈希的高位是否与条目匹配。如果是,它会从条目中检索CPUid,并将该CPU分配给数据包。如果散列与任何条目都不匹配,它将回退到使用RPS映射。sock流表的大小可以通过rps_sock_flow_entries进行调整。例如,如果我们要将表大小设置为32768:#echo32768>/proc/sys/net/core/rps_sock_flow_entriessock流表提高了应用程序的局部性,但也带来了一个问题。当调度程序将应用程序迁移到新CPU时,旧CPU队列中剩余的数据包变得突出,应用程序可能会乱序获取数据包。为了解决这个问题,RFS使用每个队列的rps_dev_flow_table来跟踪未完成的数据包。这里是结构体rps_dev_flow_table:对于socks流表,类似的rps_dev_flow_table也使用table_size-1作为掩码,表的大小也必须四舍五入为2的幂流包入队时,更新last_qtailstructrps_dev_flow{u16中央处理器;u16过滤器;/*对于aRFS*/unsignedintlast_qtail;};structrps_dev_flow_table{unsignedintmask;structrcu_headrcu;structrps_dev_flowflows[0];};到CPU队列的末尾。如果应用程序迁移到新的CPU,sock流表将反映更改,get_rps_cpu()将为该流设置新的CPU。get_rps_cpu()在设置新的CPU之前检查当前队列的头部是否已经通过last_qtail。如果是这样,这意味着队列中没有更多未完成的数据包,可以安全地更换CPU。否则,get_rps_cpu()仍将使用rps_dev_flow->cpu中记录的旧CPU。每个队列的流表(rps_dev_flow_table)大小可以通过sysfs接口配置:/sys/class/net//queues/rx-/rps_flow_cnt推荐设置rps_flow_cnt为(rps_sock_flow_entries/N)和N是RX队列的数量(假设流在队列中均匀分布)。ARFS:加速接收流量转向加速接收流控制(aRFS),以进一步将RFS扩展到RX队列硬件过滤。要启用aRFS,它需要具有可编程元组过滤器和驱动程序支持的网卡。启用ntuple过滤器。#ethtool-Keth0ntupleon对于支持aRFS的驱动程序,它必须实现ndo_rx_flow_steer以帮助set_rps_cpu()配置硬件过滤器。当get_rps_cpu()决定为流分配一个新的CPU时,它调用set_rps_cpu()。set_rps_cpu()首先检查网卡是否支持ntuple过滤器。如果是这样,它会查询rx_cpu_rmap以找到适合该流的RX队列。rx_cpu_rmap是驱动维护的特殊映射。该映射用于查找哪个RX队列适合CPU。它可以是与给定CPU直接关联的队列,或者是距离处理CPU最近的缓存位置。获得RX队列索引后,set_rps_cpu()调用ndo_rx_flow_steer()通知驱动程序为给定的流创建一个新的过滤器。ndo_rx_flow_steer()将返回过滤器id,它将存储在每个队列的流表中。除了实现ndo_rx_flow_steer()之外,驱动程序还必须调用rps_may_expire_flow()来定期检查过滤器是否仍然有效并删除过期的过滤器。SO_REUSEPORTlinuxman文档中的一段描述了它的作用:新的套接字选项允许同一主机上的多个套接字绑定到同一个端口,旨在提高运行在多核系统之上的多线程网络服务器应用程序的性能。简单的说,SO_REUSEPORT支持多个进程或线程绑定到同一个端口,以提高服务器程序的性能。我们想了解为什么这个功能如此受欢迎(各大厂商的面试官经常会问到),它解决了什么问题。Linux系统上的后台应用,为了发挥多核的优势,一般采用如下典型的多进程/多线程服务器模型:再长一个问题,但是会有:1.单线程监听器在处理高速海量连接时也会成为瓶颈;2、CPUcachelinefailure(lostsocketstructure)现象严重;所有worker线程accept()在同一个serversocket上,也存在问题:1、多线程访问serversocket锁竞争严重;2、高负载下,线程间处理不平衡,有时不平衡比例高达3:1;3.导致CPUCachelinebouncing(缓存行弹跳);4、繁忙的CPU上有较大的延迟;上述模型虽然可以实现线程和CPU核心的绑定,但是会存在以下问题:单个listenerworker线程在高速访问处理时有时会成为瓶颈,cachelinejumping很难实现CPU间的负载均衡.随着核心数量的扩大,性能并没有提升。SO_REUSEPORTsupportsmultipleprocessesorthreadsboundtothesameport:Allowsmultiplesocketsbind()/listen()同一个TCP/UDP端口1.每个线程都有自己的服务器套接字。2.serversocket没有锁竞争。负载平衡是在内核级别实现的。在安全方面,监听同一个端口的sockets只能位于同一个用户下。其核心实现主要有3点:扩展socket选项,增加SO_REUSEPORT选项设置reuseport。修改bind系统调用实现,支持绑定到同一个IP和端口。修改处理新连接的实现。搜索监听器时,可以支持在监听相同IP和端口的多个socks中均衡选择,带来意义。CPU间均衡处理,水平扩展,模型简单,易维护,进程管理与应用逻辑解耦,进程管理层面委托给程序员/管理员,由程序员/管理员根据实际控制进程的启动/停止情况,增加灵活性。这就带来了一个比较微观层面的扩展思路,线程数是否合适,状态是否共享,降低单个进程的资源依赖,最适合无状态的服务器架构。对于客户端而言,表面上感受不到变化,因为这些工作完全是在服务端完成的。服务器无缝重启/切换,热更新,提供新的可能。我们迭代了一个版本,需要上线部署。为它启动一个新的进程后,我们稍后关闭旧版本的进程程序。该服务一直在不间断地运行,需要过平衡。这就像在Erlang语言级别提供的热更新。SO_REUSEPORT已知问题SO_REUSEPORT分为两种模式,热备模式和负载均衡模式。在早期的内核版本中,即使增加了对reuseport选项的支持,也只是热备份模式。3.9内核之后,All改为负载均衡模式,两种模式并没有共存,虽然我一直希望它们可以共存。SO_REUSEPORT根据数据包的四元组{srcip,srcport,dstip,dstport}和当前绑定到同一端口的服务器套接字数来分发数据包。如果serversocket数量发生变化,内核会把clientconnection发送过来的应该已经被上一个serversocket处理过的数据包(比如三次握手时的half-connection,已经处理过的数据包)完成握手但在队列中排队)连接)到其他服务器套接字,这可能会导致客户端请求失败。如何预防以上已知问题,一般解决方法:1.使用固定数量的服务器套接字,不要在繁忙时期轻易改变。2.允许多个服务器套接字共享TCP请求表(Tcprequesttable)。3、不要用四元数作为Hash值来选择本地socket,比如选择sessionID或processID,选择属于同一个CPU的socket。4.使用一致性哈希算法。与其他特性的关系1.SO_REUSEADDR:主要是地址复用1.1让处于time_wait状态的socket快速复用原来的ip+port1.2使得0.0.0.0(ipv4通配符地址)和其他地址(127.0.0.1和10.0.0.x)无冲突1.3SO_REUSEADDR的缺点是没有安全限制,不能保证所有的连接会均匀分布。2、配合RFS/RPS/XPS-mq,可获得更进一步的性能2.1。服务器线程绑定到CPU2.2.RPS将TCPSYN数据包分发到相应的CPU内核2.3.TCP连接绑定到CPU线程accept()2.4.XPS-mq(TransmitPacketSteeringformultiqueue),传输队列绑定CPU,发送数据2.5。RFS/RPS保证了同一连接的后续数据包都会分发到同一个CPU,网卡的接收队列已经绑定了CPU,那么RFS/RPS就不需要设置了。需要注意硬件是否支持。目的是在一个CPU内核上并行处理软硬件中断、接收和数据包处理,尽可能地最大化资源利用率。改变。SO_REUSEPORT的演变3.9之前的内核可以同时绑定多个socket到同一个ip+port,但是不能实现负载均衡,实现是双机热备。Linux3.9之后,可以将多个socket同时绑定到同一个ip+port,实现负载均衡。Linux4.5版本之后,内核引入了reuseportgroups,绑定相同的IP和Port,将带有SO_REUSEPORT选项的socket组织成一个组。目的是加快套接字查询。Linux网络栈问题总结TCP处理&多核一个完整的TCP连接,中断发生在一个CPU核上,但应用数据处理可能在另一个不同CPU核的核上处理,带来了锁竞争和CPUCacheMiss(Unbalancedfluctuations)多个进程监听一个TCPsocket,共享一个监听队列,用于全局哈希表的连接管理。存在资源竞争epollIO模型多进程震荡现象LinuxVFS同步丢失严重Socket是VFS管理VFS对文件节点Inode和目录Dentry有同步需求。SOCKET只需要存在于内存中即可。它不是严格意义上的文件系统,也不需要Inode和Dentry。在代码层面跳过不必要的常规锁,但保持足够的兼容性RSS、RPS、RFS和aRFS,这些机制是在Linux3.0之前引入的,SO_REUSEPORT选项在Linux3.9被引入到内核中,因此大多数发行版已经包含并启用它们。深入研究它们,为我们的服务器系统找到最佳性能配置。性能优化无极限,我们下期继续分享!