Case 1 递归调用
当时有一个非常重要的系统,我们设计了一个链路监控机制,也就是会在一个比较核心的链路节点,写一些重要的日志到Elasticsearch集群里去,事后会基于ELK进行核心链路日志的一些分析,如下图所示。
同时我们对这个机制做了规定,如果在某个节点写日志时发生了某些异常,此时也必须将这个链路节点的异常写入ES集群里去,因为我们在分析的时候,需要知道系统运行到这里有一个异常。
不知道大家看了上面的代码是作何感想?当时这个同学居然在log()方法中一旦ES集群出现故障的时候再次调用了自己,继续尝试将日志写入ES集群。
因此在线上系统中,有一次ES集群短暂故障了一会儿,结果直接就导致log()方法中写ES集群每次都是失败的,都会抛异常。
而一旦抛异常进入了catch语句中,就会再次重新回过头来调用log()方法。
然后log()方法再次写ES集群发现不行,继续抛异常进入catch中,再次循环调用自己。
线上系统本来在ES集群故障的时候不该有什么问题的,因为核心业务逻辑都是可以运行的,最多不过就是无法把核心日志写入ES集群罢了。
但是因为这个bug,导致在ES故障时,所有系统全部在写日志的时候,陷入了一个无限循环调用log()方法的困境中。
之前演示过,一旦无限循环调用方法自己,一定会在一定时间导致线程的栈内存溢出的,此时直接会导致JVM进程的崩溃
系统居然因为这么一个小问题崩溃了!这就是一次非常真实的线上案例。
后来针对此类问题,我们都是通过严格的持续集成+严格的Code Review标准来避免的
Case2 没有缓存的动态代理
简单来说,想要实现一个动态代理机制,也就是说在系统运行的时候,针对已有的某个类,生成一个动态代理类,也就是动态生成类,然后对那个类的一些方法调用做一些额外的处理。
当时大概的一个伪代码
不知道大家发现类似这种代码里的一个问题没有?比如你用CGLIB的Enhancer针对某个类动态生成了一个子类,这个子类你完全可以缓存起来,下次直接用这个已经生成好的子类来创建对象就可以了
类似下面这样:
其实这个类只要生成一次就可以了,下次来直接用这个动态生成的类创建一个对象就可以了。
但是当时那个工程师没有缓存这个动态生成的类,就是每次调用方法都生成一个类,这就闯祸了。
有一次线上系统负载很高的时候,因为这个框架直接导致瞬间创建了一大堆的类,塞满了Metaspace区域无法回收,进而导致Metaspace区域直接内存溢出,系统也崩溃了,这也是一个很大的问题。
后来对于这类问题,是严格要求每次上线必须走严格的自动化压力测试,通过高并发压力下系统是否正常运行支撑24小时,来判断是否可以上线。
这样类似于这类代码在上线之前就会被压力测试露出马脚,因为压力一大,瞬间会引发这个问题。
小结
我们带着大家感受了一下各种内存溢出发生的场景,同时给出了几个真实的线上生产案例是如何导致各个内存区域溢出的
相信大家对内存溢出这个问题,有了一个更加深刻的理解。
接下来我们会带着大家一起来学习如何对线上的OOM进行监控,同时在OOM时如何让JVM自动保留现场,同时结合几个案例和工具学习,发生OOM之后如何快速排查和定位到底代码哪里出现了OOM,以及如何进行解决。