案发现场程序员
昨天晚上忽然短信收到 APM (即 Application Performance Management 的简称),咱们内部本身搭建了这样一套系统来对应用的性能、可靠性进行线上的监控和预警的一种机制)大量告警数据库
画外音: 监控是一种很是重要的发现问题的手段,没有的话必定要及时创建哦网络
紧接着运维打来电话告知线上部署的四台机器所有 OOM (out of memory, 内存不足),服务所有不可用,赶忙查看问题!负载均衡
问题排查运维
首先运维先重启了机器,保证线上服务可用,而后再仔细地看了下线上的日志,确实是由于 OOM 致使服务不可用ide
第一时间想到 dump 当时的内存状态,但因为为了让线上尽快恢复服务,运维重启了机器,致使没法 dump 出事发时的内存。因此我又看了下咱们 APM 中对 JVM 的监控图表性能
画外音: 一种方式不行,尝试另外的角度切入!再次强调,监控很是重要!完善的监控能还原当时的事发现场,方便定位问题。网站
不看不知道,一看吓一跳,从 16:00 开始应用中建立的线程竟然每时每刻都在上升,一直到 3w 左右,重启后(蓝色箭头),线程也一直在不断增加),正常状况下的线程数是多少呢,600!问题找到了,应该是在下午 16:00 左右发了一段有问题的代码,致使线程一直在建立,且建立的线程一直未消亡!查看发布记录,发现发布记录只有这么一段可疑的代码 diff:在 HttpClient 初始化的时候额外加了一个 evictExpiredConnections 配置this
问题定位了,应该是就是这个配置致使的!(线程上升的时间点和发布时间点彻底吻合!),因而先把这个新加的配置给干掉上线,上线以后线程数果真恢复正常了。那 evictExpiredConnections 作了什么致使线程数每时每刻在上升呢?这个配置又是为了解决什么问题而加上的呢?因而找到了相关同事来了解加这个配置的来龙去脉线程
还原事发通过
最近线上出现很多 NoHttpResponseException 的异常,那是什么致使了这个异常呢?
在说这个问题以前咱们得先了解一下 http 的 keep-alive 机制。
先看下正常的一个 TCP 链接的生命周期
能够看到每一个 TCP 链接都要通过 三次握手 创建链接后才能发送数据,要通过 四次挥手 才能断开链接,若是每一个 TCP 链接在 server 返回 response 后都立马断开,则发起多个 HTTP 请求就要屡次建立断开 TCP, 这在 Http 请求不少 的状况下无疑是很耗性能的, 若是在 server 返回 response 不当即断开 TCP 连接,而是 复用 这条连接进行下一次的 Http 请求,则无形中省略了不少建立 / 断开 TCP 的开销,性能上无疑会有很大提高。
以下图示,左图是不复用 TCP 发起多个 HTTP 请求的状况,右图是复用 TCP 的状况,能够看到发起三次 HTTP 请求,复用 TCP 的话能够省去两次创建 / 断开 TCP 的开销,理论上发起 一个应用只要启一个 TCP 链接便可,其余 HTTP 请求均可以复用这个 TCP 链接,这样 n 次 HTTP 请求能够省去 n-1 次建立 / 断开 TCP 的开销。这对性能的提高无疑是有巨大的帮助。
回过头来看 keep-alive (又称持久链接,链接复用)作的就是复用链接, 保证链接持久有效。
画中音: Http 1.1 以后 keep-alive 才默认支持并开启,不过目前大部分网站都用了 http 1.1 了,也就是说大部分都默认支持连接复用了
天下没有免费的午饭,虽然 keep-alive 省去了不少没必要要的握手/挥手操做,但因为链接长期保活,若是一直没有 http 请求的话,这条链接也就长期闲着了,会占用系统资源,有时反而会比复用链接带来更大的性能消耗。 因此咱们通常会为 keep-alive 设置一个 timeout, 这样若是链接在设置的 timeout 时间内一直处于空闲状态(未发生任何数据传输),通过 timeout 时间后,链接就会释放,就能节省系统开销。
看起来给 keep-alive 加 timeout 是完美了,可是又引入了新的问题(一波已平,一波又起!),考虑以下状况:
若是服务端关闭链接,发送 FIN 包(注:在设置的 timeout 时间内服务端若是一直未收到客户端的请求,服务端会主动发起带 Fin 标志的请求以断开链接释放资源),在这个 FIN 包发送可是还未到达客户端期间,客户端若是继续复用这个 TCP 链接发送 HTTP 请求报文的话,服务端会由于在四次挥手期间不接收报文而发送 RST 报文给客户端,客户端收到 RST 报文就会提示异常 (即 NoHttpResponseException )
咱们再用流程图仔细梳理一下上述这种产生 NoHttpResponseException 的缘由,这样能看得更明白一些
费了这么大的功夫,咱们终于知道了产生 NoHttpResponseException 的缘由,那该怎么解决呢,有两种策略
重试,收到异常后,重试一两次,因为重试后客户端会用有效的链接去请求,因此能够避免这种状况,不过一次要注意重试次数,避免引发雪崩!
设置一个定时线程,定时清理上述的闲置链接,能够将这个定时时间设置为 keep alive timeout 时间的一半以保证超时前回收。
evictExpiredConnections就是用的上述第二种策略,来看下官方用法使用说明
Makes this instance of HttpClient proactively evict idle connections from the connection pool using a background thread.
调用这个方法只会产生一个定时线程,那为啥应用中线程会一直增长呢,由于咱们对每个请求都建立了一个 HttpClient! 这样因为每个 HttpClient 实例都会调用 evictExpiredConnections ,致使有多少请求都会建立多少个 定时线程!
还有一个问题,为啥线上四台机器几乎同一时间点全挂呢?
由于因为负载均衡,这四台机器的权重是同样的,硬件配置也同样,收到的请求其实也能够认为是差很少的,这样这四台机器因为建立 HttpClient 而生成的后台线程也在同一时间达到最高点,而后同时 OOM。
解决问题
因此针对以上提到的问题,咱们首先把 HttpClient 改为了单例,这样保证服务启动后只会有一个定时清理线程,另外咱们也让运维针对应用的线程数作了监控,若是超过某个阈值直接告警,这样能在应用 OOM 前及时发现处理。
画外音:再次强调,监控至关重要,能把问题扼杀在摇篮里!
总结
本文经过线上四台机器同时 OOM 的现象,来详细剖析产定位了产生问题的缘由,能够看到咱们在应用某个库时首先要对这个库要有充分的了了解(上述 HttpClient 的建立不用单例显然是个问题),其次必要的网络知识仍是须要的,因此要成为一个合格的程序员,不关对语言自己有所了解,还要对网络,数据库等也要有所涉猎,这些对排查问题以及性能调优等会有很是大的帮助,再次,完善的监控很是重要,经过触发某个阈值提早告警,能够将问题扼杀在摇篮里!