一次线上OOM问题的个人复盘
原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,非公众号转载保留此声明。
上个月,我们一个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堆内存到文件中,但在我们的场景中,这个方案难以生效,如下:
- 在堆占满之前,会发生很多次FGC,jvm会尽最大努力腾挪空间,导致还没有OOM时,系统实际已经不响应了,然后被kill了,这种场景无dump文件生成。
- 就算有时幸运,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" ]; then
echo "Health check failed, perform dumping and cleanups...";
pid=`ps h -o pid --sort=-pmem -C java|head -n1|xargs`;
if [[ $pid ]]; then
jmap -dump:format=b,file=/home/work/logs/applogs/heap.hprof $pid
fi
else
echo "No health check failure detected. Exiting gracefully.";
fi
注:也可以考虑在堆占用高时才dump内存,效果应该差不多。
方案2:容器中挂脚本监控堆占用,占用高时自动dump
#!/bin/bash
while sleep 1; do
now_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 ))
fi
if [[ $n -ge 3 || $pre_fgc && $fgc -gt $pre_fgc && $n -ge 1 ]]; then
jstack $pid > /home/dump/jstack-$now_time.log;
if [[ "$@" =~ dump ]];then
jmap -dump:format=b,file=/home/dump/heap-$now_time.hprof $pid;
else
jmap -histo $pid > /home/dump/histo-$now_time.log;
fi
{ unset n pre_fgc; sleep 1m; continue; }
fi
pre_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,100
private 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();
}
@Override
public 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()方法调用时,还是会读取剩余未读数据,查询没法提前终止,故放弃之。
一次线上OOM问题的个人复盘的更多相关文章
- 一次线上OOM故障排查经过
转贴:http://my.oschina.net/flashsword/blog/205266 本文是一次线上OOM故障排查的经过,内容比较基础但是真实,主要是记录一下,没有OOM排查经验的同学也可以 ...
- 【转】又一次线上 OOM 排查经过
又一次线上OOM排查经过 最近线上一个服务又出现了频繁Full GC的情况,导致提供的业务经常超时.问题出现非常不稳定,经过两周的时候,终于又捕捉到了一次Full GC,于是联系运维做Heap Dum ...
- 火山引擎MARS-APM Plus x 飞书 |降低线上OOM,提高App性能稳定性
通过使用火山引擎MARS-APM Plus的memory graph功能,飞书研发团队有效分析定位问题线上case多达30例,线上OOM率降低到了0.8‰,降幅达到60%.大幅提升了用户体验,为飞书的 ...
- 线上一次大量 CLOSE_WAIT 复盘
https://mp.weixin.qq.com/s/PfM3hEsDa3CMLbbKqis-og 线上一次大量 CLOSE_WAIT 复盘 原创 ms2008 poslua 2019-07-05 最 ...
- 记一次log4j日志导致线上OOM问题案例
最近一个服务突然出现 OutOfMemoryError,两台服务因为这个原因挂掉了,一直在full gc.还因为这个问题我们小组吃了一个线上故障.很是纳闷,一直运行的好好的,怎么突然就不行了呢... ...
- 记一次ArrayList产生的线上OOM问题
前言:本以为(OutOfMemoryError)OOM问题会离我们很远,但在一次生产上线灰度的过程中就出现了Java.Lang.OutOfMemoryError:Java heap space异常,通 ...
- 记一次线上 OOM 和性能优化
大家好,我是鸭血粉丝(大家会亲切的喊我 「阿粉」),是一位喜欢吃鸭血粉丝的程序员,回想起之前线上出现 OOM 的场景,毕竟当时是第一次遇到这么 紧脏 的大事,要好好记录下来. 1 事情回顾 在某次周五 ...
- 记一次线上OOM问题分析与解决
一.问题情况 最近用户反映系统响应越来越慢,而且不是偶发性的慢.根据后台日志,可以看到系统已经有oom现象. 根据jdk自带的jconsole工具,可以监视到系统处于堵塞时期.cup占满,活动线程数持 ...
- 记一次 android 线上 oom 问题
背景 公司的主打产品是一款跨平台的 App,我的部门负责为它提供底层的 sdk 用于数据传输,我负责的是 Adnroid 端的 sdk 开发. sdk 并不直接加载在 App 主进程,而是隔离在一个单 ...
- 一次线上OOM过程的排查
https://blog.csdn.net/qq_16681169/article/details/53296137 一.出现问题 在前一段时间日常环境很不稳定,前端调用mtop接口会出网络异常或服务 ...
随机推荐
- 微软开源 Python 自动化神器 Playwright
背景 逛博客时候突然看到 Playwright web自动化,感觉很有意思,就翻看了很多博客,简单记录一下. 简介 Playwright是一个强大的Python库,仅用一个API即可自动执行Chrom ...
- java pta第三次阶段性总结
一.前言 这是这学期最后一次总结,这三次的pta大作业也是最后一次,这几次大作业主要写了电信计费系统的主要功能,第一次大作业是座机计费功能,第二次大作业是手机计费功能,第三次大作业是短信计费的功能.这 ...
- uniapp 全局注册组件注意事项
标准 根目录components 文件夹下建立 组件文件名文件夹 然后组件 autoscan 打开 别的用不到不写 全局使用 备注 因为不是vuecli 项目 只在H5 端生效 在app 上生 ...
- 面向对象程序设计 第二章 C++简单的程序设计
目录 C++语言的特点 1.兼容C语言 · 它保持了C的简洁.高效和接近汇编语言等特点. · 对C的类型系统进行了改革和扩充. · C++也支持面向过程的程序设计,不是一个纯正的面向对象的语言 2.支 ...
- docker临时指定时区
如果制作镜像时,未配置时区,默认指向了 UTC ,可使用类似如下方式临时指向北京时间,或上海时间 docker exec -it --user=root gitlab-ce_12.2.4 ln -sf ...
- 微信端手机跨域上传图片偶尔跑进error
如题.我这两天被这个问题搞得焦头烂额.其他端都没问题,就微信端的有问题,就是因为通过ajax上传图片的时候,设置了 async: false,然后客户用了测试总是跑进error,客户问什么情况,我也找 ...
- unittest "ResourceWarning: unclosed <socket.socket fd=864, family=AddressFamily.AF_INET..." 解决办法...
将代码封装,并使用unittest调用时,返回如下警告: E:\intall\python-3.7.4-amd64\lib\unittest\suite.py:84: ResourceWarning: ...
- OSIDP-虚拟内存-08
硬件和控制结构 实际内存管理特点 1.一个进程可以在执行过程中换入换出内存,因而在内存中的位置可以不断变化. 2.一个进程可以划分为多个块,这些块位于内存中的地址不需要是连续的. 进程执行的任何时候都 ...
- antv g6 出现 n.addEdge is not a function问题
问题描述直接上图 解决方式就是将edge里面边的source和target对应的id换成字符串类型就行. 例如: edges: [ { id: 299, source": 3629.toSt ...
- 在gibhub上传本地项目代码(新手入门)
一.首先注册github账号 地址:https://github.com/ 二.其次下载安装git工具 地址:https://gitforwindows.org/ 直接进入安装,这里就不多做介绍 三. ...