Работа с несовместимым изменением версии платформы сериализации

описание

у нас есть кластер Hadoop, на котором мы храним данные, которые сериализуются в байты с помощью Kryo (инфраструктура сериализации). Версия Kryo, которую мы использовали для этого, была разветвлена с официального релиза 2.21, чтобы применить наши собственные патчи к проблемам, которые мы испытали с помощью Kryo. Текущая версия Kryo 2.22 также устраняет эти проблемы, но с разными решениями. В результате мы не можем просто изменить версию Kryo, которую мы используем, потому что это это означает, что мы больше не сможем читать данные, которые уже хранятся в нашем кластере Hadoop. Чтобы решить эту проблему, мы хотим запустить задание Hadoop, которое

  1. считывает данные, хранящиеся
  2. десериализует данные, хранящиеся в старой версии Kryo
  3. сериализует восстановленные объекты с новой версией Kryo
  4. записывает новое сериализованное представление обратно в наше хранилище данных

проблема что нетривиально использовать две разные версии одного и того же класса в одной программе Java (точнее, в классе сопоставления задания Hadoop).

вопрос в двух словах

как можно десериализовать и сериализовать объект с двумя разными версиями одной и той же структуры сериализации в одном задании Hadoop?

обзор соответствующих фактов

  • у нас есть данные, хранящиеся в кластере Hadoop CDH4, сериализованные с версией Kryo 2.21.2-ourpatchbranch
  • мы хотим сериализовать данные с Kryo версии 2.22, что несовместимо с нашей версией
  • мы строим наши банки работы Hadoop с Apache Maven

возможные (и невозможные) подходы

(1) переименование пакетов

первым подходом, который пришел нам на ум, было переименование пакетов в нашем собственном филиале Kryo с помощью функциональность перемещения тени Maven плагин и отпустите его с другим идентификатором артефакта, чтобы мы могли зависеть от обоих артефактов в нашем проекте задания преобразования. Затем мы создадим экземпляр одного объекта Kryo как старой, так и новой версии и используем старый для десериализации и новый для сериализации объекта снова.

проблемы
Мы не используем Kryo явно в заданиях Hadoop, а скорее получаем доступ к нему через несколько уровней наших собственных библиотек. Для каждой из этих библиотек было бы необходимо

  1. переименовать пакетов и
  2. создайте выпуск с другой группой или идентификатором артефакта

чтобы сделать вещи еще более грязными, мы также используем сериализаторы Kryo, предоставленные другими сторонними библиотеками, для которых нам пришлось бы сделать то же самое.


(2) Использование нескольких загрузчиков класса

второй подход, который мы придумали, состоял в том, чтобы вообще не зависеть от крио в проекте 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

последняя строка бросает!--11-->

java.lang.NoClassDefFoundError: com/esotericsoftware/kryo/Serializer

потому что URISerializer класса ссылки Kryo-х Serializer class и пытается загрузить его с помощью собственного загрузчика классов (который является загрузчиком системного класса), который не знает Serializer класса.


(3) использование промежуточного сериализация

в настоящее время наиболее перспективным подходом является использование независимой промежуточной сериализации, например JSON с использованием Gson или похожие, а затем запуск двух отдельных заданий:

  1. kryo: 2.21.2-ourpatchbranch в нашем обычном магазине - > JSON во временном магазине
  2. JSON во временном магазине - > kryo:2-22 в нашем обычном магазине

проблемы
Самая большая проблема с этим решение заключается в том, что оно примерно удваивает объем потребления обрабатываемых данных. Кроме того, нам нужен другой метод сериализации, который работает без проблем на всех наших данных, которые нам нужно будет исследовать в первую очередь.

3 ответов


Я бы использовал подход с несколькими загрузчиками классов.

(переименование пакета также будет работать. Это кажется уродливым, но это одноразовый Хак, поэтому красота и правильность могут занять заднее сиденье. Промежуточная сериализация кажется рискованной - была причина, по которой вы используете Kryo, и эта причина будет отрицаться с помощью другой промежуточной формы).

общая конструкция будет:

child classloaders:      Old Kryo     New Kryo   <-- both with simple wrappers
                                \       /
                                 \     /
                                  \   /
                                   \ /
                                    |
default classloader:    domain model; controller for the re-serialization
  1. загрузите классы объектов домена по умолчанию classloader
  2. загрузите банку с измененной версией Kryo и кодом оболочки. Оболочка имеет статический метод "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. этот метод:
      1. десериализует файл как один объект graph
      2. помещает объект в общее пространство. ThreadLocal простой способ, или возвращается скрипт wrapper.
  3. когда вызов возвращается, загрузите вторую банку с новой платформой сериализации с простой оболочкой. Оболочка имеет статический метод "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");
    
    1. этот метод
      1. извлекает объект из ThreadLocal
      2. сериализует объект и записывает его в файл

соображения

в фрагментах кода создается один загрузчик классов для каждой сериализации и десериализации объекта. Вы, вероятно, хотите загрузить загрузчики классов только один раз, обнаружить основные методы и цикл над файлами, что-то вроде:

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 если используется отражение).


для 2 Вы можете создать два файла jar, которые содержат сериализатор и все зависимости для новой и старой версий вашего сериализатора, как показано здесь. Затем создайте задание map reduce, которое загружает каждую версию кода в отдельный загрузчик классов, и добавьте в середину код клея, который десериализуется со старым кодом, а затем сериализуется с новым кодом.

вам нужно будет быть осторожным, чтобы ваш объект домена загружался в тот же загрузчик классов, что и ваш код клея и код для сериализации/десериализации зависят от того же загрузчика классов, что и ваш код клея, чтобы они оба видели один и тот же класс объектов домена.


самый простой способ, который я бы придумал, не думая, - это использовать дополнительное приложение Java, выполняющее преобразование для вас. Таким образом, вы отправляете двоичные данные во вторичное приложение java (простые локальные сокеты отлично справятся), поэтому вам не нужно возиться с загрузчиками классов или пакетами.

единственное, о чем нужно думать, это промежуточное представление. Вы можете использовать другой механизм сериализации или, если время не проблема, вы можете использовать внутренняя сериализация Java.

использование второго приложения Java избавляет вас от работы с временным хранилищем и делает все в памяти.

и как только у вас есть эти сокеты + второй код приложения, вы найдете множество ситуаций, когда это пригодится.

также можно построить локальный кластер с помощью jGroups и сохранить хлопоты с сокетами в конце концов. jGroups-это самый простой API связи, который я знаю. Просто сформируйте логический канал и проверьте, кто присоединяется. И лучше всего это даже работает в том же JVM, что упрощает тестирование, и если это делается удаленно, можно связать разные физические сервера вместе точно так же, как это будет работать для локальных приложений.

другой альтернативой переменной является использование ZeroMQ с протоколом ipc (inter process communication).