乐趣区

关于linux:Linux-网络性能的-15-个优化建议

倡议 1:尽量减少不必要的网络 IO

我要给出的第一个倡议就是不必要用网络 IO 的尽量不必。

是的,网络在古代的互联网世界里承载了很重要的角色。用户通过网络申请线上服务、服务器通过网络读取数据库中数据,通过网络构建能力无比弱小分布式系统。网络很好,能升高模块的开发难度,也能用它搭建出更弱小的零碎。然而这不是你滥用它的理由!

起因是即便是本机网络 IO 开销依然是很大的。先说发送一个网络包,首先得从用户态切换到内核态,破费一次零碎调用的开销。进入到内核当前,又得通过简短的协定栈,这会破费不少的 CPU 周期,最初进入环回设施的“驱动程序”。接收端呢,软中断破费不少的 CPU 周期又得通过接管协定栈的解决,最初唤醒或者告诉用户过程来解决。当服务端解决完当前,还得把后果再发过来。又得来这么一遍,最初你的过程能力收到后果。你说麻烦不麻烦。另外还有个问题就是多个过程合作来实现一项工作就必然会引入更多的过程上下文切换开销,这些开销从开发视角来看,做的其实都是无用功。

下面咱们还剖析的只是本机网络 IO,如果是跨机器的还得会有单方网卡的 DMA 拷贝过程,以及两端之间的网络 RTT 耗时提早。所以,网络虽好,但也不能随便滥用!

倡议 2:尽量合并网络申请

在可能的状况下,尽可能地把屡次的网络申请合并到一次,这样既节约了双端的 CPU 开销,也能升高屡次 RTT 导致的耗时。

咱们举个实际中的例子可能更好了解。如果有一个 redis,外面存了每一个 App 的信息(利用名、包名、版本、截图等等)。你当初须要依据用户装置利用列表来查询数据库中有哪些利用比用户的版本更新,如果有则揭示用户更新。

那么最好不要写出如下的代码:

<?php 
for(装置列表 as 包名){redis->get( 包名)
  ...
}

下面这段代码性能上实现上没问题,问题在于性能。据咱们统计古代用户均匀装置 App 的数量在 60 个左右。那这段代码在运行的时候,每当用户来申请一次,你的服务器就须要和 redis 进行 60 次网络申请。总耗时起码是 60 个 RTT 起。更好的办法是应该应用 redis 中提供的批量获取命令,如 hmget、pipeline 等,通过一次网络 IO 就获取到所有想要的数据,如图。

倡议 3:调用者与被调用机器尽可能部署的近一些

在后面的章节中咱们看到在握手一切正常的状况下,TCP 握手的工夫根本取决于两台机器之间的 RTT 耗时。尽管咱们没方法彻底去掉这个耗时,然而咱们却有方法把 RTT 升高,那就是把客户端和服务器放得足够的近一些。尽量把每个机房外部的数据申请都在本地机房解决,缩小跨地网络传输。

举例,如果你的服务是部署在北京机房的,你调用的 mysql、redis 最好都位于北京机房外部。尽量不要跨过千里万里跑到广东机房去申请数据,即便你有专线,耗时也会大大增加!在机房外部的服务器之间的 RTT 提早大略只有零点几毫秒,同地区的不同机房之间大概是 1 ms 多一些。但如果从北京跨到广东的话,提早将是 30 – 40 ms 左右,几十倍的上涨!

倡议 4:内网调用不要用外网域名

如果说你所在负责的服务须要调用兄弟部门的一个搜寻接口,假如接口是:”http://www.sogou.com/wq?key= 开发内功修炼 ”。

那既然是兄弟部门,那很可能这个接口和你的服务是部署在一个机房的。即便没有部署在一个机房,个别也是有专线可达的。 所以不要间接申请 www.sogou.com,而是应该应用该服务在公司对应的内网域名 。在咱们公司外部,每一个外网服务都会配置一个对应的内网域名,我置信你们公司也有。

为什么要这么做,起因有以下几点

1) 外网接口慢 。原本内网可能过个交换机就能达到兄弟部门的机器,非得上外网兜一圈再回来,工夫上必定会慢。

2) 带宽老本高 。在互联网服务里,除了机器以外,另外一块很大的老本就是 IDC 机房的出入口带宽老本。两台机器在内网不论如何通信都不波及到带宽的计算。然而一旦你去外网兜了一圈回来,行了,一进一出全副要缴带宽费,你说亏不亏!!

3)NAT 单点瓶颈 。个别的服务器都没有外网 IP,所以要想申请外网的资源,必须要通过 NAT 服务器。然而一个公司的机房里几千台服务器中,承当 NAT 角色的可能就那么几台。它很容易成为瓶颈。咱们的业务就遇到过好几次 NAT 故障导致外网申请失败的情景。NAT 机器挂了,你的服务可能也就挂了,故障率大大增加。

倡议 5:调整网卡 RingBuffer 大小

在 Linux 的整个网络栈中,RingBuffer 起到一个工作的收发中转站的角色。对于接管过程来讲,网卡负责往 RingBuffer 中写入收到的数据帧,ksoftirqd 内核线程负责从中取走解决。只有 ksoftirqd 线程工作的足够快,RingBuffer 这个中转站就不会呈现问题。

然而咱们构想一下,如果某一时刻,霎时来了特地多的包,而 ksoftirqd 解决不过去了,会产生什么?这时 RingBuffer 可能霎时就被填满了,前面再来的包网卡间接就会抛弃,不做任何解决!

通过 ethtool 就能够加大 RingBuffer 这个“直达仓库”的大小。。

# ethtool -G eth1 rx 4096 tx 4096

这样网卡会被调配更大一点的”中转站“,能够解决偶发的刹时的丢包。不过这种办法有个小副作用,那就是排队的包过多会减少解决网络包的延时。所以应该让内核解决网络包的速度更快一些更好,而不是让网络包傻傻地在 RingBuffer 中排队。咱们前面会再介绍到 RSS,它能够让更多的核来参加网络包接管。

倡议 6:缩小内存拷贝

如果你要发送一个文件给另外一台机器上,那么比拟根底的做法是先调用 read 把文件读出来,再调用 send 把数据把数据收回去。这样数据须要频繁地在内核态内存和用户态内存之间拷贝,如图 9.6。

目前缩小内存拷贝次要有两种办法,别离是应用 mmap 和 sendfile 两个零碎调用。应用 mmap 零碎调用的话,映射进来的这段地址空间的内存在用户态和内核态都是能够应用的。如果你发送数据是发的是 mmap 映射进来的数据,则内核间接就能够从地址空间中读取,这样就节约了一次从内核态到用户态的拷贝过程。

不过在 mmap 发送文件的形式里,零碎调用的开销并没有缩小,还是产生两次内核态和用户态的上下文切换。如果你只是想把一个文件发送进来,而不关怀它的内容,则能够调用另外一个做的更极致的零碎调用 – sendfile。在这个零碎调用里,彻底把读文件和发送文件给合并起来了,零碎调用的开销又省了一次。再配合绝大多数网卡都反对的 ” 扩散 - 收集 ”(Scatter-gather)DMA 性能。能够间接从 PageCache 缓存区中 DMA 拷贝到网卡中。这样绝大部分的 CPU 拷贝操作就都省去了。

倡议 7:应用 eBPF 绕开协定栈的本机 IO

如果你的业务中波及到大量的本机网络 IO 能够思考这个优化计划。本机网络 IO 和跨机 IO 比拟起来,的确是节约了驱动上的一些开销。发送数据不须要进 RingBuffer 的驱动队列,间接把 skb 传给接管协定栈 (通过软中断)。然而在内核其它组件上,可是一点都没少,零碎调用、协定栈 (传输层、网络层等)、设施子系统整个走 了一个遍。连“驱动”程序都走了 (尽管对于回环设施来说这个驱动只是一个纯软件的虚构进去的东东)。

如果想用本机网络 IO,然而又不想频繁地在协定栈中绕来绕去。那么你能够试试 eBPF。应用 eBPF 的 sockmap 和 sk redirect 能够绕过 TCP/IP 协定栈,而被间接发送给接收端的 socket,业界曾经有公司在这么做了。

倡议 8:尽量少用 recvfrom 等过程阻塞的形式

在应用了 recvfrom 阻塞形式来接管 socket 上数据的时候。每次一个过程专⻔为了等一个 socket 上的数据就得被从 CPU 上拿下来。而后再换上另一个 过程。等到数据 ready 了,睡眠的过程又会被唤醒。总共两次过程上下文切换开销。如果咱们服务器上须要有大量的用户申请须要解决,那就须要有很多的过程存在,而且不停地切换来切换去。这样的毛病有如下这么几个:

  • 因为每个过程只能同时期待一条连贯,所以须要大量的过程。
  • 过程之间相互切换的时候须要耗费很多 CPU 周期,一次切换大概是 3 – 5 us 左右。
  • 频繁的切换导致 L1、L2、L3 等高速缓存的成果大打折扣

大家可能认为这种网络 IO 模型很少见了。但其实在很多传统的客户端 SDK 中,比方 mysql、redis 和 kafka 依然是沿用了这种形式。

倡议 9:应用成熟的网络库

应用 epoll 能够高效地治理海量的 socket。在服务器端。咱们有各种成熟的网络库进行应用。这些网络库都对 epoll 应用了不同水平的封装。

首先第一个要给大家参考的是 Redis。老版本的 Redis 里单过程高效地应用 epoll 就能反对每秒数万 QPS 的高性能。如果你的服务是单过程的,能够参考 Redis 在网络 IO 这块的源码。

如果是多线程的,线程之间的分工有很多种模式。那么哪个线程负责期待读 IO 事件,哪个线程负责解决用户申请,哪个线程又负责给用户写返回。依据分工的不同,又衍生出单 Reactor、多 Reactor、以及 Proactor 等多种模式。大家也不用头疼,只有了解了这些原理之后抉择一个性能不错的网络库就能够了。比方 PHP 中的 Swoole、Golang 的 net 包、Java 中的 netty、C++ 中的 Sogou Workflow 都封装的十分的不错。

倡议 10:应用 Kernel-ByPass 新技术

如果你的服务对网络要求的确特地特特地的高,而且各种优化措施也都用过了,那么当初还有终极优化大招 — Kernel-ByPass 技术。

内核在接管网络包的时候要通过很⻓的收发门路。在这期间牵涉到很多内核组件之间的协同、协定栈的解决、以及内核态和用户态的拷贝和切换。Kernel-ByPass 这类的技术计划就是绕开内核协定栈,本人在用户态来实现网络包的收发。这样岂但避开了繁冗的内核协定栈解决,也缩小了频繁了内核态用户态之间的拷贝和切换,性能将施展到极致!

目前我所晓得的计划有 SOLARFLARE 的软硬件计划、DPDK 等等。如果大家感兴趣,能够多去理解一下!

倡议 11:配置短缺的端口范畴

客户端在调用 connect 零碎调用发动连贯的时候,须要先抉择一个可用的端口。内核在选用端口的时候,是采纳从可用端口范畴中某一个随机地位开始遍历的形式。如果端口不短缺的话,内核可能须要循环撞很屡次能力选上一个可用的。这也会导致破费更多的 CPU 周期在外部的哈希表查找以及可能的自旋锁期待上。因而不要等到端口用尽报错了才开始加大端口范畴,而且应该一开始的时候就放弃一个比拟短缺的值。

# vi /etc/sysctl.conf
net.ipv4.ip_local_port_range = 5000 65000
# sysctl -p  // 使配置失效 

如果端口加大了依然不够用,那么能够思考开启端口 reuse 和 recycle。这样端口在连贯断开的时候就不须要期待 2MSL 的工夫了,能够疾速回收。开启这个参数之前须要保障 tcp_timestamps 是开启的。

# vi /etc/sysctl.conf
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tw_recycle = 1
# sysctl -p

倡议 12:小心连贯队列溢出

服务器端应用了两个连贯队列来响应来自客户端的握手申请。这两个队列的长度是在服务器 listen 的时候就确定好了的。如果产生溢出,很可能会丢包。所以如果你的业务应用的是短连贯且流量比拟大,那么肯定得学会察看这两个队列是否存在溢出的状况。因为一旦呈现因为连贯队列导致的握手问题,那么 TCP 连贯耗时都是秒级以上了。

对于半连贯队列,有个简略的方法。那就是只有保障 tcp_syncookies 这个内核参数是 1 就能保障不会有因为半连贯队列满而产生的丢包。

对于全连贯队列来说,能够通过 netstat -s 来察看。netstat -s 可查看到以后零碎全连贯队列满导致的丢包统计。但该数字记录的是总丢包数,所以你须要再借助 watch 命令动静监控。

# watch 'netstat -s | grep overflowed' 
160 times the listen queue of a socket overflowed // 全连贯队列满导致的丢包 

如果输入的数字在你监控的过程中变了,那阐明以后服务器有因为全连贯队列满而产生的丢包。你就须要加大你的全连贯队列的⻓度了。全连贯队列是应用程序调用 listen 时传入的 backlog 以及内核参数 net.core.somaxconn 二者之中较小的那个。如果须要加大,可能两个参数都须要改。

如果你手头并没有服务器的权限,只是发现自己的客户端机连贯某个 server 呈现耗时长,想定位一下是否是因为握手队列的问题。那也有间接的方法,能够 tcpdump 抓包查看是否有 SYN 的 TCP Retransmission。如果有偶发的 TCP Retransmission,那就阐明对应的服务端连贯队列可能有问题了。

倡议 13:缩小握手重试

在 6.5 节咱们看到如果握手产生异样,客户端或者服务端就会启动超时重传机制。这个超时重试的工夫距离是翻倍地增长的,1 秒、3 秒、7 秒、15 秒、31 秒、63 秒 ……。对于咱们提供给用户间接拜访的接口来说,重试第一次耗时 1 秒多曾经是重大影响用户体验了。如果重试到第三次当前,很有可能某一个环节曾经报错返回 504 了。所以在这种利用场景下,保护这么多的超时次数其实没有任何意义。倒不如把他们设置的小一些,尽早放弃。其中客户端的 syn 重传次数由 tcp_syn_retries 管制,服务器半连贯队列中的超时次数是由 tcp_synack_retries 来管制。把它们两个调成你想要的值。

倡议 14:如果申请频繁,请弃用短连贯改用长连贯

如果你的服务器频繁申请某个 server,比方 redis 缓存。和倡议 1 比起来,一个更好一点的办法是应用长连贯。这样的益处有

1) 节约了握手开销 。短连贯中每次申请都须要服务和缓存之间进行握手,这样每次都得让用户多等一个握手的工夫开销。

2) 躲避了队列满的问题 。后面咱们看到当全连贯或者半连贯队列溢出的时候,服务器间接丢包。而客户端呢并不知情,所以傻傻地等 3 秒才会重试。要晓得 tcp 自身并不是专门为互联网服务设计的。这个 3 秒的超时对于互联网用户的体验影响是致命的。

3) 端口数不容易出问题 。端连贯中,在开释连贯的时候,客户端应用的端口须要进入 TIME_WAIT 状态,期待 2 MSL 的工夫能力开释。所以如果连贯频繁,端口数量很容易不够用。而长连贯就固定应用那么几十上百个端口就够用了。

倡议 15:TIME_WAIT 的优化

很多线上服务如果应用了短连贯的状况下,就会呈现大量的 TIME_WAIT。

首先,我想说的是没有必要见到两三万个 TIME_WAIT 就恐慌的不行。从内存的⻆度来思考,一条 TIME_WAIT 状态的连贯仅仅是 0.5 KB 的内存而已。从端口占用的角度来说,的确是消耗掉了一个端口。但如果你下次再连贯的是不同的 Server 的话,该端口依然能够应用。只有在所有 TIME_WAIT 都汇集在和一个 Server 的连贯上的时候才会有问题。

那怎么解决呢? 其实方法有很多。第一个方法是按下面倡议开启端口 reuse 和 recycle。第二个方法是限度 TIME_WAIT 状态的连贯的最大数量。

# vi /etc/sysctl.conf
net.ipv4.tcp_max_tw_buckets = 32768
# sysctl -p

如果再彻底一些,也能够罗唆间接用⻓连贯代替频繁的短连贯。连贯频率大大降低当前,天然也就没有 TIME_WAIT 的问题了。

退出移动版