共计 6255 个字符,预计需要花费 16 分钟才能阅读完成。
一、背景
本文将用一个蚂蚁团体线上理论案例,分享咱们是如何排查因为 inflation 引起的 MetaSpace FGC 问题。
蚂蚁团体的智能监控平台深度利用了 Spark 的能力进行多维度数据聚合,Spark 因为其高效、易用、分布式的能力在大数据处理中非常受欢迎。
对于智能监控的计算能力能够参考《蚂蚁金服在 Service Mesh 监控落地经验总结》。
二、案例背景
在某次线上问题中呈现不间断性的工作彪高与积压,数据产出提早,十分不合乎预期。查看 SparkUI 的 Event Timeline 发现下边的景象:
大家应该都晓得 Spark job 工作节点分为 driver 和 executor,driver 更多的是工作治理和散发,executor 负责工作的执行。在整个 Spark job 生命周期开始时这两种角色被新建进去,存活到 Spark job 生命周期完结。而上图中的状况个别为 executor 因为各种异常情况失去心跳而被被动替换。查看对应 executor 的日志发现有 2 分钟没有打印,狐疑为 FGC 卡死了。最终在另一个现场找到了 gc 日志:
2020-06-29T13:59:44.454+0800: 55336.665: [Full GC (Metadata GC Threshold) 2020-06-29T13:59:44.454+0800: 55336.665: [CMS[YG occupancy: 2295820 K (5242880 K)]2020-06-29T13:59:45.105+0800: 55337.316: [weak refs processing, 0.0004879 secs]2020-06-29T13:59:45.105+0800: 55337.316: [class unloading, 0.1113617 secs]2020-06-29T13:59:45.217+0800: 55337.428: [scrub symbol table, 0.0316596 secs]2020-06-29T13:59:45.248+0800: 55337.459: [scrub string table, 0.0018447 secs]: 5326206K->1129836K(8388608K), 85.6151442 secs] 7622026K->3425656K(13631488K), [Metaspace: 370361K->105307K(1314816K)], 85.8536592 secs] [Times: user=88.94 sys=0.07, real=85.85 secs]
察看到因为 Metadata 的起因,导致 FGC,整个利用解冻 80 秒。家喻户晓 Metadata 次要存储一些类等相干的元信息,其应该是绝对恒定的,那么到底是什么起因导致了 MetaSpace 的变动,让咱们一探到底。
二、排查过程
MetaSpace 的目前参数为 -XX:MetaspaceSize=400m -XX:MaxMetaspaceSize=512m 这个曾经十分多了,查看监控,发现 MetaSpace 的曲线如下图的锯齿状,这个阐明一直的有类对象生成和卸载,在极其状况会到 400m 以上,所以触发 FGC 是合乎常理的。然而整个利用的生命周期中,实践上不应该有大量的类在一直的生成和卸载。
先看下代码,是否有类动静生成,发现有 2 个中央比拟可疑:
- QL 表达式,这个中央会有动静的类生成;
- 要害门路上泛型的应用;
然而通过排查和验证,发现这些都不是要害的点,因为尽管是泛型但类的数量是固定的,并且 QL 表达式有 cache。
最终定位到一个 Spark 算子,发现一个景象:每次执行 reduce 这个操作时都会有大量的类对象生成。
那么能够大胆的猜想:是因为 reduce 时产生 shuffle,由数据的序列化和反序列化引起。
增加启动参数,-XX:+TraceClassLoading -XX:+TraceClassUnloading,在类加载和卸载的状况下能够看到明细信息,同时对问题现场做内存 dump,发现有大量的 DelegatingClassLoader,并动静的在内存中生成了 sun.reflect.GeneratedSerializationConstructorAccessor 类。
那么,很显著引起 MetaSpace 抖动的起因就是 DelegatingClassLoader 生成了很多 ConstructorAccessor 对应的类对象,这个类是动静生成的,保留在内存中,无奈找到原型。
为了查看内存中这个类的具体信息,找到原型,这里用到了一个十分弱小的工具:arthas,arthas 是 Alibaba 开源的 Java 诊断工具,举荐每一位研发人员学习,具体教程见:
https://alibaba.github.io/arthas/quick-start.html
arthas 能够很不便的察看运行中的 JVM 的各种状态,找一个现场用 classloader 命令察看,发现有好几千 DelegatingClassLoader:
轻易挑一个 DelegatingClassLoader 下的类反序列化看下,整个类没什么特地的,就是 new 一个对象进去,然而有个细节:引入了 com.alipay 这个包下的类,这个中央应该能提供什么有用的信息。
咱们尝试把所有 GeneratedSerializationConstructorAccessor 的类 dump 下来做下统计,OpenJDK 能够做 ClassDump,找了下社区发现个小工具:
https://github.com/hengyunabc/dumpclass
java -jar dumpclass.jar -p 1234 -o /home/hadoop/dump/classDump sun.reflect.GeneratedSerializationConstruc*
能够看到导出了大略 9000 个 GeneratedSerializationConstructorAccessor 相干的类:
用 javap 反编译后做下统计:
find ./ -name "GeneratedSerializationConstructorAccessor*" | xargs javap -verbose | grep "com.alipay.*" -o | sort | uniq -c
发现有的类只生成 3 次,有的上千次,那么他们区别是什么?比照下发现差异在是否有默认的构造函数。
三、根因剖析
根因是因为在反序列化时触发了 JVM 的“inflation”操作,对于这个术语,下边这个解释十分通俗易懂:
When using Java reflection, the JVM has two methods of accessing the information on the class being reflected. It can use a JNI accessor, or a Java bytecode accessor. If it uses a Java bytecode accessor, then it needs to have its own Java class and classloader (sun/reflect/GeneratedMethodAccessor class and sun/reflect/DelegatingClassLoader). Theses classes and classloaders use native memory. The accessor bytecode can also get JIT compiled, which will increase the native memory use even more. If Java reflection is used frequently, this can add up to a significant amount of native memory use. The JVM will use the JNI accessor first, then after some number of accesses on the same class, will change to use the Java bytecode accessor. This is called inflation, when the JVM changes from the JNI accessor to the bytecode accessor. Fortunately, we can control this with a Java property. The sun.reflect.inflationThreshold property tells the JVM what number of times to use the JNI accessor. If it is set to 0, then the JNI accessors are always used. Since the bytecode accessors use more native memory than the JNI ones, if we are seeing a lot of Java reflection, we will want to use the JNI accessors. To do this, we just need to set the inflationThreshold property to zero.
因为 spark 应用了 kryo 序列化,翻译了相干代码和文档:
InstantiatorStrategy
Kryo provides DefaultInstantiatorStrategy which creates objects using ReflectASM to call a zero argument constructor. If that is not possible, it uses reflection to call a zero argument constructor. If that also fails, then it either throws an exception or tries a fallback InstantiatorStrategy. Reflection uses
setAccessible
, so a private zero argument constructor can be a good way to allow Kryo to create instances of a class without affecting the public API.
DefaultInstantiatorStrategy is the recommended way of creating objects with Kryo. It runs constructors just like would be done with Java code. Alternative, extralinguistic mechanisms can also be used to create objects. The Objenesis StdInstantiatorStrategy uses JVM specific APIs to create an instance of a class without calling any constructor at all. Using this is dangerous because most classes expect their constructors to be called. Creating the object by bypassing its constructors may leave the object in an uninitialized or invalid state. Classes must be designed to be created in this way.
Kryo can be configured to try DefaultInstantiatorStrategy first, then fallback to StdInstantiatorStrategy if necessary.
kryo.setInstantiatorStrategy(new DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));
Another option is SerializingInstantiatorStrategy, which uses Java’s built-in serialization mechanism to create an instance. Using this, the class must implement java.io.Serializable and the first zero argument constructor in a super class is invoked. This also bypasses constructors and so is dangerous for the same reasons as StdInstantiatorStrategy.
kryo.setInstantiatorStrategy(new DefaultInstantiatorStrategy(new SerializingInstantiatorStrategy()));
论断很清晰:
如果 Java 对象有默认的构造函数,DefaultInstantiatorStrategy 调用 Class.getConstructor().newInstance() 构建进去。在这个过程中 JDK 会将结构进去的 constructor accessor 缓存起来,防止重复生成。
否则 StdInstantiatorStrategy 调用 Java 的非凡 API(如下图的 newConstructorForSerialization)间接生成对象而不通过构造函数。
相干的代码在:org.objenesis.instantiator.sun.SunReflectionFactoryHelper#getNewConstructorForSerializationMethod
这个过程中没有 cache 的过程,导致一直的生成 constructor accessor,最初产生 inflation 生成了十分多的 Metadata。
四、总结
inflation 是一个比拟冷门的常识,然而每一个研发应该都会在有意无意见遇到它。那么在应用反射的能力时、甚至是第三方库在大量应用反射来实现某些性能时,都须要咱们去留神和思考。
同时,问题的排查是须要按逻辑去思考和渐进寻找根因的,脑袋一团乱麻只会走不少弯路,引以为戒。最初本文问题通过增加公有构造函数后解决,MetaSpace 监控空锯齿状隐没:
作者介绍
凌屿,高级开发工程师,始终从事智能监控相干研发工作,在海量数据荡涤、大数据集解决、分布式系统建设等有深入研究。
对于咱们
欢送来到「蚂蚁智能运维」的世界。本公众号由蚂蚁团体技术危险中台团队出品,面向关注智能运维、技术危险等技术的同学,将不定期与大家分享云原生时代下蚂蚁团体在智能运维的架构设计与翻新方面的思考与实际。
蚂蚁技术危险中台团队,负责蚂蚁团体的技术危险底座平台建设,包含智能监控、资金核查、性能容量、全链路压测以及危险数据基础设施等平台和业务能力建设,解决世界级的分布式解决难题,辨认和解决潜在的技术危险,参加蚂蚁双十一等大型流动,通过平台能力保障整体蚂蚁零碎在极限申请量下的高可用和资金平安。
对于「智能运维」有任何想要交换、探讨的话题,欢送留言通知咱们。
PS:技术危险中台正在招聘技术专家,欢送退出咱们,有趣味分割 techrisk-platform-hire@list.alibaba-inc.com