Сравнение 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()
.
Конфигурация системы, на которой запускались тесты:
Processor | Intel® Core™ i3-4160 CPU, 3600 Mhz, 2 Cores, 4 Logical Processors |
Installed Physical Memory (RAM) | 8 GB |
Operating System | Microsoft Windows 7 |
JVM | Java HotSpot™ |
JRE Version | 1.8.0_91 |
Initial Heap Size | 2 GB |
Max Heap Size | 2 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.
Сравнение времени сериализации и десериализации маленьких объектов:
Библиотека | Сериализации, мс | Десериализация, мс | Общее время, мс | Общее время, % |
fastjson | 0,04 | 0,05 | 0,09 | 100,00% |
FST Unsafe | 0,05 | 0,06 | 0,11 | 122,22% |
FST | 0,04 | 0,09 | 0,13 | 144,44% |
Jackson Smile | 0,06 | 0,07 | 0,13 | 144,44% |
Kryo | 0,07 | 0,08 | 0,15 | 166,67% |
Kryo Unsafe | 0,07 | 0,08 | 0,15 | 166,67% |
Jackson JSON | 0,08 | 0,07 | 0,15 | 166,67% |
Java serialization | 0,21 | 0,9 | 1,11 | 1233,33% |
Сравнение размера вывода сериализации маленьких объектов:
Библиотека | Размер вывода, KB | Размер вывода, % |
Kryo | 6 | 100,00% |
Kryo Unsafe | 7 | 116,67% |
Jackson Smile | 10 | 166,67% |
Jackson JSON | 11 | 183,33% |
fastjson | 11 | 183,33% |
FST | 13 | 216,67% |
FST Unsafe | 21 | 350,00% |
Java serialization | 32 | 533,33% |
Средние объекты
Количество объектов — 100. Общий размер объектов в формате JSON — 130 KB.
Сравнение времени сериализации и десериализации средних объектов:
Библиотека | Сериализации, мс | Десериализация, мс | Общее время, мс | Общее время, % |
FST | 0,23 | 0,28 | 0,51 | 100,00% |
Kryo Unsafe | 0,26 | 0,27 | 0,53 | 103,92% |
Kryo | 0,27 | 0,28 | 0,55 | 107,84% |
FST Unsafe | 0,31 | 0,24 | 0,55 | 107,84% |
Jackson Smile | 0,27 | 0,34 | 0,61 | 119,61% |
fastjson | 0,39 | 0,51 | 0,9 | 176,47% |
Jackson JSON | 0,55 | 0,43 | 0,98 | 192,16% |
Java serialization | 0,91 | 2,72 | 3,63 | 711,76% |
Сравнение размера вывода сериализации средних объектов:
Библиотека | Размер вывода, KB | Размер вывода, % |
Kryo | 72 | 100,00% |
Kryo Unsafe | 75 | 104,17% |
FST | 86 | 119,44% |
Jackson Smile | 90 | 125,00% |
Jackson JSON | 102 | 141,67% |
fastjson | 102 | 141,67% |
Java serialization | 145 | 201,39% |
FST Unsafe | 163 | 226,39% |
Большие объекты
Количество объектов — 100. Общий размер объектов в формате JSON — 10 MB.
Сравнение времени сериализации и десериализации больших объектов:
Библиотека | Сериализации, мс | Десериализация, мс | Общее время, мс | Общее время, % |
FST Unsafe | 12,1 | 7,44 | 19,54 | 100,00% |
FST | 10,53 | 10,92 | 21,45 | 109,77% |
Jackson Smile | 13,11 | 10,9 | 24,01 | 122,88% |
Kryo Unsafe | 28,25 | 16,62 | 44,87 | 229,63% |
Kryo | 28,68 | 17,11 | 45,79 | 234,34% |
Jackson JSON | 35,11 | 20,98 | 56,09 | 287,05% |
Java serialization | 27,05 | 46,32 | 73,37 | 375,49% |
fastjson | 52,55 | 37,48 | 90,03 | 460,75% |
Сравнение размера вывода сериализации больших объектов:
Библиотека | Размер вывода, MB | Размер вывода, % |
Kryo | 8 | 100,00% |
Kryo Unsafe | 8 | 100,00% |
FST | 8 | 100,00% |
Jackson Smile | 8 | 100,00% |
Java serialization | 9 | 112,50% |
Jackson JSON | 9 | 112,50% |
fastjson | 9 | 112,50% |
FST Unsafe | 17 | 212,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 утверждается, что эта библиотека предоставляет лучшую производительность, последний тест с большими объектами показал, что это не во всех случаях так.