Sérialisation Java: vitesse maximale sans structure de données rigide

Notre équipe de Sberbank développe un service de données de session qui organise l'échange d'un contexte de session Java unique entre des applications distribuées. Notre service a un besoin urgent de sérialisation très rapide des objets Java, car cela fait partie de notre tâche critique. Au départ, ils nous sont venus à l'esprit: Google Protocol Buffers , Apache Thrift , Apache Avro , CBORLes trois premières de ces bibliothèques nécessitent la sérialisation des objets pour décrire le schéma de leurs données. CBOR est si bas niveau qu'il ne peut sérialiser que les valeurs scalaires et leurs ensembles. Ce dont nous avions besoin, c'était d'une bibliothèque de sérialisation Java qui «n'a pas posé trop de questions» et n'a pas forcé le tri manuel des objets sérialisables «en atomes». Nous voulions sérialiser des objets Java arbitraires sans en savoir pratiquement rien, et nous voulions le faire le plus rapidement possible. Par conséquent, nous avons organisé un concours pour les solutions Open Source disponibles pour le problème de sérialisation Java.

KDPV

Concurrents


Pour le concours, nous avons sélectionné les bibliothèques de sérialisation Java les plus populaires, utilisant principalement le format binaire, ainsi que des bibliothèques qui ont bien fonctionné dans d' autres revues de sérialisation Java.
1Norme 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 bibliothèque  RuedigerMoeller / de sérialisation rapide qui convertit les objets Java en son propre format binaire.
12FST (dangereux)La même bibliothèque que ci-dessus, mais configurée pour utiliser la classe sun.misc.Unsafe pour accélérer la sérialisation / désérialisation.
Caractéristiques des paramètres de la bibliothèque ...
:
FSTConfiguration fst = FSTConfiguration.createDefaultConfiguration();
:
FSTConfiguration fst = FSTConfiguration.createUnsafeBinaryConfiguration();
treizeOne nioLa  bibliothèque odnoklassniki / one-nio qui convertit les objets Java en son propre format binaire.
14Un Nio (pour persister)La même bibliothèque que ci-dessus, mais configurée de manière à inclure des métadonnées détaillées sur la classe de l'objet Java sérialisable dans le résultat de la sérialisation. Cela peut être demandé pendant le stockage à long terme byte[](par exemple, dans la base de données) avant la désérialisation. Autrement dit, l'objectif poursuivi est le même que celui de Jackson JSON (avec types).
Caractéristiques des paramètres de la bibliothèque ...
:
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 , .
Et c'est parti!

Course


La vitesse est le critère principal pour évaluer les bibliothèques de sérialisation Java qui participent à notre concours improvisé. Afin d'évaluer objectivement laquelle des bibliothèques de sérialisation est la plus rapide, nous avons pris des données réelles des journaux de notre système et composé des données de session synthétiques de différentes longueurs : de 0 à 1 Mo. Le format des données était des chaînes et des tableaux d'octets.
Remarque: Pour l' avenir, il faut dire que les gagnants et les perdants sont déjà apparus sur les tailles d'objets sérialisables de 0 à 10 Ko. Une nouvelle augmentation de la taille des objets à 1 Mo n'a pas changé le résultat du concours.
À cet égard, pour plus de clarté, les graphiques suivants des performances des sérialiseurs Java sont limités par la taille des objets de 10 Ko.
, :
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 .
De plus, dans notre fork, la sérialisation des chaînes a été optimisée - les chaînes ont commencé à être sérialisées 30 à 40% plus rapidement lorsque vous travaillez sur IBM JRE.

À cet égard, dans cette publication, tous les résultats de la bibliothèque One Nio ont été obtenus sur notre propre fork, et non sur la bibliothèque d'origine.
La mesure directe de la vitesse de sérialisation / désérialisation a été effectuée en utilisant Java Microbenchmark Harness (JMH) - un outil d'OpenJDK pour la construction et l'exécution de benchmark-s. Pour chaque mesure (un point sur le graphique), 5 secondes ont été utilisées pour «réchauffer» la machine virtuelle Java et 5 secondes supplémentaires pour les mesures de temps elles-mêmes, suivies d'une moyenne.
UPD:
Code de référence JMH sans quelques détails
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() );
    }
}

Voici ce qui s'est passé: Tout d'abord, nous notons que les options de bibliothèque qui ajoutent des métadonnées supplémentaires au résultat de la sérialisation sont plus lentes que les configurations par défaut des mêmes bibliothèques (voir les configurations «avec types» et «pour persister»). En général, quelle que soit la configuration, Jackson JSON et Bson4Jackson, qui sont hors course, deviennent des outsiders selon les résultats de la sérialisation . De plus, Java Standard abandonne la course en fonction des résultats de désérialisation , comme pour n'importe quelle taille de données sérialisables, la désérialisation est beaucoup plus lente que ses concurrents. Examinez de plus près les participants restants: selon les résultats de la sérialisation, la bibliothèque FST est en leaders confiants

Racing - tous les participants







Racing - sauf pour les étrangers

, et avec une augmentation de la taille des objets, One Nio «marche sur ses talons» . Notez que pour One Nio, l'option «for persist» est beaucoup plus lente que la configuration par défaut pour la vitesse de sérialisation.
Si vous regardez la désérialisation, nous voyons que One Nio a réussi à dépasser FST avec l'augmentation de la taille des données . Dans ce dernier, au contraire, la configuration non standard «unsafe» effectue la désérialisation beaucoup plus rapidement.

Afin de mettre tous les points sur ET, regardons le résultat total de la sérialisation et de la désérialisation: Il est devenu évident qu'il y a deux leaders sans équivoque: FST (dangereux) et One Nio . Si sur de petits objets FST (dangereux)

Course - sauf pour les étrangers (classement général)


 mène en toute confiance, puis avec l'augmentation de la taille des objets sérialisables, il commence à concéder et, finalement, inférieur à One Nio .

La troisième position avec l'augmentation de la taille des objets sérialisables est prise en toute confiance par BSON MongoDb , même si elle est presque deux fois en avance sur les leaders.

Pesée


La taille du résultat de la sérialisation est le deuxième critère le plus important pour évaluer les bibliothèques de sérialisation Java. D'une certaine manière, la vitesse de sérialisation / désérialisation dépend de la taille du résultat: il est plus rapide de former et de traiter un résultat compact qu'un volume. Pour «pondérer» les résultats de la sérialisation, tous les mêmes objets Java ont été utilisés, formés à partir de données réelles extraites des journaux système (chaînes et tableaux d'octets).

De plus, une propriété importante du résultat de la sérialisation est également combien il se comprime bien (par exemple, pour l'enregistrer dans la base de données ou d'autres stockages). Dans notre compétition, nous avons utilisé l' algorithme de compression Deflate , qui est la base de ZIP et gzip.

Les résultats de la "pesée" étaient les suivants:

Pesée

Les résultats les plus compacts devraient être la sérialisation d'un des leaders de la course: One Nio .
La deuxième place en compacité est revenue à BSON MongoDb  (qui a pris la troisième place de la course).
En troisième position en termes de compacité, la bibliothèque Kryo s'est «échappée» , qui n'avait pas réussi auparavant à faire ses preuves en course.

Les résultats de sérialisation de ces 3 leaders de la "pesée" sont également parfaitement compressés (près de deux). Il s'est avéré être le plus incompressible: l'équivalent binaire de JSON est Smile et JSON lui-même.

Un fait curieux - tous les gagnants de la "pesée" lors de la sérialisation ajoutent la même quantité de données de service aux petits et grands objets sérialisables.

Souplesse


Avant de prendre une décision responsable concernant le choix d'un gagnant, nous avons décidé de vérifier minutieusement la flexibilité de chaque sérialiseur et son utilisation.
Pour cela, nous avons compilé 20 critères pour évaluer nos sérialiseurs participant au concours afin que «pas une seule souris ne glisse» devant nos yeux.

Souplesse
Notes de bas de page avec explications
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: Selon le 13ème critère, One Nio (pour persist) a reçu un autre point (19ème).

Cet «examen minutieux des candidats» a été peut-être l'étape la plus longue de notre «casting». Mais ces résultats de comparaison ouvrent bien la commodité d'utiliser des bibliothèques de sérialisation. Par conséquent, vous pouvez utiliser ces résultats comme référence.

C'était une honte à réaliser, mais nos leaders selon les résultats des courses et des pesées - FST (dangereux) et One Nio- s'est avéré être des étrangers en termes de flexibilité ... Cependant, nous étions intéressés par un fait curieux: un Nio dans la configuration "for persist" (pas le plus rapide et le plus compact) a marqué le plus de points en termes de flexibilité - 19/20. L'opportunité de faire fonctionner la configuration One Nio par défaut (rapide et compacte) de manière flexible semblait également très attrayante - et il y avait un moyen.

Au tout début, lorsque nous avons présenté les participants au concours, il a été dit que One Nio (pour persist) inclus dans le résultat de sérialisation des méta-informations détaillées sur la classe de l'objet Java sérialisable(*). En utilisant ces méta-informations pour la désérialisation, la bibliothèque One Nio sait exactement à quoi ressemblait la classe de l'objet sérialisable au moment de la sérialisation. C'est sur la base de ces connaissances que l'algorithme de désérialisation One Nio est si flexible qu'il offre la compatibilité maximale résultant de la sérialisation byte[].

Il s'est avéré que les méta-informations (*) peuvent être obtenues séparément pour la classe spécifiée, sérialisées  byte[] et envoyées du côté où les objets Java de cette classe seront désérialisés:
Avec du code par étapes ...
//  №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 vous effectuez cette procédure explicite pour échanger des méta-informations sur les classes entre les services distribués, ces services pourront s’envoyer des objets Java sérialisés en utilisant la configuration One Nio par défaut (rapide et compacte). Après tout, pendant que les services sont en cours d'exécution, les versions des classes de leur côté sont inchangées, ce qui signifie qu'il n'y a aucune raison de «faire glisser d'avant en arrière» les métadonnées constantes dans chaque résultat de sérialisation lors de chaque interaction. Ainsi, après avoir fait un peu plus d'action au début, vous pouvez utiliser la vitesse et la compacité de One Nio simultanément avec la flexibilité de One Nio (pour persist) . Exactement ce qui est nécessaire!

En conséquence, pour le transfert d'objets Java entre services distribués sous forme sérialisée (c'est pour cela que nous avons organisé ce concours) One Nio a été le vainqueur de la flexibilité  (19/20).
Parmi les sérialiseurs Java qui se sont distingués plus tôt en course et en pesage, une bonne flexibilité n'a pas été démontrée:

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

Piédestal


Rappelez-vous les résultats des précédents concours de sérialisation Java:

  • en course, les deux premières lignes du classement ont été divisées par FST (dangereux) et One Nio , et BSON MongoDb a pris la troisième place ,
  • Un Nio a battu la pesée , suivi de BSON MongoDb et Kryo ,
  • en termes de flexibilité, juste pour notre tâche d'échange de contexte de session entre des applications distribuées, One Nio a de nouveau obtenu la première place  , et BSON MongoDb et Kryo ont excellé .

Ainsi, en termes de la totalité des résultats obtenus, le piédestal que nous avons obtenu est le suivant:

  1. One Nio
    Dans la compétition principale - courses - a partagé la première place avec FST (dangereux) , mais a pesé de manière significative le concurrent en termes de flexibilité de pesage et de test.
  2. FST (unsafe)
    Également une bibliothèque de sérialisation Java très rapide, cependant, il lui manque la compatibilité directe et rétrospective des tableaux d'octets résultant de la sérialisation.
  3. BSON MongoDB + Kryo
    2 3- Java-, . 2- , . Collection Map, BSON MongoDB custom- / (Externalizable ..).

À Sberbank, dans notre service de données de session, nous avons utilisé la bibliothèque One Nio , qui a remporté la première place de notre concours. À l'aide de cette bibliothèque, les données de contexte de session Java ont été sérialisées et transférées entre les applications. Grâce à cette révision, la vitesse de transport des sessions s'est accélérée à plusieurs reprises. Les tests de charge ont montré que dans des scénarios proches du comportement réel de l'utilisateur dans Sberbank Online, une accélération allant jusqu'à 40% n'a été obtenue que grâce à cette seule amélioration. Un tel résultat signifie une réduction du temps de réponse du système aux actions des utilisateurs, ce qui augmente le degré de satisfaction de nos clients.

Dans le prochain article, je vais essayer de démontrer en action l'accélération supplémentaire de One Niodérivé de l'utilisation de la classe sun.reflect.MagicAccessorImpl. Malheureusement, IBM JRE ne prend pas en charge les propriétés les plus importantes de cette classe, ce qui signifie que le plein potentiel de One Nio sur cette version de JRE n'a pas encore été révélé. À suivre.

All Articles