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.
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.¡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.
, :: , 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 detallespublic 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" } )
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
, 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)
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 MongoDbtoma 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:
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".
Notas al pie con explicaciones1 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 ...
one.nio.serial.Serializer<SomeDto> dtoSerializerWithMeta = Repository.get( SomeDto.class );
byte[] dtoMeta = serializeByDefaultOneNioAlgorithm( dtoSerializerWithMeta );
// №1: dtoMeta №2
one.nio.serial.Serializer<SomeDto> dtoSerializerWithMeta = deserializeByOneNio( dtoMeta );
Repository.provideSerializer( dtoSerializerWithMeta );
byte[] bytes1 = serializeByDefaultOneNioAlgorithm( object1 );
byte[] bytes2 = serializeByDefaultOneNioAlgorithm( object2 );
...
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:- 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. - 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. - 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á.