我们有一个Hadoop集群,我们在其上存储使用Kryo(序列化框架)序列化为字节的数据。我们用来做这个的Kryo版本已从官方版本2.21中分离出来,将我们自己的补丁应用于我们使用Kryo遇到的问题。当前的Kryo 2.22版也修复了这些问题,但采用了不同的解决方案。因此,我们不能只改变我们使用的Kryo版本,因为这意味着我们将无法再读取已存储在Hadoop集群中的数据。为了解决这个问题,我们想要运行一个
的Hadoop作业问题在于,在一个Java程序中使用同一类的两个不同版本并不是一件容易的事情(更确切地说,在Hadoop作业的映射器类中)。
如何在一个Hadoop作业中使用同一序列化框架的两个不同版本反序列化和序列化对象?
我们想到的第一种方法是使用relocation functionality of the Maven Shade plugin在我们自己的Kryo分支中重命名包,并使用不同的工件ID释放它,这样我们就可以依赖转换作业项目中的两个工件。然后,我们将实例化旧版本和新版本的一个Kryo对象,并使用旧版本进行反序列化,并使用新版本再次序列化对象。
问题
我们不在Hadoop作业中明确使用Kryo,而是通过我们自己的库的多个层访问它。对于这些库中的每一个,都需要
为了使事情变得更加混乱,我们还使用其他第三方库提供的Kryo序列化程序,我们必须做同样的事情。
我们提出的第二种方法是在包含转换作业的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
类。
目前最有希望的方法似乎是使用独立的中间序列化,例如,使用Gson或类似的JSON,然后运行两个单独的作业:
问题
该解决方案的最大问题在于它大致使处理的数据的空间消耗增加了一倍。此外,我们需要另一种序列化方法,该方法对我们的所有数据都没有问题,我们需要首先进行调查。
答案 0 :(得分:6)
我会使用多种类加载器方法。
(包装重命名也会起作用。看起来确实很丑陋,但这是一次性的黑客攻击,所以美观和正确性可能会退居二线。中级序列化似乎有风险 - 你有使用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");
当调用返回时,使用带有简单包装器的新序列化框架加载第二个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");
<强>考虑强>
在代码片段中,为每个对象序列化和反序列化创建一个类加载器。您可能只想加载类加载器一次,发现主要方法并循环遍历文件,例如:
for (String file: files) {
Object graph = mainIn.invoke(null, file + ".in");
mainOut.invoke(null, graph, file + ".out");
}
域对象是否有对任何 Kryo类的引用?如果是这样,你就有困难:
在任何一种情况下,您的第一种方法应该是检查这些参考并消除它们。确保您完成此操作的一种方法是确保默认的类加载器无法访问任何 Kryo版本。如果域对象以任何方式引用Kryo,则引用将失败(如果直接引用类,则为ClassNotFoundError;如果使用反射,则为ClassNotFoundException)。
答案 1 :(得分:1)
对于2,您可以创建两个包含序列化程序的jar文件以及序列化程序的新旧版本的所有依赖项,如here所示。然后创建一个map reduce工作,在单独的类加载器中加载每个版本的代码,并在中间添加一些使用旧代码反序列化的粘合代码,然后使用新代码进行序列化。
您必须要小心,您的域对象在与胶水代码相同的类加载器中加载,并且序列化/反序列化的代码取决于与胶水代码相同的类加载器,以便它们都看到相同的域对象类。
答案 2 :(得分:1)
我想到的最简单的方法是使用额外的Java应用程序为您进行转换。因此,您将二进制数据发送到辅助Java应用程序(简单的本地套接字可以很好地完成这一操作),因此您不必使用类加载器或包。
唯一要考虑的是中间表示。您可能希望使用其他序列化机制,或者如果时间没有问题,您可能希望使用Java的内部序列化。
使用第二个Java应用程序可以避免处理临时存储并在内存中执行所有操作。
一旦你拥有了这些套接字+第二个应用程序代码,就会发现很多情况,这些代码很方便。
也可以使用jGroups构建一个本地集群,毕竟用插槽保存麻烦。 jGroups是我所知道的最简单的通信API。只需形成一个逻辑通道并检查谁加入。最好的是它甚至可以在同一个JVM中工作,这使得测试很容易,如果远程完成,可以将不同的物理服务器绑定在一起,就像它对本地应用程序一样。
另一个变量替代方案是使用ZeroMQ及其ipc(进程间通信)协议。