Комментарии 67
Хочу отметить, что компактный вариант сериализации изначально задумывался для быстрого RPC с поддержкой эволюции классов. При этом не требуется передавать схемы вручную: главная фишка one-nio — в динамическом обмене схемами между клиентом и сервером. Это работает из коробки в RpcClient/RpcServer из той же библиотеки.
Хотел узнать, почему в таблице отмечена невозможность десериализовать классы, отсутствующие в classpath? Для отсутствующих классов one-nio автоматически генерирует классы-заглушки, и это неотъемлемая часть процедуры обмена схемами.
На счёт генерируемых заглушек при десериализации классов, отсутствующих в classpath. Действительно, эта любопытная механика заслуживает галочки в таблице раздела «Гибкость» — скорректирую. Спасибо за наводку, изначально я не разглядел всю прелесть этой фичи.
. Для того чтобы объективно оценить, какая из библиотек сериализации быстрее, мы взяли реальные данные из логов нашей системы и скомпоновали из них синтетические сессионные данные разной длины: от 0 до 1 МБ
А можно глянуть на примеры данных для теста «Десериализация до 1КБ»? Кроме того, непонятно, в какие структуры это все десериализовалось. Хотелось бы и на исходники микробенчмарков глянуть.
PS: Сразу приношу извинения, если это есть в статье, а я пропустил :)
Структура сериализуемых/десериализуемых данных при «Гонках» и «Взвешивании» была примерно следующая:
- объекты, вложенные друг в друга до 3-его уровня
- помимо других объектов, в каждом объекте присутствовали поля с типами
String
,Long
,Map
,byte[]
Микробенчмарки тривиальные:
- в
@Setup
-методе готовим сериализатор, объект для сериализации иbyte[]
с результатами сериализации (для benchmark-а десериализации) - далее в самих
@Benchmark
-ах просто выполняем serialize/deserialze
Спасибо за интересное сравнение. А вы не сравнивали, например, топ-5 участников с тем же protobuf-ом? Если выходит, что последний не дает никаких плюсов, то зачем с ним мучиться(встречал конторы где protobuf как укроп, пихали везде, даже во фронт).
protobuf позволяет данные в не гомогенной среде передавать. One Nio и другие только с Java работают.
Это все понятно, зачастую работа с jvm и нужна, KISS. Но вот есть, некоторые, у кого руки чешутся прикрутить protobuf где ненужно(payload сообщений в PubSub, например), а то что он без дела в джаве только шуршит.
кстати, где можно посмотреть сами тесты и json'ы?
Описание
@Benchmark
-ов и сериализуемых объектов привёл выше. БОльшая часть «участников соревнования» используют бинарный формат, а не JSON.- прогнать на оборудовании, похожем на клиентское :)
- сравнить с рукопашной упаковкой в byte[], поскольку конкретно у меня — есть всего с десяток классов, которые нужно держать в памяти и сбрасывать на диск, но суммарно может быть под миллиард экземпляров
- проверить, не окажется ли выгоднее хранить в JVM бинарные представления для объектов с десериализацией при обращении «по требованию», нежели честные экземпляры классов
- проверить на объектах размером в 20, 40 и 80К, поскольку, к сожалению, таковые вполне могут случиться
@Benchmark
-ов и сериализуемых объектов привёл выше.Используемые для сериализации данные, боюсь, что показать не могу, т.к. это реальные данные из логов системы.
При увеличении размера сериализуемых объектов до 1 МБ кривые на графиках скорости предсказуемо расходятся практически по прямым линиям. Победители и аутсайдеры видны уже, начиная с 10 КБ.
В целом, сериализация Java объектов может быть применима для большого круга задач, была бы фантазия. Но конкретный profit нужно мерить.
В инете и у Шипилёв вчастности, можно найти статьи, которые описывают неочевидные ошибки разработчиков при написании бенчмарков. Возможно, посмотрев код, кто-то смог бы его улучшить или использовать как отправную точку для своих бенчмарков.
Пока могу показать лишь «скелет» без деталей реализации:
public class SerializationPerformanceBenchmark {
@State( Scope.Benchmark )
public static class Parameters {
@Param( {
"Java standard",
"Jackson default",
"Jackson system",
"JacksonSmile default",
"JacksonSmile system",
"Bson4Jackson default",
"Bson4Jackson system",
"Bson MongoDb",
"Kryo default",
"Kryo unsafe",
"FST default",
"FST unsafe",
"One-Nio default",
"One-Nio for persist"
} )
public String serializer;
public Serializer serializerInstance;
@Param( { "0", "100", "200", "300", /*... */ "1000000" } ) // Toward 1 MB
public int sizeOfDto;
public Object dtoInstance;
public byte[] serializedDto;
@Setup( Level.Trial )
public void setup() throws IOException {
serializerInstance = Serializers.getMap().get( serializer );
dtoInstance = DtoFactory.createWorkflowDto( sizeOfDto );
serializedDto = serializerInstance.serialize( dtoInstance );
}
@TearDown( Level.Trial )
public void tearDown() {
serializerInstance = null;
dtoInstance = null;
serializedDto = null;
}
}
@Benchmark
public byte[] serialization( Parameters parameters ) throws IOException {
return parameters.serializerInstance.serialize(
parameters.dtoInstance );
}
@Benchmark
public Object unserialization( Parameters parameters ) throws IOException, ClassNotFoundException {
return parameters.serializerInstance.deserialize(
parameters.serializedDto,
parameters.dtoInstance.getClass() );
}
}
За абстракцицей
Serializer
скрыты все 14 исследуемых реализаций Java-сериализаторов.Конфигурация jackson во всех вариациях была дефолтной? Т.е. ObjectMapper без подключения модуля Afterburner и с дефолтным LRUMap в ObjectMapper->typeFactory->typeCache?
Однако добавлять геттеры и сеттеры в сериализуемые объекты — это не то, что нам нужно: объекты готовят потребители нашего сервиса, а мы должны быстро сериализовывать всё подряд.
Как я написал во введении, нам нужна библиотека, «не задающая лишних вопросов».
# JMH version: 1.23
# VM version: JDK 11.0.5, OpenJDK 64-Bit Server VM, 11.0.5+10
Benchmark (fileName) Mode Cnt Score Error Units
JacksonDeserialization.afterburnerModule request.json thrpt 719475.022 ops/s
JacksonDeserialization.afterburnerModule user.json thrpt 24939.785 ops/s
JacksonDeserialization.afterburnerModule repos.json thrpt 985.490 ops/s
JacksonDeserialization.afterburnerModule cities.json thrpt 57.935 ops/s
JacksonDeserialization.blackbirdModule request.json thrpt 718768.416 ops/s
JacksonDeserialization.blackbirdModule user.json thrpt 20479.817 ops/s
JacksonDeserialization.blackbirdModule repos.json thrpt 815.607 ops/s
JacksonDeserialization.blackbirdModule cities.json thrpt 56.831 ops/s
JacksonDeserialization.defaultMapper request.json thrpt 669093.374 ops/s
JacksonDeserialization.defaultMapper user.json thrpt 22144.445 ops/s
JacksonDeserialization.defaultMapper repos.json thrpt 780.062 ops/s
JacksonDeserialization.defaultMapper cities.json thrpt 55.894 ops/s
JacksonSerialization.afterburnerModule request.json thrpt 1207945.981 ops/s
JacksonSerialization.afterburnerModule user.json thrpt 131274.019 ops/s
JacksonSerialization.afterburnerModule repos.json thrpt 1368.781 ops/s
JacksonSerialization.afterburnerModule cities.json thrpt 59.140 ops/s
JacksonSerialization.blackbirdModule request.json thrpt 1216882.119 ops/s
JacksonSerialization.blackbirdModule user.json thrpt 122842.650 ops/s
JacksonSerialization.blackbirdModule repos.json thrpt 1204.178 ops/s
JacksonSerialization.blackbirdModule cities.json thrpt 56.534 ops/s
JacksonSerialization.defaultMapper request.json thrpt 1214062.085 ops/s
JacksonSerialization.defaultMapper user.json thrpt 123109.757 ops/s
JacksonSerialization.defaultMapper repos.json thrpt 1165.919 ops/s
JacksonSerialization.defaultMapper cities.json thrpt 55.973 ops/s
Похоже на правду?
ps данные и модели взяты тут
Используя эту мета-информацию при десериализации, библиотека One Nio точно знает, как выглядел класс сериализуемого объекта на момент сериализации. Именно на основании этого знания алгоритм десериализации One Nio является таким гибким, что обеспечивает максимальную совместимость получающихся при сериализации byte[].
Это будет работать при условии полного совпадения классов в сервисе-источнике и сервисе-получателе? А если они различаются, тогда десериализация завершится с ошибкой, без возможности восстановления?
Например, у отправителя появились новые поля в классе — более старый получатель просто их проигнорирует. У получателя появились новые поля — при получении данных от более старого отправителя новые поля останутся
null
-ами, либо значениями по умолчанию. Данных примеров с отличиями в классах отправителя и получателя масса.Обратите внимание на столбик One Nio (for persist) в таблице раздела «Гибкость». Почти по каждому критерию-сценарию там зачтённый бал.
Одинаковое имя поля, но разного типа, п.8 — не поддерживает. И, если я правильно понял, нет возможности добавить метод конверсии для типов, не имеющих родственных связей.
sun.reflect.MagicAccessorImpl
. Об этом написано в сноске #4 под таблицей раздела «Гибкость».И да, у других библиотек не удалось найти даже custom-изируемых средств достижения данного свойства.
Почему-то если имя не совпадает, то поле останется null, а если тип не совпадает, то это критическая ошибка.Потому что имя — это всего лишь строка, а тип определяет структуру данных. Смена типа — гораздо более серьёзное изменение, нежели смена имени поля.
Если меняется тип поля, то, вероятнее всего, это уже другое поле.
Так и я про то же. Почему при различии в типе поля нужно обязательно пытаться туда записать значение? Почему не просто проигнорировать поля с неподходящими типами?
Если честно, то мне сложно представить case-ы, когда реально, а не надуманно, было бы необходимо поддержать смену типа у поля при развитии класса.
А мы всё ещё говорим про мультисервисную архитектуру и сессионные данные в ней? Я вполне могу представить ситуацию, когда один не слишком аккуратно написанный сервис положил в сессию данные одного типа, а другой сервис попытался их прочитать в другой тип и упал.
Исходников с повторяемыми результатами нет, значит сравнение сделано черти как. Кэп.
пример подобного теста
@Version
не подходит для нашей задачи, когда классы сериализуемых объектов готовятся потребителями нашего сервиса, а для самого сервиса это «чёрный ящик»./**
* support for adding fields without breaking compatibility to old streams.
* For each release of your app increment the version value. No Version annotation means version=0.
* Note that each added field needs to be annotated.
*
* e.g.
*
* class MyClass implements Serializable {
*
* // fields on initial release 1.0
* int x;
* String y;
*
* // fields added with release 1.5
* @Version(1) String added;
* @Version(1) String alsoAdded;
*
* // fields added with release 2.0
* @Version(2) String addedv2;
* @Version(2) String alsoAddedv2;
*
* }
Как видим, нашим потребителям пришлось бы «заморочиться» со своими классами, сохраняемыми в сессию. А ведь из-за цепочек зависимостей объектов потребитель может даже не осознавать, что объекты какого-то из классов он сохраняет в сессию…
Таким образом, по умолчанию («не задавая лишних вопросов») у FST нет обратной совместимости со старыми сериализуемыми классами.
up to 10 times faster 100% JDK Serialization compatible drop-in replacement
There is limited support for readObject/writeObject. These methods will be called, but they should not work with the stream directly. The only stream methods they may call are defaultReadObject and defaultWriteObject. Other calls will result in exception.
Externalizable is completely supported.
serialization.jboss.org
В отличие от стандартной джавы — он поддерживает расширенный список классов без каких-либо ограничений.
Там сериализация JBoss сильно уступает Jackson Smile-у, Kryo и FST, которые попали в наше исследование.
Не понимаю, как Вы умудрились намерить для Java Standart такой "плачевный" результат.
У меня есть постоянные "измерители" полного цикла запроса и ответа на локальном и на удаленном узлах. На удаленном узле десериализация запроса, обработка запроса, сериализация ответа суммарно всегда отрабатывает менее 1 мс. На запрашиваемом узле сериализация запроса, ожидание, десериализация ответа суммарно менее 5 мс. Используется стандартная Java сериализация (из коробки). Используется ZIP для сжатия данных при передаче по сети по протоколу HTTP. По протоколу RMI результат будет еще ниже (не будет передачи надстройки HTTP протокола над передаваемыми данными по сети).
Объем переданных данных у меня не отслеживается (по косвенным признакам менее 1к в сжатом виде). Используется OpenJDK 1.8.
Чем меньше выполняется различных преобразований при сериализации — десериализации, тем лучше должен быть результат по определению. Java Standart — делает подобное, на мой взгляд, с наименьшими преобразованиями данных по сравнению со всем остальным перечисленным вами. Поэтому, думаю, что у Вас есть погрешность измерений для Java Standart.
Взял статистику по одну из запросов — минимальное время 1 мс, максимальное время — 1005 мс, среднее время — 4 мс, счетчик запросов — 71 тыс. Это время замера — отправки запроса на удаленный узел (сериализация запроса — сеть — десериализация запроса — обработка — сериализация ответа — сеть — десериализация ответа)
Во-вторых, если вы взглянете на первые графики из раздела «Гонки», то увидите, что для Java Standard цикл сериализации/десериализации данных размером порядка 1 КБ (примерно ваш размер) у нас занял 0,007 + 0,021 = 0,028 мс. У вас же получилось 4 мс за 2 цикла сериализации/десериализации + сетевые задержки. Это, без учёта сети, в 2000/28=~71 раз медленнее нашего результата. И где здесь «плачевный» результат?..
Кстати, исходя из того, что сама фаза измерения в нашем случае длилась 5 сек (я это указывал в начале «Гонок»), то для получения значения 0,007 мс по сериализации у нас ушло 714 285 повторений с усреднением результата. Для десериализации (0,021 мс) было использовано 238 095 повторений. Эти цифры кратно больше ваших 71 000 запросов, что говорит о том, что точность опубликованных измерений выше, чем в ваших «измерителях».
Java Standart — делает подобное, на мой взгляд, с наименьшими преобразованиями данных по сравнению со всем остальным перечисленным вами.Субъективное суждение, это лично ваше мнение.
Измерять можно по разному и ссылка на использование софта еще ни о чем не говорит. Разные люди, при использовании одного и того инструмента, могут получить разный результат. Доказано практикой.
Я Вам привел цифры ведь тоже не с потолка, а результат сбора статистики. И этот результат кардинально отличается от ваших измерений. Многократность большого значения не имеет, а лишь позволяет "подавить" различные погрешности измерений, вносимых синхронизаций часов и не равномерной загрузкой используемых внешних ресурсов (сети например).
Мои измерения строятся на фиксации временных точек непосредственно перед вызовом удаленного метода и сразу после получения результата этого вызова. Внутри вызова "спрятан" код "упаковки" объектов в поток. Какие либо другие преобразования данных отсутствуют.
В вашем случае, из-за обилия рассмотренных решений, думаю, внутренний код вызовов в каждом из них кардинально отличаются. Соответственно, большую погрешность измерений будет вносит эти существенные отличия.
Но мне интересно, где Вы умудрились потерять столько времени на использовании "Java Standart" в своих измерениях. Различия в JVM IBM и SUN (Oracle) я отвергаю. Я работал и с тем и другим, в них есть различия, но другого рода.
Поясните, что измерялось в вашем случае использования "Java Standart". Как реализован вызов? Сериализованы ли объекты передаваемые через сеть?
Во-вторых, если вы взглянете на первые графики из раздела «Гонки», то увидите, что для Java Standard цикл сериализации/десериализации данных размером порядка 1 КБ (примерно ваш размер) у нас занял 0,007 + 0,021 = 0,028 мс. У вас же получилось 4 мс за 2 цикла сериализации/десериализации + сетевые задержки. Это, без учёта сети, в 2000/28=~71 раз медленнее нашего результата. И где здесь «плачевный» результат?..
Вы не путаете результат своего измерения с секундами? Системные часы точнее миллисекунд измерять не позволяют!
Конечно, измерять с такой точностью системные часы компьютеров не позволяют.
Сериализованы ли объекты передаваемые через сеть?Передачи через сеть в наших измерениях нет, только сериализация/десериализация.
Рекомендую ознакомиться с Java Microbenchmark Harness (JMH).
«JMH-бенчмарк без деталей» я привёл в одном из комментариев выше. В ближайшее время добавлю этот кусок Java-кода в тело самой публикации.
Передачи через сеть в наших измерениях нет
Тогда мы говорим о совершенно разных понятиях "сериализации" и "десериализации". В моем понимании эти понятия связаны с выводом в поток и восстановлением объекта из потока.
Что намерили Вы и чем помогли вам результаты ваших измерений — не понимаю. Все же преамбуле к статье Вы написали "передача состояния в распределенной среде", а в результатах измерений распределенной среды получается у Вас нет.
И получается, что мой результат 0 мс на приемной стороне (удаленном узле) ничем не хуже вашего.
В моем понимании эти понятия связаны с выводом в поток и восстановлением объекта из потока.У сериализации другое определение, если посмотреть даже в Wikipedia:
In computer science, in the context of data storage, serialization (or serialisation) is the process of translating data structures or object state into a format that can be stored (for example, in a file or memory buffer) or transmitted (for example, across a network connection link) and reconstructed later (possibly in a different computer environment).Для Java, фактически, сериализация — это только преобразование
Object
-а в byte[]|String|...
, которые можно передать по сети/сохранить в БД и т.д., но эти действия не относятся к самой сериализации.Останемся при своих мнениях, не вижу смысла их друг другу навязывать.
Это лукавство — ваша цитата понятия сериализации. В распределенной системе (указано в вашей преамбуле) понятие сериализации одно — преобразование программного объекта в поток (массив байт).
В этом преобразовании Java Standart всегда будет работать быстрее любой другой рассмотренной вами в сравнении технологии, ввиду отсутствия других преобразований. Object -> byte[] всегда быстрее по сравнению с Object -> JSON -> byte[].
Ни одна из перечисленных библиотек не выполняет преобразований вида: Object -> JSON -> byte[].
Либо Object -> JSON, либо Object -> byte[]. В последнем случае как раз и наблюдается более высокая скорость, чем у Java Standard.
Что понимаете под Object? Java-object c полями объекта, JSON (String) или что то другое?
java.lang.Object
с полями объекта
Вы понимаете, что все участвующие в сравнении библиотек предназначены для преобразования в формат отличный от byte[]? Грубо преобразует Object в строку, которая далее будет преобразована в массив байт при передаче в распределенной системе! Т.е. выполняется преобразование Object -> JSON -> byte[]. Это всегда будет работать хуже, чем прямое преобразование Object в byte[]. ВСЕГДА!
Если Вы получили другой результат, то Вам нужно проверить правильность тестирования.
Почему заигнорили мирового лидера по многофункциональности и скорости fastjson от Alibaba: https://github.com/alibaba/fastjson
Пробовал различные вариации JSON-сериализации и различные варианты бинарной сериализации - общая тенденция такова, что бинарные форматы быстрее за счёт того, что в них нет логически лишних данных для хранения разметки.
Конкретно, Fast JSON я нарочно не игнорировал - думаю, стоит попробовать "пропустить" через benchmark. Спасибо за наводку!
Java-сериализация: максимум скорости без жёсткой структуры данных