引子
前段时间我们的服务由于一台交换机网络出现故障,导致数据库连接不上,但是在数据库的连接超时参数设置不合理,connect timeout设置的过长,导致接口耗时增加。DB连接超时后线程未正常结束,上游请求又持续进来,最终耗光了Java线程,JVM进入持续GC状态,无法恢复,直到手工重启才恢复服务。
于是在服务的保护方面新增了两个措施,第一,调小服务端workThread的最大线程数。第二,在Server端设置Accept后Socket的readTimeout时间,当Socket调用read方法后在一定时间内读不到数据的时候会自动关闭socket。
说了这么多背景,但是本篇描述的不是这两个问题,而是我们上线了保护措施后遇到的奇怪的问题。
我们上线了这两个保护措施后,上游调用方向我们报异常,说请求总是返回错误,表现的情形为Socket的InputStream的read方法返回-1。
对方一口咬定是我们的问题,因为发送已经成功了,发送时候没有报任何异常,然而读消息的时候就返回-1了。
猜测应该是复用了已超时的连接所导致的,询问了一下,发现对方果然使用了连接池,而且这条业务线请求量比较小,所以极有可能是连接池中的socket其实已经在服务端被超时关闭了,所以调用服务的时候会发生异常,可是为什么发送的时候不会报错呢?而是在读结果的时候发现连接关闭了呢?
Flush?
首先很容易想到的是有一个时间差。对方正在发送的时候,socket并还没有被关闭,但是这些发送的内容在网络传输过程中的时候,服务端这边把socket给关闭了,所以出现了上文所描述的问题。对于rpc调用来说,一旦涉及到网络的,都可以认为时间间隔是完全随机的值,这种情形也是能解释得通。
但是,上游反馈这个异常非常的多,而且所有的都是同一个异常,读操作时候报错,连接已关闭。如果是上文中的情形才出现,那么首先这种错误应该比较少,而且绝大多数应该都是写出错,而不是读出错。
socket.getOutputStream.flush()返回成功到底是一个什么样的行为?
于是写的小程序进行测试,逻辑很简单,Server端accept后立即关闭socket,而client端则是连上1s后发送消息。
在flush()成功返回后立即记录下System.currentTimeMillis(),同时使用ngrep进行抓包,分析TCP层面数据发送和接受的时间。
当时显示的时间的11:46:52.154 flush返回成功,建议测试的时候使用两台间隔远一点的机器来测试,我当时在本机上测试的,抓到一条这个跨ms的记录还费了好大一番功夫。
其实java的socket的outputstream在进行flush的时候,是不需要block到直到ACK到达才返回成功,或者说RST到达返回失败的。
只是简单地把数据交给TCP/IP层后就直接返回success了,也有可能是TCP层返回PUSH成功后在返回success。
但是可以肯定的是,success的时间在PUSH之后,在收到ACK之前。目前还不知道怎么把nanoseconds转化成DateTime的形式
,不然就可以更加明确地确认,这个返回success是在TCP层面PUSH之前还是TCP层面PUSH之后。
无论是将Socket的setTcpNoDelay设置成true或false,都是一样的表现,outputStream.flush()收到ACP/RST之前就已经成功返回success了。
所以说,当一个Socket已经被remote方close后,执行write方法却返回success是一个正常的行为,而且从Stack Overflow的问答如何判断一个socket已经关闭。可以了解到其实在java层面,一个socket如何确认自己的连接状态是好的,还是已经不可用了,没有一个好的方法。唯一有用的方法就是执行read操作或者是一个write操作,在连接已经断开的情况下,read会返回-1或者是Connection Reset,write会抛出Broken PipeLine。
但是对于一个尚且活着的socket调用read方法,会导致线程阻塞,这个验证方法基本不可取。
做了一些测试,查看这些字段的值,isConnected、isClosed、isInputShutdown、isOutputShutdown,都是徒劳。
继续测试
但是之前描述过,flush操作返回值是不靠谱的。所以在对一个已经被remote端close的socket,进行read or write操作返回结果的情形还不是像上文描述的这么简单。其实此socket处于CLOSE_WAIT状态。
方便说明的话,假设W是写操作,R是读操作,统计一下依次做如下操作的结果。
测试的过程,本机启动一个server,accept之后立即关闭。本机client连接server后,先sleep 1 s,之后做接下来的工作。
不同操作顺序的结果如下
操作 | 结果 |
W | success |
R | connection reset |
W | broken pipeline |
操作 | 结果 |
R | -1 |
W | success |
W | broken pipeline |
操作 | 结果 |
R * N | -1 |
W | success |
W | broken pipeline |
所以说,其实读其实也根本没有对write操作产生影响,只有W的操作,才会真正地触发这个socket去看看自身的状态到底是什么。
而读这个操作,读一个关闭后的socket,会返回-1,但是不会对下一次的R或W操作的结果产生影响,也就是说,不会去更新本地是socket的状态。
后来才发现,其实是因为这个Socket处于CLOSE_WAIT状态,进行了FLUSH操作后,会收到一个RST的包,Socket在收到RST后会立即关闭此Socket,不会进行所谓的LAST_ACK和TIME_WAIT状态。
直到这里,还是没有复现之前说过的那个问题。
操作 | 结果 |
W | success |
R | -1 |
仔细想一想,既然Write操作是不等待ACK之后就立即返回的,所以说明这个更新Socket状态的动作是异步做的,异步的动作肯定是有时间差的。
所以说,没有复现R返回-1的情形有可能是因为之前的操作都是在本机在测试,网络的处理很快,PUSH后立马就收到了RST,所以导致R的操作就抛出Connection Reset的异常。
于是,将Server放在另一台远一点的机器,延迟1ms左右,于是就复现了刚才的问题。当然,有趣的是,上文图中的那些情况,都不成立了,出现了这种情况:
操作 | 结果 |
W | success |
R | -1 |
W | success |
是不是意味着之前的所有结论都是错的?当然没关系,因为第一次的情形都是出于网络基本无延迟的情况下。
其实这些结果是程序严格按顺序执行时的结果,编程理想情况下的结果。其实这就是我们真实想要的,所有步骤都顺序执行的结果。
所以说,真实确认这个socket关闭不可用的结论,其实是在收到RST之后异步处理的。
现在我们回到图1
去仔细看看,从网络抓包情况来看,即使是在收到RST后,还是有W返回success的情况,因为从TCP层面收到RST后,到应用层对socket的内存内容进行一些修改,还是需要一定时间的。
四次挥手?
之所以会出现这么些问题,都是因为处于中间状态所导致,Server端的socket处于FIN_WAIT2状态,而Client端的Socket处于CLOST_WAIT状态,并没有走到一个完全关闭的终态。
在上面的测试用例中,我把client端的等待时间改成了120S,用netstat命名查询,这两个Socket还是处于两个状态,更长的时间并没有进行测试。
在TCP/IP Vol1 18章中描述到“许多伯克利的实现会在空闲10分钟75秒的情况下关闭处于FIN_WAIT2状态的Socket”,Client端会如何表现,目前我没测试,等哪天有心情了测试一下
TCP/IP协议的四次挥手流程图在这就不赘述了,感觉被课本骗了,说好的四次挥手呢,为什么总是进行到一半就不动了呢?
什么情况下会正常地走完这个流程?
1、Client的socket也主动调用close方法。
2、Client程序在CLOSE_WAIT状态下终止了,四次握手会立即就完成,操作系统层面做了这个FIN包的发送工作。
3、如果Client程序如果没有结束,也不主动发FIN包,会停留在此状态,直至被操作系统回收。
这从另外一方面也验证了为什么一定得主动关闭连接,既是帮助别人也是帮助自己。
即使是Server已经把你的连接关闭了,你这边也会一直停留在CLOSE_WAIT状态,Server端会停留在FIN_WAIT_2状态。
这两个状态是否会进入完全关闭的状态,以及进入完全关闭的状态所需的时间,由操作系统来决定,也就是说,和操作系统的实现有关。
小现象
1、在我的操作系统,OS X EI Capitan 10.11.5 (15F34)情况下,FIN_WAIT_2自动关闭的时间符合10分钟75秒这个描述,精确时间我也没统计到,差距1分钟内。已经30分钟过去了,Client端的Socket还傻傻停留在CLOST_WAIT状态,我也不知道啥时候它会完全关闭。
2、OSX作为Server服务的时候,无论是TCP连接建立的时候,还是TCP关闭的时候,都会重发一个ACK,linux下不存在这样的问题。
参考文档
java-socket-api-how-to-tell-if-a-connection-has-been-closed
TCP/IP Illustrated Volume 1: The Protocols
后记
后续看到有可以通过sendUrgentData()来实现发送urgent data,既实现写探活,同时数据还会被对端忽略,不进入业务层。但是也不能达到实时性的效果,也是会在第一次成功,收到RST后才失败。
所以还是觉得NIO的项目好,比如netty的channel,可以直接收到INACTIVE和UNREGISTERED事件。