处理序列化框架的不兼容版本更改

Mic*_*ßer 14 java serialization hadoop maven kryo

问题描述

我们有一个Hadoop集群,我们在其上存储使用Kryo(序列化框架)序列化为字节的数据.我们用来做这个的Kryo版本已从官方版本2.21中分离出来,将我们自己的补丁应用于我们使用Kryo遇到的问题.当前的Kryo 2.22版也修复了这些问题,但使用了不同的解决方案.因此,我们不能只改变我们使用的Kryo版本,因为这意味着我们将无法再读取已存储在Hadoop集群中的数据.为了解决这个问题,我们想要运行一个Hadoop作业

  1. 读取存储的数据
  2. 反序列化使用旧版Kryo存储的数据
  3. 使用新版本的Kryo序列化已还原的对象
  4. 将新的序列化表示写回我们的数据存储

问题是在一个Java程序中使用同一个类的两个不同版本并不是一件容易的事情(更确切地说,在Hadoop作业的mapper类中).

问题简而言之

如何在一个Hadoop作业中使用同一序列化框架的两个不同版本反序列化和序列化对象?

相关事实概述

  • 我们将数据存储在Hadoop CDH4集群上,使用Kryo版本2.21.2-ourpatchbranch进行序列化
  • 我们希望将数据与Kryo 2.22版本序列化,这与我们的版本不兼容
  • 我们使用Apache Maven构建我们的Hadoop作业JAR

可能的(也是不可能的)方法

(1)重命名包

我们想到的第一种方法是使用Maven Shade插件重定位功能重命名我们自己的Kryo分支中的包,并使用不同的工件ID释放它,这样我们就可以依赖转换作业项目中的两个工件.然后,我们将实例化旧版本和新版本的一个Kryo对象,并使用旧版本进行反序列化,并使用新版本再次序列化对象.

问题
我们不在Hadoop作业中明确使用Kryo,而是通过我们自己的库的多个层访问它.对于这些库中的每一个,都有必要

  1. 重命名涉及包和
  2. 使用不同的组或工件ID创建发布

为了使事情更加混乱,我们还使用其他第三方库提供的Kryo序列化程序,我们必须做同样的事情.


(2)使用多个类加载器

我们提出的第二种方法是在包含转换作业的Maven项目中完全不依赖于Kryo,而是从每个版本的JAR加载所需的类,该版本存储在Hadoop的分布式缓存中.然后序列化对象看起来像这样:

public byte[] serialize(Object foo, JarClassLoader cl) {
    final Class<?> kryoClass = cl.loadClass("com.esotericsoftware.kryo.Kryo");
    Object k = kryoClass.getConstructor().newInstance();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    final Class<?> outputClass = cl.loadClass("com.esotericsoftware.kryo.io.Output");

    Object output = outputClass.getConstructor(OutputStream.class).newInstance(baos);
    Method writeObject = kryoClass.getMethod("writeObject", outputClass, Object.class);
    writeObject.invoke(k, output, foo);
    outputClass.getMethod("close").invoke(output);
    baos.close();
    byte[] bytes = baos.toByteArray();
    return bytes;
}
Run Code Online (Sandbox Code Playgroud)

问题
虽然这种方法可能用于实例化未配置的Kryo对象并序列化/恢复某些对象,但我们使用了更为复杂的Kryo配置.这包括几个自定义序列化程序,已注册的类ID等.例如,我们无法找到为类设置自定义序列化程序而无法获取NoClassDefFoundError的方法 - 以下代码不起作用:

Class<?> kryoClass = this.loadClass("com.esotericsoftware.kryo.Kryo");
Object kryo = kryoClass.getConstructor().newInstance();
Method addDefaultSerializer = kryoClass.getMethod("addDefaultSerializer", Class.class, Class.class);
addDefaultSerializer.invoke(kryo, URI.class, URISerializer.class); // throws NoClassDefFoundError
Run Code Online (Sandbox Code Playgroud)

最后一行抛出一个

java.lang.NoClassDefFoundError: com/esotericsoftware/kryo/Serializer
Run Code Online (Sandbox Code Playgroud)

因为URISerializer该类引用了Kryo的Serializer类并尝试使用自己的类加载器(即System类加载器)加载它,该类加载器不知道Serializer该类.


(3)使用中间序列化

目前最有希望的方法似乎是使用独立的中间序列化,例如使用Gson或类似的JSON ,然后运行两个单独的作业:

  1. kryo:我们常规商店中的2.21.2-ourpatchbranch - >临时商店中的JSON
  2. 临时商店中的JSON - > kryo:我们常规商店中的2-22

问题
这个解决方案的最大问题是它大大加倍了处理数据的空间消耗.此外,我们需要另一种序列化方法,它对我们的所有数据都没有问题,我们需要首先进行调查.

And*_*ock 6

我会使用多种类加载器方法.

(包重命名也会起作用.看起来确实很难看,但这是一次性的黑客攻击,所以美观和正确性可能会退居二线.中级序列化似乎有风险 - 你有使用Kryo的理由,这个理由将被否定通过使用不同的中间形式).

整体设计将是:

child classloaders:      Old Kryo     New Kryo   <-- both with simple wrappers
                                \       /
                                 \     /
                                  \   /
                                   \ /
                                    |
default classloader:    domain model; controller for the re-serialization
Run Code Online (Sandbox Code Playgroud)
  1. 在默认的类加载器中加载域对象类
  2. 使用修改后的Kryo版本和包装器代码加载Jar.包装器有一个带有一个参数的静态"main"方法:要反序列化的文件的名称.通过默认类加载器的反射调用main方法:

        Class deserializer = deserializerClassLoader.loadClass("com.example.deserializer.Main");
        Method mainIn = deserializer.getMethod("main", String.class);
        Object graph = mainIn.invoke(null, "/path/to/input/file");
    
    Run Code Online (Sandbox Code Playgroud)
    1. 这个方法:
      1. 将文件反序列化为一个对象图
      2. 将对象放入共享空间.ThreadLocal是一种简单的方法,或将其返回到包装器脚本.
  3. 当调用返回时,使用带有简单包装器的新序列化框架加载第二个Jar.包装器有一个静态'main'方法和一个参数,用于传递要序列化的文件名.通过默认类加载器的反射调用main方法:

        Class serializer = deserializerClassLoader.loadClass("com.example.serializer.Main");
        Method mainOut = deserializer.getMethod("main", Object.class, String.class);
        mainOut.invoke(null, graph, "/path/to/output/file");
    
    Run Code Online (Sandbox Code Playgroud)
    1. 这种方法
      1. 从ThreadLocal中检索对象
      2. 序列化对象并将其写入文件

注意事项

在代码片段中,为每个对象序列化和反序列化创建一个类加载器.您可能只想加载类加载器一次,发现主要方法并循环遍历文件,例如:

for (String file: files) {
    Object graph = mainIn.invoke(null, file + ".in");
    mainOut.invoke(null, graph, file + ".out");
}
Run Code Online (Sandbox Code Playgroud)

域对象是否可以引用任何 Kryo类?如果是这样,你有困难:

  1. 如果引用只是一个类引用,例如调用一个方法,那么第一次使用该类会将两个Kryo版本中的一个加载到默认的类加载器中.这可能会导致问题,因为序列化的一部分或反序列化可能由错误的Kryo版本执行
  2. 如果引用用于实例化任何Kryo对象并将引用存储在域模型(类或实例成员)中,那么Kryo实际上将在模型中序列化自身的一部分.这可能是这种方法的交易破坏者.

在任何一种情况下,您的第一种方法应该是检查这些参考并消除它们.确保您完成此操作的一种方法是确保默认类加载器无法访问任何 Kryo版本.如果域对象以任何方式引用Kryo,则引用将失败(如果直接引用类,则为ClassNotFoundError;如果使用反射,则为ClassNotFoundException).