当前位置: 首页 > 科技观察

TCP三次握手的原理你知道多少?

时间:2023-03-14 00:22:58 科技观察

最近遇到一个问题,客户端在连接服务器时总是抛出异常。经过反复定位分析,查阅各种资料了解后,发现并没有文章能够清楚地说明这两个队列,以及如何观察他们的指标。因此,我写这篇文章,希望把这个问题说清楚。欢迎大家一起交流讨论。问题描述场景:Java的Client和Server使用Socket进行通信。服务器使用NIO。问题:间歇性的,当Client与Server建立连接时三次握手已经完成,但是Server的Selector没有响应连接。在出现问题的时间点上,会有很多个连接同时出现这个问题。Selector没有被摧毁和重建,一直只使用一个。程序刚启动的时候肯定有,然后断断续续出现。分析问题是正常的。TCP连接建立的三次握手过程分为以下三个步骤:Client向Server发送Syn,发起握手。Server收到Syn后,回复Syn+Ack给Client。Client收到Syn+Ack后,回复Server一个Ack,表示收到了Server的Syn+Ack(此时Client的56911端口的连接已经建立)。从问题的描述来看,有点像TCP建立连接时fullconnectionqueue(Acceptqueue,后面会讲到)满了。尤其是症状2和4,为了证明这个原因,马上使用netstat-s|egrep"listen"查看队列的溢出统计:连接队列肯定溢出了。然后查看OS是如何处理溢出的:tcp_abort_on_overflow为0,表示如果在三次握手的第三步全连接队列已满,那么服务器会丢弃客户端发送的Ack(服务器端认为连接尚未建立)。为了证明客户端应用代码异常与连接队列满有关,我先将tcp_abort_on_overflow改为1。1表示如果第三步full连接队列已满,则Server向Client发送Reset包,表示握手过程和连接被取消(Server端还没有建立连接)。然后测试,这时候在client异常中可以看到很多connectionresetbypeererrors,证明clienterror就是这个原因导致的(逻辑严密,很快证明了问题的关键点)。于是开发者查看了Java源码,发现Socket默认的backlog(这个值控制了全连接队列的大小,后面会详细介绍)是50。于是我改了大小再跑。经过12个多小时的压测,这个错误没有出现过一次,观察到overflow也没有再增加了。至此,问题解决。简单的说就是TCP三次握手后有一个Accept队列。只有进入这个队列,才能从Listen变为Accept。默认的backlog值为50,很容易被填满。满后,服务器忽略第三步握手客户端发送的Ack包(一段时间后,服务器重新向客户端发送第二步握手的Syn+Ack包),如果连接还没有排队,就会异常。但是我不能仅仅满足于问题的解决,而是要回顾一下解决的过程,过程中涉及到哪些知识点是自己欠缺的或者理解不好的。除了上面显示的异常信息,有没有更明确的提示来检查确认这个问题。深入理解TCP握手过程中连接建立的过程和队列。如上图所示,有两个队列:SynsQueue(半连接队列);AcceptQueue(全连接队列)。在三次握手中,Server在第一步收到Client的Syn后,将连接信息放入半连接队列,同时向Client回复Syn+Ack(第2步):在第三步,Server收到Client的Ack发来的Syn,如果此时全连接队列没有满,则从半连接队列中取出连接信息放入全连接队列,否则执行as由tcp_abort_on_overflow指示。此时如果全连接队列已满,tcp_abort_on_overflow为0,Server会隔一段时间再次向Client发送Syn+Ack(即再次握手的第二步)。如果Client等待的时间很短,Client很容易出现异常。在我们的OS中,第二步Retry的默认次数是2次(Centos默认是5次):如果TCP连接队列溢出,可以看到哪些指标?上面的解决过程有点绕,听上去比较费解,那么下次出现类似问题的时候,用什么方法可以最快最清楚的确认这个问题呢?(通过具体的、感性的东西加强我们对知识点的理解和吸收。)netstat-s,比如上面看到的667399次,表示全连接队列溢出的次数。每隔几秒执行一次。如果这个数字一直在增加,那么全连接队列肯定偶尔会满。ss命令上面看到的第二列的Send-Q值是50,也就是说第三列Listen端口的全连接队列最大数是50,第一列Recv-Q是当前使用的完整的连接队列。全连接队列的大小取决于:min(backlog,somaxconn)。backlog是在创建Socket的时候传入的,Somaxconn是OS级别的系统参数。这时候我们就可以和我们的代码建立连接了。比如Java创建ServerSocket的时候,会让你传入backlog的值:半连接队列的大小取决于:max(64,/proc/sys/net/ipv4/tcp_max_syn_backlog),不同版本操作系统会有一些差异。我们在写代码的时候,从来没有想过这个backlog或者大多数时候我们没有给它赋值(那时默认是50),只是忽略了它。首先,这是一个知识盲点;其次,也许有一天你在一篇文章中看到了这个参数,当时还有些印象,但过一段时间就忘记了。这是因为知识之间没有联系,不系统。但如果你像我一样,先经历这个问题的痛苦,然后你就会被压力和痛苦驱使去找出原因。同时,如果你能理解为什么从代码层推理到OS层,那么你对这个知识点的把握就比较好,也会成为你的知识体系成长和自我提升的有力起点。在TCP或性能方面增长。netstat命令和ss命令一样,也可以看到Send-Q和Recv-Q的状态信息。但是,如果连接不是Listen状态,Recv-Q表示接收到的数据还在缓存中,没有被进程读取。取,这个值就是进程还没有读到的字节数。而Send是发送队列中未被远程主机确认的字节数,如下图:netstat-tn看到的Recv-Q与全连接和半连接无关,而这里特意拿出来是因为比较容易和ss-lntRecv-Q混淆,顺便搭建一个知识体系,巩固相关知识点。比如下面的netstat-t显示Recv-Q有大量的数据堆积,一般是CPU处理不过来造成的:以上是理解fullconnectionqueue(一种工程效率的手段)通过一些特定的工具和指标。实践验证以上理解。将Java中的backlog改成10(越小越容易溢出),继续顶着压力跑。这时候Client又开始报异常,然后在Server上通过ss命令观察:按照前面的理解,这个时候可以看到3306端口最大服务全连接队列是10.但是现在队列中有11个等待入队,肯定有一个连接进不了队列需要溢出,同时我们也确实可以看到溢出的值在不断增加。Tomcat和Nginx中的Accept队列参数对于Tomcat默认为短连接,backlog(Tomcat中的术语是Accept计数)阿里-tomcat默认为200,ApacheTomcat默认为100,Nginx默认为511,如下图:因为Nginx是多进程模式,看到多个8085,也就是多个进程监听同一个端口,尽量避免上下文切换,提高性能。综上所述,全连接队列、半连接队列溢出等问题很容易被忽略,但却很关键,尤其是对于一些短连接应用(比如Nginx、PHP,当然也支持长连接)。一旦溢出,从CPU和线程状态看是正常的,但是压力上不去,从Client的角度RT也比较高(RT=网络+排队+真实服务时间),但是从Server日志中记录的真实服务时间见rt很短。一些框架,比如JDK、Netty,默认情况下backlog比较小,在某些情况下可能会导致性能不佳。希望这篇文章能帮助大家理解TCP连接过程中半连接队列和全连接队列的概念、原理和作用,更重要的是有什么指标可以清楚的看出这些问题(工程效率有助于加强理论理解).此外,每个具体问题都是最好的学习机会。光靠看书肯定是不够深入的。请珍惜每一个具体问题。遇到了,才能搞清楚来龙去脉。每道题都是你对具体知识点的清算。好机会。最后提出相关问题供大家思考:全连接队列满了会影响半连接队列吗?netstat-s看到的overflowed和ignored的值有什么联系吗?如果客户端完成了TCP握手的第三步,客户端看到连接已经建立,但是服务器端对应的连接实际上还没有准备好。如果此时客户端向服务端发送数据,那么服务端会怎么做呢?(有同学说会重置,你怎么看?)通过提出这些问题,我希望以这个知识点为起点,让你的知识体系开始自己成长。参考文章:http://veithen.github.io/2014/01/01/how-tcp-backlog-works-in-linux.htmlhttp://www.cnblogs.com/zengkefu/p/5606696.htmlhttp://www.cnxct.com/something-about-phpfpm-s-backlog/http://jaseywang.me/2014/07/20/tcp-queue-%E7%9A%84%E4%B8%80%E4%BA%9B%E9%97%AE%E9%A2%98/http://jin-yang.github.io/blog/network-synack-queue.html#http://blog.chinaunix.net/uid-20662820-id-4154399.html