Spark 类加载器导致的Kryo序列化问题

背景

最近项目中设计到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将所有的依赖分为两类:

  1. 系统依赖
    由Spark classpath、spark.driver.extraLibraryPath、spark.executor.extraClassPath等定义的依赖。系统依赖指定的jar包最终都是放到了AppClassLoader的classpath里,由AppClassLoader完成加载
  2. 用户依赖
    由–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先加载用户依赖而不是系统依赖,从而解决以下两种场景

  1. 用户引入一些依赖,和spark的依赖相互冲突
  2. 用户针对性的改了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只包含了传入的用户依赖。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
Kryo是一个快速、高效的Java序列化框架,比Java自带的序列化框架更快、更小、更节省内存。在Spark中,使用Kryo作为默认的序列化框架可以显著地提高性能。下面是一个使用Kryo序列化的案例: ```java import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.JavaSparkContext; import org.apache.spark.serializer.KryoRegistrator; import com.esotericsoftware.kryo.Kryo; public class KryoExample { public static void main(String[] args) { SparkConf conf = new SparkConf().setAppName("KryoExample").setMaster("local"); conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer"); conf.set("spark.kryo.registrator", MyKryoRegistrator.class.getName()); JavaSparkContext sc = new JavaSparkContext(conf); JavaRDD<String> lines = sc.textFile("input.txt"); JavaRDD<MyObject> objects = lines.map(line -> { MyObject obj = new MyObject(); obj.setId(Integer.parseInt(line.split(",")[0])); obj.setName(line.split(",")[1]); return obj; }); objects.foreach(obj -> System.out.println(obj.getId() + ": " + obj.getName())); } public static class MyObject { private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } public static class MyKryoRegistrator implements KryoRegistrator { @Override public void registerClasses(Kryo kryo) { kryo.register(MyObject.class); } } } ``` 在上面的代码中,我们首先创建了一个SparkConf对象,并设置了应用名称和运行模式。然后,我们设置了序列化器为KryoSerializer,并指定了KryoRegistrator为MyKryoRegistrator。MyKryoRegistrator类是一个自定义的Kryo注册器,用于注册我们需要序列化的类MyObject。 接下来,我们使用JavaSparkContext读取了一个文本文件,并将每一行转换成一个MyObject对象。最后,我们对这些对象进行了遍历,并输出了它们的id和name属性。 需要注意的是,如果我们没有使用KryoSerializer序列化器,程序将默认使用Java自带的序列化器,这样可能会导致性能瓶颈。因此,建议在Spark中使用Kryo作为默认的序列化器。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值