什么?-你的服务竟然被探活搞死了?
背景
今年开发了好多服务,着实踩了不少的坑。这不,分分钟就被探活搞的死去活來。这里我把这些经验分享给大家,避免大家再继续犯这种错误。
通用tcp探活原理
其实,探活原理特别简单,只要稍懂计算机网络就能够理解。
- 检测端 发起tcp三次握手,建立新连接,连接建立成功代表服务活着,建立失败代表服务死了,之后发送rst包主动断开连接。
- 被检测端 接受检测端发送的三次握手手建立连接,当接收到检测端的rst包后,被检测端断开连接,释放资源。
但是假如rst包丢失了,会发生什么样的情况呢?我们以thrift为例讲述一下之后所发生的事情,如下图所示。
经过三次握手后,服务端已经建立一个新的数据连接,并把连接丢给工作线程。服务端的工作线程监听连接,并准备接收请求(毕竟,任何一个thrift服务都是先接收请求数据,之后进行计算,最后返回响应数据,所以当新连接建立后,thrift默认首先读取连接上的数据)。
检测端建立连接成功后,认为下游服务还活着,所以立刻发送rst包(检测端认为这个rst包一定会到达被检测服务),并且释放连接资源。
但是如果网络状态不好,rst包丢失,那么服务端(被检测端)的工作线程就会无限制的hang在读取连接数据上(因为检测端已经单方面认为连接断开,不会写任何数据,所以服务端也读不到任何数据)。如果多丢几个rst包,那可以预期无论你有多少thrift工作线程,都将会hang死。
此时真正调用服务的客户端的请求也无限制的hang住,因为这些请求得不到thrift工作线程的处理。
最最可怕的是:由于探活只是单纯的建立连接而并不发送或者接受额外数据,并且thrift服务有单独的线程进行accept,这导致了连接建立每次都成功,但实际上服务已经没有了计算能力。
最后的结局就是:整个系统流量突然降低,下游接收不到请求(以为是上游调用的锅),上游发出的请求得不到响应(认为是下游服务的锅),并且没有报警短信发出(以为是运维的锅),对排查问题造成很大的困扰。
出现这种问题时有一个非常明显的现象:即使在系统流量为0的情况下,服务端也会不断有新连接建立并处于Establish状态(这是因为周期性的探活导致的),并且服务端日志不滚动,客户端请求无响应。如果你的系统有这种情况,那么多半和探活有关。
我当时遇到的情况比较糟糕,因为之前一段时间rst丢包率特别低,平均几天丢一个rst包,所以服务hang死造成的影响并不大,只要抽空重新启动服务即可。可是突然某一天,单台服务的rst丢包数达到了秒级:隔几秒丢一个rst包,导致我的服务短时间内全部hang死。
HTTP探活原理
在实际工作中,对于HTTP服务的探活通常也采取TCP探活原理,毕竟HTTP建立在TCP服务之上,所以TCP探活同样适用于HTTP服务。很多HTTP服务实现原理和thrift大同小异,所以当TCP探活的rst包丢失后,HTTP服务同样会hang在读取连接数据。
为了避免rst包丢失,HTTP服务通常使用HTTP探活请求:探测端对被探测端发送特定的HTTP请求,并且验证被探测端的响应数据是否符合预期。如果rst包丢失,服务端连接最差的情况下会处于TIME_WAIT状态,经过2倍超时时间后服务端会主动关闭连接释放资源,这样服务端的工作线程不会长时间的被占用,相比于用TCP探活要好的多。HTTP探活原理如下图所示。
此时rst包虽然丢失,但是由于探测使用的HTTP请求,那么整个请求的交互就会完成(探测端会等待服务端的响应数据,所以能够确保服务端能够完成这个响应),服务端发送完响应数据后会主动关闭连接,发送FIN包(一般的HTTP服务框架默认都会这么做),服务端连接处于TIME_WAIT状态,经过一段时间后连接释放,不会长时间占用服务端资源。
对于HTTP服务,我们需要提供一个不耗费cpu和内存的调用接口,这个接口只是为了满足HTTP探活请求调用,如果探活接口死了,可以认为这个服务也死了。
如何确保不被探活搞死?
我们不能保证网络时时刻刻正常rst包不丢失,所以我们只能改造服务使得其更健壮。
正确解决方案如下:
- 服务端加入读超时时间。当rst包丢失导致服务端线程hang在读取上,超过一定时间后服务端线程会主动断开连接,释放资源,从而确保服务端线程能够服务于其它请求。
- 客户端请求服务端加入超时限制并进行重试。当一个服务hang死,导致连接其上的客户端也hang住后,超过一段时间后,客户端主动断开连接并且尝试请求其它可用服务。
现在我们清楚探活问题的来龙去脉,那么我想问:你的服务加入超时时间了么?你的服务会被探活搞死么?