[转]TCP三次握手原理及故障排查
最近碰到一个问题,Client 端连接服务器总是抛异常。在反复定位分析、并查阅各种资料搞懂后,我发现并没有文章能把这两个队列以及怎么观察他们的指标说清楚。
因此写下这篇文章,希望借此能把这个问题说清楚。欢迎大家一起交流探讨。
1. 问题描述
场景:Java 的 Client 和 Server,使用 Socket 通信。Server 使用 NIO。
问题:
- 间歇性出现 Client 向 Server 建立连接三次握手已经完成,但 Server 的 Selector 没有响应到该连接。
- 出问题的时间点,会同时有很多连接出现这个问题。
- Selector 没有销毁重建,一直用的都是一个。
- 程序刚启动的时候必会出现一些,之后会间歇性出现。
2.
分析问题
2-1-1-1. 正常 TCP 建连接三次握手过程,分为如下三个步骤:
- Client 发送 Syn 到 Server 发起握手。
- Server 收到 Syn 后回复 Syn + Ack 给 Client。
- Client 收到 Syn + Ack后,回复 Server 一个 Ack 表示收到了 Server 的 Syn + Ack(此时 Client 的 56911 端口的连接已经是 Established)。
从问题的描述来看,有点像 TCP 建连接的时候全连接队列(Accept 队列,后面具体讲)满了。
尤其是症状 2、4 为了证明是这个原因,马上通过 netstat -s | egrep “listen” 去看队列的溢出统计数据:
1 |
667399 times the listen queue of a socket overflowed |
反复看了几次之后发现这个 overflowed 一直在增加,可以明确的是 Server 上全连接队列一定溢出了。
接着查看溢出后,OS 怎么处理:
1 2 |
# cat /proc/sys/net/ipv4/tcp_abort_on_overflow 0 |
tcp_abort_on_overflow 为 0 表示如果三次握手第三步的时候全连接队列满了那么 Server 扔掉 Client 发过来的 Ack(在 Server 端认为连接还没建立起来)。
为了证明客户端应用代码的异常跟全连接队列满有关系,我先把 tcp_abort_on_overflow 修改成 1。
tcp_abort_on_overflow 为 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),如果这个连接一直排不上队就异常了。
但是不能只是满足问题的解决,而是要去复盘解决过程,中间涉及到了哪些知识点是我所缺失或者理解不到位的。
这个问题除了上面的异常信息表现出来之外,还有没有更明确地指征来查看和确认这个问题。
2-1-1.
3. 深入理解 TCP 握手过程中建连接的流程和队列
如上图所示,这里有两个队列:Syns Queue(半连接队列);Accept Queue(全连接队列)。
三次握手中,在第一步 Server 收到 Client 的 Syn 后,把这个连接信息放到半连接队列中,同时回复 Syn + Ack 给 Client(第二步):
题外话,比如syn floods攻击就是针对半连接队列的,攻击方不停地建链接,但是建链接的时候只是做第一步,第二步中攻击方收到server的syn+ack后故意扔掉什么也不做,导致server上这个队列syns queue满其它正常请求无法进来
第三步的时候 Server 收到 Client 的 Ack,如果这时全连接队列accept queue没满,那么从半连接队列syns queue拿出这个连接的信息放入到全连接队列accept queue中,否则按 tcp_abort_on_overflow 指示的执行。
这时如果全连接队列accept queue满了并且 tcp_abort_on_overflow 是 0 的话,Server 过一段时间再次发送 Syn + Ack 给 Client(也就是重新走握手的第二步),如果 Client 超时等待比较短,Client 就很容易异常了。
在我们的 OS 中 Retry 第二步的默认次数是 2(Centos 默认是 5 次):
1 |
net.ipv4.tcp_synack_retries = 2 |
4. 如果 TCP 连接队列溢出,有哪些指标可以看呢?
上述解决过程有点绕,听起来懵,那么下次再出现类似问题有什么更快更明确的手段来确认这个问题呢?(通过具体的、感性的东西来强化我们对知识点的理解和吸收。)
1 2 3 |
[root@server ~]# netstat -s | egrep "listen|LISTEN" 667399 times the listen queue of a socket overflowed 667399 SYNC to LISTEN sockets ignored |
比如上面看到的 667399 times ,表示全连接队列溢出的次数,隔几秒钟执行下,如果这个数字一直在增加的话肯定全连接队列偶尔满了。
1 2 3 |
[root@server ~]# ss -lnt Recv-Q Send-Q Local 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 的值:
半连接队列的大小取决于: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。
而 Send 则是发送队列中没有被远程主机确认的 bytes 数,如下图:
netstat -tn 看到的 Recv-Q 跟全连接半连接没有关系,这里特意拿出来说一下是因为容易跟 ss -lnt 的 Recv-Q 搞混淆,顺便建立知识体系,巩固相关知识点 。
比如如下 netstat -t 看到的 Recv-Q 有大量数据堆积,那么一般是 CPU 处理不过来导致的:
上面是通过一些具体的工具、指标来认识全连接队列(工程效率的手段)。
5. 实践验证一下上面的理解
把 Java 中 backlog 改成 10(越小越容易溢出),继续跑压力,这个时候 Client 又开始报异常了,然后在 Server 上通过 ss 命令观察到:
1 2 3 |
[root@server ~]# ss -lnt Recv-Q Send-Q Local Address:Port Peer Address:Port 11 10 *:3306 *:* |
按照前面的理解,这个时候我们能看到 3306 这个端口上的服务全连接队列最大是 10。
但是现在有 11 个在队列中和等待进队列的,肯定有一个连接进不去队列要 overflow 掉,同时也确实能看到 overflow 的值在不断地增大。
6. Tomcat 和 Nginx 中的 Accept 队列参数
Tomcat 默认短连接,backlog(Tomcat 里面的术语是 Accept count)Ali-tomcat 默认是 200,Apache Tomcat 默认 100。
1 2 3 |
[root@server ~]# ss -lnt Recv-Q Send-Q Local Address:Port Peer Address:Port 0 100 *:8080 *:* |
Nginx 默认是 511,如下图:
1 2 3 4 |
[root@server ~]# ss -lnt State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 511 *:8085 *:* LISTEN 0 511 *:8085 *:* |
因为 Nginx 是多进程模式,所以看到了多个 8085,也就是多个进程都监听同一个端口以尽量避免上下文切换来提升性能。
7. 总结
全连接队列、半连接队列溢出这种问题很容易被忽视,但是又很关键,特别是对于一些短连接应用(比如 Nginx、PHP,当然它们也是支持长连接的)更容易爆发。
一旦溢出,从 CPU、线程状态看起来都比较正常,但是压力上不去,在 Client 看来 RT 也比较高(RT = 网络 + 排队 + 真正服务时间),但是从 Server 日志记录的真正服务时间来看 rt 又很短。
JDK、Netty 等一些框架默认 backlog 比较小,可能有些情况下导致性能上不去。
希望通过本文能够帮大家理解 TCP 连接过程中的半连接队列和全连接队列的概念、原理和作用,更关键的是有哪些指标可以明确看到这些问题(工程效率帮助强化对理论的理解)。
另外每个具体问题都是最好学习的机会,光看书理解肯定是不够深刻的,请珍惜每个具体问题,碰到后能够把来龙去脉弄清楚,每个问题都是你对具体知识点通关的好机会。
7-1-1. 最后提出相关问题给大家思考:
- 全连接队列满了会影响半连接队列吗?
- netstat -s 看到的 overflowed 和 ignored 的数值有什么联系吗?
- 如果 Client 走完了 TCP 握手的第三步,在 Client 看来连接已经建立好了,但是 Server 上的对应连接实际没有准备好,这个时候如果 Client 发数据给 Server,Server 会怎么处理呢?(有同学说会 Reset,你觉得呢?)
提出这些问题,希望以这个知识点为抓手,让你的知识体系开始自我生长。
参考文章:
- http://veithen.github.io/2014/01/01/how-tcp-backlog-works-in-linux.html
- http://www.cnblogs.com/zengkefu/p/5606696.html
- http://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/
- Lessons learned tuning TCP and Nginx in EC2 1
- Lessons learned tuning TCP and Nginx in EC2 2
- http://blog.chinaunix.net/uid-20662820-id-4154399.html
[source]TCP三次握手原理及故障排查