游戏开发网站建设,做暧电影在线观看网站,关闭网站怎么不保存我做的更改,企业网络拓扑图的设计方案我们一个java服务上线后#xff0c;偶尔会发生内存OOM(Out Of Memory)问题#xff0c;但由于OOM导致服务不响应请求#xff0c;健康检查多次不通过#xff0c;最后部署平台kill了java进程#xff0c;这导致定位这次OOM问题也变得困难起来。 最终#xff0c;在多次review代…我们一个java服务上线后偶尔会发生内存OOM(Out Of Memory)问题但由于OOM导致服务不响应请求健康检查多次不通过最后部署平台kill了java进程这导致定位这次OOM问题也变得困难起来。 最终在多次review代码后发现是SQL意外地查出大量数据导致的如下
sql idconditionswhereif testouterId ! nulland outer_id #{outerId}/ifif testorderType ! null and orderType ! and order_type #{orderType}/if.../where
/sqlselect idqueryListByConditions resultMaporderResultMapselect * from order include refidconditions/
/select查询逻辑类似上面的示例在Service层有个根据outer_id的查询方法然后直接调用了Mapper层一个通用查询方法queryListByConditions。
但我们有个调用量极低的场景可以不传outer_id这个参数导致这个通用查询方法没有添加这个过滤条件导致查了全表进而导致OOM问题。
我们内部对这个问题进行了复盘考虑到OOM问题还是蛮常见的所以给大家也分享下。
事前#
在OOM问题发生前为什么测试阶段没有发现问题
其实在编写技术方案时是有考虑到这个场景的但在提测时忘记和测试同学沟通此场景导致遗漏了此场景的测试验证。
关于测试用例不全面其实不管是疏忽问题、经验问题、质量意识问题或人手紧张问题从人的角度来说都很难彻底避免人没法像机器那样很听话的、不疏漏的执行任何指令。
既然人做不到那就让机器来做这就是单元测试、自动化测试的优势通过逐步积累测试用例可覆盖的场景就会越来越多。
当然实施单元测试等方案也会增加不少成本需要权衡质量与研发效率谁更重要毕竟在需求不能砍的情况下质量与效率的关系是得此失彼这是任何一本项目管理的书都提到过的。
事中#
在感知到OOM问题发生时由于进程被部署平台kill导致现场丢失难以快速定位到问题点。
一般java里面是推荐使用-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/home/dump/这种JVM参数来保存现场的这两个参数的意思是当JVM发生OOM异常时自动dump堆内存到文件中但在我们的场景中这个方案难以生效如下
在堆占满之前会发生很多次FGCjvm会尽最大努力腾挪空间导致还没有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脚本中判断存在此文件才dumppreStop脚本如下
if [ -f /tmp/health_check_failed ]; thenecho Health check failed, perform dumping and cleanups...;pidps h -o pid --sort-pmem -C java|head -n1|xargs;if [[ $pid ]]; thenjmap -dump:formatb,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)pidps h -o pid --sort-pmem -C java|head -n1|xargs;[[ ! $pid ]] { unset n pre_fgc; sleep 1m; continue; }data$(jstat -gcutil $pid|awk NR1{print $4,$(NF-2)});read old fgc $data;echo $now_time: $old $fgc;if [[ $(echo $old|awk $180{print $0}) ]]; then(( n ))else(( n0 ))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:formatb,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()方法调用时还是会读取剩余未读数据查询没法提前终止故放弃之。