Подтвердить что ты не робот

Работа с несовместимой версией изменения структуры сериализации

Описание проблемы

У нас есть кластер 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 в нашем обычном магазине

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

4b9b3361

Ответ 1

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

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

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

child classloaders:      Old Kryo     New Kryo   <-- both with simple wrappers
                                \       /
                                 \     /
                                  \   /
                                   \ /
                                    |
default classloader:    domain model; controller for the re-serialization
  • Загрузите классы объектов домена в загрузчик классов по умолчанию
  • Загрузите Jar с модифицированной версией Kryo и кодом оболочки. Оболочка имеет статический "основной" метод с одним аргументом: имя файла для десериализации. Вызовите основной метод с помощью отражения от загрузчика классов по умолчанию:

        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 является простым способом или возвращает его в оболочку script.
  • Когда вызов возвращается, загрузите второй Jar с новой структурой сериализации с помощью простой оболочки. Обертка имеет статический "основной" метод и аргумент для передачи имени файла для сериализации. Вызовите основной метод с помощью отражения от загрузчика классов по умолчанию:

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

Ответ 2

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

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

Ответ 3

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

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

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

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

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

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