当前位置: 首页 > 面试题库 >

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

叶琦
2023-03-14
问题内容

我们有一个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对象,并使用旧的Kryo对象进行反序列化,并使用新的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;
}

问题
尽管此方法可能会实例化未配置的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该类引用了Kryo的Serializer类,并尝试使用其自己的类加载器(即System类加载器)加载它,而该类加载器不知道Serializer该类。

(3)使用中间序列化

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

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

问题
此解决方案的最大问题是它使处理的数据的空间消耗大致翻了一番。此外,我们需要另一种序列化方法,该方法对所有数据均无问题,我们需要首先对其进行研究。


问题答案:

我将使用多重类加载器方法。

(软件包重命名也可以。看起来确实很丑陋,但这是一次性的技巧,因此美观和正确性可以让位。中间序列化似乎有风险-
您使用Kryo是有原因的,而该理由将被否定。通过使用其他中间形式)。

总体设计为:

child classloaders:      Old Kryo     New Kryo   <-- both with simple wrappers
                                \       /
                                 \     /
                                  \   /
                                   \ /
                                    |
default classloader:    domain model; controller for the re-serialization
  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");
    
    1. 该方法:
    2. 将文件反序列化为一个对象图
    3. 将对象放置在共享空间中。ThreadLocal是一种简单的方法,或者将其返回到包装器脚本。
    4. 当调用返回时,使用一个简单的包装器用新的序列化框架加载另一个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”);

    5. 这个方法

    6. 从ThreadLocal检索对象
    7. 序列化对象并将其写入文件

注意事项

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

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

域对象是否引用 任何 Kryo类?如果是这样,您将遇到困难:

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

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



 类似资料:
  • 我们目前正在使用以下框架及其版本。 Spring启动父-1.4.5。发布 Vaadin-Bom-7.7.3 Vaadin-Spring启动-1.1.0 Vaadin-Spring-1.1.0 计划升级Spring引导父版本。但我敢肯定,如果我们升级Spring引导父版本,那么需要升级Vaadin依赖。我花了几天时间找到最新的Vaadin框架与Spring Boot jar的兼容性。但无法得出解决方

  • 问题内容: 我有一个公共类,该类实现了Serializable,并由其他多个类进行了扩展。只有那些子类曾经被序列化过-从来没有超类。 超类已定义了serialVersionUID。 我不确定是否重要,但是它没有标记为私有,而是仅具有默认保护-您可能会说它是受软件包保护的 但是,超类或任何子类均未实现readObject或writeObject,并且这些子类均未明确定义serialVersionUI

  • 问题内容: 我正在构建一个小型Java应用程序,并希望使用logback进行日志记录。 我的应用程序依赖于一个较旧的项目,该项目通过 …所以我的计划是使用 …将JCL日志记录重定向到 …最终 因此我的应用可以通过其slf4j API通过logback登录,而旧的库代码可以通过重定向登录到同一位置。 las,这导致 我在其中一些罐子上尝试了较高和较低的版本号,还通过API文档等进行了挖掘……但是我无

  • 我试图使用kafkastreams进行聚合,但得到的错误如下所示 这是我正在做的事情: 我收到的错误如下。 由以下原因引起:A 序列化程序(密钥:类型:a 序列化程序(密钥:类型:网站名称:通用序列化程序 / 值:在流配置中更改默认的 Serdes 或通过方法参数提供正确的 Serdes。在 org.apache.kafka.流.处理器.内部.sinkNode.进程 (SinkNode.java:

  • 我目前安装了Java8(更新171)和Oxygen.3a(4.7.3a)(Eclipse版本)。我以前下载过Java10,但我读到有一个bug,WindowBuilder不能在该版本上工作,它被推荐为版本8,这是我下载并安装的版本(在卸载版本10后)。但问题还在。 我能做些什么来解决这个问题?谢谢你。

  • 问题内容: 我刚刚安装了Eclipse Oxygen并创建了一个JFrame项目。从源视图切换到设计视图时,出现“不兼容的Java版本错误”。 完全错误Desc:Eclipse在0以下运行,但是此Java项目的Java兼容级别为10,因此WindowBuilder将无法从该项目中加载类。对项目使用较低级别的Java,或使用较新的Java版本运行Eclipse。 我在Windows中检查了版本->首