一次线上OOM问题的个人复盘

我们一个java服务上线后,偶尔会发生内存OOM(Out Of Memory)问题,但由于OOM导致服务不响应请求,健康检查多次不通过,最后部署平台kill了java进程,这导致定位这次OOM问题也变得困难起来。

 

最终,在多次review代码后发现,是SQL意外地查出大量数据导致的,如下:

<sql id="conditions"><where><if test="outerId != null">and `outer_id` = #{outerId}</if><if test="orderType != null and orderType != ''">and `order_type` = #{orderType}</if>...</where>
</sql><select id="queryListByConditions" resultMap="orderResultMap">select * from order <include refid="conditions"/> 
</select>

查询逻辑类似上面的示例,在Service层有个根据outer_id的查询方法,然后直接调用了Mapper层一个通用查询方法queryListByConditions。

但我们有个调用量极低的场景,可以不传outer_id这个参数,导致这个通用查询方法没有添加这个过滤条件,导致查了全表,进而导致OOM问题。

我们内部对这个问题进行了复盘,考虑到OOM问题还是蛮常见的,所以给大家也分享下。

事前#

在OOM问题发生前,为什么测试阶段没有发现问题?

其实在编写技术方案时,是有考虑到这个场景的,但在提测时,忘记和测试同学沟通此场景,导致遗漏了此场景的测试验证。

关于测试用例不全面,其实不管是疏忽问题、经验问题、质量意识问题或人手紧张问题,从人的角度来说,都很难彻底避免,人没法像机器那样很听话的、不疏漏的执行任何指令。

既然人做不到,那就让机器来做,这就是单元测试、自动化测试的优势,通过逐步积累测试用例,可覆盖的场景就会越来越多。

当然,实施单元测试等方案,也会增加不少成本,需要权衡质量与研发效率谁更重要,毕竟在需求不能砍的情况下,质量与效率的关系是得此失彼,这是任何一本项目管理的书都提到过的。

事中#

在感知到OOM问题发生时,由于进程被部署平台kill,导致现场丢失,难以快速定位到问题点。

一般java里面是推荐使用-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/dump/这种JVM参数来保存现场的,这两个参数的意思是,当JVM发生OOM异常时,自动dump堆内存到文件中,但在我们的场景中,这个方案难以生效,如下:

  1. 在堆占满之前,会发生很多次FGC,jvm会尽最大努力腾挪空间,导致还没有OOM时,系统实际已经不响应了,然后被kill了,这种场景无dump文件生成。
  2. 就算有时幸运,JVM发生了OOM异常开始dump,由于dump文件过大(我们约10G),导致dump文件还没保存完,进程就被kill了,这种场景dump文件不完整,无法使用。

为了解决这个问题,有如下2种方案:

方案1:利用k8s容器生命周期内的Hook#

我们部署平台是套壳k8s的,k8s提供了preStop生命周期钩子,在容器销毁前会先执行此钩子,只要将jmap -dump命令放入preStop中,就可以在k8s健康检查不通过并kill容器前将内存dump出来。

要注意的是,正常发布也会调用此钩子,需要想办法绕过,我们的办法是将健康检查也做成脚本,当不通过时创建一个临时文件,然后在preStop脚本中判断存在此文件才dump,preStop脚本如下:

if [ -f "/tmp/health_check_failed" ]; thenecho "Health check failed, perform dumping and cleanups...";pid=`ps h -o pid --sort=-pmem -C java|head -n1|xargs`;if [[ $pid ]]; thenjmap -dump:format=b,file=/home/work/logs/applogs/heap.hprof $pidfi
elseecho "No health check failure detected. Exiting gracefully.";
fi 

注:也可以考虑在堆占用高时才dump内存,效果应该差不多。

方案2:容器中挂脚本监控堆占用,占用高时自动dump#

#!/bin/bashwhile sleep 1; donow_time=$(date +%F_%H-%M-%S)pid=`ps h -o pid --sort=-pmem -C java|head -n1|xargs`;[[ ! $pid ]] && { unset n pre_fgc; sleep 1m; continue; }data=$(jstat -gcutil $pid|awk 'NR>1{print $4,$(NF-2)}');read old fgc <<<"$data";echo "$now_time: $old $fgc";if [[ $(echo $old|awk '$1>80{print $0}') ]]; then(( n++ ))else(( n=0 ))fiif [[ $n -ge 3 || $pre_fgc && $fgc -gt $pre_fgc && $n -ge 1 ]]; thenjstack $pid > /home/dump/jstack-$now_time.log;if [[ "$@" =~ dump ]];thenjmap -dump:format=b,file=/home/dump/heap-$now_time.hprof $pid;elsejmap -histo $pid > /home/dump/histo-$now_time.log;fi{ unset n pre_fgc; sleep 1d; continue; }fipre_fgc=$fgc
done

每秒检查老年代占用,3次超过80%或发生一次FGC后还超过80%,记录jstack、jmap数据,此脚本保存为jvm_old_mon.sh文件。

然后在程序启动脚本中加入nohup bash jvm_old_mon.sh dump &即可,添加dump参数时会执行jmap -dump导全部堆数据,不添加时执行jmap -histo导对象分布情况。

事后#

为了避免同类OOM case再次发生,可以对查询进行兜底,在底层对查询SQL改写,当发现查询没有limit时,自动添加limit xxx,避免查询大量数据。
优点:对数据库友好,查询数据量少。
缺点:添加limit后可能会导致查询漏数据,或使得本来会OOM异常的程序,添加limit后正常返回,并执行了后面意外的处理。

我们使用了Druid连接池,使用Druid Filter实现的话,大致如下:

public class SqlLimitFilter extends FilterAdapter {// 匹配limit 100或limit 100,100private static final Pattern HAS_LIMIT_PAT = Pattern.compile("LIMIT\\s+[\\d?]+(\\s*,\\s*[\\d+?])?\\s*$", Pattern.CASE_INSENSITIVE);private static final int MAX_ALLOW_ROWS = 20000;/*** 若查询语句没有limit,自动加limit* @return 新sql*/private String rewriteSql(String sql) {String trimSql = StringUtils.stripToEmpty(sql);// 不是查询sql,不重写if (!StringUtils.lowerCase(trimSql).startsWith("select")) {return sql;}// 去掉尾部分号boolean hasSemicolon = false;if (trimSql.endsWith(";")) {hasSemicolon = true;trimSql = trimSql.substring(0, trimSql.length() - 1);}// 还包含分号,说明是多条sql,不重写if (trimSql.contains(";")) {return sql;}// 有limit语句,不重写int idx = StringUtils.lowerCase(trimSql).indexOf("limit");if (idx > -1 && HAS_LIMIT_PAT.matcher(trimSql.substring(idx)).find()) {return sql;}StringBuilder sqlSb = new StringBuilder();sqlSb.append(trimSql).append(" LIMIT ").append(MAX_ALLOW_ROWS);if (hasSemicolon) {sqlSb.append(";");}return sqlSb.toString();}@Overridepublic PreparedStatementProxy connection_prepareStatement(FilterChain chain, ConnectionProxy connection, String sql)throws SQLException {String newSql = rewriteSql(sql);return super.connection_prepareStatement(chain, connection, newSql);}//...此处省略了其它重载方法
}

本来还想过一种方案,使用MySQL的流式查询并拦截jdbc层ResultSet.next()方法,在此方法调用超过指定次数时抛异常,但最终发现MySQL驱动在ResultSet.close()方法调用时,还是会读取剩余未读数据,查询没法提前终止,故放弃之。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/8781.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

electron的electron-packager打包运行和electron-builder生产安装包过程,学透 Electron 自定义 Dock 图标

electron的electron-packager打包运行和electron-builder生产安装包过程 开发electron客户端程序&#xff0c;打包是绕不开的问题。 macOS 应用构建&#xff0c;看似近在咫尺&#xff0c;实则坑坑致命。 场景&#xff1a;mac笔记本打包&#xff0c;以及生产出可交付的软件安装…

什么是事件循环Event Loop

一、含义 事件循环是指不断从任务队列中取出任务&#xff0c;并执行其对应的回调函数的过程。 二、事件循环流程 1.主线程执行同步任务&#xff0c;直到遇到异步任务时&#xff0c;将其回调函数他家到任务队列中&#xff0c;然后继续执行同步任务 2.当所有同步任务执行完之后&a…

如何利用plotly和geopandas根据美国邮政编码(Zip-Code)绘制美国地图

对于我自己来说&#xff0c;该需求源自于分析Movielens-1m数据集的用户数据&#xff1a; UserID::Gender::Age::Occupation::Zip-code 1::F::1::10::48067 2::M::56::16::70072 3::M::25::15::55117 4::M::45::7::02460 5::M::25::20::55455 6::F::50::9::55117我希望根据Zip-…

在 3ds Max 和 After Effects 中创建逼真的蜘蛛网模型

推荐&#xff1a; NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 1. 创建蜘蛛网 步骤 1 打开 3ds Max。 打开 3ds Max 步骤 2 转到创建>标准基元>平面并创建一个平面 在前视图中。 创建平面 步骤 3 保持其长度和宽度 segs 为 80。 段 步骤 4 打开修改器列表…

Python爬虫之Scrapy框架系列(23)——分布式爬虫scrapy_redis浅实战【XXTop250部分爬取】

目录&#xff1a; 1.实战讲解&#xff08;XXTop250完整信息的爬取&#xff09;&#xff1a;1.1 使用之前做的完整的XXTOP250项目&#xff0c;但是设置为只爬取一页&#xff08;共25个电影&#xff09;,便于观察1.2 配置settings文件中使用scrapy_redis的必要配置&#xff0c;并…

智能汽车的主动悬架工作原理详述

摘要&#xff1a; 本文将详细介绍主动悬架功能原理设计。 主动悬架是车辆上的一种汽车悬架。它使用车载系统来控制车轮相对于底盘或车身的垂直运动&#xff0c;而不是由大弹簧提供的被动悬架&#xff0c;后者的运动完全由路面决定。主动悬架分为两类&#xff1a;真正的主动悬架…

fSGAT批量候选基因关联分析丨快速单基因关联分析

候选基因如何分析&#xff1f; 通常情况下关联分析会得到一大堆候选基因&#xff0c;总不可能每个都有用&#xff0c;因此需要对候选基因进行深一步分析&#xff0c;本篇笔记分享一下群体遗传学研究中GWAS候选位点与候选基因的筛选思路。主要的方式包括单基因关联分析、连锁程度…

ubuntu 静态IP设置

ubuntu 静态IP设置&#xff1a; 1.输入&#xff1a; sudo vim /etc/netplan/01-network-manager-all.yaml Let NetworkManager manage all devices on this system network: ethernets: ens33: dhcp4: no addresses: [192.168.1.119/24] gateway4: 192.168.1.1 nameservers: …

CASAtomic原子操作详解

一、CAS&#xff08;Compare And Swap&#xff09; 1、CAS介绍 CAS原理&#xff1a;假设有三个值&#xff0c;E&#xff08;旧值&#xff09;、U&#xff08;需要更新的值&#xff09;、V&#xff08;内存中真实的值&#xff09;&#xff0c;具体参照下图&#xff1a; 作用&a…

[C++] C++入门第二篇 -- 引用 -- 内联函数inline -- auto+for

目录 1、引用 -- & 1.1 引用的概念 1.2 引用特性 1.3 常引用 -- 权限问题 1.4 引用的使用场景 1.4.1 做参数 1.4.2 做返回值 注意 1.5 传值、传引用的效率比较 1.6 引用和指针的区别 2、内联函数 2.1 概念 转存失败重新上传取消​编辑转存失败重新上传取消​编…

flink cdc环境搭建

1.下载flink https://archive.apache.org/dist/flink/flink-1.12.2/ 2.修改flink-conf.yaml #根据自己电脑核数修改&#xff0c;这里我设置为4&#xff0c;因为系统分配了4核 jobmanager.rpc.address: localhost #主机名根据自己设定 taskmanager.numberOfTaskSlots: 4 3.下载…

前端JS识别二维码内容

原文&#xff1a;https://www.cnblogs.com/houxianzhou/p/15030351.html <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>图片二维码识别</title><script src"https://cdn.bootcss.com/jquery/3.4.1/jque…

Springboot中 AOP实现日志信息的记录到数据库

1、导入相关的依赖 <!--spring切面aop依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency> 注意&#xff1a;在application.properties文件里加这样一…

【前端设计】使用Verdi查看波形时鼠标遮住了parameter值怎么整

盆友&#xff0c;你们在使用Verdi的时候&#xff0c;有没有遇到过鼠标遮挡着了parameter数值的场景&#xff1f;就跟下面这个示意图一样&#xff1a; 最可恨的是这个参数值他会跟着你的鼠标走&#xff0c;你想把鼠标移开看看看这个例化值到底是多大吧&#xff0c;这个数他跟着你…

Python实现人脸识别功能

Python实现人脸识别功能 闲来没事&#xff0c;记录一下前几天学习的人脸识别小项目。 要想实现人脸识别&#xff0c;我们首先要搞明白&#xff0c;人脸识别主要分为哪些步骤&#xff1f;为了提高人脸识别的准确性&#xff0c;我们首先要把图像或视频中的人脸检测出来&#xf…

基于DNN深度学习网络的OFDM+QPSK信号检测算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 ............................................................................. Transmitt…

XGBoost实例——皮马印第安人糖尿病预测和特征筛选

利用皮马印第安人糖尿病数据集来预测皮马印第安人的糖尿病&#xff0c;以下是数据集的信息&#xff1a; Pregnancies&#xff1a;怀孕次数Glucose&#xff1a;葡萄糖BloodPressure&#xff1a;血压 (mm Hg)SkinThickness&#xff1a;皮层厚度 (mm)Insulin&#xff1a;胰岛素 2…

区块链学习笔记

区块链技术与应用 数组 列表 二叉树 哈希函数 BTC中的密码学原理 cryptographic hash function collsion resistance(碰撞抵抗) 碰撞指的是找到两个不同的输入值&#xff0c;使得它们的哈希值相同。也就是说&#xff0c;如果存在任意两个输入x和y&#xff0c;满足x ≠ y…

【ES】---ES的聚合(aggregations)

目录 一、前言1、聚合分类2、聚合的实现方式二、RestAPI--bucket聚合案例11、按照类型分bucket2、按照(String)时间分bucket三、RestAPI-- metric聚合案例11、metric指标统计四、RestAPI-- pipeline聚合案例1一、前言 聚合是对文档数据的统计、分析、计算。 注意:参与聚合的字…

YOLOX-PAI 论文学习

1. 解决了什么问题&#xff1f; 对 YOLOX 做加速&#xff0c;在单张 Tesla V100 上取得了 42.8 42.8 42.8mAP&#xff0c;推理速度为 1 毫秒。 2. 提出了什么方法&#xff1f; 2.1 主干网络 YOLOv6 和 PP-YOLOE 都将主干网络从 CSPNet 切换到了 RepVGG。RepVGG 在推理时&a…