company_banner

Java-сериализация: максимум скорости без жёсткой структуры данных

    Наша команда в Сбербанке занимается разработкой сервиса сессионных данных, который организует взаимообмен единым Java-контекстом сессии между распределёнными приложениями. Наш сервис крайне нуждается в очень быстрой сериализации Java-объектов, поскольку это часть нашей mission critical задачи. Изначально нам на ум приходили: Google Protocol Buffers, Apache Thrift, Apache Avro, CBOR и др. Первая тройка из перечисленных библиотек требует для сериализации объектов описания схемы их данных. CBOR такой низкоуровневый, что умеет сериализовывать только скалярные значения и их наборы. Нам же была нужна библиотека Java-сериализации, «не задающая лишних вопросов» и не заставляющая вручную разбирать сериализуемые объекты «на атомы». Мы хотели сериализовывать произвольные Java-объекты, не зная о них практически ничего, и хотели делать это максимально быстро. Поэтому мы устроили соревнование для имеющихся Open Source решений задачи Java-сериализации.

    КДПВ

    Участники соревнования


    Для соревнования мы отобрали наиболее популярные библиотеки Java-сериализации, главным образом, использующие бинарный формат, а также библиотеки, хорошо зарекомендовавшие себя в других обзорах Java-сериализаторов.
    1 Java standard Стандартная Java-сериализация «из коробки», преобразующая Java-объекты в собственный бинарный формат.
    2 Jackson JSON Популярная библиотека FasterXML/jackson-databind, преобразующая Java-объекты в стандартный JSON-формат.
    3 Jackson JSON (with types) Та же библиотека, что и выше, но настроенная таким образом, чтобы включать в результат сериализации full qualified имена Java-классов. Это может быть востребованным при длительном хранении JSON-а (например, в БД) перед десериализацией.
    Вот, как выглядит такой JSON...
    [
      "ru.sbrf.ufs.dto.PersonDto",
      {
        "firstName":"Ivan",
        "lastName":"Ivanov"
      }
    ]
    
    Особенности настройки библиотеки...
    Вместо:
    public ObjectMapper createMapper() {
        return new ObjectMapper();
    }
    
    используется:
    public ObjectMapper createMapper() {
        return new ObjectMapper()
                .enable(
                        ACCEPT_SINGLE_VALUE_AS_ARRAY,
                        ACCEPT_EMPTY_STRING_AS_NULL_OBJECT,
                        ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT,
                        READ_UNKNOWN_ENUM_VALUES_AS_NULL,
                        UNWRAP_SINGLE_VALUE_ARRAYS
                )
                .disable(
                        FAIL_ON_INVALID_SUBTYPE,
                        FAIL_ON_NULL_FOR_PRIMITIVES,
                        FAIL_ON_IGNORED_PROPERTIES,
                        FAIL_ON_UNKNOWN_PROPERTIES,
                        FAIL_ON_NUMBERS_FOR_ENUMS,
                        FAIL_ON_UNRESOLVED_OBJECT_IDS,
                        WRAP_EXCEPTIONS
                )
                .enable(ALLOW_SINGLE_QUOTES)
                .disable(FAIL_ON_EMPTY_BEANS)
                .enable(MapperFeature.PROPAGATE_TRANSIENT_MARKER)
                .setVisibility(FIELD, ANY)
                .setVisibility(ALL, NONE)
                .enableDefaultTyping(NON_FINAL);  // !
    }
    
    4 Jackson Smile Библиотека FasterXML/jackson-dataformats-binary/smile, являющаяся расширением Jackson-а, преобразующая Java-объекты в бинарный эквивалент JSON-формата – Smile.
    5 Jackson Smile (with types) Та же библиотека, что и выше, но настроенная идентично «Jackson JSON (with types)» (full qualified имена Java-классов включаются в результат сериализации).
    6 Bson4Jackson Библиотека michel-kraemer/bson4jackson, являющаяся расширением Jackson-а, преобразующая Java-объекты в бинарный эквивалент JSON-формата – BSON.
    7 Bson4Jackson (with types) Та же библиотека, что и выше, но настроенная идентично «Jackson JSON (with types)» (full qualified имена Java-классов включаются в результат сериализации).
    8 BSON MongoDb Библиотека mongodb/mongo-java-driver/bson, также преобразующая Java-объекты в BSON-формат.
    9 Kryo Библиотека EsotericSoftware/kryo, преобразующая Java-объекты в собственный бинарный формат.
    10 Kryo (unsafe) Та же библиотека, что и выше, но настроенная на использование класса sun.misc.Unsafe для ускорения сериализации/десериализации.
    Особенности настройки библиотеки...
    Вместо классов:
    com.esotericsoftware.kryo.io.Input
    com.esotericsoftware.kryo.io.Output
    
    используются:
    com.esotericsoftware.kryo.io.UnsafeInput
    com.esotericsoftware.kryo.io.UnsafeOutput
    11 FST Библиотека RuedigerMoeller/fast-serialization, преобразующая Java-объекты в собственный бинарный формат.
    12 FST (unsafe) Та же библиотека, что и выше, но настроенная на использование класса sun.misc.Unsafe для ускорения сериализации/десериализации.
    Особенности настройки библиотеки...
    Вместо:
    FSTConfiguration fst = FSTConfiguration.createDefaultConfiguration();
    
    используется:
    FSTConfiguration fst = FSTConfiguration.createUnsafeBinaryConfiguration();
    13 One Nio Библиотека odnoklassniki/one-nio, преобразующая Java-объекты в собственный бинарный формат.
    14 One Nio (for persist) Та же библиотека, что и выше, но настроенная таким образом, чтобы включать в результат сериализации детальную мета-информацию о классе сериализуемого Java-объекта. Это может быть востребованным при длительном хранении byte[] (например, в БД) перед десериализацией. То есть преследуемая цель та же, что у «Jackson JSON (with types)».
    Особенности настройки библиотеки...
    Вместо:
    byte[] bufWithoutSerializers = new byte[bufferSize];
    SerializeStream out = new SerializeStream( bufWithoutSerializers );
    out.writeObject(object);
    // bufWithoutSerializers is the result
    
    используется:
    byte[] bufWithSerializers = new byte[bufferSize];
    PersistStream out = new PersistStream( bufWithSerializers );
    out.writeObject(object);
    bufWithSerializers = out.toByteArray();
    // bufWithSerializers is the result
    

    При такой сериализации object-а в result дополнительно помещается мета-информация:
    1) full qualified имя класса объекта object,
    2) имена всех полей этого класса,
    3) full qualified имена классов всех этих полей,
    4) предыдущие два пункта рекурсивно для класса каждого поля.
    Используя эту мета-информацию при десериализации, библиотека One Nio будет точно знать, как выглядел класс сериализуемого объекта на момент сериализации.
    Ну что, поехали!

    Гонки


    Скорость – вот основной критерий оценки библиотек Java-сериализации, которые являются участниками нашего импровизированного соревнования. Для того чтобы объективно оценить, какая из библиотек сериализации быстрее, мы взяли реальные данные из логов нашей системы и скомпоновали из них синтетические сессионные данные разной длины: от 0 до 1 МБ. По формату данные представляли собой строки и байтовые массивы.
    Примечание: Забегая вперёд, следует сказать, что победители и проигравшие выявились уже на размерах сериализуемых объектов от 0 до 10 КБ. Дальнейшее увеличение размера объектов до 1 МБ не изменило исход соревнования.
    В связи с этим, для лучшей наглядности, приведённые ниже графики эффективности работы Java-сериализаторов ограничены размером объектов в 10 КБ.
    Конфигурация системы, на которой производились измерения:
    Процессор IntelR CoreTM i7-6700 CPU, 3.4GHz, 8 cores
    Память 16 GB
    Операционная система Microsoft Windows 10 (64-bit)
    JRE IBM J9 VM 1.7.0
    Примечание: К нашему сожалению, на IBM JRE отказалась работать библиотека One Nio (участники под номерами 13 и 14). Эта библиотека использует класс sun.reflect.MagicAccessorImpl для обращения к private и final (при десериализации) полям классов, минуя проверки уровня доступа. Оказалось, IBM JRE не поддерживает этих основных свойств класса sun.reflect.MagicAccessorImpl, несмотря на то, что сам класс в runtime имеется.

    Для того чтобы не удалять данных участников гонки на самом старте (а, согласно Serialization-FAQ, библиотека One Nio обладает широкими возможностями), мы решили сделать fork данной библиотеки, в котором использование класса sun.reflect.MagicAccessorImpl было бы выключаемым. При выключенном использовании sun.reflect.MagicAccessorImpl в нашем fork-е используется класс sun.misc.Unsafe для достижения тех же целей.
    Кроме того, в нашем fork-е была выполнена оптимизация сериализации строк – строки стали сериализовываться на 30-40% быстрее при работе на IBM JRE.

    В связи с этим, в данной публикации все результаты для библиотеки One Nio получены на собственном fork-е, а не на оригинальной библиотеке.
    Непосредственное измерение скорости сериализации/десериализации выполнялось с помощью Java Microbenchmark Harness (JMH) – инструмента от OpenJDK для построения и запуска benchmark-ов. Для каждого измерения (одной точки на графике) использовалось 5 секунд для «прогрева» JVM и ещё 5 секунд для самих измерений времени с последующим усреднением.
    UPD:
    Код JMH-бенчмарка без некоторых деталей
    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() );
        }
    }
    

    Вот, что получилось:

    Гонки - все участники

    Сначала заметим, что варианты библиотек, добавляющие в результат сериализации дополнительные мета-данные, работают медленнее, чем дефолтные конфигурации этих же библиотек (см. конфигурации «with types» и «for persist»).
    В целом, не зависимо от конфигурации аутсайдерами по результатам сериализации становятся Jackson JSON и Bson4Jackson, которые выбывают из гонки.
    Кроме того, по результатам десериализации из гонки выбывает Java Standard, т.к. при любом размере сериализуемых данных десериализация кратно медленнее конкурентов.

    Взглянем поближе на оставшихся участников:

    Гонки - кроме аутсайдеров

    По результатам сериализации в уверенных лидерах идёт библиотека FST, а при увеличении размера объектов ей «на пятки наступает» One Nio. Заметим, что у One Nio вариант «for persist» сильно медленнее дефолтной конфигурации по скорости сериализации.
    Если взглянуть на десериализацию, то видим, что One Nio с увеличением размера данных смог обогнать FST. У последнего, напротив, нестандартная конфигурация «unsafe» заметно быстрее выполняет десериализацию.

    Для того чтобы расставить все точки над И, давайте посмотрим на суммарный результат по сериализации и десериализации:

    Гонки - кроме аутсайдеров (общий зачёт)

    Стало очевидно, что однозначных лидеров два: FST (unsafe) и One Nio.
    Если на небольших объектах FST (unsafe) уверенно лидирует, то с ростом размера сериализуемых объектов он начинает уступать и, в конечном счёте, уступает One Nio.

    Третью позицию с ростом размера сериализуемых объектов уверенно занимает BSON MongoDb, хотя отрыв от лидеров у него почти двукратный.

    Взвешивание


    Размер результата сериализации – второй важнейший критерий оценки библиотек Java-сериализации. В каком-то плане, от размера результата зависит скорость сериализации/десериализации: компактный результат формировать и обрабатывать быстрее, чем объёмный. Для «взвешивания» результатов сериализации использовались всё те же Java-объекты, сформированные из реальных данных, взятых из логов системы (строк и байтовых массивов).

    Кроме того, важным свойством результата сериализации является и то, на сколько он хорошо сжимается (например, для сохранения в БД или других хранилищах). В нашем соревновании мы использовали алгоритм сжатия Deflate, являющийся основой для ZIP и gzip.

    Результаты «взвешивания» получились следующими:

    Взвешивание

    Ожидаемо, самыми компактными оказались результаты сериализации у одного из лидеров гонки: One Nio.
    Второе место по компактности досталось BSON MongoDb (который занял третье место в гонке).
    На третье место по компактности «вырвалась» библиотека Kryo, ранее не сумевшая проявить себя в гонке.

    Результаты сериализации этих 3-х лидеров «взвешивания» ещё и отлично сжимаются (почти в двое). Самыми плохосжимаемыми оказались: бинарный эквивалент JSON-а – Smile и сам JSON.

    Любопытный факт – все победители «взвешивания» при сериализации добавляют одинаковое количество служебных данных и к маленьким, и к большим сериализуемым объектам.

    Гибкость


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

    Гибкость
    Сноски с пояснениями
    1    Десериализуется LinkedHashMap.
    2   Для объекта в целом — ДА, для поля объекта — НЕТ.
    3   Для объекта в целом — НЕТ, для поля объекта — ДА.
    4   С использованием sun.reflect.MagicAccessorImpl — ДА: boxing/unboxing, примитивы в BigInteger/BigDecimal/String. Без использования MagicAccessorImpl (доработанный в СберТех'е fork One Nio) — НЕТ.
    5   Десериализуется ArrayList.
    6   Десериализуется ArrayList или HashSet в зависимости от конкретного сериализованного типа.
    7   Десериализуется HashMap.
    8   Для объекта в целом и для поля объекта — НЕТ, но если отсутствует класс объекта, располагавшегося в коллекции/Map-е, то ДА (при этом десериализуется HashMap).
    9   Десериализуется автоматически сгенерированный класс-заглушка.
    10 В оригинальной библиотеке One Nio — НЕТ, в доработанном в СберТех'е fork-е — ДА.
    11 При десериализации даже конструктор не вызывается.
    UPD: По 13-ому критерию One Nio (for persist) получил ещё один балл (19-ый).

    Данный скрупулёзный «осмотр претендентов» был, пожалуй, самым трудоёмким этапом нашего «кастинга». Но зато эти результаты сравнения хорошо открывают глаза на удобство использования библиотек сериализации. В последствие можно использовать эти результаты как справочник.

    Как ни обидно было осознавать, но наши лидеры по результатам гонок и взвешивания – FST (unsafe) и One Nio – оказались аутсайдерами по гибкости... Однако нас заинтересовал любопытный факт: One Nio в конфигурации «for persist» (не самая быстрая и не самая компактная) набрала больше всех баллов по гибкости — 19/20. Очень привлекательной выглядела возможность заставить дефолтную (быструю и компактную) конфигурацию One Nio работать также гибко – и способ нашёлся.

    В самом начале, когда мы представляли участников соревнования, говорилось о том, что One Nio (for persist) включает в результат сериализации детальную мета-информацию о классе сериализуемого Java-объекта (*). Используя эту мета-информацию при десериализации, библиотека One Nio точно знает, как выглядел класс сериализуемого объекта на момент сериализации. Именно на основании этого знания алгоритм десериализации One Nio является таким гибким, что обеспечивает максимальную совместимость получающихся при сериализации byte[].

    Оказалось, что мета-информацию (*) можно отдельно получить для указанного класса, сериализовать в byte[] и отправить на ту сторону, где будет происходить десериализация Java-объектов данного класса:
    С кодом по шагам...
    // Сервис №1: Получаем мета-информацию о классе SomeDto
    one.nio.serial.Serializer<SomeDto> dtoSerializerWithMeta = Repository.get( SomeDto.class );
    byte[] dtoMeta = serializeByDefaultOneNioAlgorithm( dtoSerializerWithMeta );
    // Сервис №1: Отправляем dtoMeta сервису №2
    
    // Сервис №2: Восстанавливаем мета-информацию об удалённом классе SomeDto и сообщаем об этом библиотеке One Nio
    one.nio.serial.Serializer<SomeDto> dtoSerializerWithMeta = deserializeByOneNio( dtoMeta );
    Repository.provideSerializer( dtoSerializerWithMeta );
    
    // Сервис №1: Сериализуем объекты класса SomeDto
    byte[] bytes1 = serializeByDefaultOneNioAlgorithm( object1 );
    byte[] bytes2 = serializeByDefaultOneNioAlgorithm( object2 );
    ...
    // Сервис №1: Отправляем байты сервису №2
    
    // Сервис №2: Десериализуем байты в объекты класса SomeDto
    SomeDto object1 = deserializeByOneNio( bytes1 );
    SomeDto object2 = deserializeByOneNio( bytes2 );
    ...
    

    Если произвести эту явную процедуру взаимообмена мета-информацией о классах между распределёнными сервисами, то такие сервисы смогут отправлять друг другу сериализованные Java-объекты, используя дефолтную (быструю и компактную) конфигурацию One Nio. Ведь, пока сервисы запущены, версии классов на их сторонах неизменны, а значит не зачем при каждом взаимодействии «таскать туда-сюда» константную мета-информацию внутри каждого результата сериализации. Таким образом, сделав немного больше действий в начале, затем можно использовать скорость и компактность One Nio одновременно с гибкостью One Nio (for persist). То что нужно!

    В результате, для передачи Java-объектов между распределёнными сервисами в сериализованном виде (то, для чего мы и устроили данное соревнование) One Nio оказался победителем по гибкости (19/20).
    Среди отличившихся ранее в гонках и взвешивании Java-сериализаторов не плохую гибкость продемонстрировали:

    • BSON MongoDb (14,5/20),
    • Kryo (13/20).

    Пьедестал


    Вспомним результаты прошедших соревнований Java-сериализаторов:

    • в гонках первые две строчки рейтинга поделили FST (unsafe) и One Nio, а третье место занял BSON MongoDb,
    • на взвешивании победил One Nio, за которым шли BSON MongoDb и Kryo,
    • по гибкости, именно для нашей задачи обмена сессионным контекстом между распределёнными приложениями, первое место снова досталось One Nio, а также отличились BSON MongoDb и Kryo.

    Таким образом, по совокупности достигнутых результатов пьедестал у нас получился следующим:

    1. One Nio
      В главном соревновании – гонках – делил первое место с FST (unsafe), но на взвешивании и при проверке гибкости существенно обошёл конкурента.
    2. FST (unsafe)
      Также очень быстрая библиотека Java-сериализации, однако ей не хватает прямой и обратной совместимости получающихся в результате сериализации байтовых массивов.
    3. BSON MongoDB + Kryo
      Эти 2 библиотеки поделили 3-ю строчку нашего рейтинга самых быстрых Java-сериализаторов, не требующих описания структуры данных. Обе библиотеки достаточно сильно отстали от 2-х лидеров по скорости, но при этом являются практически идентичными по компактности и гибкости. У обеих библиотек есть проблемы при сериализации Collection и Map, а у BSON MongoDB ещё и нет возможности custom-ного управления сериализацией/десериализацией (Externalizable и т.п.).

    В Сбербанке в нашем сервисе сессионных данных мы использовали библиотеку One Nio, занявшую первое место в нашем соревновании. С помощью данной библиотеки сериализовывались данные сессионного Java-контекста и передавались между приложениями. Благодаря данной доработке скорость работы сессионного транспорта кратно ускорилась. Нагрузочное же тестирование показало, что на сценариях, приближенных к реальному поведению пользователей в Сбербанк Online, было получено ускорение до 40% только лишь за счёт одной этой доработки. Такой результат означает снижение времени отклика системы на действия пользователей, что увеличивает степень удовлетворённости наших клиентов.

    В следующей статье я постараюсь продемонстрировать в действии дополнительное ускорение One Nio, получаемое за счёт использования класса sun.reflect.MagicAccessorImpl. К сожалению IBM JRE не поддерживает самых главных свойств этого класса, а, значит, весь потенциал One Nio на этой версии JRE ещё не раскрыт. Продолжение следует.
    Сбербанк
    Компания

    Комментарии 65

      +1

      https://github.com/tarantool/tarantool-java (Message Pack) не участвует?

        0
        Нет, эту библиотеку не рассматривали.
        Отбирали только библиотеки, позволяющие локально в самой JVM выполнять сериализацию/десериализацию произвольных java Object-ов, без знания схемы/структуры их данных.
        0
        Чем обусловлено использование openJ9? Вы используете только эту jvm?
          0
          Выбор IBM J9 VM 1.7.0 обусловлен историческими причинами. Мы были вынуждены «научиться жить» в таком runtime окружении и делать это максимально быстро. Планы использовать другие JVM есть, как у всех перспективных продуктов :) В следующих публикациях я постараюсь выполнить сравнительный анализ на разных JVM.
          +10
          Спасибо за подробную статью! Было приятно увидеть one-nio в топе.

          Хочу отметить, что компактный вариант сериализации изначально задумывался для быстрого RPC с поддержкой эволюции классов. При этом не требуется передавать схемы вручную: главная фишка one-nio — в динамическом обмене схемами между клиентом и сервером. Это работает из коробки в RpcClient/RpcServer из той же библиотеки.

          Хотел узнать, почему в таблице отмечена невозможность десериализовать классы, отсутствующие в classpath? Для отсутствующих классов one-nio автоматически генерирует классы-заглушки, и это неотъемлемая часть процедуры обмена схемами.
            +1
            Динамический обмен схемами в One Nio мы видели и, честно говоря, впечатлились. Я попытался отразить суть этой процедуры в разделе «Гибкость», когда говорил про одновременное использование достоинств One Nio (скорость) и One Nio (for persist) (гибкость). Из коробки RpcClient/RpcServer «as is» нам не подошли из-за нашей специфики, поэтому мы делаем обмен схемами немного по-своему.

            На счёт генерируемых заглушек при десериализации классов, отсутствующих в classpath. Действительно, эта любопытная механика заслуживает галочки в таблице раздела «Гибкость» — скорректирую. Спасибо за наводку, изначально я не разглядел всю прелесть этой фичи.
            0
            . Для того чтобы объективно оценить, какая из библиотек сериализации быстрее, мы взяли реальные данные из логов нашей системы и скомпоновали из них синтетические сессионные данные разной длины: от 0 до 1 МБ


            А можно глянуть на примеры данных для теста «Десериализация до 1КБ»? Кроме того, непонятно, в какие структуры это все десериализовалось. Хотелось бы и на исходники микробенчмарков глянуть.

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

              Структура сериализуемых/десериализуемых данных при «Гонках» и «Взвешивании» была примерно следующая:
              • объекты, вложенные друг в друга до 3-его уровня
              • помимо других объектов, в каждом объекте присутствовали поля с типами String, Long, Map, byte[]

              Микробенчмарки тривиальные:
              • в @Setup-методе готовим сериализатор, объект для сериализации и byte[] с результатами сериализации (для benchmark-а десериализации)
              • далее в самих @Benchmark-ах просто выполняем serialize/deserialze
              0

              Спасибо за интересное сравнение. А вы не сравнивали, например, топ-5 участников с тем же protobuf-ом? Если выходит, что последний не дает никаких плюсов, то зачем с ним мучиться(встречал конторы где protobuf как укроп, пихали везде, даже во фронт).

                0

                protobuf позволяет данные в не гомогенной среде передавать. One Nio и другие только с Java работают.

                  0

                  Это все понятно, зачастую работа с jvm и нужна, KISS. Но вот есть, некоторые, у кого руки чешутся прикрутить protobuf где ненужно(payload сообщений в PubSub, например), а то что он без дела в джаве только шуршит.

                    0
                    Зачастую лучший инструмент это тот, который лучше знаешь.
                    Затраты на изучение и применение другого инструмента редко окупаются.
                  0
                  Protobuf отсеяли сразу, т.к. у нас, в принципе, нет схем сериализуемых данных. Не стали создавать «Франкенштейна», притягивая за уши Protobuf.
                  0
                  есть ли шанс увидеть тестирование более широкого круга (например, +Gson) и на HotSpot?
                  кстати, где можно посмотреть сами тесты и json'ы?
                    0
                    Как уже писал, в следующих публикациях я постараюсь выполнить сравнительный анализ на разных JVM. Более широкий круг сериализаторов?.. Возможно, если обнаружатся достаточно быстрые аналоги.

                    Описание @Benchmark-ов и сериализуемых объектов привёл выше. БОльшая часть «участников соревнования» используют бинарный формат, а не JSON.
                      0
                      если интересны бенчмарки сериализаторов в json, то можно посмотреть тут github.com/wizzardo/json-benchmarks
                        0
                        спасибочки!
                      0
                      А исходный код бенчмарков — коммерческая тайна? Было бы интересно:
                      • прогнать на оборудовании, похожем на клиентское :)
                      • сравнить с рукопашной упаковкой в byte[], поскольку конкретно у меня — есть всего с десяток классов, которые нужно держать в памяти и сбрасывать на диск, но суммарно может быть под миллиард экземпляров
                      • проверить, не окажется ли выгоднее хранить в JVM бинарные представления для объектов с десериализацией при обращении «по требованию», нежели честные экземпляры классов
                      • проверить на объектах размером в 20, 40 и 80К, поскольку, к сожалению, таковые вполне могут случиться
                        0
                        Описание @Benchmark-ов и сериализуемых объектов привёл выше.
                        Используемые для сериализации данные, боюсь, что показать не могу, т.к. это реальные данные из логов системы.
                        При увеличении размера сериализуемых объектов до 1 МБ кривые на графиках скорости предсказуемо расходятся практически по прямым линиям. Победители и аутсайдеры видны уже, начиная с 10 КБ.

                        В целом, сериализация Java объектов может быть применима для большого круга задач, была бы фантазия. Но конкретный profit нужно мерить.
                          0
                          Жалко как. Было бы очень полезно, если выложили бы код бенчмарков без данных из реальных логов системы. А то получается как в маркетинговах статьях на примере IMDG: Hazelcast быстрее Ignite(и наоборот) в N раз, но код мы вам показывать не будем, а воспроизвести не получается.

                          В инете и у Шипилёв вчастности, можно найти статьи, которые описывают неочевидные ошибки разработчиков при написании бенчмарков. Возможно, посмотрев код, кто-то смог бы его улучшить или использовать как отправную точку для своих бенчмарков.

                            0
                            Исходники бенчмарков постараюсь к следующей публикации разместить на Github-е.
                            Пока могу показать лишь «скелет» без деталей реализации:
                            JMH-бенчмарк без деталей
                            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-сериализаторов.
                        0

                        Конфигурация jackson во всех вариациях была дефолтной? Т.е. ObjectMapper без подключения модуля Afterburner и с дефолтным LRUMap в ObjectMapper->typeFactory->typeCache?

                          0
                          Конфигурации Jackson-а приведены в spoiler-е для 3-его участника — Jackson JSON (with types). Возможности тюнинга нам были бы интересны.
                            0
                            Попробуйте включить afterburner и во все классы данных для сериализации/десериализации добавить геттеры и сеттеры, т.к. инъекция данных в поля через рефлексию — дорогостоящая операция. Ну и кэш потюнить, если количество типов, проходящих через ObjectMapper >= 100.
                              0
                              Потюнить Jackson, конечно, интересно, и я постараюсь это сделать, как будет время. Спасибо за наводки.
                              Однако добавлять геттеры и сеттеры в сериализуемые объекты — это не то, что нам нужно: объекты готовят потребители нашего сервиса, а мы должны быстро сериализовывать всё подряд.
                              Как я написал во введении, нам нужна библиотека, «не задающая лишних вопросов».
                                0
                                Наколеночный тест говорит, что afterburner дает грубо от 3% до 7% прироста производительности при десериализации и от 5% до 6% при сериализации. Зависит от размера данных.
                                Benchmarks
                                # 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 данные и модели взяты тут
                                  0
                                  хм, странно, у нас на проекте он дал ~+30%-50% прироста при профилировке в операциях сериализации/десериализации на различных REST-хэндлерах по результатам нагрузочных стрельб Яндекс.Танком. Правда, у нас DTO были иммутабельными, проект собирался с ключом «javac -parameters», и ObjectMapper инициализировался с подключенным «jackson-module-parameter-names».
                              0
                              а какой кэш посоветуете?
                                0
                                Такой, например. Дефолтный сбрасывает полностью данные после 100 накопленных записей, причем дефолтный кэш — статичный и шарится между всеми инстансами ObjectMapper
                              0
                              Используя эту мета-информацию при десериализации, библиотека One Nio точно знает, как выглядел класс сериализуемого объекта на момент сериализации. Именно на основании этого знания алгоритм десериализации One Nio является таким гибким, что обеспечивает максимальную совместимость получающихся при сериализации byte[].

                              Это будет работать при условии полного совпадения классов в сервисе-источнике и сервисе-получателе? А если они различаются, тогда десериализация завершится с ошибкой, без возможности восстановления?

                                0
                                Нет, это будет работать при достаточно обширном наборе различий в классах источника и получателя. В этом и состоит гибкость.
                                Например, у отправителя появились новые поля в классе — более старый получатель просто их проигнорирует. У получателя появились новые поля — при получении данных от более старого отправителя новые поля останутся null-ами, либо значениями по умолчанию. Данных примеров с отличиями в классах отправителя и получателя масса.
                                Обратите внимание на столбик One Nio (for persist) в таблице раздела «Гибкость». Почти по каждому критерию-сценарию там зачтённый бал.
                                  0

                                  Одинаковое имя поля, но разного типа, п.8 — не поддерживает. И, если я правильно понял, нет возможности добавить метод конверсии для типов, не имеющих родственных связей.

                                    +1
                                    Такое поддерживает только One Nio (for persist) и только в оригинальном виде с использованием sun.reflect.MagicAccessorImpl. Об этом написано в сноске #4 под таблицей раздела «Гибкость».
                                    И да, у других библиотек не удалось найти даже custom-изируемых средств достижения данного свойства.
                                      0
                                      Мне кажется, что проблема не только в том, что нет кастомной конверсии, но и в том, что сама попытка конверсии должна быть опциональной. Поле имеет несколько атрибутов — владельца, порядковый номер, тип и имя. Почему-то если имя не совпадает, то поле останется null, а если тип не совпадает, то это критическая ошибка.
                                        0
                                        Если честно, то мне сложно представить case-ы, когда реально, а не надуманно, было бы необходимо поддержать смену типа у поля при развитии класса. Если меняется тип поля, то, вероятнее всего, это уже другое поле.

                                        Почему-то если имя не совпадает, то поле останется null, а если тип не совпадает, то это критическая ошибка.
                                        Потому что имя — это всего лишь строка, а тип определяет структуру данных. Смена типа — гораздо более серьёзное изменение, нежели смена имени поля.
                                          0
                                          Если меняется тип поля, то, вероятнее всего, это уже другое поле.

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

                                            0
                                            Если честно, то мне сложно представить case-ы, когда реально, а не надуманно, было бы необходимо поддержать смену типа у поля при развитии класса.

                                            А мы всё ещё говорим про мультисервисную архитектуру и сессионные данные в ней? Я вполне могу представить ситуацию, когда один не слишком аккуратно написанный сервис положил в сессию данные одного типа, а другой сервис попытался их прочитать в другой тип и упал.

                                  0
                                  С почином на Хабре. А вот код с бенчмарками на каком-нибудь GitHub'е не помешал бы. Народ это любит. Может получится к следующей публикации подготовить?
                                    +1
                                    Спасибо!
                                    Думаю, может получиться. Подумаю, как лучше сделать, и обязательно учту это пожелание.
                                    У нас всё по-честному :)
                                    –2
                                    Простите, а взять любой серьезный тест на скорость java json сериализаторов, найти там топ5 и дальше уже тестить их в ваших условиях правда очень сложно? Надо обязательно брать непойми что и сравнивать непойми как?
                                    Исходников с повторяемыми результатами нет, значит сравнение сделано черти как. Кэп.

                                    пример подобного теста
                                      0
                                      Судя по вашему уровню погруженности в тему, вы могли бы сами опубликовать отличный пост. Дерзайте!
                                        –1
                                        Я пишу совершенно очевидные и тривиальные вещи:
                                        Пользуйтесь опенсорс софтом для своей выгоды.
                                        Открывайте исходники тестов.
                                        Тесты должны быть повторяемы.

                                        Писать статью о правилах проведения экпериментов? Так они уже написанны. О пользе опенсорса? Так тоже миллон уже.
                                      +1
                                      Обидно что о Caucho Hessian уже даже не вспоминают. Я пробовал пару лет назад — получалось компактное представление, без схем и с возможностью кастомизации представления.
                                        0
                                        У FST есть аннотация Version

                                        Add fields to classes without breaking compatibility to streams written with prior versions. See javadoc of the Version annotation and related test cases.

                                        Warning versioning enables newer code to read objects written by an older version, not vice versa.
                                          0
                                          Да, мы смотрели в эту сторону, но предложенный способ использования аннотации @Version не подходит для нашей задачи, когда классы сериализуемых объектов готовятся потребителями нашего сервиса, а для самого сервиса это «чёрный ящик».
                                          Кусок javadoc-а для аннотации @Version из FST
                                          /**
                                           * 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 нет обратной совместимости со старыми сериализуемыми классами.
                                            0
                                            Не спорю. Для нас в своё время киллер-фичей FST послужила
                                            up to 10 times faster 100% JDK Serialization compatible drop-in replacement
                                              0
                                              У One Nio те же плюсы, за одним непринципиальным исключением:
                                              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.
                                              
                                                0
                                                Сейчас, я бы FST не выбрал уже. Автор маленько подзабросил его. Issues на GitHub забиты просьбами поддержать 11,12 и т.п.
                                                В 2016-ом выбор был не столь очевиден :)
                                          0
                                          Было бы интересно услышать/увидеть про тестирование сериализации от jboss:
                                          serialization.jboss.org

                                          В отличие от стандартной джавы — он поддерживает расширенный список классов без каких-либо ограничений.
                                            0
                                            JBoss Serialization исключили из участников на основании вот этого теста: eishay/jvm-serializers
                                            Там сериализация JBoss сильно уступает Jackson Smile-у, Kryo и FST, которые попали в наше исследование.
                                            0

                                            Не понимаю, как Вы умудрились намерить для Java Standart такой "плачевный" результат.


                                            У меня есть постоянные "измерители" полного цикла запроса и ответа на локальном и на удаленном узлах. На удаленном узле десериализация запроса, обработка запроса, сериализация ответа суммарно всегда отрабатывает менее 1 мс. На запрашиваемом узле сериализация запроса, ожидание, десериализация ответа суммарно менее 5 мс. Используется стандартная Java сериализация (из коробки). Используется ZIP для сжатия данных при передаче по сети по протоколу HTTP. По протоколу RMI результат будет еще ниже (не будет передачи надстройки HTTP протокола над передаваемыми данными по сети).


                                            Объем переданных данных у меня не отслеживается (по косвенным признакам менее 1к в сжатом виде). Используется OpenJDK 1.8.


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

                                              0

                                              Взял статистику по одну из запросов — минимальное время 1 мс, максимальное время — 1005 мс, среднее время — 4 мс, счетчик запросов — 71 тыс. Это время замера — отправки запроса на удаленный узел (сериализация запроса — сеть — десериализация запроса — обработка — сериализация ответа — сеть — десериализация ответа)

                                                +2
                                                Во-первых, коллега, я не «умудрился намерить», а выполнил качественные измерения с использованием JMH — специального софта для измерения производительности JVM. Опубликованные результаты измерений многократно повторялись на нашем софте и железе.

                                                Во-вторых, если вы взглянете на первые графики из раздела «Гонки», то увидите, что для 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 — делает подобное, на мой взгляд, с наименьшими преобразованиями данных по сравнению со всем остальным перечисленным вами.
                                                Субъективное суждение, это лично ваше мнение.
                                                  0

                                                  Измерять можно по разному и ссылка на использование софта еще ни о чем не говорит. Разные люди, при использовании одного и того инструмента, могут получить разный результат. Доказано практикой.


                                                  Я Вам привел цифры ведь тоже не с потолка, а результат сбора статистики. И этот результат кардинально отличается от ваших измерений. Многократность большого значения не имеет, а лишь позволяет "подавить" различные погрешности измерений, вносимых синхронизаций часов и не равномерной загрузкой используемых внешних ресурсов (сети например).


                                                  Мои измерения строятся на фиксации временных точек непосредственно перед вызовом удаленного метода и сразу после получения результата этого вызова. Внутри вызова "спрятан" код "упаковки" объектов в поток. Какие либо другие преобразования данных отсутствуют.


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


                                                  Но мне интересно, где Вы умудрились потерять столько времени на использовании "Java Standart" в своих измерениях. Различия в JVM IBM и SUN (Oracle) я отвергаю. Я работал и с тем и другим, в них есть различия, но другого рода.


                                                  Поясните, что измерялось в вашем случае использования "Java Standart". Как реализован вызов? Сериализованы ли объекты передаваемые через сеть?

                                                    0
                                                    Во-вторых, если вы взглянете на первые графики из раздела «Гонки», то увидите, что для Java Standard цикл сериализации/десериализации данных размером порядка 1 КБ (примерно ваш размер) у нас занял 0,007 + 0,021 = 0,028 мс. У вас же получилось 4 мс за 2 цикла сериализации/десериализации + сетевые задержки. Это, без учёта сети, в 2000/28=~71 раз медленнее нашего результата. И где здесь «плачевный» результат?..

                                                    Вы не путаете результат своего измерения с секундами? Системные часы точнее миллисекунд измерять не позволяют!

                                                      0
                                                      Нет, не путаю. Речь здесь идёт именно про микросекунды (тысячные доли миллисекунды). Это результат усреднения сотен тысяч повторений сериализации/десериализации, о чём я написал выше в комментарии.
                                                      Конечно, измерять с такой точностью системные часы компьютеров не позволяют.
                                                      Сериализованы ли объекты передаваемые через сеть?
                                                      Передачи через сеть в наших измерениях нет, только сериализация/десериализация.

                                                      Рекомендую ознакомиться с Java Microbenchmark Harness (JMH).
                                                      «JMH-бенчмарк без деталей» я привёл в одном из комментариев выше. В ближайшее время добавлю этот кусок Java-кода в тело самой публикации.
                                                        –1
                                                        Передачи через сеть в наших измерениях нет

                                                        Тогда мы говорим о совершенно разных понятиях "сериализации" и "десериализации". В моем понимании эти понятия связаны с выводом в поток и восстановлением объекта из потока.


                                                        Что намерили Вы и чем помогли вам результаты ваших измерений — не понимаю. Все же преамбуле к статье Вы написали "передача состояния в распределенной среде", а в результатах измерений распределенной среды получается у Вас нет.


                                                        И получается, что мой результат 0 мс на приемной стороне (удаленном узле) ничем не хуже вашего.

                                                          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|..., которые можно передать по сети/сохранить в БД и т.д., но эти действия не относятся к самой сериализации.

                                                          Останемся при своих мнениях, не вижу смысла их друг другу навязывать.
                                                            0

                                                            Это лукавство — ваша цитата понятия сериализации. В распределенной системе (указано в вашей преамбуле) понятие сериализации одно — преобразование программного объекта в поток (массив байт).


                                                            В этом преобразовании Java Standart всегда будет работать быстрее любой другой рассмотренной вами в сравнении технологии, ввиду отсутствия других преобразований. Object -> byte[] всегда быстрее по сравнению с Object -> JSON -> byte[].

                                                              0

                                                              Ни одна из перечисленных библиотек не выполняет преобразований вида: Object -> JSON -> byte[].
                                                              Либо Object -> JSON, либо Object -> byte[]. В последнем случае как раз и наблюдается более высокая скорость, чем у Java Standard.

                                                                0

                                                                Что понимаете под Object? Java-object c полями объекта, JSON (String) или что то другое?

                                                                  0

                                                                  java.lang.Object с полями объекта

                                                                    –1

                                                                    Вы понимаете, что все участвующие в сравнении библиотек предназначены для преобразования в формат отличный от byte[]? Грубо преобразует Object в строку, которая далее будет преобразована в массив байт при передаче в распределенной системе! Т.е. выполняется преобразование Object -> JSON -> byte[]. Это всегда будет работать хуже, чем прямое преобразование Object в byte[]. ВСЕГДА!


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

                                                                      0
                                                                      Вы понимаете, что все участвующие в сравнении библиотек предназначены для преобразования в формат отличный от byte[]?
                                                                      Это неправда. Не путайте, пожалуйста, народ.
                                                                        –1

                                                                        :-) Хорошо. У каждого свое мнение.


                                                                        Но ваши выводы сомнительны.

                                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                Самое читаемое