原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,非公众号转载保留此声明。

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

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

  1. <sql id="conditions">
  2. <where>
  3. <if test="outerId != null">
  4. and `outer_id` = #{outerId}
  5. </if>
  6. <if test="orderType != null and orderType != ''">
  7. and `order_type` = #{orderType}
  8. </if>
  9. ...
  10. </where>
  11. </sql>
  12. <select id="queryListByConditions" resultMap="orderResultMap">
  13. select * from order <include refid="conditions"/>
  14. </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脚本如下:

  1. if [ -f "/tmp/health_check_failed" ]; then
  2. echo "Health check failed, perform dumping and cleanups...";
  3. pid=`ps h -o pid --sort=-pmem -C java|head -n1|xargs`;
  4. if [[ $pid ]]; then
  5. jmap -dump:format=b,file=/home/work/logs/applogs/heap.hprof $pid
  6. fi
  7. else
  8. echo "No health check failure detected. Exiting gracefully.";
  9. fi

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

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

  1. #!/bin/bash
  2. while sleep 1; do
  3. now_time=$(date +%F_%H-%M-%S)
  4. pid=`ps h -o pid --sort=-pmem -C java|head -n1|xargs`;
  5. [[ ! $pid ]] && { unset n pre_fgc; sleep 1m; continue; }
  6. data=$(jstat -gcutil $pid|awk 'NR>1{print $4,$(NF-2)}');
  7. read old fgc <<<"$data";
  8. echo "$now_time: $old $fgc";
  9. if [[ $(echo $old|awk '$1>80{print $0}') ]]; then
  10. (( n++ ))
  11. else
  12. (( n=0 ))
  13. fi
  14. if [[ $n -ge 3 || $pre_fgc && $fgc -gt $pre_fgc && $n -ge 1 ]]; then
  15. jstack $pid > /home/dump/jstack-$now_time.log;
  16. if [[ "$@" =~ dump ]];then
  17. jmap -dump:format=b,file=/home/dump/heap-$now_time.hprof $pid;
  18. else
  19. jmap -histo $pid > /home/dump/histo-$now_time.log;
  20. fi
  21. { unset n pre_fgc; sleep 1m; continue; }
  22. fi
  23. pre_fgc=$fgc
  24. 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实现的话,大致如下:

  1. public class SqlLimitFilter extends FilterAdapter {
  2. // 匹配limit 100或limit 100,100
  3. private static final Pattern HAS_LIMIT_PAT = Pattern.compile(
  4. "LIMIT\\s+[\\d?]+(\\s*,\\s*[\\d+?])?\\s*$", Pattern.CASE_INSENSITIVE);
  5. private static final int MAX_ALLOW_ROWS = 20000;
  6. /**
  7. * 若查询语句没有limit,自动加limit
  8. * @return 新sql
  9. */
  10. private String rewriteSql(String sql) {
  11. String trimSql = StringUtils.stripToEmpty(sql);
  12. // 不是查询sql,不重写
  13. if (!StringUtils.lowerCase(trimSql).startsWith("select")) {
  14. return sql;
  15. }
  16. // 去掉尾部分号
  17. boolean hasSemicolon = false;
  18. if (trimSql.endsWith(";")) {
  19. hasSemicolon = true;
  20. trimSql = trimSql.substring(0, trimSql.length() - 1);
  21. }
  22. // 还包含分号,说明是多条sql,不重写
  23. if (trimSql.contains(";")) {
  24. return sql;
  25. }
  26. // 有limit语句,不重写
  27. int idx = StringUtils.lowerCase(trimSql).indexOf("limit");
  28. if (idx > -1 && HAS_LIMIT_PAT.matcher(trimSql.substring(idx)).find()) {
  29. return sql;
  30. }
  31. StringBuilder sqlSb = new StringBuilder();
  32. sqlSb.append(trimSql).append(" LIMIT ").append(MAX_ALLOW_ROWS);
  33. if (hasSemicolon) {
  34. sqlSb.append(";");
  35. }
  36. return sqlSb.toString();
  37. }
  38. @Override
  39. public PreparedStatementProxy connection_prepareStatement(FilterChain chain, ConnectionProxy connection, String sql)
  40. throws SQLException {
  41. String newSql = rewriteSql(sql);
  42. return super.connection_prepareStatement(chain, connection, newSql);
  43. }
  44. //...此处省略了其它重载方法
  45. }

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

一次线上OOM问题的个人复盘的更多相关文章

  1. 一次线上OOM故障排查经过

    转贴:http://my.oschina.net/flashsword/blog/205266 本文是一次线上OOM故障排查的经过,内容比较基础但是真实,主要是记录一下,没有OOM排查经验的同学也可以 ...

  2. 【转】又一次线上 OOM 排查经过

    又一次线上OOM排查经过 最近线上一个服务又出现了频繁Full GC的情况,导致提供的业务经常超时.问题出现非常不稳定,经过两周的时候,终于又捕捉到了一次Full GC,于是联系运维做Heap Dum ...

  3. 火山引擎MARS-APM Plus x 飞书 |降低线上OOM,提高App性能稳定性

    通过使用火山引擎MARS-APM Plus的memory graph功能,飞书研发团队有效分析定位问题线上case多达30例,线上OOM率降低到了0.8‰,降幅达到60%.大幅提升了用户体验,为飞书的 ...

  4. 线上一次大量 CLOSE_WAIT 复盘

    https://mp.weixin.qq.com/s/PfM3hEsDa3CMLbbKqis-og 线上一次大量 CLOSE_WAIT 复盘 原创 ms2008 poslua 2019-07-05 最 ...

  5. 记一次log4j日志导致线上OOM问题案例

    最近一个服务突然出现 OutOfMemoryError,两台服务因为这个原因挂掉了,一直在full gc.还因为这个问题我们小组吃了一个线上故障.很是纳闷,一直运行的好好的,怎么突然就不行了呢... ...

  6. 记一次ArrayList产生的线上OOM问题

    前言:本以为(OutOfMemoryError)OOM问题会离我们很远,但在一次生产上线灰度的过程中就出现了Java.Lang.OutOfMemoryError:Java heap space异常,通 ...

  7. 记一次线上 OOM 和性能优化

    大家好,我是鸭血粉丝(大家会亲切的喊我 「阿粉」),是一位喜欢吃鸭血粉丝的程序员,回想起之前线上出现 OOM 的场景,毕竟当时是第一次遇到这么 紧脏 的大事,要好好记录下来. 1 事情回顾 在某次周五 ...

  8. 记一次线上OOM问题分析与解决

    一.问题情况 最近用户反映系统响应越来越慢,而且不是偶发性的慢.根据后台日志,可以看到系统已经有oom现象. 根据jdk自带的jconsole工具,可以监视到系统处于堵塞时期.cup占满,活动线程数持 ...

  9. 记一次 android 线上 oom 问题

    背景 公司的主打产品是一款跨平台的 App,我的部门负责为它提供底层的 sdk 用于数据传输,我负责的是 Adnroid 端的 sdk 开发. sdk 并不直接加载在 App 主进程,而是隔离在一个单 ...

  10. 一次线上OOM过程的排查

    https://blog.csdn.net/qq_16681169/article/details/53296137 一.出现问题 在前一段时间日常环境很不稳定,前端调用mtop接口会出网络异常或服务 ...

随机推荐

  1. java Comparator和Comparable的区别?

    参考:https://blog.csdn.net/m0_71087031/article/details/124850080 Comparable是一个内比较器,可以和自己比较的 Comparator ...

  2. python+pytest接口自动化

    本篇文章是用python+pytest写了一个简单的接口自动化脚本,外加循环请求接口的语法,大家可以参考~ 实例一: import requestsimport pytestimport time c ...

  3. c# WinForm 多次点击这个按钮会弹出多个窗体, 怎么才能只显示一个窗体。解决方案!

    第一种解决方法 "单例" <mark> 书上有 private void toolStripLabel1_Click(object sender, EventArgs ...

  4. Array of products

    refer to: https://www.algoexpert.io/questions/Array%20Of%20Products Problem Statement Sample input A ...

  5. doy 20 系统优化

    系统优化 1.yum源的优化 CentOS   base   epel ​自建yum仓库​使用一个较为稳定的仓库​wget -O /etc/yum.repos.d/CentOS-Base.repo h ...

  6. 自学JavaDay02_class01

    注释 单行注释: //单行注释 多行注释 /** 多行注释* 多行注释* */ 文档注释 /** * 文档注释 * 文档注释 */ 标识符 关键字 标识符 所有的标识符都应该以字母(A-Z 或者 a- ...

  7. idea 改变片段内相同变量的快捷键

    在 win系统中 shift+F6 在 ios系统中Fn+shift+F6

  8. MySql 字符串时间转换

    MySql中经常遇到字符串格式时间转换成时间类型的情况: SELECT STR_TO_DATE('Jul 20 2013 7:49:14:610AM','%b %d %Y %h:%i:%s:%f%p' ...

  9. Q:oracle 日期筛选

    一.oracle where条件日期筛选 两种方法:tochar和todate todate:将字符串按照指定的格式输出,得到的是日期类型. to_date('2019-12-01','yyyy-MM ...

  10. Docker 环境规划 (Docker安装)

    一.环境规划 支持Java.dotNet.Vue项目构建 二.切换系统镜像源   1.备份      mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.rep ...