TCP三次握⼿详解
问题描述
场景:JAVA的client和server,使⽤socket通信。server使⽤NIO。
1.间歇性得出现client向server建⽴连接三次握⼿已经完成,但server的selector没有响应到这连接。
2.出问题的时间点,会同时有很多连接出现这个问题。
3.selector没有销毁重建,⼀直⽤的都是⼀个。
4.程序刚启动的时候必会出现⼀些,之后会间歇性出现。
分析问题
正常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" 去看队列的溢出统计数据:socket通信在哪一层
667399 times the listen queue of socket overflowed
反复看了⼏次之后发现这个overflowed ⼀直在增加,那么可以明确的是server上全连接队列⼀定溢出了。
接着查看溢出后,OS怎么处理:
cat /proc/sys/net/ipv4/tcp_abort_on_overflow
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),如果这个连接⼀直排不上队就异常了。
但是不能只是满⾜问题的解决,⽽是要去复盘解决过程,中间涉及到了哪些知识点是我所缺失或者理解不到位的;这个问题除了上⾯的异常信息表现出来之外,还有没有更明确地指征来查看和确认这个问题。
深⼊理解TCP握⼿过程中建连接的流程和队列
如上图所⽰,这⾥有两个队列: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次):
p_synack_retries=2
如果TCP连接队列溢出,有哪些指标可以看呢?
上述解决过程有点绕,听起来懵,那么下次再出现类似问题有什么更快更明确的⼿段来确认这个问题呢?(通过具体的、感性的东西来强化我们对知识点的理解和吸收。)
netstat -s|egrep"listen|LISTEN"
667399 times the listen queue of a socket overflowed
⽐如上⾯看到的 667399 times ,表⽰全连接队列溢出的次数,隔⼏秒钟执⾏下,如果这个数字⼀直在增加的话肯定全连接队列偶尔满了。ss 命令
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的值:
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)
Create 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 命令
netstat跟ss命令⼀样也能看到Send-Q、Recv-Q这些状态信息,不过如果这个连接不是Listen状态的话,Recv-Q就是指收到的数据还在缓存中,还没被进程读取,这个值就是还没被进程读取的 bytes;⽽ Send 则是发送队列中没有被远程主机确认的 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
netstat -tn 看到的 Recv-Q 跟全连接半连接没有关系,这⾥特意拿出来说⼀下是因为容易跟 ss -lnt 的 Recv-Q 搞混淆,顺便建⽴知识体系,巩固相关知识点。
⽐如如下netstat -t 看到的Recv-Q有⼤量数据堆积,那么⼀般是CPU处理不过来导致的
上⾯是通过⼀些具体的⼯具、指标来认识全连接队列(⼯程效率的⼿段)。
实践验证⼀下上⾯的理解
把java中backlog改成10(越⼩越容易溢出),继续跑压⼒,这个时候client⼜开始报异常了,然后在server上通过 ss 命令观察到:
ss -lnt
Recv-Q Send-Q Local Address:Port  Peer Address:Port
11    10              *:3306            *:*
按照前⾯的理解,这个时候我们能看到3306这个端⼝上的服务全连接队列最⼤是10,但是现在有11个在
队列中和等待进队列的,肯定有⼀个连接进不去队列要overflow掉,同时也确实能看到overflow的值在不断地增⼤。
Tomcat和Nginx中的Accept队列参数
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
ss -lnt
State  Recv-Q  Send-Q  Local Address:Port    Peer Address:Port
LISTEN      0511              *:8085                *:*
LISTEN      0511              *:8085                *:*
因为Nginx是多进程模式,所以看到了多个8085,也就是多个进程都监听同⼀个端⼝以尽量避免上下⽂切换来提升性能
总结
全连接队列、半连接队列溢出这种问题很容易被忽视,但是⼜很关键,特别是对于⼀些短连接应⽤(⽐如Nginx、PHP,当然他们也是⽀持长连接的)更容易爆发。⼀旦溢出,从cpu、线程状态看起来都⽐较正常,但是压⼒上不去,在client看来rt也⽐较⾼(rt=⽹络+排队+真正服务时间),但是从server⽇志记录的真正服务时间来看rt⼜很短。
jdk、netty等⼀些框架默认backlog⽐较⼩,可能有些情况下导致性能上不去。
希望通过本⽂能够帮⼤家理解TCP连接过程中的半连接队列和全连接队列的概念、原理和作⽤,更关键的是有哪些指标可以明确看到这些问题(⼯程效率帮助强化对理论的理解)。
另外每个具体问题都是最好学习的机会,光看书理解肯定是不够深刻的,请珍惜每个具体问题,碰到后能够把来龙去脉弄清楚,每个问题都是你对具体知识点通关的好机会。
最后提出相关问题给⼤家思考
1. 全连接队列满了会影响半连接队列吗?
2. netstat -s看到的overflowed和ignored的数值有什么联系吗?
3. 如果client⾛完了TCP握⼿的第三步,在client看来连接已经建⽴好了,但是server上的对应连接实际没有准备好,这个时候如果client
发数据给server,server会怎么处理呢?(有同学说会reset,你觉得呢?)
提出这些问题,希望以这个知识点为抓⼿,让你的知识体系开始⾃我⽣长。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。