问题描述场景:JAVA客户端与服务端使用socket通信。服务器使用NIO。1.间歇性的,client与server建立连接,三次握手已经完成,但是server的selector没有响应连接。2.出现问题的时候,会有很多个连接同时出现这个问题。3.选择器不销毁重建,一直只用一个。4.程序刚启动的时候会有一些,然后断断续续的出现。问题分析正常的TCP连接建立三次握手过程:第1步:客户端向服务器发送syn,发起握手;第二步:服务端收到syn后回复syn+ack给客户端;第三步:客户端收到syn+ack后,向服务端回复一个ack,表示已经收到服务端的syn+ack(此时客户端56911端口的连接已经建立)。从问题的描述来看,有点像TCP连接建立时fullconnectionqueue(acceptqueue,后面会讲到)满了,特别是症状2和4,为了证明这个原因,马上pass网络统计-s|egrep"listen"查看队列的溢出统计:看了好几遍,发现溢出的一直在增加,可见服务器上的全连接队列肯定溢出了。然后查看溢出后OS是如何处理溢出的:tcp_abort_on_overflow为0,表示如果在三次握手的第三步全连接队列满了,那么服务器就丢弃客户端发送的ack(服务器认为连接未建立)证明客户端应用代码异常与连接队列满有关。我先把tcp_abort_on_overflow改成1。1表示如果第三步fullconnectionqueue满了,server向client发送resetpacket,说明握手过程和这个Connection(这个connection还没有在server上建立边)。然后测试,这时候可以在客户端异常中看到很多connectionresetbypeererrors,证明客户端错误就是这个原因造成的(逻辑严密,问题重点很快证明).于是开发同学看了java源码,发现socket默认的backlog(这个值控制全连接队列的大小,后面会详细介绍)是50,于是改了大小再跑.经过12个多小时的压力测试,这个错误失败了一次。没有出现,观察到溢出不再增加。至此,问题解决。简单来说就是TCP三次握手后有一个accept队列。只有进入这个队列,才能从Listen变为accept。默认的backlog值为50,很容易被填满。满后,服务器忽略第三步握手时客户端发送的ack包(一段时间后,服务器重新向客户端发送第二步握手的syn+ack包),如果连接还没有排队,就会异常。但是,我们不能仅仅满足于问题的解决,而是要回顾解决的过程。过程中涉及到哪些知识点是我欠缺或不太了解的;除了上面的异常信息,还有没有更清楚这个问题的?查看和确认问题的本地指示。深入理解TCP握手过程中建立连接的过程和队列(图片来源:http://www.cnxct.com/something-about-phpfpm-s-backlog/)如上图所示,有这里有两个队列:syns队列(半连接队列);接受队列(全连接队列)。在三次握手中,服务器在第一步收到客户端的syn后,将连接信息放入半连接队列,同时回复syn+ack给客户端(第二步);第三步,服务端收到客户端的ack,如果此时全连接队列还没有满,则从半连接队列中取出连接信息放入全连接队列,否则按指令执行tcp_abort_on_overflow。此时,如果全连接队列已满,tcp_abort_on_overflow为0,服务器会在一段时间后再次向客户端发送syn+ack(即再次握手的第二步)。如果客户端等待时间很短,客户端很容易出现异常。.在我们的os中,默认的第二步重试次数是2次(centos默认是5次):如果TCP连接队列溢出,可以看到哪些指标?上面的解决过程有点绕,听上去比较费解,那么下次出现类似问题的时候,用什么方法可以最快最清楚的确认这个问题呢?(通过具体的、感性的事物加强我们对知识点的理解和吸收。)netstat-s,比如上面看到的667399次,表示全连接队列溢出的次数,每隔几秒执行一次。如果这个数字不断增加,那么全连接队列肯定偶尔会满。在ss命令中看到的第二列Send-Q值为50,表示第三列监听端口的全连接队列最大数为50,第一列Recv-Q为当前使用情况完整的连接队列。全连接队列的大小取决于:min(backlog,somaxconn)。backlog是socket创建时传入的,somaxconn是os级的系统参数。这时候我们就可以和我们的代码建立连接了。比如Java在创建ServerSocket的时候,会让你传入backlog的值:(来自JDK帮助文档:https://docs.oracle.com/javase/7/docs/api/java/net/ServerSocket.html)半连接队列的大小取决于:max(64,/proc/sys/net/ipv4/tcp_max_syn_backlog),不同版本的os会有一些差异。我们写代码的时候根本没想过这个backlog或者大部分时候没有给他赋值(那时候默认是50),直接无视他。首先,这是一个知识盲点;第二,可能你哪天在哪篇文章里看到了这个参数,当时有点印象,但过一会就忘记了。这是因为知识之间没有联系,不系统。但是如果你像我一样先经历这个问题的痛苦,然后在压力和痛苦下驱使自己去寻找原因,同时能够从代码层到OS层去推理和理解为什么,那么你更好地掌握这个知识点,也会成为你的知识体系在TCP或性能方面成长和自我成长的有力起点。netstat命令和ss命令一样,也可以看到Send-Q和Recv-Q的状态信息。但是,如果连接不是Listen状态,Recv-Q表示接收到的数据还在缓存中,没有被进程读取。取,这个值是进程还没有读到的字节数;Send是发送队列中尚未被远程主机确认的字节数。netstat-tn看到的Recv-Q与全连接和半连接无关。这里特意提一下,因为容易和ss-lnt的Recv-Q混淆。顺便建立知识体系,巩固相关知识点。比如下面的netstat-t显示Recv-Q有大量的数据堆积,一般是CPU处理不来造成的:以上是了解fullconnectionqueue(一种手段工程效率)通过一些特定的工具和指标。实践验证以上理解。把java中的backlog改成10(越小越容易溢出),继续顶着压力跑。这时候客户端又开始报异常了,然后在服务端通过ss命令观察:按照之前的理解,这个时候可以看到3306端口的最大服务全连接队列是10,但现在队列中有11个等待进入队列。一定有一个连接不能进入队列,必须溢出。同时我们也确实可以看到overflow的值在不断增加。Tomcat和Nginx中Accept队列参数默认为Tomcat的短连接,backlog(Tomcat中的术语是Accept计数)阿里-tomcat默认为200,ApacheTomcat默认为100,Nginx默认为511,因为Nginx是多线程的process模式,所以看到的是多个8085,也就是多个进程监听同一个端口,尽可能避免上下文切换,提高性能被忽视,但是很关键,特别是对于一些短连接的应用(比如Nginx,PHP,的当然,他们也支持长期连接)更容易爆发。一旦溢出,从cpu和线程状态看是正常的,但是压力上不去。从client的角度来看,rt也比较高(rt=network+queuing+realservicetime),但是从serverlog中记录的realservicetime看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【本文为《阿里巴巴官方技术》专栏作者原创稿件,转载请联系原作者】点此查看该作者更多好文
