Описание проблемы
У нас есть кластер Hadoop, на котором мы храним данные, которые сериализуются в байты, используя Kryo (a структура сериализации). Версия Kryo, которую мы использовали для этого, была раздвоена из официального выпуска 2.21, чтобы применить наши собственные исправления к проблемам, которые мы испытали с помощью Kryo. Текущая версия Kryo 2.22 также устраняет эти проблемы, но с различными решениями. В результате мы не можем просто изменить используемую нами версию Kryo, потому что это будет означать, что мы больше не сможем читать данные, которые уже хранятся в нашем кластере Hadoop. Чтобы решить эту проблему, мы хотим запустить Hadoop-задание, которое
- считывает сохраненные данные
- десериализует данные, хранящиеся в старой версии Kryo
- сериализует восстановленные объекты с новой версией Kryo
- записывает новое сериализованное представление обратно в наше хранилище данных
Проблема заключается в том, что нет тривиального использования двух разных версий одного и того же класса в одной программе Java (точнее, в классе сопоставления заданий Hadoop).
Вопрос в двух словах
Как можно десериализовать и сериализовать объект с двумя разными версиями одной и той же структуры сериализации в одном задании Hadoop?
Обзор релевантных фактов
- У нас есть данные, хранящиеся на кластере Hadoop CDH4, сериализованы с версией Kryo 2.21.2-ourpatchbranch
- Мы хотим, чтобы данные были сериализованы с версией Kryo 2.22, которая несовместима с нашей версией
- Мы строим наши JAR-сервисы Hadoop с Apache Maven
Возможные (и невозможные) подходы
(1) Переименование пакетов
Первым подходом, который пришел на ум, было переименование пакетов в нашем собственном филиале Kryo с использованием функции перемещения плагина Maven Shade и выпустить его с другим идентификатором артефакта, чтобы мы могли зависеть от обоих артефактов в нашем проекте работы с конверсией. Затем мы создадим экземпляр одного объекта Kryo как старой, так и новой версий и используем старый для десериализации, а новый - для сериализации объекта снова.
Проблемы
Мы не используем Kryo явно в Hadoop-заданиях, а имеем доступ к нему через несколько слоев наших собственных библиотек. Для каждой из этих библиотек необходимо
- переименовать связанные пакеты и
- создать выпуск с другим идентификатором группы или артефакта
Чтобы сделать вещи еще более грязными, мы также используем сериализаторы Kryo, предоставляемые другими сторонними библиотеками, для которых мы должны были бы сделать то же самое.
(2) Использование нескольких загрузчиков классов
Второй подход, с которым мы столкнулись, заключался в том, чтобы вообще не зависел от Kryo в проекте Maven, который содержит задание на преобразование, но загружает требуемые классы из 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. Это включает в себя несколько пользовательских сериализаторов, зарегистрированных идентификаторов классов и т.д. Например, мы не смогли определить способ установки пользовательских сериализаторов для классов без получения 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) Использование промежуточной сериализации
В настоящее время наиболее перспективным подходом является использование независимой промежуточной сериализации, например. JSON, используя Gson или аналогично, а затем запуская два отдельных задания:
- kryo: 2.21.2-ourpatchbranch в нашем обычном магазине → JSON во временном магазине
- JSON во временном магазине → kryo: 2-22 в нашем обычном магазине
Проблемы
Самая большая проблема с этим решением заключается в том, что он примерно удваивает потребление пространства обработанными данными. Более того, нам нужен еще один метод сериализации, который без проблем работает во всех наших данных, которые нам нужно будет исследовать в первую очередь.