最近碰到一个问题,Client 端链接服务器老是抛异常。在反复定位分析、并查阅各类资料搞懂后,我发现并无文章能把这两个队列以及怎么观察他们的指标说清楚。html
场景:Java 的 Client 和 Server,使用 Socket 通讯。Server 使用 NIO。java
问题:nginx
正常 TCP 建链接三次握手过程,分为以下三个步骤:centos
从问题的描述来看,有点像 TCP 建链接的时候全链接队列(Accept 队列,后面具体讲)满了。api
尤为是症状 二、4 为了证实是这个缘由,立刻经过 netstat -s | egrep "listen"
去看队列的溢出统计数据:缓存
667399 times the listen queue of a socket overflowed
反复看了几回以后发现这个overflowed 一直在增长,那么能够明确的是server上全链接队列必定溢出了。tomcat
接着查看溢出后,OS怎么处理:服务器
# cat /proc/sys/net/ipv4/tcp_abort_on_overflow 0
tcp_abort_on_overflow
为0表示:若是三次握手第三步的时候全链接队列满了那么server扔掉client 发过来的ack(在server端认为链接还没创建起来)微信
为了证实客户端应用代码的异常跟全链接队列满有关系,我先把tcp_abort_on_overflow
修改为 1,1表示第三步的时候若是全链接队列满了,server发送一个reset包给client,表示废掉这个握手过程和这个链接(原本在server端这个链接就还没创建起来)。网络
接着测试,这时在客户端异常中能够看到不少connection reset by peer
的错误,到此证实客户端错误是这个缘由致使的(逻辑严谨、快速证实问题的关键点所在)。
因而开发同窗翻看java 源代码发现socket 默认的backlog(这个值控制全链接队列的大小,后面再详述)是50,因而改大从新跑,通过12个小时以上的压测,这个错误一次都没出现了,同时观察到 overflowed 也再也不增长了。
到此问题解决,简单来讲TCP三次握手后有个accept队列,进到这个队列才能从Listen变成accept,默认backlog 值是50,很容易就满了。满了以后握手第三步的时候server就忽略了client发过来的ack包(隔一段时间server重发握手第二步的syn+ack包给client),若是这个链接一直排不上队就异常了。
可是不能只是知足问题的解决,而是要去复盘解决过程,中间涉及到了哪些知识点是我所缺失或者理解不到位的。
这个问题除了上面的异常信息表现出来以外,还有没有更明确地指征来查看和确认这个问题。
如上图所示,这里有两个队列:syns queue
(半链接队列);accept queue
(全链接队列)。
三次握手中,在第一步server收到client的syn后,把这个链接信息放到半链接队列中,同时回复syn+ack给client(第二步);
题外话,好比
syn floods
攻击就是针对半链接队列的,攻击方不停地建链接,可是建链接的时候只作第一步,第二步中攻击方收到server的syn+ack后故意扔掉什么也不作,致使server上这个队列满其余正常请求没法进来。
第三步的时候server收到client的ack,若是这时全链接队列没满,那么从半链接队列拿出这个链接的信息放入到全链接队列中,不然按tcp_abort_on_overflow
指示的执行。
这时若是全链接队列满了而且tcp_abort_on_overflow
是0的话,server过一段时间再次发送syn+ack给client(也就是从新走握手的第二步),若是client超时等待比较短,client就很容易异常了。
在咱们的os中retry 第二步的默认次数是2(centos默认是5次):
net.ipv4.tcp_synack_retries =2
上述解决过程有点绕,听起来懵,那么下次再出现相似问题有什么更快更明确的手段来确认这个问题呢?(经过具体的、感性的东西来强化咱们对知识点的理解和吸取。)
[root@server ~] # netstat -s | egrep "listen|LISTEN" 667399 times the listen queue of a socket overflowed 667399 SYNs to LISTEN sockets ignored
好比上面看到的 667399 times ,表示全链接队列溢出的次数,隔几秒钟执行下,若是这个数字一直在增长的话确定全链接队列偶尔满了。
[root@server ~]#ss -lnt Recv -Q Send -Q Loacl Address:Port Peer Address:Port 0 50 *:3306 *:*
上面看到的第二列Send-Q 值是50,表示第三列的listen端口上的全链接队列最大为50,第一列Recv-Q为全链接队列当前使用了多少。
全链接队列的大小取决于:min(backlog, somaxconn) 。backlog是在socket建立的时候传入的,somaxconn是一个os级别的系统参数。
这个时候能够跟咱们的代码创建联系了,好比Java建立ServerSocket的时候会让你传入backlog的值:
ServerSocket() Creates an unbound server socket. ServerSocket(int port) Creates a server socket,bound to the specified port. ServerSocket(int port, int backlog) Creates a server socket and binds it to the specified local port number, with the specified backlog. ServerSocket(int port, int backlog, InetAddress bindAddr) Creates a server with the specified port, listen backlog, and local IP address to bind to.
半链接队列的大小取决于:max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog),不一样版本的os会有些差别。
咱们写代码的时候历来没有想过这个backlog或者说大多时候就没给他值(那么默认就是50),直接忽视了他。
首先这是一个知识点的盲点;其次也许哪天你在哪篇文章中看到了这个参数,当时有点印象,可是过一阵子就忘了,这是知识之间没有创建链接,不是体系化的。
可是若是你跟我同样首先经历了这个问题的痛苦,而后在压力和痛苦的驱动本身去找为何,同时可以把为何从代码层推理理解到OS层,那么这个知识点你才算是比较好地掌握了,也会成为你的知识体系在TCP或者性能方面成长自我生长的一个有力抓手。
netstat 跟 ss 命令同样也能看到 Send-Q、Recv-Q 这些状态信息,不过若是这个链接不是 Listen 状态的话,Recv-Q 就是指收到的数据还在缓存中,还没被进程读取,这个值就是还没被进程读取的 bytes。
$netstat -tn Active Internet connections(w/o servers) Proto Recv -Q Send -Q Local Address Foreign Address State tcp0 0 server:8182 client-1:15260 SYN_RECV tcp0 28 server:22 client-1:51708 ESTABLISHED tcp0 0 server:2376 client-1:60269 ESTABLISHED
netstat -tn 看到的 Recv-Q 跟全链接半链接没有关系,这里特地拿出来讲一下是由于容易跟 ss -lnt 的 Recv-Q 搞混淆,顺便创建知识体系,巩固相关知识点 。
好比以下netstat -t 看到的Recv-Q有大量数据堆积,那么通常是CPU处理不过来致使的:
上面是经过一些具体的工具、指标来认识全链接队列(工程效率的手段)。
把java中backlog改为10(越小越容易溢出),继续跑压力,这个时候client又开始报异常了,而后在server上经过 ss 命令观察到:
Fri May 5 13:50:23 CST 2017 Recv -Q Send -Q Local Address:port Peer Address:Port 11 10 *:3306 *:*
按照前面的理解,这个时候咱们能看到3306这个端口上的服务全链接队列最大是10,可是如今有11个在队列中和等待进队列的,确定有一个链接进不去队列要overflow掉,同时也确实能看到overflow的值在不断地增大。
Tomcat默认短链接,backlog(Tomcat里面的术语是Accept count)Ali-tomcat默认是200, Apache Tomcat默认100。
#ss -lnt Recv -Q Send -Q Local Address:port Peer Address:Port 0 100 *: 8080 *:*
Nginx默认是511。
#sudo ss -lnt State Recv -Q Send -Q Local Address:Port Peer Address:Port LISTEN 0 511 *: 8085 *:* LISTEN 0 511 *: 8085 *:*
由于Nginx是多进程模式,因此看到了多个8085,也就是多个进程都监听同一个端口以尽可能避免上下文切换来提高性能。
全链接队列、半链接队列溢出这种问题很容易被忽视,可是又很关键,特别是对于一些短链接应用(好比Nginx、PHP,固然他们也是支持长链接的)更容易爆发。 一旦溢出,从cpu、线程状态看起来都比较正常,可是压力上不去,在client看来rt也比较高(rt=网络+排队+真正服务时间),可是从server日志记录的真正服务时间来看rt又很短。jdk、netty等一些框架默认backlog比较小,可能有些状况下致使性能上不去。
但愿经过本文可以帮你们理解TCP链接过程当中的半链接队列和全链接队列的概念、原理和做用,更关键的是有哪些指标能够明确看到这些问题(工程效率帮助强化对理论的理解)。
另外每一个具体问题都是最好学习的机会,光看书理解确定是不够深入的,请珍惜每一个具体问题,碰到后可以把前因后果弄清楚,每一个问题都是你对具体知识点通关的好机会。
最后提出相关问题给你们思考
来源:阿里技术微信公众号