likes
comments
collection
share

记一次 stackoverflowerror 线上排查过程

作者站长头像
站长
· 阅读数 5

一.线上 stackOverFlowError

记一次 stackoverflowerror 线上排查过程

    xxx日,突然收到线上日志关键字频繁告警 classCastException.从字面上的报警来看,仅仅是类型转换异常,查看细则发现其实是 stackOverFlowError.很多同学面试的时候总会被问到有没有遇到过线上stackOverFlowError?有么有遇到后栈溢出?今天他来了,他带着问题走来了.话不说多,直入正题.具体细则如下

记一次 stackoverflowerror 线上排查过程

二.优先线上问题解决

记一次 stackoverflowerror 线上排查过程

请原谅我抽象画风

    temp 方案.首先的线上的稳定性肯定是第一要义,客户可不会等你长篇大论抓包,分析,debug.过了30min还不恢复,资本的大刀就要砍到你身上了.所以我们先想到的是代码回退,镜像回滚解决问题优先.虽然说是临时方案,那这时候我觉得这可能是最重要的最佳方案.毕竟老镜像是不会出任何问题的.

三.继续深入分析

    解决完线上的问题后,先从外层的堆栈打印来看,找到 ClassCastException 这里找到真实的原因,毕竟退下来的不仅仅是坏代码,还有需求迭代的正常需求还是需要继续推上去上线.

3.1 整体的流程梳理

记一次 stackoverflowerror 线上排查过程

    找到报错第一步:

3.1.1 step1: classCastException

    先表象开始分析

记一次 stackoverflowerror 线上排查过程
记一次 stackoverflowerror 线上排查过程

从这里可以看到判断了是否为 Throwable 类型.如果是就进行 Exception 强转.这里就要复习一下了.

记一次 stackoverflowerror 线上排查过程

StackOverFlowError 继承 Error ,ErrorThrowable 继承而来. Exception 则是另外的分支. 对于 ErrorException 也有通行的原则. Exception 一般是程序中用以来抛出程序异常所使用的且一般是能够通过编码优化来解决的,或是用来 try catch exception 来进行捕获处理的. Error 则是用来表达程序运行期间出现的严重错误,这时候通常是jvm级别的.如常见的OutOfMemoryError,stackOverFlowError.等.通常则是无法通过代码来进行捕获的.

    有了这些基础知识后,再回来这里虽然StackOverFlowErrorException都继承于 Throwable .但这是两个子的实现,没法做到强转.由之得到了 ClassCastException .后面这就是转成了 ClassCastException .这个类则是继承自 Exception .通过 try catch 捕获异常后,得到了正常的日志打印,也就是收到的日志告警. 然后这仅仅是表现.根因还没有找到.

    当然这段代码也需要进行优化.如果得到的是Error的类型就要对应的进行Error的处理而不是仅仅对Throwable都统一强转为Exception 代码优化

 Exception exception = null;
 f(ar instanceof Error){
      Error arError=(Error)ar; 
      exception=new Exception (arError);
 }else if(ar instanceof Exception){
      exception = (Exception) ar;
 }

3.1.2 step2:事情远没有结束,到底是哪里出问题 StackOverFlowError

    本质上还是由于StackOverFlowError才得到的如上的 ClassCastException. 回忆下 JVM 的内存布局(如下图)

记一次 stackoverflowerror 线上排查过程

    能发生 StackOverFlowError 只有在线程私有的 stack(native method stack | virtual method stack) 这里.这里通常发生这个错误的原因是因为方法调度的深度过长了或是线程本身分别的内存太小不足以支持现在的复杂调用.

  • 第一种场景:常见的如递归调用.
  • 第二种场景: jvm 在1.5 之后默认的xss 大小默认为 1m.一般场景下支持1000-2000个深度调用没问题.包括递归.(没试过.数值参考自:深入理解java虚拟机)

3.1.3 找到问题对比代码

    从一般情况下第二种场景不太可能出现.还是回到递归调用引起的.排查代码.花不多少,看代码,通过对比版本之间diff(对比时间稍微有点长).简略如下:

无问题代码

private static void error(Logger logger, String message, Object... arg) {
        if (isLogOn(LogLevelEnum.ERROR, logger)) {
            if (arg != null && arg.length > 0 && arg[0] instanceof Throwable) {
                logger.error(message, arg[0]);
            } else {
                logger.error(message, arg);
            }

            TRACER_LOGGER.error(message, arg);
        }
    }
public static void error(Object... arg) {
        String message = getMessage("{}", 4, arg);
        error(getSoaErrorLogger(), message, arg);
    }

    public static void error(String message, Object... arg) {
        message = getMessage(message, 4, arg);
        error(getSoaErrorLogger(), message, arg);
    }

代码优化后的代码 有问题版

private static void error(Logger logger,String realMessage, String message, Object... arg) {
        if (isLogOn(LogLevelEnum.ERROR, logger)) {
            if (arg != null && arg.length > 0 && arg[0] instanceof Throwable) {
                logger.error(message, arg[0]);
            } else {
                logger.error(message, arg);
            }

            TRACER_LOGGER.error(message, arg);
        }
    }
public static void error(Object... arg) {
        String message = getMessage("{}", 4, arg);
        error(getSoaErrorLogger(), message, arg);
    }

    public static void error(String message, Object... arg) {
        message = getMessage(message, 4, arg);
        final String realMessage=message;
        error(getSoaErrorLogger(),realMessage, message, arg);
    }

代码优化后的代码 完善版

private static void error(Logger logger,String realMessage, String message, Object... arg) {
        if (isLogOn(LogLevelEnum.ERROR, logger)) {
            if (arg != null && arg.length > 0 && arg[0] instanceof Throwable) {
                logger.error(message, arg[0]);
            } else {
                logger.error(message, arg);
            }

            TRACER_LOGGER.error(message, arg);
        }
    }
public static void error(Object... arg) {
        String message = getMessage("{}", 4, arg);
        final String realMessage=message;
        error(getSoaErrorLogger(),realMessage, message, arg);
    }

    public static void error(String message, Object... arg) {
		final String realMessage=message;
        message = getMessage(message, 4, arg);        
        error(getSoaErrorLogger(),realMessage, message, arg);
    }

    咋一看没有任何问题.但是上线后出现第二个方法递归调用自身(但是第二个方法没有变更内容哈).本质上的原因就是因为修改第一个方法增加了入参.但是仅修改了第三个方法,第二个方法没有修改.没有出现编译问题.因为本身第二个方法是一个Object... arg的数组调用.好坑.

四.总结

  • 区别ErrorException
  • 尽量不使用,少使用数组式使用.如String... args.Integer... args .即使要用,也尽量不要用Object... args .避免调用错误.
  • 在做技术优化时,尽可能评估影响,对线上抱有充分的敬畏.慎之又慎.如没有特别的收益,可不上线.上线也要保证每一行改动与本次受影响的代码做到测试
  • 修改代码找到所有find usage ,避免出现错改,漏改.可以利用自带IDE的工具 做到.
记一次 stackoverflowerror 线上排查过程