震惊!线上四台机器同一时间全部 OOM,到底发生了什么?

简介: 震惊!线上四台机器同一时间全部 OOM,到底发生了什么?

案发现场


昨天晚上突然短信收到 APM (即 Application Performance Management 的简称,我们内部自己搭建了这样一套系统来对应用的性能、可靠性进行线上的监控和预警的一种机制)大量告警 画外音: 监控是一种非常重要的发现问题的手段,没有的话一定要及时建立哦

紧接着运维打来电话告知线上部署的四台机器全部 OOM (out of memory, 内存不足),服务全部不可用,赶紧查看问题!


问题排查


首先运维先重启了机器,保证线上服务可用,然后再仔细地看了下线上的日志,确实是因为 OOM 导致服务不可用

b01ee70b16dbc6f1f2b4591a6c1dd080_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

第一时间想到 dump 当时的内存状态,但由于为了让线上尽快恢复服务,运维重启了机器,导致无法 dump 出事发时的内存。所以我又看了下我们 APM 中对 JVM 的监控图表

画外音:一种方式不行,尝试另外的角度切入!再次强调,监控非常重要!完善的监控能还原当时的事发现场,方便定位问题。

3eafc93f69dd6357bc1582a1a0448596_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

不看不知道,一看吓一跳,从 16:00 开始应用中创建的线程居然每时每刻都在上升,一直到 3w 左右,重启后(蓝色箭头),线程也一直在不断增长),正常情况下的线程数是多少呢,600!问题找到了,应该是在下午 16:00 左右发了一段有问题的代码,导致线程一直在创建,且创建的线程一直未消亡!查看发布记录,发现发布记录只有这么一段可疑的代码 diff:在 HttpClient 初始化的时候额外加了一个 evictExpiredConnections 配置

5649ebf73abeb41ed53a7740c8c4e8da_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

问题定位了,应该是就是这个配置导致的!(线程上升的时间点和发布时间点完全吻合!),于是先把这个新加的配置给干掉上线,上线之后线程数果然恢复正常了。那 evictExpiredConnections 做了什么导致线程数每时每刻在上升呢?这个配置又是为了解决什么问题而加上的呢?于是找到了相关同事来了解加这个配置的前因后果


还原事发经过


最近线上出现不少 NoHttpResponseException 的异常, 而上面那个配置就是为了解决这个异常而添加的,那是什么导致了这个异常呢?

在说这个问题之前我们得先了解一下 http 的 keep-alive 机制。

先看下正常的一个 TCP 连接的生命周期

41e4df360c6f348cf167f9e9f266e22c_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

可以看到每个 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 的开销。这对性能的提升无疑是有巨大的帮助。

14d83da31c4b3170f78ad47bad48714a_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

回过头来看 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 的原因,这样能看得更明白一些

bb72d716d142595cf2022855a740e092_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

费了这么大的功夫,我们终于知道了产生 NoHttpResponseException 的原因,那该怎么解决呢,有两种策略

  1. 重试,收到异常后,重试一两次,由于重试后客户端会用有效的连接去请求,所以可以避免这种情况,不过一次要注意重试次数,避免引起雪崩!
  2. 设置一个定时线程,定时清理上述的闲置连接,可以将这个定时时间设置为 keep alive timeout 时间的一半以保证超时前回收。

evictExpiredConnections 就是用的上述第二种策略,来看下官方用法使用说明

Makes this instance of HttpClient proactively evict idle connections from the
connection pool using a background thread.

调用这个方法只会产生一个定时线程,那为啥应用中线程会一直增加呢,因为我们对每一个请求都创建了一个 HttpClient! 这样由于创建每一个 HttpClient 实例j时都会调用  evictExpiredConnections ,导致有多少请求就会创建多少个定时线程!

还有一个问题,为啥线上四台机器几乎同一时间点全挂呢?因为由于负载均衡,这四台机器的权重是一样的,硬件配置也一样,收到的请求其实也可以认为是差不多的,这样这四台机器由于创建 HttpClient 而生成的后台线程也在同一时间达到最高点,然后同时 OOM。


解决问题


所以针对以上提到的问题,我们首先把 HttpClient 改成了单例,这样保证服务启动后只会有一个定时清理线程,另外我们也让运维针对应用的线程数做了监控,如果超过某个阈值直接告警,这样能在应用 OOM 前及时发现处理。

画外音:再次强调,监控相当重要,能把问题扼杀在摇篮里!


总结


本文通过线上四台机器同时 OOM 的现象,来详细剖析定位了产生问题的原因,可以看到我们在应用某个库时首先要对这个库要有充分的了解(上述 HttpClient 的创建不用单例显然是个问题),其次必要的网络知识还是需要的,所以要成为一个合格的程序员,不光对语言本身有所了解,还要对网络,数据库等也要有所涉猎,这些对排查问题以及性能调优等会有非常大的帮助,再次,完善的监控非常重要,通过触发某个阈值提前告警,可以将问题扼杀在摇篮里!


相关文章
|
存储 安全 Java
从0开始回顾JVM---系列四
虚拟机执行 1、类的生命周期吗? 一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 、验证、准备、解析、初始化 、使用 和 卸载,其中验证、准备、解析三个部分统称为连接。 2、什么是类加载?类加载的过程? 类加载: 虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象。 类加载过程如下: ● 加载:1、根据类的全限定类名获取二进制字节流;2、将字节流代表的静态存储结构转为方法区运行时存储数据结构;3、在堆中生成Class对象,作为方法区这个类数据访问入口; ● 验证: 检验加载的class文
|
11月前
|
前端开发 搜索推荐 C++
Marp 教程:如何在 VSCode 中引入自定义样式和主题
本文介绍了如何在 Marp 中引入自定义样式和主题,使你的幻灯片更加个性化和独特。首先,你需要安装 VSCode 和 Marp 插件,了解 Marp 的基本结构。接着,通过创建自定义 CSS 文件并在 Markdown 文件中引入,实现样式定制。此外,还可以创建和使用自定义主题,以及进行高级自定义,如调整布局、引入自定义字体和定义复杂动画。最后,使用 Marp 的预览功能实时查看效果。
|
JavaScript API
再不学vue3就没有人要你了!速来围观vue3
这篇技术文章介绍了作者从最初对学习 Vue3 抵触到最终决定投入学习的心路历程,展示了 Vue3 相较于 Vue2 的诸多改进和新特性,如更优的性能、更小的代码体积及更佳的 TypeScript 支持。文章重点阐述了 Vue3 中 createApp 的使用变化、emits 机制、多事件处理、Fragment 的引入等重要功能升级。此外,还深入探讨了 Composition API 和 Options API 的区别,强调 Composition API 在代码组织和逻辑复用方面的优势,并给出了在不同项目规模中选择合适 API 的建议。
252 0
|
缓存 数据安全/隐私保护 虚拟化
windows端口被占用,无法通过netstat找到进程,占用的端口又不能修改,该怎么办?
项目运行时服务器8080端口被占用,常规`netstat`命令找不到占用进程。解决方法包括:1) 强制关机重启释放端口;2) 使用`netstat -anobq`或Windows 10的`Get-NetTCPConnection` PowerShell命令查找BOUND状态的进程;3) 调整Windows动态端口范围,避免冲突。注意,强制关机可能影响数据安全。
4780 2
电脑登录某些网站失败的解决方法-关闭网络代理
电脑登录某些网站失败的解决方法-关闭网络代理
423 0
电脑登录某些网站失败的解决方法-关闭网络代理
|
JavaScript 数据库 图形学
对象模型(Object Model)
对象模型(Object Model)是一种用于描述计算机程序中对象的结构、行为和关系的概念模型。它提供了一种组织和操作对象的方式,通常用于面向对象编程(OOP)语言中。下面是一个简单的示例,演示如何使用对象模型来实现一个简单的学生信息管理系统的代码。
608 1
|
索引
面试题 08.10:颜色填充
面试题 08.10:颜色填充
130 0
|
存储 缓存 运维
8个 数据库性能优化方案,你知道几个?(建议收藏) 下
8个 数据库性能优化方案,你知道几个?(建议收藏) 下
292 0
8个 数据库性能优化方案,你知道几个?(建议收藏) 下
|
敏捷开发 架构师 安全
「敏捷架构」敏捷架构:规模化敏捷开发的策略(上)
「敏捷架构」敏捷架构:规模化敏捷开发的策略
|
存储 人工智能 BI
【蓝桥杯集训·周赛】AcWing 第91场周赛
文章目录 第一题 AcWing 4861. 构造数列 一、题目 1、原题链接 2、题目描述 二、解题报告 1、思路分析 2、时间复杂度 3、代码详解 第二题 AcWing 4862. 浇花 一、题目 1、原题链接 2、题目描述 二、解题报告 1、思路分析 2、时间复杂度 3、代码详解 第三题 AcWing 4861. 构造数列 一、题目 1、原题链接 2、题目描述 二、解题报告 1、思路分析 2、时间复杂度 3、代码详解
139 0