gpt4 book ai didi

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

转载 作者:搜寻专家 更新时间:2023-10-30 19:45:36 24 4
gpt4 key购买 nike

问题描述

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

  • 读取存储的数据
  • 反序列化旧版本 Kryo 存储的数据
  • 使用新版本的 Kryo 序列化恢复的对象
  • 将新的序列化表示写回我们的数据存储

  • 问题在于,在一个 Java 程序中(更准确地说,在 Hadoop 作业的映射器类中)使用同一类的两个不同版本并非易事。

    简而言之问题

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

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

  • 可能(和不可能)的方法

    (1) 重命名包

    我们想到的第一种方法是使用 relocation functionality of the Maven Shade plugin 重命名我们自己的 Kryo 分支中的包。并使用不同的 Artifact ID 发布它,以便我们可以依赖转换作业项目中的两个 Artifact 。然后我们将实例化旧版本和新版本的一个 Kryo 对象,并使用旧版本进行反序列化,使用新版本再次序列化对象。

    问题
    我们不会在 Hadoop 作业中明确使用 Kryo,而是通过我们自己的库的多层访问它。对于这些库中的每一个,都需要
  • 重命名涉及的包和
  • 创建具有不同组或 Artifact ID 的版本

  • 为了让事情变得更加困惑,我们还使用其他 3rd 方库提供的 Kryo 序列化程序,我们必须为这些库做同样的事情。

    (2) 使用多个类加载器

    我们提出的第二种方法是在包含转换作业的 Maven 项目中完全不依赖 Kryo,而是从存储在 Hadoop 分布式缓存中的每个版本的 JAR 加载所需的类。序列化一个对象看起来像这样:
    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;
    }

    问题
    尽管这种方法可能适用于实例化未配置的 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

    最后一行抛出一个
    java.lang.NoClassDefFoundError: com/esotericsoftware/kryo/Serializer

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

    (3) 使用中间序列化

    目前最有前途的方法似乎是使用独立的中间序列化,例如JSON 使用 Gson或类似的,然后运行两个单独的作业:
  • kryo:2.21.2-ourpatchbranch 在我们的常规商店 -> JSON 在临时商店
  • 临时存储中的 JSON -> 我们常规存储中的 kryo:2-22

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

    最佳答案

    我会使用多类加载器方法。

    (包重命名也可以使用。它看起来确实很丑,但这是一次性的,因此美观和正确性可以退居二线。中间序列化似乎有风险 - 您使用 Kryo 是有原因的,而这个原因将被否定通过使用不同的中间形式)。

    总体设计将是:

    child classloaders:      Old Kryo     New Kryo   <-- both with simple wrappers
    \ /
    \ /
    \ /
    \ /
    |
    default classloader: domain model; controller for the re-serialization
  • 在默认类加载器中加载域对象类
  • 使用修改后的 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");
  • 这种方法:
  • 将文件反序列化为一个对象图
  • 将对象放入共享空间。 ThreadLocal是一种简单的方法,或者将其返回给包装器脚本。
  • 当调用返回时,使用带有简单包装器的新序列化框架加载第二个 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");
  • 这种方法
  • 从 ThreadLocal
  • 检索对象
  • 序列化对象并将其写入文件

  • 注意事项

    在代码片段中,为每个对象序列化和反序列化创建了一个类加载器。您可能只想加载类加载器一次,发现主要方法并遍历文件,例如:
    for (String file: files) {
    Object graph = mainIn.invoke(null, file + ".in");
    mainOut.invoke(null, graph, file + ".out");
    }

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

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

    关于java - 处理序列化框架不兼容的版本更改,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16083263/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com