当前位置: 首页 > Linux

优化Linux网络性能的15个技巧!

时间:2023-04-06 23:00:39 Linux

建议一:尽量减少不必要的网络IO我想给的第一个建议是尽量不要使用不必要的网络IO。是的,网络在现代互联网世界中扮演着非常重要的角色。用户通过网络请求在线服务,服务器通过网络读取数据库中的数据,通过网络构建一个极其强大的分布式系统。网络很好,可以降低模块开发的难度,也可以用来搭建更强大的系统。但这不是你滥用它的借口!原因是即使是原生网络IO开销仍然很大。先说发送一个网络包,你必须先从用户态切换到内核态,这需要一个系统调用的开销。进入内核后,要经过一个冗长的协议栈,会占用大量的CPU周期,最后进入环回设备的“驱动程序”。在接收端,软中断耗费大量的CPU周期,必须经过接收协议栈的处理,最后唤醒或通知用户进程进行处理。服务器处理完后,还得重新发送结果。你必须再次这样做,最后你的过程将收到结果。你说麻烦不麻烦。还有一个问题就是多个进程协同完成一个工作,必然会引入更多的进程上下文切换开销。从开发的角度来看,这些开销其实没什么用。上面我们也只分析了本地网络IO。如果是跨机,肯定有两块网卡的DMA拷贝过程,两端之间的网络RTT耗时延迟。所以,网络虽好,也不能随意滥用!建议二:尽量合并网络请求。在可能的情况下,尽可能将多个网络请求合并为一个,这样不仅可以节省两端的CPU开销,也可以减少多次RTT带来的耗时。让我们举一个实际的例子来更好地理解它。如果有一个redis,里面存放着各个App的信息(应用名、包名、版本、截图等)。现在需要根据用户安装的应用列表查询数据库中有哪些应用比用户的版本更新,如果有则提醒用户更新。那么最好不要写下面的代码:根据我们的统计,现代用户平均安装的app数量在60个左右。这段代码运行时,用户每发起一次请求,你的服务器需要用redis发起60次网络请求。花费的总时间至少为60RTT。更好的方法是使用redis中提供的批量获取命令,如hmget、pipeline等,一次网络IO后获取所有需要的数据,如图。建议三:调用方和被调用机尽量靠近部署。在前面的章节中,我们看到在握手正常的情况下,TCP握手的时间基本上取决于两台机器之间的RTT时间。虽然我们不能完全去掉这个耗时,但是我们有办法降低RTT,就是把client和server放得足够近。尽量在本地机房解决各个机房的数据请求,减少跨区域网络传输。比如你的服务部署在北京机房,你调用的mysql和redis最好在北京机房里面。尽量不要千里迢迢跑到广东的机房去要数据,即使有专线也需要很多时间!机房内服务器之间的RTT时延只有零点几毫秒,同一区域不同机房之间的RTT延迟大约在1毫秒以上。但是如果从北京穿越到广东,延迟会在30-40毫秒左右,增加了几十倍!建议四:内部网络调用不要使用外部域名。如果你负责的业务需要调用兄弟部门的搜索接口,假设接口是:“http://www.sogou.com/wq?key=发展内功培训”。既然是兄弟部门,很可能这个接口和你的服务部署在同一个机房??。即使不部署在机房,一般也可以通过专线到达。所以不要直接请求www.sogou.com,而是使用公司内服务对应的内网域名。在我们公司,每一个外网服务都会配置一个对应的内网域名,相信你们公司也有。原因如下:1)外网接口慢。本来内网可能通过交换机就可以到达兄弟部门的机器。如果非得在外网绕一圈再回来,时间肯定会很慢。2)带宽成本高。在互联网服务中,除了机器之外,另一大成本就是IDC机房的出入口带宽成本。不管两台机器在内网怎么通信,都不涉及带宽的计算。但是你上网一会再回来就好了,每次进出都要交带宽费,还说亏本呢!!3)NAT单点瓶颈。一般的服务器是没有外网IP的,所以如果要在外网请求资源,就必须经过NAT服务器。但是在公司机房的几千台服务器中,承担NAT角色的可能只有几台。它很容易成为瓶颈。我们业务遇到过几种NAT故障导致外网请求失败的情况。如果NAT机器挂了,你的服务也可能挂了,故障率会大大增加。建议五:调整网卡RingBuffer的大小在Linux的整个网络栈中,RingBuffer起到了一个任务收发中转站的作用。对于接收过程,网卡负责将接收到的数据帧写入RingBuffer,ksoftirqd内核线程负责取出来处理。只要ksoftirqd线程工作得够快,RingBuffer中转站就不会有问题。但是我们试想一下,如果在某个时刻,瞬间来了很多包,ksoftirqd处理不了,会怎样?这时候RingBuffer可能会瞬间被填满,后面来的网卡不做任何处理就会被丢弃!RingBuffer的“中转仓库”的大小可以通过ethtool来增加。.#ethtool-Geth1rx4096tx4096这样网卡就会分配一个更大的“中转站”,可以解决偶尔出现的瞬时丢包。但是,这种方法有一个小的副作用,就是队列中的数据包过多会增加处理网络数据包的延迟。所以内核处理网络数据包应该比在RingBuffer中排队网络数据包更快更好。后面我们会介绍RSS,它可以让更多的核参与网络包的接收。建议六:减少内存拷贝如果要发送文件到另一台机器,更基本的方法是先调用read读取文件,再调用send发送数据。这样就需要经常在内核态内存和用户态内存之间进行数据拷贝,如图9.6所示。目前减少内存拷贝的方法主要有两种,即使用两个系统调用,mmap和sendfile。如果使用mmap系统调用,映射地址空间的内存在用户态和内核态都可以使用。如果你发送的数据是mmap映射的数据,内核可以直接从地址空间读取,省去了内核态到用户态的拷贝过程。但是在mmap发送文件的方式中,系统调用的开销并没有减少,内核态和用户态之间仍然存在两次上下文切换。如果你只想发送一个文件而不关心它的内容,你可以调用另一个更极端的系统调用——sendfile。在这个系统调用中,文件的读取和发送完全结合在一起,再次节省了系统调用的开销。再加上大多数网卡都支持的“分散-聚集”(Scatter-gather)DMA功能。可以直接从PageCache缓冲区直接DMA复制到网卡。这节省了大部分CPU复制操作。建议七:使用eBPF绕过协议栈的原生IO如果你的业务涉及到大量的原生网络IO,可以考虑这种优化方案。对比原生网络IO和跨机IO,确实节省了驱动的一些开销。发送数据不需要进入RingBuffer的驱动队列,直接将skb传递给接收协议栈(通过软中断)。但是在内核的其他组件上,一点都不缺。系统调用、协议栈(传输层、网络层等)、设备子系统走完整个流程。连“驱动”程序都没有了(虽然对于环回设备来说,这个驱动只是一个纯软件虚拟的东西)。如果你想使用原生的网络IO,又不想频繁的在协议栈里转来转去。那么你可以试试eBPF。使用eBPF的sockmap和skredirect可以绕过TCP/IP协议栈,直接发送到接收端的socket。业内一些公司已经在这样做了。建议八:在socket上使用recvfrom阻塞方式接收数据时,尽量少用recvfrom等进程阻塞方式。每次进程在套接字上等待数据时,它都必须离开CPU。然后切换到另一个进程。当数据准备好后,休眠进程会再次被唤醒。总共有两个进程上下文切换的开销。如果我们需要在我们的服务器上处理大量的用户请求,那么我们需要有很多进程,需要不断地来回切换。这样的缺点是:因为每个进程同时只能等待一个连接,所以需要大量的进程。进程之间的切换需要大量的CPU周期,一次切换大约需要3-5us。频繁切换导致L1、L2、L3等缓存的作用大大降低。您可能认为这种网络IO模型很少见。但实际上,很多传统的客户端SDK,如mysql、redis、kafka,仍然在使用这种方式。建议九:使用成熟的网络库,使用epoll高效管理大量socket。在服务器端。我们有各种成熟的网络库供使用。这些网络库都对epoll使用了不同程度的封装。首先我要给大家参考的第一个就是Redis。在老版本的Redis中,单进程可以高效的使用epoll来支持每秒几万QPS的高性能。如果你的服务是单进程的,可以参考网络IO部分Redis的源码。如果是多线程,线程之间有多种分工模式。那么哪个线程负责等待读取IO事件,哪个线程负责处理用户请求,哪个线程负责写入返回给用户。根据分工不同,衍生出单反应器、多反应器、前置反应器等多种模式。大可不必头疼,了解了这些原理后,选择一个性能好的网络库就可以了。比如PHP中的Swoole,Golang中的netpackage,Java中的netty,C++中的SogouWorkflow,都封装的很好。建议10:使用新的Kernel-ByPass技术。如果你的服务对网络要求极高,各种优化措施都用过了,那还有终极优化招数——Kernel-ByPass技术。内核在接收网络数据包时,要经过很长的发送和接收路径。这期间涉及到很多内核组件之间的协作,协议栈的处理,以及内核态和用户态之间的复制和切换。Kernel-ByPass等技术方案是绕过内核协议栈,实现用户态网络包的收发。这样既避免了内核协议栈的复杂处理,又减少了内核态和用户态之间频繁的拷贝和切换,性能将得到最大化!目前我知道的方案有SOLARFLARE的软硬件方案、DPDK等。有兴趣的可以多了解一下!建议十一:配置足够的端口范围当客户端调用connect系统调用发起连接时,需要先选择一个可用的端口。内核选择端口时,会从可用端口范围内的随机位置开始遍历。如果没有足够的端口,内核可能需要循环多次才能选择一个可用的端口。这也会导致更多的CPU周期花费在内部哈希表查找和可能的自旋锁等待上。所以不要等到端口耗尽报错再开始增加端口范围,一开始应该保持一个相对充足的值。#vi/etc/sysctl.confnet.ipv4.ip_local_port_range=500065000#sysctl-p//使配置生效如果端口扩大了还是不够,可以考虑开启端口复用和回收。这样端口在连接断开时不需要等待2MSL时间,可以快速恢复。在启用该参数之前,您需要确保启用了tcp_timestamps。#vi/etc/sysctl.confnet.ipv4.tcp_timestamps=1net.ipv4.tcp_tw_reuse=1net.ipv4.tw_recycle=1#sysctl-p建议12:注意连接队列溢出服务器使用两个连接队列来响应来自的请求客户端握手请求。这两个队列的长度是服务器监听的时候决定的。如果发生溢出,则可能会丢失数据包。所以如果你的业务使用的是短连接,而且流量比较大,一定要学会观察这两个队列是否溢出。因为一旦出现连接队列导致的握手问题,TCP连接就需要一秒多的时间。对于半连接队列,有一个简单的解决方案。即只要tcp_syncookies的内核参数为1,就可以保证不会因为半连接队列满而丢包。对于全连接队列,可以通过netstat-s观察。netstat-s可以查看当前系统连接队列满导致的丢包统计。但是这个数字记录的是丢包总数,所以需要用watch命令动态监控。#watch'netstat-s|grepoverflowed'160timesthelistenqueueofasocketoverflowed//全连接队列满导致的丢包如果你在监控过程中输出的数字有变化,说明当前服务器有全连接导致的丢包队列满的。您需要增加完整连接队列的长度。全连接队列是应用调用listen时传入的backlog和内核参数net.core.somaxconn中较小的一个。如果需要增加,则可能需要更改两个参数。如果你手边没有服务器的权限,但是发现你的客户端连接某个服务器的时间比较长,你要定位是不是因为握手队列的问题。还有一种间接的方法,可以用tcpdump抓包看有没有SYNTCPRetransmission。如果偶尔出现TCPRetransmissions,说明对应的服务器连接队列可能有问题。建议13:减少握手重试在6.5节中,我们看到如果握手异常,客户端或服务端会启动超时重传机制。这次超时重试的时间间隔加倍,1秒、3秒、7秒、15秒、31秒、63秒……。对于我们提供给用户直接访问的接口,第一次重试需要1秒以上,严重影响用户体验。如果第三次之后重试,很有可能是某个链接报错返回了504。所以在这种应用场景下,维护那么多超时没有任何意义。最好将它们设置得更小并尽快放弃。client的syn重传次数由tcp_syn_retries控制,server的半连接队列超时次数由tcp_synack_retries控制。将它们都调整为您想要的值。建议十四:如果请求频繁,请放弃短连接,改用长连接。如果你的服务器经常请求某个服务器,比如redis缓存。与建议1相比,更好的方法是使用持久连接。这样做的好处是1)节省握手开销。短连接中的每个请求都需要在服务和缓存之间进行一次握手,这样用户每次都要等待额外的握手时间开销。2)避免队列满的问题。前面我们看到,当全连接或半连接队列溢出时,服务器直接丢包。客户端并不知道,所以它等了3秒再重试。请注意,tcp本身并不是专门为Internet服务设计的。这个3秒的超时对于上网用户的体验来说是致命的。3)端口数量不易出错。在结束连接时,当连接被释放时,客户端使用的端口需要进入TIME_WAIT状态,等待2MSL释放。所以如果连接频繁的话,端口数很容易不够用。而如果固定长连接的话,几十个、几百个端口就够了。建议15:TIME_WAIT的优化如果很多在线服务使用短连接,TIME_WAIT就会很多。首先,我想说的是,看到20000到30000个TIME_WAIT不用惊慌。从内存的角度来看,一个处于TIME_WAIT状态的连接只需要0.5KB的内存。从端口占用的角度来看,确实是消耗了一个端口。但如果下次连接到不同的Server,该端口仍然可以使用。如果所有TIME_WAIT都集中在与一台服务器的连接上,这只是一个问题。那么如何解决呢?其实方法有很多。第一种方法是按照上面的建议启用端口重用和回收。第二种方式是限制TIME_WAIT状态下的最大连接数。#vi/etc/sysctl.confnet.ipv4.tcp_max_tw_buckets=32768#sysctl-p如果更彻底,可以简单的把频繁的短连接换成长连接。连接频率大大降低后,自然不会出现TIME_WAIT问题。