首页 > 其他分享 >一次线上OOM问题的个人复盘

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

时间:2023-04-06 21:01:51浏览次数:49  
标签:dump OOM pid trimSql limit 线上 sql 复盘

上个月,我们一个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" ]; 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()方法调用时,还是会读取剩余未读数据,查询没法提前终止,故放弃之。

标签:dump,OOM,pid,trimSql,limit,线上,sql,复盘
From: https://www.cnblogs.com/babyb/p/17294136.html

相关文章

  • 大意了,一次多线程操作不当导致的线上事故...
    大家好,我是飘渺。今天给大家分享个生产事故,一个由于多线程操作不当导致的线上事故,事情是这样的~事故描述从6点32分开始少量用户访问app时会出现首页访问异常,到7点20分首页服务大规模不可用,7点36分问题解决。整体经过6:58发现报警,同时发现群里反馈首页出现网络繁忙,考虑到前几日晚上......
  • 2023 海外工具站 2 月复盘
    观点:关于AIGC最近看的这块挺多。分享下我对AIGC的一些观点AIGC(AIGeneratedContent)是由AI生成的内容。我认为的内容很多,文字、图片、视频、音频、3D等等观点1:AIGC不应该卷互联网行业,for工业for生产。比如服装来源于设计稿,应该由AI辅助,让服装设计plus下观点2......
  • Codeforces Round 863 (Div. 3) A-C 赛后思路复盘
    A(思维)思路:观察样例可知数越大放在前面越优。遍历字符串,判断当前位置的数字和要插入的数字的关系,如果要插入的数大于当前数,那么就插入到当前数的前面。string里有一个insert函数,可以把指定字符串插入到指定下标之前。在原串下标为pos的字符前插入字符串strbasic_string&insert......
  • 深度学习平台——百度AI Studio线上构建
    PaddleDetection的安装和使用以及训练和评估这里是百度的AI开发平台:https://ai.baidu.com/ai-doc/AISTUDIO/Tk39ty6ho目前对于个人来说的,学习深度学习的来说的,需要时一定的计算资源。但是考虑到学校实验室资源有限,所以本人采用AI系统平台的提高的服务来实现个人的实验的相关问题。......
  • 记几次 [线上环境] Dubbo 线程池占满原因分析(第二次:CompletableFuture)
    转载:https://blog.csdn.net/wsmalltiger/article/details/124236189文章目录[线上环境]Dubbo线程池占满原因排查系列前言一、问题分析1、分析日志2、定位原因二、解决方案三、总结前言  某天早上9点左右收到线上故障报警,超过3个商家反馈“无法正常进入功能页面,点击相关操作提......
  • 记几次 [线上环境] Dubbo 线程池占满原因分析(第一次:HttpClient)
    转载:https://blog.csdn.net/wsmalltiger/article/details/124236055前言  我们一个核心应用,线上部署了4台机器(4c8g),某天晚上8点左右线上忽然出现dubbo线程池占满告警,上游应用error日志也疯狂报警,整个过程持续了4分钟左右系统自动恢复正常。  dubbo默认200个线程池,报错日志信......
  • 线上发现了bug该如何处理
    线上发现了bug该如何处理今天在国外论坛看到了个很有意思的发帖,有人提问:线上发现了bug该如何处理。我知道大家已经问过很多次类似的问题了,不过工作还是很让我失望。我在生产环境上漏掉了1个很明显的bug没测出来,我想知道你们是怎么处理这种情况的。我的项目经理发现了这个bug。......
  • 记几次 [线上环境] Dubbo 线程池占满原因分析(第三次:GC STW)
    转载:https://blog.csdn.net/wsmalltiger/article/details/124236206前言  某天晚上正在开开心心写代码,忽然收到了线上告警:dubbo线程池活跃线程数告警、应用错误日志告警、dubbo线程池队列长度告警;瞬间意识到要出大事情了,得赶紧定位到原因并解决问题,不然时间长了肯定会影响商......
  • 吉林互联网医院牌照代办|开发线上问诊APP的优势
     吉林互联网医院牌照代办|开发线上问诊APP的优势?近年来,网上问诊、在线就医、以及无接触购药等模式与日常的生活紧密的相融,互联网医疗的用户规模也在急速的攀升。其中,很多的医院都在第一时间开发了线上问诊的服务,最大限度地服务患者,提升了用户的就诊体验,对于排查那些疑似病例、避......
  • 2023 海外工具站 1 月复盘
    认知:海外工具站的杠杆大家都听过:商业杠杆来自资本、劳动力和复制边际成本为零的产品(代码和媒体)。-《纳瓦尔宝典》海外工具站,是属于代码杠杆,属于边际成本几乎零。所以找到Niche的海外付费工具站,然后拼命堆流量。认知:商业思维转型经常对自己说:“别成为PPT程序员,少巴拉......