Статьи

Mule 3.7 с Kyro предлагает повышение производительности на 77%

В настоящее время Mule использует обычную сериализацию Java для хранения объектов в файлах или для репликации их через кластер Mule. Это накладывает ограничения на:

  • Типы объектов, которые мы можем хранить: они должны реализовывать интерфейс Serializable и обеспечивать надлежащие конструкторы. Кроме того, этого недостаточно для того, чтобы класс был Serializable, должны быть также все его составные объекты.
  • Производительность : Сериализатор Java далеко не самый быстрый в городе.

Если вы часто пользуетесь пакетным модулем , то вы уже знаете, что в пакетном режиме мы уже решили эту проблему, используя платформу Kryo , которая позволяет сериализовать более широкий диапазон объектов с большей скоростью, чем по умолчанию в Java. Однако до выпуска Mule 3.7.0 возможности Kryo не были включены в пакетный модуль. Что если вы хотите использовать его в постоянном ObjectStore? А как насчет распределенной очереди виртуальных машин? Более того: почему я не могу выбрать сериализацию с использованием какой-либо инфраструктуры, которую я выберу? Мы решили эти вопросы в Mule 3.7.0:

  • Создание Serialization API , чтобы позволить любому разработчику Java внести свой вклад и использовать свой собственный сериализатор.
  • Мы создали реализацию этого Serialization API на основе Kryo, которая поставляется с дистрибутивом EE и может быть легко настроена.

API сериализации

Мы взялись за это с нуля, начав с создания API-интерфейса сериализации, который отделяет Mule и его расширения от фактического используемого механизма сериализации, а также позволяет пользователям настраивать, какой механизм они хотят использовать, и даже предоставлять свой собственный.

Мул полагается на сериализацию каждый раз, когда:

  • Вы читаете / пишете из постоянного ObjectStore.
  • Вы читаете / пишете из постоянной очереди VM или JMS.
  • Объект распространяется через кластер Мул.
  • Вы читаете / пишете объект из файла.

Изменение механизма сериализации, используемого для таких задач, может значительно улучшить функциональность и производительность.

API сериализации в основном определяется следующим интерфейсом:

/*
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package org.mule.api.serialization;

import org.mule.util.store.DeserializationPostInitialisable;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;

/**
 * Defines a component capable to serialize/deserialize objects into/from an array of
 * {@link byte}s. Unlike usual serializing components, this one doesn't enforce the
 * serialized object to implement {@link Serializable}. However, some implementations
 * might require that condition and throw {@link IllegalArgumentException} if not
 * met.
 * <p/>
 * Implementations are also responsible for the correct initialization of classes
 * implementing the {@link DeserializationPostInitialisable}
 * interface.
 * <p/>
 * <p/>
 * Unexpected behavior can result of deserializing objects that were generated with
 * a different implementation of {@link ObjectSerializer}.
 * <p/>
 * Implementations are required to be thread-safe
 *
 * @since 3.7.0
 */
public interface ObjectSerializer
{

    /**
     * Serializes the given object into a an array of {@link byte}s
     *
     * @param object the object to be serialized. Might be <code>null</code>
     * @return an array of {@link byte}
     * @throws SerializationException in case of unexpected exception
     */
    byte[] serialize(Object object) throws SerializationException;

    /**
     * Serializes the given object and writes the result into {@code out}
     *
     * @param object the object to be serialized. Might be <code>null</code>
     * @param out    an {@link OutputStream} where the result will be written
     * @return an array of {@link byte}
     * @throws SerializationException in case of unexpected exception
     */
    void serialize(Object object, OutputStream out) throws SerializationException;

    /**
     * Deserializes the given bytes. Unexpected behavior can result of deserializing
     * a byte[] that was generated with another implementation.
     * <p/>
     * If the obtained object implements {@link DeserializationPostInitialisable}
     * then this serializer will be responsible for properly initializing
     * the object before returning it.
     * <p/>
     * Implementation will choose the {@link java.lang.ClassLoader}
     * to use for deserialization.
     *
     * @param bytes an array of byte that an original object was serialized into
     * @return the deserialized object
     * @throws IllegalArgumentException if {@code bytes} is {@code null}
     * @throws SerializationException   in case of unexpected exception
     */
    <T> T deserialize(byte[] bytes) throws SerializationException;

    /**
     * Deserializes the given bytes.
     * <p/>
     * If the obtained object implements {@link DeserializationPostInitialisable}
     * then this serializer will be responsible for properly initializing
     * the object before returning it.
     *
     * @param bytes       an array of byte that an original object was serialized into
     * @param classLoader the {@link java.lang.ClassLoader} to deserialize with
     * @return the deserialized object
     * @throws IllegalArgumentException if {@code bytes} is {@code null}
     * @throws SerializationException   in case of unexpected exception
     */
    <T> T deserialize(byte[] bytes, ClassLoader classLoader) throws SerializationException;

    /**
     * Deserializes the given stream of bytes.
     * <p/>
     * Implementation will choose the {@link java.lang.ClassLoader}
     * to use for deserialization.
     * <p/>
     * Even if deserialization fails, this method will close the
     * {@code inputStream}
     * <p/>
     * If the obtained object implements {@link DeserializationPostInitialisable}
     * then this serializer will be responsible for properly initializing
     * the object before returning it.
     *
     * @param inputStream a stream of bytes that an original object was serialized into
     * @return the deserialized object
     * @throws IllegalArgumentException if {@code inputStream} is {@code null}
     * @throws SerializationException   in case of unexpected exception
     */
    <T> T deserialize(InputStream inputStream) throws SerializationException;

    /**
     * Deserializes the given stream of bytes.
     * <p/>
     * Even if deserialization fails, this method will close the
     * {@code inputStream}
     * <p/>
     * If the obtained object implements {@link DeserializationPostInitialisable}
     * then this serializer will be responsible for properly initializing
     * the object before returning it.
     *
     * @param inputStream a stream of bytes that an original object was serialized into
     * @param classLoader the {@link java.lang.ClassLoader} to deserialize with
     * @return the deserialized object
     * @throws IllegalArgumentException if {@code inputStream} is {@code null}
     * @throws SerializationException   in case of unexpected exception
     */
    <T> T deserialize(InputStream inputStream, ClassLoader classLoader) throws SerializationException;
}

Основная концепция этого контракта заключается в том, что:

  • Он сериализует в и из байта [], а также потоков.
  • Это потокобезопасно.
  • При сериализации потоковая передача поддерживается путем передачи OutputStream.
  • При десериализации потоковая передача поддерживается, позволяя InputStream в качестве источника ввода.
  • При десериализации вы можете указать, какой загрузчик классов использовать. По умолчанию используется текущее исполнение.
  • При десериализации, если полученный объект реализует интерфейс DeserializationPostInitialisable, сериализатор будет отвечать за правильную инициализацию объекта перед его возвратом.

конфигурация

По умолчанию Mule продолжит использовать сериализацию Java, как и всегда, без изменений. Однако каждое приложение сможет настроить ObjectSerializer, которое оно хочет использовать, с помощью тега Mule <configuration>:

<mule>
  <spring:beans>
      <spring:bean id="customSerializer" class="com.my.CustomSerializer" />
  </spring:beans>

  <configuration defaultObjectSerializer-ref="customSerializer"/>
</mule>

ПРИМЕЧАНИЕ . Единственный компонент, на поведение которого этот компонент не повлияет, — это пакетный модуль, который по своим функциональным причинам должен работать с использованием Kryo, несмотря ни на что.

Получение настроенного ObjectSerializer

Есть много способов получить ObjectSerializer. Рекомендуемый подход — через внедрение зависимости. Ниже показано, как получить ObjectSerializer, настроенный по умолчанию:

public class MyClass {

  @Inject
  @DefaultObjectSerializer
  private ObjectSerializer objectSerializer;

}

Вместо этого, если вам нужен определенный именованный сериализатор (по умолчанию или нет), вы также можете сделать это следующим образом:

public class MyClass {

  @Inject
  @Named("mySerializer")
  private ObjectSerializer objectSerializer;
}

Наконец, вы всегда можете извлечь его из muleContext, хотя внедрение зависимостей предпочтительнее:

// returns the default object serializer
muleContext.getObjectSerializer();

Крио Сериализатор

Для пользователей EE мы также предоставим вторую реализацию ObjectSerializer, основанную на платформе Kryo. Использование Kryo обеспечивает:

  • Лучшая производительность. Kryo намного быстрее сериализации Java
  • Поддержка более широкого диапазона типов Java. Kryo не ограничен большинством ограничений, налагаемых сериализацией Java, таких как требование реализации интерфейса Serializable, наличие конструктора по умолчанию и т. Д. (Это не означает, что он может сериализовать НИЧЕГО)
  • Поддержка сжатия: Вы можете использовать алгоритмы сжатия Deflate или GZip.

Новое пространство имен Kryo было добавлено для настройки этого сериализатора:

<mule>
  <kryo:serializer name="kryo" />
  <configuration defaultObjectSerializer-ref="kryo" />
</mule>

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

<!-- default option, equivalent to simply not setting any compressionMode -->
<kryo:serializer name="noCompression" compressionMode="NONE"/>

<kryo:serializer name="deflate" compressionMode="DEFLATE"/>

<kryo:serializer name="gzip" compressionMode="GZIP"/>

Производительность при использовании Kryo

Использование Kryo обеспечит повышение производительности при использовании следующих компонентов:

  • Постоянные или кластерные хранилища объектов
  • Постоянные или распределенные очереди виртуальных машин
  • JMS транспорт

Поскольку никаких улучшений нет, пока вы их не измерите, мы попросили могучего Лучано Гандини из рабочей группы провести несколько тестов. Он провел несколько тестов для сравнения транзакций в секунду (TPS) постоянных / распределенных очередей виртуальных машин и ObjectStores при использовании стандартного сериализатора Java и различных настроек Kryo. Вот некоторые результаты …

Постоянные очереди VM

образ

Постоянные хранилища объектов

изображение (1)

Распределенные очереди VM (HA)

изображение (2)

Распределенный ObjectStore (HA)

изображение (3)

Относительно сжатия

Как вы можете видеть на графиках выше, Kryo без сжатия значительно быстрее, чем стандартный сериализатор во всех случаях. Однако режим сжатия обеспечивает только фактическое улучшение в случаях высокой доступности. Почему это? Среда, в которой проводились эти тесты, имеет действительно хорошую скорость ввода-вывода. Чтобы сжатие было достойным, количество времени, которое ЦП тратит на сжатие-распаковку, должно быть значительно меньше, чем количество времени ввода-вывода, сэкономленное за счет уменьшения размера полезной нагрузки. Поскольку сетевые операции выполняются медленнее, чем операции на диске (по крайней мере, в нашей среде), и поскольку кластеризация HA требует репликации узлов (что приводит к увеличению трафика), только в случае HA сжатие окупилось. Это универсальная константа? ТОЧНО НЕТ!Возможно, вы запускаете mule на машинах с более медленными дисками или более высокими требованиями к вводу-выводу, при которых сжатие может быть оправдано в любом случае. Кроме того, эти тесты были выполнены с полезной нагрузкой 1 МБ, но чем больше поток данных, тем более достойным становится сжатие.

Сводка производительности

Проще говоря, это были результаты представления:

Выводы из приведенной выше таблицы таковы:

  • при использовании распределенных ObjectStores производительность может повыситься на 77,13%, при использовании распределенных очередей виртуальных машин — на 63,77%, а при использовании локальных постоянных очередей виртуальных машин — на 64,71%.
  • Хотя локальные хранилища объектов не показывают значительного улучшения (они на самом деле медленнее при использовании сжатия), не существует варианта использования, при котором вы не получаете некоторый уровень усиления при использовании Kryo.

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

Ограничения и соображения

Как бы хорошо это ни было, это все еще не серебряная пуля. Пожалуйста, учтите следующие соображения:

Смена сериализаторов требует чистого листа

Сериализаторы не совместимы и не взаимозаменяемы (по крайней мере те, которые мы поставляем OOTB). Это означает, что если вы решите изменить сериализатор, используемый вашим приложением, вам нужно убедиться, что все сообщения в очередях VM / JMS были использованы и что эти очереди пусты к моменту запуска нового сериализатора. Это потому, что сериализатор Kryo не сможет читать дейтаграммы, написанные Java, и наоборот. То же самое относится и к постоянным ObjectStores. Если вы попытаетесь прочитать запись, созданную с помощью другого сериализатора, вам не повезет.

Сериализация в Shared VM Connector

Версия 3.5.0 Mule ESB представила концепцию доменов как способ совместного использования ресурсов между приложениями. Например, вы можете определить разъем виртуальной машиныв домене, чтобы разрешить связь между приложениями через очереди сообщений ВМ. Однако сериализаторы можно настроить только на уровне приложения, их нельзя настроить в домене. Так что же произойдет, если два приложения (A и B) будут взаимодействовать друг с другом через соединитель виртуальной машины, определенный в домене, к которому принадлежат оба, но A сериализуется с использованием Java, а B — с помощью Kryo? Ответ: это просто работает. Всякий раз, когда какое-либо приложение пытается выполнить запись в конечную точку, которая использует общий соединитель, это конкретное сообщение не будет сериализовано с сериализатором приложения, а тем, которое использует соединитель виртуальной машины. Так что это хорошо, верно? Да, это хорошо с точки зрения опыта Plug & Play. Но обратите внимание, что вы не сможете указать этому соединителю виртуальной машины использовать Kryo и получить от него повышение производительности.

Почему в локальном постоянном объектном хранилище практически нет улучшений?

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

Почему нет диаграммы, показывающей улучшение в JMS?

Согласно JMS API, очереди не работают с необработанными объектами полезной нагрузки. Вместо этого вы должны предоставить экземпляр класса javax.jms.Message. Клиент-брокер отвечает за его сериализацию, а не за мул. Поэтому влияние Kryo в таком сценарии минимально. Единственный выигрыш в производительности при использовании Kryo с JMS состоит в том, что Mule сериализует MuleSession и помещает его в качестве заголовка в формате Base64. Сериализация MuleSession с Kryo может повысить производительность до 10%, но мы не рассматриваем ее в качестве примера использования, поскольку большая часть сериализации зависит от JMS-брокера, а не от mule.

Проблемные типы

Хотя Kryo способен сериализовать объекты, которые не реализуют интерфейс Serializable, установка Kryo в качестве сериализатора по умолчанию не означает, что такие компоненты, как транспорт VM, ObjectSerializer или Cluster, смогут обрабатывать объекты, которые не реализуют такой интерфейс. , Это потому, что, хотя Kryo может работать с этими объектами, API-интерфейсы Java этих компонентов все еще ожидают экземпляры Serializable в сигнатурах своих методов. Обратите внимание, что, хотя стандартная сериализация завершится неудачно с объектом, который реализует интерфейс Serializable, но содержит другой, который этого не делает, Kryo, вероятно, (но не гарантировано) будет успешным. Типичным случаем может быть Pojo, содержащий org.apache.xerces.jaxp.datatype.XMLGregorianCalendarImpl, например NetSuite или MS Dynamic.Облачные коннекторы делают

Завершение

Когда мы решили сделать это улучшение, мы знали, что это окажет большое влияние, но, честно говоря, наши ожидания были значительно превышены. Лично я не ожидал улучшения более чем на 30% в лучшем случае. Пик улучшения в 77% случаев в лучшем случае и 64% в наиболее обычном случае просто порадовал меня. Я надеюсь, что это делает вас счастливыми.

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