Сравнение Java библиотек для сериализации

Сериализация — процесс преобразования структур данных, которые хранятся в памяти, в формат, пригодный для хранения или передачи. В ООП языках, как Java, под сериализацией объектов подразумевается преобразование состояния объекта в последовательность бит или текст. Преобразование текста или последовательности битов обратно в копию исходного объекта называется десериализацией.

Сериализация используется для передачи объектов по сети, сохранения в файлы и другие хранилища. С вопросом о сериализации часто сталкиваются при разработке распределенных приложений, в которых разные узлы кластера обмениваются данными по сети, или интеграции с внешними системами. Для обмена данные необходимо сериализировать независимо от того, как интегрируются приложения: при помощи обмена файлами, через Remote Procedure Call (RPC) или обмен сообщениями (messaging).

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

Сегодня, когда речь заходит о сериализации, сразу вспоминаются такие модные библиотеки, как Protocol Buffers от Google, Apache Thrift, который был разработан в Facebook, и Apache Avro, разработанный в рамках Hadoop. Несмотря на то, что эти библиотеки сейчас в тренде, в сравнении они участвовать не будут по ряду причин.

Apache Thrift и Apache Avro — это фреймворки для RPC и сериализации данных. Protocol Buffers предназначен для сериализации структурированных данных и не предоставляет стандартных средств для RPC. API этих фреймворков доступны для множества языков программирования.

Так как эти фреймворки предоставляют API для разных языков программирования, сначала должна быть описана структура данных (schema), которая затем, в случае Thrift и Protocol Buffers, компилируется в классы. Avro не требует генерации кода. Структура данных в Thrift описывается в .thrift файлах, в Protocol Buffers — в .proto файлах, а в Avro — в JSON. Thrift и Protocol Buffers предоставляют компиляторы, которые компилируют .thrift и .proto файлы в классы для разных языков программирования.

Дополнительные затраты на описание структуры данных и генерацию кода не всегда оправданны. Например, когда интегрируются две подсистемы, которые написаны на Java, или приложение на Java сериализирует объект для сохранения, и нет других приложений, которые десериализируют объекты и т.д.

Также в данном сравнении не принимают участия библиотеки для сериализации в XML из-за их большого количества (которое тянет на отдельную статью). К тому же XML часто используется совместно с XSD (описанием схемы), что делает его похожим на фреймворки, которые рассматривались выше.

В данной статье будут рассмотрены библиотеки, которые позволяют сериализировать экземпляры классов, исходный код которых доступен, без необходимости описания схемы. Другими словами, аналоги Java Serialization API.

Известно, что Java Serialization является медленным (из тестов мы узнаем насколько) и сдержит уязвимости. Стандартная десериализация в Java — операция с огромной историей уязвимостей:
— Secure Coding Guidelines for Java SE;
— CWE-502: Deserialization of Untrusted Data;
— Deserialization of untrusted data.

Поэтому и возникла необходимость чем-то заменить стандартную сериализацию Java.

В тестах будут сравниваться следующие библиотеки:
— Java serialization, стандартная сериализация JDK;
— Kryo, используется в Apache Storm и Hazelcast;
— Kryo Unsafe, использует функциональность sun.misc.Unsafe;
— FST;
— FST Unsafe, использует функциональность sun.misc.Unsafe;
— Jackson JSON;
— Jackson Smile, чтение и запись данных в формате Smile («binary JSON»);
— fastjson, разработанный в Alibaba.

Сравнение форматов:

БиблиотекаФормат
БинарныйТекстовый
fastjson
FST
FST Unsafe
Jackson JSON
Jackson Smile
Java serialization
Kryo
Kryo Unsafe

Для сравнения производительности был создан проект, доступный на Gihub. Для сборки проекта используется Maven. Тесты запускаются при помощи JUnit и Maven Surefire Plugin.

Так как скорость сериализации и десериализации, а также размер результата сериализации меняется нелинейно в зависимости от структуры сериализируемого класса и количества данных в объекте, используется 3 теста, которые оперируют разными по структуре и размеру объектами. Условно выделено 3 вида объектов: маленькие, средние и большие. Объекты для тестов хранятся в трех файлах в формате JSON. В каждом файле по 100 объектов одинаковой структуры и похожего размера. Тестовые данные были созданы при помощи сервиса JSON Generator.

Сравнение размеров тестовых данных в формате JSON:

Тип объектаРазмера файла
Маленький16 KB
Средний130 KB
Большой10 MB

Тест для каждой библиотеки состоит из последовательной сериализации и десериализации 100 объектов каждого типа. Эти операции повторяются 10000 раз для минимизации погрешности. В качестве результата используется среднее арифметическое продолжительности сериализации и десериализации по всем итерациям. Для измерения времени используется высокоточный метод System.nanoTime().

Конфигурация системы, на которой запускались тесты:

ProcessorIntel® Core™ i3-4160 CPU, 3600 Mhz, 2 Cores, 4 Logical Processors
Installed Physical Memory (RAM)8 GB
Operating SystemMicrosoft Windows 7 (64-bit)
JVMJava HotSpot™ 64-Bit Server VM
JRE Version1.8.0_91
Initial Heap Size2 GB
Max Heap Size2 GB

Чтоб убедиться, что Garbage Collection (GC) не повлиял на статистику, тесты были запущены с JVM флагом “-Xloggc:gc.log”, который позволяет записать всю активность GC в файл gc.log. Из файла gc.log видно, что работа GC не могла повлиять на точность результатов тестов, всего 4 коротких запуска GC:

24.279: [GC (Metadata GC Threshold) 209944K->26531K(2010112K), 0.0612603 secs]
24.340: [Full GC (Metadata GC Threshold) 26531K->25897K(2010112K), 0.0873326 secs]
4997.614: [GC (System.gc()) 151850K->30430K(2010112K), 0.0065655 secs]
4997.620: [Full GC (System.gc()) 30430K->15343K(2010112K), 0.1173038 secs]

Далее представлены результаты тестов для трех типов объектов.

Маленькие объекты

Количество объектов — 100. Общий размер объектов в формате JSON — 16 KB.

Сравнение времени сериализации и десериализации маленьких объектов:

БиблиотекаСериализации, мсДесериализация, мсОбщее время, мсОбщее время, %
fastjson0,040,050,09100,00%
FST Unsafe0,050,060,11122,22%
FST0,040,090,13144,44%
Jackson Smile0,060,070,13144,44%
Kryo0,070,080,15166,67%
Kryo Unsafe0,070,080,15166,67%
Jackson JSON0,080,070,15166,67%
Java serialization0,210,91,111233,33%

Сравнение размера вывода сериализации маленьких объектов:

БиблиотекаРазмер вывода, KBРазмер вывода, %
Kryo6100,00%
Kryo Unsafe7116,67%
Jackson Smile10166,67%
Jackson JSON11183,33%
fastjson11183,33%
FST13216,67%
FST Unsafe21350,00%
Java serialization32533,33%

Средние объекты

Количество объектов — 100. Общий размер объектов в формате JSON — 130 KB.

Сравнение времени сериализации и десериализации средних объектов:

БиблиотекаСериализации, мсДесериализация, мсОбщее время, мсОбщее время, %
FST0,230,280,51100,00%
Kryo Unsafe0,260,270,53103,92%
Kryo0,270,280,55107,84%
FST Unsafe0,310,240,55107,84%
Jackson Smile0,270,340,61119,61%
fastjson0,390,510,9176,47%
Jackson JSON0,550,430,98192,16%
Java serialization0,912,723,63711,76%

Сравнение размера вывода сериализации средних объектов:

БиблиотекаРазмер вывода, KBРазмер вывода, %
Kryo72100,00%
Kryo Unsafe75104,17%
FST86119,44%
Jackson Smile90125,00%
Jackson JSON102141,67%
fastjson102141,67%
Java serialization145201,39%
FST Unsafe163226,39%

Большие объекты

Количество объектов — 100. Общий размер объектов в формате JSON — 10 MB.

Сравнение времени сериализации и десериализации больших объектов:

БиблиотекаСериализации, мсДесериализация, мсОбщее время, мсОбщее время, %
FST Unsafe12,17,4419,54100,00%
FST10,5310,9221,45109,77%
Jackson Smile13,1110,924,01122,88%
Kryo Unsafe28,2516,6244,87229,63%
Kryo28,6817,1145,79234,34%
Jackson JSON35,1120,9856,09287,05%
Java serialization27,0546,3273,37375,49%
fastjson52,5537,4890,03460,75%

Сравнение размера вывода сериализации больших объектов:

БиблиотекаРазмер вывода, MBРазмер вывода, %
Kryo8100,00%
Kryo Unsafe8100,00%
FST8100,00%
Jackson Smile8100,00%
Java serialization9112,50%
Jackson JSON9112,50%
fastjson9112,50%
FST Unsafe17212,50%

Вопросы совместимости

Для длительного хранения сериализованных байт может быть важно, как библиотека сериализации обрабатывает изменения в классах. Прямая совместимость (forward compatibility) — чтение сериализированных байт более новых версий классов. Обратная совместимость (backward compatibility) — чтение сериализированных байт более старых версий классов.

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

Exception in thread "main" java.io.InvalidClassException:
serializationtest.SerializationTest$TestClass; local class incompatible: stream classdesc serialVersionUID = 42, local class serialVersionUID = 43

Список совместимых и несовместимых изменений и описание их обработки есть в официальной документации.

В Kryo по умолчанию используется FieldSerializer. FieldSerializer не поддерживает добавление, удаление или изменение типа поля без аннулирования ранее сериализованных байт.

VersionFieldSerializer позволяет добавлять аннотацию @Since(int)для указания версии, в которой было добавлено поле. Этот класс предоставляет обратную совместимость. Это означает, что новые поля могут быть добавлены, но удаление, переименование или изменение типа любого поля аннулирует ранее сериализированные байты.

TaggedFieldSerializer сериализирует только поля с аннотацией @Tag(int), предоставляя обратную совместимость, позволяя добавлять новые поля. Этот класс также предоставляет прямую совместимость, если установить setIgnoreUnknownTags(true), что позволит игнорировать любое неизвестное поле. Поля могут быть переименованы, а поля, помеченные аннотацией @Deprecated, будут проигнорированы при чтении старых байтов и не будут записаны в новые байты.

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

FST поддерживает добавление полей без нарушения совместимости при помощи аннотации @Version. Для каждой версии приложения увеличивайте значение версии. Отсутствие аннотации Version означает, что версия равна 0. Каждое новое поле должно быть аннотировано. Если читается старый класс, новые поля будут инициализированы значениями по умолчанию. Удаление полей приведет к нарушению обратной совместимости, допускается только добавление полей.

JSON не требует описания схемы. Благодаря этому проще обеспечить прямую и обратную совместимость.

Чтобы обеспечить обратную совместимость в JSON всегда только добавляйте новые свойства и никогда не удаляйте и не переименовываете существующие свойства. Например, рассмотрим следующий JSON:

{
 "version": "1.0",
 "foo": true
}

Вместо переименования свойства "foo", добавьте новое свойство "bar":

{
 "version": "1.1",
 "foo": true,
 "bar": true
}

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

Jackson предоставляет прямую совместимость через установку свойства DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES объекта ObjectMapper в false. Таким образом неизвестные свойства JSON объекта будут игнорироваться.

Выводы

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

Результаты, которые показал Jackson при сериализации в JSON, меня удивили. Для небольших и средних объектов Jackson показал отличный результат с небольшим отставанием от библиотек, которые позиционируют себя как «fast» и «efficient». А учитывая то, что JSON — человекочитаемый формат, в определенных случаях Jackson может быть фаворитом при выборе библиотеки для сериализации. Несмотря на то, что на GiHub странице Fastjson от Alibaba утверждается, что эта библиотека предоставляет лучшую производительность, последний тест с большими объектами показал, что это не во всех случаях так.

Похожие статьи:
Научись использовать мощь и простоту Python для решения одной из важнейших задач автоматизации тестирования. Редкое современное...
Українська станція «Академік Вернадський» оголосила конкурс для фахівців різних напрямів, які з весни 2024 року до весни 2025 року...
З початку російського вторгнення в Україну повернулося близько 497 тисяч наших співвітчизників. Серед них і Антон — айтівець,...
Вы спросите: «Почему мы?» Наш ответ: «Мы не даем пустых обещаний!» Команда Учебного Центра QA START UP — это квалифицированные...
Цель данной статьи — уменьшить объем заблуждений и синхронизировать понимание основных принципов REST с сообществом. REST...
Яндекс.Метрика