来自公众号:研发内功修炼
大众好,我是飞哥!
近期我出了一本非常受欢迎的新书──《深入理解Linux网络》。在这本书中咱们深入地讨论了非常多内核网络模块关联的问题。讨论了一个网络包是怎样从网卡到达用户进程的,聊了同步阻塞和多路复用 epoll,亦仔细讨论了数据是怎样从进程发送出去的等等一系列深度的网络工作原理。
这本书首发当日就登上了京东的科技类销量日冠军,刚上市三个星期就已然起始了第三次的加印,能够说是非常的热门。倘若你能看完这本书,你就会正和庖丁同样,从今日往后咱们看到的亦再也不是全部的 Linux (整头牛)了,而是内核的内部各个模块(筋⻣肌理)。
那样具备了对网络的深刻的理解之后,咱们在性能方面有那些优化手段可用呢?我这儿给出有些研发或运维中的性能优化意见。这些意见都是从书中摘录的。不外要重视的是,每一种性能优化办法都有它适用或不适用的应用场景。你应当按照你当前的项目状况灵活来选取用或不消。
意见1:尽可能减少不必要的网络 IO
我要给出的第1个意见便是不必要用网络 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 运用了区别程度的封装。
首要第1个要给大众参考的是 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 overflowed160 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次耗时 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 的连接上的时候才会有问题。
那怎么处理呢? 其实办法有非常多。第1个办法是按上面意见开启端口 reuse 和 recycle。 第二个办法是限制 TIME_WAIT 状态的连接的最大数量。 # vi /etc/sysctl.conf
net.ipv4.tcp_max_tw_buckets = 32768 # sysctl -p
倘若再彻底有些,亦能够干脆直接用⻓连接代替频繁的短连接。连接频率大大降低以后,自然亦就无 TIME_WAIT 的问题了。
好了,以上便是飞哥为大众准备的网络性能关联的 15 条意见。更加多网络性能关联的底层原理能够仔细阅读《深入理解Linux网络》。
--- EOF --- 举荐↓↓↓
|