文章目录
背景
最近项目中设计到Spark BulkLoad Hbase,需要将一个自定义类的对象序列化成字节数组,存到Hbase中。考虑到Spark自带了java通用的Kryo序列化框架,所以参考网上的代码用Kryo实现了一个通用的序列化方法
public class KryoUtils {
private static final String DEFAULT_ENCODING = "UTF-8";
private static final ThreadLocal<Kryo> kryoLocal = new ThreadLocal<Kryo>() {
@Override
protected Kryo initialValue() {
Kryo kryo = new Kryo();
/**
* 不要轻易改变这里的配置!更改之后,序列化的格式就会发生变化,
* 上线的同时就必须清除 Redis 里的所有缓存,
* 否则那些缓存再回来反序列化的时候,就会报错
*/
//支持对象循环引用(否则会栈溢出)
kryo.setReferences(true); //默认值就是 true,添加此行的目的是为了提醒维护者,不要改变这个配置
//不强制要求注册类(注册行为无法保证多个 JVM 内同一个类的注册编号相同;而且业务系统中大量的 Class 也难以一一注册)
kryo.setRegistrationRequired(false); //默认值就是 false,添加此行的目的是为了提醒维护者,不要改变这个配置
//Fix the NPE bug when deserializing Collections.
((Kryo.DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy())
.setFallbackInstantiatorStrategy(new StdInstantiatorStrategy());
return kryo;
}
};
public static Kryo getInstance() {
return kryoLocal.get();
}
public static <T> byte[] writeToByteArray(T obj) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream);
Kryo kryo = getInstance();
kryo.writeClassAndObject(output, obj);
output.flush();
return byteArrayOutputStream.toByteArray();
}
@SuppressWarnings("unchecked")
public static <T> T readFromByteArray(byte[] byteArray) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
Input input = new Input(byteArrayInputStream);
Kryo kryo = getInstance();
return (T) kryo.readClassAndObject(input);
}
项目中使用到了四叉树结构,需要对其序列化和反序列化,在本地测试正确,但是打包放到服务器上的Spark环境下则会报错:
00:00 WARN: [kryo] Unable to load class index.quadtree.Z2QuadTree with kryo's ClassLoader. Retrying with current..
Exception in thread "main" com.esotericsoftware.kryo.KryoException: Unable to find class: index.quadtree.Z2QuadTree
at com.esotericsoftware.kryo.util.DefaultClassResolver.readName(DefaultClassResolver.java:160)
at com.esotericsoftware.kryo.util.DefaultClassResolver.readClass(DefaultClassResolver.java:133)
at com.esotericsoftware.kryo.Kryo.readClass(Kryo.java:693)
at com.esotericsoftware.kryo.Kryo.readClassAndObject(Kryo.java:804)
at index.util.KryoUtils.readFromByteArray(KryoUtils.java:107)
at index.quadtree.Z2QuadTree.deserialize(Z2QuadTree.java:187)
at bulkload.driver.Test.main(Test.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.spark.deploy.JavaMainApplication.start(SparkApplication.scala:52)
at org.apache.spark.deploy.SparkSubmit.org$apache$spark$deploy$SparkSubmit$$runMain(SparkSubmit.scala:845)
at org.apache.spark.deploy.SparkSubmit.doRunMain$1(SparkSubmit.scala:161)
at org.apache.spark.deploy.SparkSubmit.submit(SparkSubmit.scala:184)
at org.apache.spark.deploy.SparkSubmit.doSubmit(SparkSubmit.scala:86)
at org.apache.spark.deploy.SparkSubmit$$anon$2.doSubmit(SparkSubmit.scala:920)
at org.apache.spark.deploy.SparkSubmit$.main(SparkSubmit.scala:929)
at org.apache.spark.deploy.SparkSubmit.main(SparkSubmit.scala)
Caused by: java.lang.ClassNotFoundException: index.quadtree.Z2QuadTree
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at com.esotericsoftware.kryo.util.DefaultClassResolver.readName(DefaultClassResolver.java:154)
... 18 more
明明index.quadtree.Z2QuadTree是在Spark运行的jar包中的,怎么会出现ClassNotFoundException呢?这其实与Spark的类加载器有关。
Spark类加载器
参考 :Spark 如何摆脱java双亲委托机制优先从用户jar加载类
简单来说Spark将所有的依赖分为两类:
- 系统依赖
由Spark classpath、spark.driver.extraLibraryPath、spark.executor.extraClassPath等定义的依赖。系统依赖指定的jar包最终都是放到了AppClassLoader的classpath里,由AppClassLoader完成加载 - 用户依赖
由–jars参数、spark.jars配置、sparkContext.addjar定义的依赖。用户依赖会放到Spark自定义的MutableURLClassLoader或者ChildFirstURLClassLoader的classpath中,由其完成加载。
MutableURLClassLoader和ChildFirstURLClassLoader
Spark的用户依赖由ChildFirstURLClassLoader和MutableURLClassLoader类加载器进行加载,从而与系统依赖进行隔离。如果spark.executor.userClassPathFirst/spark.driver.userClassPathFirst
spark.executor.userClassPathFirst为true,则会使用ChildFirstURLClassLoader,否则使用MutableURLClassLoader。
例如 :在Executor端会创建对应的classloader,会将用户依赖的jarUrls传入classloader。
ChildFirstURLClassLoader和MutableURLClassLoader都是继承自URLClassLoader,会将urls记录在URLClassPath,作为类加载时的搜索路径。
ChildFirstURLClassLoader和MutableURLClassLoader的父类加载器有以下方法获取,一般来说都是系统类加载器:
需要注意的是:Spark的用户依赖的urls只保存在MutableURLClassLoader和ChildFirstURLClassLoader类中,而其父类加载器是没有这部分urls的!!!
MutableURLClassLoader
可以看到MutableURLClassLoader没有重写方法,其只是使用继承自URLClassLoader的方法完成类加载,所以其符合双亲委托机制,其父类加载器由以下方法得到,一般来说都是系统类加载器。
所以MutableURLClassLoader在进行类加载时,首先请求其父类加载器进行尝试加载,由于其父类加载器没有用户依赖的urls,所以父类加载器对于用户依赖会加载失败,最终由MutableURLClassLoader进行用户依赖的类加载。Spark的系统依赖依然是由其父类加载器(系统类加载器)完成加载。
ChildFirstURLClassLoader
顾名思义,ChildFirstURLClassLoader先使用子类加载器进行加载类。
loadClass方法中先调用继承自URLClassLoader的loadClass方法进行加载类。如果失败,再调用父类加载器进行加载。
这样做的好处是可以让Spark先加载用户依赖而不是系统依赖,从而解决以下两种场景
- 用户引入一些依赖,和spark的依赖相互冲突
- 用户针对性的改了Spark底层源码,又不想干扰其他用户
利用ChildFirstURLClassLoader可以使用用户依赖中的类,而屏蔽Spark的系统依赖。
问题分析
有了上面的基础知识后,再去分析项目中出现的序列化问题。
当调用KryoUtils.readFromByteArray方法时会调用kryo.readClassAndObject方法。
最后可追踪到DefaultClassResolver.readName方法中,其会调用Class.forName(className)加载类。
Class.forName会获取调用者的classloader去加载所需的类。
由于Spark的classpath下包含了Kryo依赖,所以我在打包时并没有将kryo打包进jar。在Spark运行时,Kryo依赖处于Spark Classpath下,所以其使用系统类加载器进行加载。但是在系统类加载器中并没有用户依赖的urls,而Z2QuadTree是在项目jar包中,通过spark-submit命令行传入,最终会会通过sparkContext.addjar加入用户依赖,所以在反序列化Z2QuadTree时,是无法找到class文件的,所以报出java.lang.ClassNotFoundException。
验证
对于上述的分析,我们进行实验验证
上述代码在本地运行时,输出的path是相同的,因为不在Spark环境下,两者的类加载器是相同的。但是在Spark环境下,输出:
可以看到Kryo对应的类加载器的classpath包含了Spark的系统依赖,Z2QuadTree对应的类加载器的classpath只包含了传入的用户依赖。