在开发Flink程序中,遇到了两次OOM的处理,挺有代表性的,其中第二次的难度很高,需要对Java虚拟机有很深入的理解。

1 第一次

第一次问题不是很复杂,我们业务使用RabblitMQ作为数据源,当数据积压时,任务在启动就会导致TaskManager出现OOM的错误。
错误现象:

An exception occurred processing Appender DefaultConsole-3 org.apache.logging.log4j.core.appender.AppenderLoggingException: 
    java.lang.OutOfMemoryError: Java heap space

对于TaskManager的内存划分,资料很多,最有用的是内存计算Excel表,可以输入配置内存后自动算出所有的内存大小,在UI上也能看出,意思就不做介绍了。

1.1 定位问题出现原因

为了查看OOM时JVM的状态,我们先在TaskManager的启动项上加入参数,在flink-conf.yaml加入:

env.java.opts.taskmanager: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/bigdata/tmp

再次错误时,能看到/home/bigdata/tmp生成了文件:java_pid***.hprof。将文件拷贝到本机,使用jvisualvm打开后,按照大小排序。然后再实例内分析。

env.java.opts env.java.opts.taskmanager_flink


可以将byte拷贝出来转换一下,看到内容是rabbitmq source的内容,右边的类Delivery的next属性包含相似的内容,所以猜测是因为接收数据推送过多,处理不过来。这些内存又不能回收,从而导致Heap区OOM。解决办法就是一方面,加大TaskManager的内存,另一方面对rabbitmq source限流(正道),可以通过实际环境配置合适的参数。

1.2 解决

限流开启对RabblitMQ的版本有一定要求,我们使用的Ubuntu14.04需要手动升级。
Rabbitmq版本太低,导致不能使用prefetch,升级rabbitmq到3.6解决。
升级参考这个不要太简单。下载地址
prefetch开启,必须要使用checkpoint来才有效,否则会自动提交ack。这时,checkpoint周期和prefetch count共同决定了处理速率(checkpoint时才会提交ack,prefetch count控制rabbitmq server端未提交的数量,超过了就不发送ack了)。

第二次

第二次的OOM就复杂多了,这个现象应该是Flink应用特有的(相比于Spark),Flink的任务会共用虚拟机运行,为了隔离不同的任务环境,使用了自定义的ClassLoader来加载应用,当出现类加载泄露时,就会很容易出现Metaspace OOM(java 8环境)。

1 背景

一般的应用,不会导致大量的类加载,但是我为Flink任务集成了Spring Cloud,目的是更好的与现有的业务系统集成,Flink应用一般处理实时业务,相对Spark SQL单纯的进行数据处理,业务复杂程度更高,和Spring Cloud集成会有很大的优势。带来的问题时,加载了相对多的类,如果不能正确释放,很容易造成类加载泄露。
这个过程,相当痛苦,最后成功集成InfluxDB Client、mybatis、Eureka Client和openFeign时,解决了大概5-6出类加载泄露的来源。
类加载的问题比较难解决的原因,是因为如果ClassLoader不能回收,意味着所有的类都不能回收,不借助能够解析ClassLoader回收路径的工具,根本无从分析。而ClassLoader的回收的条件也比较严格,需要:类实例回收、类可回收和ClassLoader可回收同时满足,由于Spring Cloud依赖非常多,使用中不经意就被其他ClassLoader类加载的类所引用,从而导致类加载泄露。

2 常见的场景

1、线程没有释放
2、静态变量、方法产生的对象没有释放(被其他ClassLoader入bootstrap、app等加载的类引用)

3 解决手段和思路

1、编写测试框架,可以手动多次使用自定义加载类模拟flink task执行程序库(SpringCloudInFlink被设计成一个库,有时间整理一下传到github上)
2、使用jVisualVM,查看多次执行程序并手动触发gc后,未释放的线程和类资源,判断是否有类加载泄露问题
3、使用eclipse MAT,分析类加载器不能释放的类,有时候还要分析弱引用的情况(不容易啊),针对每种未释放的类进行解决

4 解决过程记录

解决类加载泄露,首先要加入-XX:+TraceClassLoading -XX:+TraceClassUnloading参数,查看类加载的情况。通过分步骤引入依赖,来解决类加载泄露问题。

4.1 InfluxDB Client类加载泄露解决

通过jVisualVM查看线程,发现有异步写入线程没有退出,在相关Spring Boot Configuration的destroy属性设置关闭该线程的方法得到解决。

4.2 mybatis类加载泄露解决

通过jVisualVM查看线程,发现Abandoned connection cleanup的线程没有退出。解决退出后,还是无法卸载类。
再通过经验,查看Mysql driver注册源码,发现使用DriverManager进行了注册,DriverManager是javax开头会使用bootstrap加载,所以在自定义Classloader退出时不能被卸载,需要将mysql driver从DriverManager里退注册。

4.3 Eureka Client类加载泄露解决

这里用到了eclipse MAT,使用方法见博客。MAT简直不要太强大。
通过MAT很容易找出ClassLoader的不能回收的引用,解决jboss log问题,但是还是不能卸载。进一步分析软引用,通过调用jackson的release解决。

4.4 openFeign无泄露

总结

有点儿流水账,解决OMM特别是Metaspace的OOM并不轻松,所幸现在的分析工具已经非常犀利,但是你必须要有深厚的JVM虚拟机基础,特别是对类加载机制、双亲委派、垃圾回收等原理有深刻理解,才能对遇到的千奇百怪的类加载泄露来源从容处理。感谢jVisualVM/MAT工具、《深入理解JVM虚拟机》和Google到的各种博客,在多次准备放弃时,给我启示,最终能实现出一个比较完美的在Flink任务中使用的集成Spring Cloud框架。