Serialización de Java: velocidad máxima sin una estructura de datos rígida

Nuestro equipo en Sberbank está desarrollando un servicio de datos de sesión que organiza el intercambio de un solo contexto de sesión Java entre aplicaciones distribuidas. Nuestro servicio necesita urgentemente una serialización muy rápida de objetos Java, ya que esto es parte de nuestra tarea crítica. Inicialmente, nos vinieron a la mente: Buffers de protocolo de Google , Apache Thrift , Apache Avro , CBORy otros. Las primeras tres de estas bibliotecas requieren la serialización de objetos para describir el esquema de sus datos. CBOR tiene un nivel tan bajo que solo puede serializar valores escalares y sus conjuntos. Lo que necesitábamos era una biblioteca de serialización Java que "no hiciera demasiadas preguntas" y no forzara la clasificación manual de objetos serializables "en átomos". Queríamos serializar objetos Java arbitrarios sin saber prácticamente nada sobre ellos, y queríamos hacer esto lo más rápido posible. Por lo tanto, organizamos una competencia por las soluciones de código abierto disponibles para el problema de serialización de Java.

KDPV

Competidores


Para la competencia, seleccionamos las bibliotecas de serialización Java más populares, principalmente utilizando el formato binario, así como las bibliotecas que han funcionado bien en otras revisiones de serialización Java.
1Estándar de JavaJava- « »,  Java- .
2Jackson JSON FasterXML/jackson-databind, Java- JSON-.
3Jackson 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);  // !
}
4Jackson Smile FasterXML/jackson-dataformats-binary/smile, Jackson-, Java- JSON- – Smile.
5Jackson Smile (with types), , «Jackson JSON (with types)» (full qualified Java- ).
6Bson4Jackson michel-kraemer/bson4jackson, Jackson-, Java- JSON- – BSON.
7Bson4Jackson (with types), , «Jackson JSON (with types)» (full qualified Java- ).
8BSON MongoDb mongodb/mongo-java-driver/bson,   Java- BSON-.
9Kryo EsotericSoftware/kryo,  Java- .
10Kryo (unsafe), , sun.misc.Unsafe /.
...
:
com.esotericsoftware.kryo.io.Input
com.esotericsoftware.kryo.io.Output
:
com.esotericsoftware.kryo.io.UnsafeInput
com.esotericsoftware.kryo.io.UnsafeOutput
11FSTLa biblioteca  RuedigerMoeller / serialización rápida que convierte los objetos de Java a su propio formato binario.
12FST (inseguro)La misma biblioteca que la anterior, pero configurada para usar la clase sun.misc.Unsafe para acelerar la serialización / deserialización.
Características de la configuración de la biblioteca ...
:
FSTConfiguration fst = FSTConfiguration.createDefaultConfiguration();
:
FSTConfiguration fst = FSTConfiguration.createUnsafeBinaryConfiguration();
treceUn nioLa  biblioteca odnoklassniki / one-nio que convierte los objetos de Java en su propio formato binario.
14One Nio (para persistir)La misma biblioteca que la anterior, pero configurada de manera que incluya metainformación detallada sobre la clase del objeto Java serializable en el resultado de la serialización. Esto puede ser solicitado durante el almacenamiento a largo plazo byte[](por ejemplo, en la base de datos) antes de la deserialización. Es decir, el objetivo perseguido es el mismo que el de Jackson JSON (con tipos).
Características de la configuración de la biblioteca ...
:
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 , .
¡Aquí vamos!

Carrera


La velocidad es el criterio principal para evaluar las bibliotecas de serialización de Java que participan en nuestra competencia improvisada. Para evaluar objetivamente cuál de las bibliotecas de serialización es más rápida, tomamos datos reales de los registros de nuestro sistema y compilamos datos de sesión sintéticos de diferentes longitudes : de 0 a 1 MB. El formato de los datos era cadenas y conjuntos de bytes.
Nota: Mirando hacia el futuro, debe decirse que los ganadores y perdedores ya han aparecido en los tamaños de objetos serializables de 0 a 10 KB. Un aumento adicional en el tamaño de los objetos a 1 MB no cambió el resultado de la competencia.
En este sentido, para mayor claridad, los siguientes gráficos del rendimiento de los serializadores Java están limitados por el tamaño de los objetos de 10 KB.
, :
IntelR CoreTM i7-6700 CPU, 3.4GHz, 8 cores
16 GB
Microsoft Windows 10 (64-bit)
JREIBM 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 .
Además, en nuestra bifurcación, se optimizó la serialización de cadenas: las cadenas comenzaron a serializarse un 30-40% más rápido cuando se trabajaba en IBM JRE.

En este sentido, en esta publicación, todos los resultados de la biblioteca One Nio se obtuvieron en nuestro propio tenedor, y no en la biblioteca original.
La medición directa de la velocidad de serialización / deserialización se realizó utilizando Java Microbenchmark Harness (JMH), una herramienta de OpenJDK para construir y ejecutar benchmark-s. Para cada medición (un punto en el gráfico), se usaron 5 segundos para "calentar" la JVM y otros 5 segundos para las mediciones de tiempo, seguidas de un promedio.
UPD:
Código de referencia de JMH sin algunos detalles
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() );
    }
}

Esto es lo que sucedió: Primero, observamos que las opciones de biblioteca que agregan metadatos adicionales al resultado de la serialización son más lentas que las configuraciones predeterminadas de las mismas bibliotecas (consulte las configuraciones "con tipos" y "para persistir"). En general, independientemente de la configuración, Jackson JSON y Bson4Jackson, que están fuera de la carrera, se convierten en extraños de acuerdo con los resultados de la serialización . Además, Java Standard abandona la carrera en función de los resultados de deserialización , ya que Para cualquier tamaño de datos serializables, la deserialización es mucho más lenta que la competencia. Eche un vistazo más de cerca a los participantes restantes: de acuerdo con los resultados de la serialización, la biblioteca FST cuenta con líderes confiables

Racing - todos los participantes







Racing - excepto para extraños

, y con un aumento en el tamaño de los objetos, One Nio "pisa los talones" . Tenga en cuenta que para One Nio, la opción "para persistir" es mucho más lenta que la configuración predeterminada para la velocidad de serialización.
Si observa la deserialización, vemos que One Nio pudo superar a FST al aumentar el tamaño de los datos . En el último, por el contrario, la configuración no estándar "insegura" realiza la deserialización mucho más rápido.

Para poner todos los puntos encima Y, veamos el resultado total de la serialización y deserialización: se hizo evidente que hay dos líderes inequívocos: FST (inseguro) y One Nio . Si se trata de objetos pequeños FST (inseguro)

Carreras - excepto para los de afuera (clasificación general)


 conduce con confianza, luego, con el aumento en el tamaño de los objetos serializables, comienza a ceder y, en última instancia, es inferior a One Nio . BSON MongoDb

toma la tercera posición con el aumento en el tamaño de los objetos serializables , aunque está casi dos veces por delante de los líderes.

Peso


El tamaño del resultado de la serialización es el segundo criterio más importante para evaluar las bibliotecas de serialización de Java. En cierto modo, la velocidad de serialización / deserialización depende del tamaño del resultado: es más rápido formar y procesar un resultado compacto que uno de volumen. Para "sopesar" los resultados de la serialización, se utilizaron los mismos objetos Java, formados a partir de datos reales tomados de los registros del sistema (cadenas y conjuntos de bytes).

Además, una propiedad importante del resultado de la serialización es también cuánto se comprime bien (por ejemplo, guardarlo en la base de datos u otros almacenamientos). En nuestra competencia, utilizamos el algoritmo de compresión Deflate , que es la base de ZIP y gzip.

Los resultados del "pesaje" fueron los siguientes:

Peso

Se espera que los resultados más compactos fueron la serialización de uno de los líderes de la carrera: One Nio .
El segundo lugar en compacidad fue para BSON MongoDb  (que obtuvo el tercer lugar en la carrera).
En tercer lugar en términos de compacidad, la biblioteca de Kryo "escapó" , que previamente no había demostrado su valía en la carrera.

Los resultados de serialización de estos 3 líderes de "pesaje" también están perfectamente comprimidos (casi dos). Resultó ser el más incompresible: el equivalente binario de JSON es Smile y JSON.

Un dato curioso: todos los ganadores del "pesaje" durante la serialización agregan la misma cantidad de datos de servicio a los objetos serializables pequeños y grandes.

Flexibilidad


Antes de tomar una decisión responsable sobre la elección de un ganador, decidimos verificar exhaustivamente la flexibilidad de cada serializador y su usabilidad.
Para esto, compilamos 20 criterios para evaluar a nuestros serializadores que participan en la competencia para que "no se nos escape un solo mouse".

Flexibilidad
Notas al pie con explicaciones
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: Según el criterio 13, One Nio (por persistir) recibió otro punto (19).

Este meticuloso "examen de los solicitantes" fue quizás la etapa más lenta de nuestro "casting". Pero entonces estos resultados de comparación abren bien la conveniencia de usar bibliotecas de serialización. Como consecuencia, puede usar estos resultados como referencia.

Fue una pena darse cuenta, pero nuestros líderes de acuerdo con los resultados de las carreras y el pesaje: FST (inseguro) y One Nio- resultaron ser extraños en términos de flexibilidad ... Sin embargo, estábamos interesados ​​en un hecho curioso: un Nio en la configuración “para persistir” (no el más rápido ni el más compacto) obtuvo la mayor cantidad de puntos en términos de flexibilidad - 19/20. La oportunidad de hacer que la configuración predeterminada de One Nio funcione de manera flexible (rápida y compacta) también parecía muy atractiva, y había una manera.

Al principio, cuando presentamos a los participantes a la competencia, se dijo que One Nio (por persistir) incluido en el resultado de la serialización metainformación detallada sobre la clase del objeto Java serializable(*) Utilizando esta metainformación para la deserialización, la biblioteca One Nio sabe exactamente cómo era la clase del objeto serializable en el momento de la serialización. Sobre la base de este conocimiento, el algoritmo de deserialización One Nio es tan flexible que proporciona la máxima compatibilidad resultante de la serialización byte[].

Resultó que la metainformación (*) se puede obtener por separado para la clase especificada, serializada  byte[] y enviada al lado donde se deserializarán los objetos Java de esta clase:
Con código en pasos ...
//  №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 );
...

Si realiza este procedimiento explícito para intercambiar metainformación sobre clases entre servicios distribuidos, dichos servicios podrán enviarse objetos Java serializados entre sí mediante la configuración predeterminada (rápida y compacta) de One Nio. Después de todo, mientras se ejecutan los servicios, las versiones de las clases en sus lados no cambian, lo que significa que no hay necesidad de "arrastrar hacia adelante y hacia atrás" la metainformación constante dentro de cada resultado de serialización durante cada interacción. Por lo tanto, después de haber hecho un poco más de acción al principio, puede usar la velocidad y la compacidad de One Nio simultáneamente con la flexibilidad de One Nio (para persistir) . ¡Exactamente lo que se necesita!

Como resultado, para transferir objetos Java entre servicios distribuidos en forma serializada (esto es para lo que organizamos esta competencia) One Nio fue el ganador en flexibilidad  (19/20).
Entre los serializadores de Java que se distinguieron anteriormente en las carreras y el pesaje, no se demostró una gran flexibilidad:

  • BSON MongoDb  (14.5 / 20),
  • Kryo (13/20).

Pedestal


Recordemos los resultados de anteriores concursos de serialización de Java:

  • En las carreras, las dos primeras líneas de la clasificación se dividieron entre FST (inseguro) y One Nio , y BSON MongoDb tomó el tercer lugar ,
  • Un Nio derrotó al pesaje , seguido por BSON MongoDb y Kryo ,
  • En términos de flexibilidad, solo para nuestra tarea de intercambiar el contexto de sesión entre aplicaciones distribuidas, One Nio obtuvo el primer lugar nuevamente  , y BSON MongoDb y Kryo se destacaron .

Por lo tanto, en términos de la totalidad de los resultados obtenidos, el pedestal que obtuvimos es el siguiente:

  1. One Nio
    En la competencia principal, las carreras, compartió el primer lugar con FST (inseguro) , pero pesó significativamente al competidor en la flexibilidad de pesaje y prueba.
  2. FST (inseguro)
    También es una biblioteca de serialización Java muy rápida, sin embargo, carece de la compatibilidad directa y hacia atrás de los conjuntos de bytes resultantes de la serialización.
  3. BSON MongoDB + Kryo
    2 3- Java-, . 2- , . Collection Map, BSON MongoDB custom- / (Externalizable ..).

En Sberbank, en nuestro servicio de datos de sesión, utilizamos la biblioteca One Nio , que ganó el primer lugar en nuestra competencia. Usando esta biblioteca, los datos de contexto de la sesión de Java se serializaron y transfirieron entre aplicaciones. Gracias a esta revisión, la velocidad del transporte de la sesión se ha acelerado varias veces. Las pruebas de carga mostraron que en escenarios cercanos al comportamiento real de los usuarios en Sberbank Online, se obtuvo una aceleración de hasta el 40% solo debido a esta mejora. Tal resultado significa una reducción en el tiempo de respuesta del sistema a las acciones del usuario, lo que aumenta el grado de satisfacción de nuestros clientes.

En el próximo artículo intentaré demostrar en acción la aceleración adicional de One Nioderivado del uso de la clase sun.reflect.MagicAccessorImpl. Desafortunadamente, IBM JRE no admite las propiedades más importantes de esta clase, lo que significa que el potencial completo de One Nio en esta versión de JRE aún no se ha revelado. Continuará.

All Articles