昨晚突然收到大量APM短信报警。紧接着,运维打来电话,通知线上部署的四台机器全部OOM(outofmemory,内存不足),所有服务不可用。请快速检查问题!图片来自Pexels故障排除。在线服务可用,然后仔细阅读离线日志。确实是因为OOM导致服务不可用:当时第一时间想到了Dump的内存状态,但是为了尽快恢复在线服务,运维重启了机器,导致无法在事故发生时转储内存。于是又看了看我们APM中JVM的监控图。画外音:一个方法不行,换个角度试试!再次强调,监控很重要!完善的监控可以还原当时的事发现场,方便定位问题。我不知道我是否没有看到它。当我看到它时,我感到震惊。从16:00开始,应用中创建的线程一直在增加,直到3W左右。重启后(蓝色箭头),线程一直在不断增长。一般情况下线程数是多少,600!问题找到了。应该是下午16:00左右发了一段有问题的代码,导致线程一直在创建,创建的线程一直没有死!查看release记录,发现release记录只有这样一个可疑的代码diff:在HttpClient初始化的时候额外增加了一个evictExpiredConnections配置。问题定位了,应该是这个配置引起的!(提线程的时间点和释放时间点是一模一样的!),所以先kill掉这个新加的配置再上线。上线后,线程数已经恢复正常。那么evictExpiredConnections做了什么导致线程数每时每刻都在上升呢?增加这个配置是为了解决什么问题?于是找了相关的同事了解了加这个配置的前因后果。还原事件。最近网上出现了很多NoHttpResponseException异常。是什么导致了这个异常?在谈这个问题之前,我们首先要了解一下Http的保活机制。先看一个正常的TCP连接的生命周期:可以看到每个TCP连接在发送数据前都要经过3次握手建立连接,4次挥手断开连接。如果在Server返回Response后立即断开每个TCP连接,则会创建多个HTTP请求多次断开TCP连接,在HTTP请求较多的情况下无疑会消耗大量的性能。如果在Server返回Response后不立即断开TCP连接,而是将连接重新用于下一次Http请求,那么无形中省去了很多创建/断开TCP的开销,性能无疑会得到很大的提升。如下图,左图是多个Http请求没有和TCP复用的情况,右图是有TCP复用的情况。可以看出发起了三个Http请求,TCP的复用可以节省两次建立/断开TCP的开销。理论上,启动一个应用程序,只需要启动一个TCP连接,其他Http请求可以重用这个TCP连接,这样N次Http请求就可以节省N-1次创建/断开TCP的开销。这对于提高性能无疑是一个巨大的帮助。回过头来看,keep-alive(也叫持久连接,连接重用)所做的就是重用连接,保证连接持久有效。画外音:Http1.1之后,默认支持并开启keep-alive。但是,目前大多数网站都使用HTTP1.1,这意味着它们中的大多数默认都支持链接重用。世界上没有免费的午餐。keep-alive虽然省去了很多不必要的handshake/waving操作,但是由于连接是长时间保持alive的,如果没有Http请求,这个连接会长时间空闲,会占用系统资源,有时会带来比多路复用连接更大的性能消耗。所以我们一般会为keep-alive设置一个超时时间,这样如果连接在设置的超时时间内一直处于空闲状态(没有发生数据传输),超时时间过后就会释放连接,这样可以节省系统开销。keep-alive加timeout看起来很完美,但是引入了新的问题(一波平了,一波又涨了)。考虑如下情况:如果服务器关闭连接,发送一个FIN包(注意:如果服务器在设置的超时时间内没有收到客户端的请求,服务器会主动发起带有FIN标志的请求断开连接,释放资源)。在发送FIN包还没有到达客户端期间,如果客户端继续复用TCP连接发送Http请求报文,服务器会因为没有收到报文而向客户端发送RST报文四波浪潮期间。客户端收到RST报文时会提示异常(NoHttpResponseException)。下面我们用流程图仔细梳理一下上面NoHttpResponseException的原因,让我们看的更清楚:费了九牛二虎之力,终于知道了NoHttpResponseException的原因,那么如何解决呢?Strategy有两种:Retry,收到异常后,重试一次或两次,因为重试后客户端会使用有效的连接请求,所以这种情况可以避免,但是要注意重试一次的次数,避免引发雪崩!设置一个定时线程,定时清理上述空闲连接,可以将这个定时时间设置为keepalive超时时间的一半,以保证超时前恢复。evictExpiredConnections就是上面提到的第二种策略。看一下官方的使用说明:让这个HttpClient实例使用后台线程主动驱逐连接池中的空闲连接。调用这个方法只会产生一个定时线程,为什么要用它呢?中间线程会一直增加吗?因为我们为每个请求创建一个HttpClient!这样一来,由于每个HttpClient实例都会调用evictExpiredConnections,那么有多少请求就创建多少个定时线程!还有一个问题,为什么有四个在线?两台机器几乎同时宕机吗?因为负载均衡,四台机器的权重是一样的,硬件配置也是一样的,所以收到的请求其实可以认为是相似的。这样这四台机器因为创建HttpClient产生的后台线程也同时达到最高点,然后同时OOM。解决问题所以针对上面提到的问题,我们首先将HttpClient改为单例,保证服务启动后只有一个定时清理线程。此外,我们还要求运维监控应用中的线程数。如果超过一定阈值,则直接报警,以便在应用OOM之前及时发现并处理。画外音:再次强调,监控很重要,可以把问题扼杀在萌芽状态!综上所述,本文通过四台在线机器同时出现OOM现象,对问题原因进行了详细的分析和定位。可见,我们在应用某个库的时候,首先要对这个库有充分的了解(没有单例创建上面的HttpClient显然是有问题的),其次,必要的网络知识还是需要的。因此,要成为一名合格的程序员,不仅要了解语言本身,还要涉猎网络、数据库等,这对故障排除和性能调优都有很大帮助。同样,完善的监控非常重要。通过触发一定的阈值进行预警,可以将问题扼杀在萌芽状态!
