在互联网后端平常开发接口的时候中,无论你使用的是C、Java、PHP仍是Golang,都避免不了须要调用mysql、redis等组件来获取数据,可能还须要执行一些rpc远程调用,或者再调用一些其它restful api。 在这些调用的底层,基本都是在使用TCP协议进行传输。这是由于在传输层协议中,TCP协议具有可靠的链接,错误重传,拥塞控制等优势,因此目前应用比UDP更普遍一些。
相信你也必定听闻过TCP也存在一些缺点,那就是老生常谈的开销要略大。可是各路技术博客里都在单单说开销大、或者开销小,而少见不给出具体的量化分析。不客气一点,这都是养分不大的废话。通过平常工做的思考以后,我更想弄明白的是,开销到底多大。一条TCP链接的创建须要耗时延迟多少,是多少毫秒,仍是多少微秒?能不能有一个哪怕是粗略的量化估计?固然影响TCP耗时的因素有不少,好比网络丢包等等。我今天只分享我在工做实践中遇到的比较高发的各类状况。php
要想搞清楚TCP链接的创建耗时,咱们须要详细了解链接的创建过程。在前文《图解Linux网络包接收过程》中咱们介绍了数据包在接收端是怎么被接收的。数据包从发送方出来,通过网络到达接收方的网卡。在接收方网卡将数据包DMA到RingBuffer后,内核通过硬中断、软中断等机制来处理(若是发送的是用户数据的话,最后会发送到socket的接收队列中,并唤醒用户进程)。mysql
在软中断中,当一个包被内核从RingBuffer中摘下来的时候,在内核中是用struct sk_buff
结构体来表示的(参见内核代码include/linux/skbuff.h
)。其中的data成员是接收到的数据,在协议栈逐层被处理的时候,经过修改指针指向data的不一样位置,来找到每一层协议关心的数据。linux
对于TCP协议包来讲,它的Header中有一个重要的字段-flags。以下图:redis
经过设置不一样的标记为,将TCP包分红SYNC、FIN、ACK、RST等类型。客户端经过connect系统调用命令内核发出SYNC、ACK等包来实现和服务器TCP链接的创建。在服务器端,可能会接收许许多多的链接请求,内核还须要借助一些辅助数据结构-半链接队列和全链接队列。咱们来看一下整个链接过程:sql
在这个链接过程当中,咱们来简单分析一下每一步的耗时后端
以上几步操做,能够简单划分为两类:api
1ms就等于1000us,所以网络传输耗时比双端的CPU开销要高1000倍左右,甚至更高可能还到100000倍。因此,在正常的TCP链接的创建过程当中,通常能够考虑网络延时便可。一个RTT指的是包从一台服务器到另一台服务器的一个来回的延迟时间。因此从全局来看,TCP链接创建的网络耗时大约须要三次传输,再加上少量的双方CPU开销,总共大约比1.5倍RTT大一点点。不过从客户端视角来看,只要ACK包发出了,内核就认为链接是创建成功了。因此若是在客户端打点统计TCP链接创建耗时的话,只须要两次传输耗时-既1个RTT多一点的时间。(对于服务器端视角来看同理,从SYN包收到开始算,到收到ACK,中间也是一次RTT耗时)缓存
上一节能够看到在客户端视角,,在正常状况下一次TCP链接总的耗时也就就大约是一次网络RTT的耗时。若是全部的事情都这么简单,我想个人此次分享也就没有必要了。事情不必定老是这么美好,总会有意外发生。在某些状况下,可能会致使链接时的网络传输耗时上涨、CPU处理开销增长、甚至是链接失败。如今咱们说一下我在线上遇到过的各类沟沟坎坎。服务器
1)客户端connect系统调用耗时失控restful
正常一个系统调用的耗时也就是几个us(微秒)左右。可是在《追踪将服务器CPU耗光的凶手!》一文中笔者的一台服务器当时遇到一个情况,某次运维同窗转达过来讲该服务CPU不够用了,须要扩容。当时的服务器监控以下图:
该服务以前一直每秒抗2000左右的qps,CPU的idel一直有70%+。怎么忽然就CPU一下就不够用了呢。并且更奇怪的是CPU被打到谷底的那一段时间,负载却并不高(服务器为4核机器,负载3-4是比较正常的)。 后来通过排查之后发现当TCP客户端TIME_WAIT有30000左右,致使可用端口不是特别充足的时候,connect系统调用的CPU开销直接上涨了100多倍,每次耗时达到了2500us(微秒),达到了毫秒级别。
当遇到这种问题的时候,虽然TCP链接创建耗时只增长了2ms左右,总体TCP链接耗时看起来还可接受。可是这里的问题在于这2ms多都是在消耗CPU的周期,因此问题不小。
解决起来也很是简单,办法不少:修改内核参数net.ipv4.ip_local_port_range多预留一些端口号、改用长链接均可以。
2)半/全链接队列满
若是链接创建的过程当中,任意一个队列满了,那么客户端发送过来的syn或者ack就会被丢弃。客户端等待很长一段时间无果后,而后会发出TCP Retransmission重传。拿半链接队列举例:
要知道的是上面TCP握手超时重传的时间是秒级别的。也就是说一旦server端的链接队列致使链接创建不成功,那么光创建链接就至少须要秒级以上。而正常的在同机房的状况下只是不到1毫秒的事情,整整高了1000倍左右。尤为是对于给用户提供实时服务的程序来讲,用户体验将会受到较大影响。若是连重传也没有握手成功的话,极可能等不及二次重试,这个用户访问直接就超时了。
还有另一个更坏的状况是,它还有可能会影响其它的用户。假如你使用的是进程/线程池这种模型提供服务,好比php-fpm。咱们知道fpm进程是阻塞的,当它响应一个用户请求的时候,该进程是没有办法再响应其它请求的。假如你开了100个进程/线程,而某一段时间内有50个进程/线程卡在和redis或者mysql服务器的握手链接上了(注意:这个时候你的服务器是TCP链接的客户端一方)。这一段时间内至关于你能够用的正常工做的进程/线程只有50个了。而这个50个worker可能根本处理不过来,这时候你的服务可能就会产生拥堵。再持续稍微时间长一点的话,可能就产生雪崩了,整个服务都有可能会受影响。
既而后果有可能这么严重,那么咱们如何查看咱们手头的服务是否有由于半/全链接队列满的状况发生呢?在客户端,能够抓包查看是否有SYN的TCP Retransmission。若是有偶发的TCP Retransmission,那就说明对应的服务端链接队列可能有问题了。
在服务端的话,查看起来就更方便一些了。netstat -s
可查看到当前系统半链接队列满致使的丢包统计,但该数字记录的是总丢包数。你须要再借助watch
命令动态监控。若是下面的数字在你监控的过程当中变了,那说明当前服务器有由于半链接队列满而产生的丢包。你可能须要加大你的半链接队列的长度了。
$ watch 'netstat -s | grep LISTEN' 8 SYNs to LISTEN sockets ignored
对于全链接队列来讲呢,查看方法也相似。
$ watch 'netstat -s | grep overflowed' 160 times the listen queue of a socket overflowed
若是你的服务由于队列满产生丢包,其中一个作法就是加大半/全链接队列的长度。 半链接队列长度Linux内核中,主要受tcp_max_syn_backlog影响 加大它到一个合适的值就能够。
# cat /proc/sys/net/ipv4/tcp_max_syn_backlog 1024 # echo "2048" > /proc/sys/net/ipv4/tcp_max_syn_backlog
全链接队列长度是应用程序调用listen时传入的backlog以及内核参数net.core.somaxconn两者之中较小的那个。你可能须要同时调整你的应用程序和该内核参数。
# cat /proc/sys/net/core/somaxconn 128 # echo "256" > /proc/sys/net/core/somaxconn
改完以后咱们能够经过ss命令输出的Send-Q
确认最终生效长度:
$ ss -nlt Recv-Q Send-Q Local Address:Port Address:Port 0 128 *:80 *:*
Recv-Q
告诉了咱们当前该进程的全链接队列使用长度状况。若是Recv-Q
已经逼近了Send-Q
,那么可能不须要等到丢包也应该准备加大你的全链接队列了。
若是加大队列后仍然有很是偶发的队列溢出的话,咱们能够暂且容忍。若是仍然有较长时间处理不过来怎么办?另一个作法就是直接报错,不要让客户端超时等待。例如将Redis、Mysql等后端接口的内核参数tcp_abort_on_overflow为1。若是队列满了,直接发reset给client。告诉后端进程/线程不要痴情地傻等。这时候client会收到错误“connection reset by peer”。牺牲一个用户的访问请求,要比把整个站都搞崩了仍是要强的。
我写了一段很是简单的代码,用来在客户端统计每建立一个TCP链接须要消耗多长时间。
<?php $ip = {服务器ip}; $port = {服务器端口}; $count = 50000; function buildConnect($ip,$port,$num){ for($i=0;$i<$num;$i++){ $socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP); if($socket ==false) { echo "$ip $port socket_create() 失败的缘由是:".socket_strerror(socket_last_error($socket))."\n"; sleep(5); continue; } if(false == socket_connect($socket, $ip, $port)){ echo "$ip $port socket_connect() 失败的缘由是:".socket_strerror(socket_last_error($socket))."\n"; sleep(5); continue; } socket_close($socket); } } $t1 = microtime(true); buildConnect($ip, $port, $count); echo (($t2-$t1)*1000).'ms';
在测试以前,咱们须要本机linux可用的端口数充足,若是不够50000个,最好调整充足。
# echo "5000 65000" /proc/sys/net/ipv4/ip_local_port_range
1)正常状况
注意:不管是客户端仍是服务器端都不要选择有线上服务在跑的机器,不然你的测试可能会影响正经常使用户访问
首先个人客户端位于河北怀来的IDC机房内,服务器选择的是公司广东机房的某台机器。执行ping命令获得的延迟大约是37ms,使用上述脚本创建50000次链接后,获得的链接平均耗时也是37ms。这是由于前面咱们说过的,对于客户端来看,第三次的握手只要包发送出去,就认为是握手成功了,因此只须要一次RTT、两次传输耗时。虽然这中间还会有客户端和服务端的系统调用开销、软中断开销,但因为它们的开销正常状况下只有几个us(微秒),因此对总的链接创建延时影响不大。
接下来我换了一台目标服务器,该服务器所在机房位于北京。离怀来有一些距离,可是和广东比起来可要近多了。这一次ping出来的RTT是1.6~1.7ms左右,在客户端统计创建50000次链接后算出每条链接耗时是1.64ms。
再作一次实验,此次选中实验的服务器和客户端直接位于同一个机房内,ping延迟在0.2ms~0.3ms左右。跑了以上脚本之后,实验结果是50000 TCP链接总共消耗了11605ms,平均每次须要0.23ms。
线上架构提示:这里看到同机房延迟只有零点几ms,可是跨个距离不远的机房,光TCP握手耗时就涨了4倍。若是再要是跨地区到广东,那就是百倍的耗时差距了。线上部署时,理想的方案是将本身服务依赖的各类mysql、redis等服务和本身部署在同一个地区、同一个机房(再变态一点,甚至能够是甚至是同一个机架)。由于这样包括TCP连接创建啥的各类网络包传输都要快不少。要尽量避免长途跨地区机房的调用状况出现。
2)链接队列溢出
测试完了跨地区、跨机房和跨机器。此次为了快,直接和本机创建链接结果会咋样呢?
Ping本机ip或127.0.0.1的延迟大概是0.02ms,本机ip比其它机器RTT确定要短。我以为确定链接会很是快,嗯实验一下。连续创建5W TCP链接,总时间消耗27154ms,平均每次须要0.54ms左右。嗯!?怎么比跨机器还长不少?
有了前面的理论基础,咱们应该想到了,因为本机RTT过短,因此瞬间链接创建请求量很大,就会致使全链接队列或者半链接队列被打满的状况。一旦发生队列满,当时撞上的那个链接请求就得须要3秒+的链接创建延时。因此上面的实验结果中,平均耗时看起来比RTT高不少。
在实验的过程当中,我使用tcpdump抓包看到了下面的一幕。原来有少部分握手耗时3s+,缘由是半链接队列满了致使客户端等待超时后进行了SYN的重传。
咱们又从新改为每500个链接,sleep 1秒。嗯好,终于没有卡的了(或者也能够加大链接队列长度)。结论是本机50000次TCP链接在客户端统计总耗时102399 ms,减去sleep的100秒后,平均每一个TCP链接消耗0.048ms。比ping延迟略高一些。这是由于当RTT变的足够小的时候,内核CPU耗时开销就会显现出来了,另外TCP链接要比ping的icmp协议更复杂一些,因此比ping延迟略高0.02ms左右比较正常。
TCP链接创建异常状况下,可能须要好几秒,一个坏处就是会影响用户体验,甚至致使当前用户访问超时都有可能。另一个坏处是可能会诱发雪崩。因此当你的服务器使用短链接的方式访问数据的时候,必定要学会要监控你的服务器的链接创建是否有异常状态发生。若是有,学会优化掉它。固然你也能够采用本机内存缓存,或者使用链接池来保持长链接,经过这两种方式直接避免掉TCP握手挥手的各类开销也能够。
再说正常状况下,TCP创建的延时大约就是两台机器之间的一个RTT耗时,这是避免不了的。可是你能够控制两台机器之间的物理距离来下降这个RTT,好比把你要访问的redis尽量地部署的离后端接口机器近一点,这样RTT也能从几十ms削减到最低可能零点几ms。
最后咱们再思考一下,若是咱们把服务器部署在北京,给纽约的用户访问可行吗?
前面的咱们同机房也好,跨机房也好,电信号传输的耗时基本能够忽略(由于物理距离很近),网络延迟基本上是转发设备占用的耗时。可是若是是跨越了半个地球的话,电信号的传输耗时咱们可得算一算了。
北京到纽约的球面距离大概是15000千米,那么抛开设备转发延迟,仅仅光速传播一个来回(RTT是Rround trip time,要跑两次),须要时间 = 15,000,000 *2 / 光速 = 100ms。实际的延迟可能比这个还要大一些,通常都得200ms以上。创建在这个延迟上,要想提供用户能访问的秒级服务就很困难了。因此对于海外用户,最好都要在当地建机房或者购买海外的服务器。
相关阅读:
个人公众号是「开发内功修炼」,在这里我不是单纯介绍技术理论,也不仅介绍实践经验。而是把理论与实践结合起来,用实践加深对理论的理解、用理论提升你的技术实践能力。欢迎你来关注个人公众号,也请分享给你的好友~~~