Java-Serialisierung: maximale Geschwindigkeit ohne starre Datenstruktur

Unser Team bei Sberbank entwickelt einen Sitzungsdatendienst, der den Austausch eines einzelnen Java-Sitzungskontexts zwischen verteilten Anwendungen organisiert. Unser Service erfordert dringend eine sehr schnelle Serialisierung von Java-Objekten, da dies Teil unserer geschäftskritischen Aufgabe ist. Anfangs kamen sie uns in den Sinn: Google-Protokollpuffer , Apache Thrift , Apache Avro , CBORund andere. Die ersten drei dieser Bibliotheken erfordern die Serialisierung von Objekten, um das Schema ihrer Daten zu beschreiben. CBOR ist so niedrig, dass nur Skalarwerte und ihre Mengen serialisiert werden können. Was wir brauchten, war eine Java-Serialisierungsbibliothek, die "nicht zu viele Fragen stellte" und keine manuelle Sortierung von serialisierbaren Objekten "in Atome" erzwang. Wir wollten beliebige Java-Objekte serialisieren, ohne praktisch etwas darüber zu wissen, und wir wollten dies so schnell wie möglich tun. Aus diesem Grund haben wir einen Wettbewerb für die verfügbaren Open Source-Lösungen für das Java-Serialisierungsproblem organisiert.

KDPV

Wettbewerber


Für den Wettbewerb haben wir uns für die beliebtesten Java Serialisierung Bibliotheken, vor allem das Binärformat sowie Bibliotheken , die auch in gearbeitet haben andere Java Serialisierung Bewertungen .
1Java-StandardJava- « »,  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
11FSTDie RuedigerMoeller / Fast-Serialization- Bibliothek  , die Java-Objekte in ihr eigenes Binärformat konvertiert.
12FST (unsicher)Dieselbe Bibliothek wie oben, jedoch so konfiguriert, dass die Klasse sun.misc.Unsafe verwendet wird, um die Serialisierung / Deserialisierung zu beschleunigen.
Funktionen der Bibliothekseinstellungen ...
:
FSTConfiguration fst = FSTConfiguration.createDefaultConfiguration();
:
FSTConfiguration fst = FSTConfiguration.createUnsafeBinaryConfiguration();
dreizehnEin nioDie odnoklassniki / one-nio- Bibliothek  , die Java-Objekte in ihr eigenes Binärformat konvertiert.
14Ein Nio (für bestehen)Dieselbe Bibliothek wie oben, jedoch so konfiguriert, dass detaillierte Metainformationen über die Klasse des serialisierbaren Java-Objekts in das Serialisierungsergebnis aufgenommen werden. Dies kann während der Langzeitspeicherung byte[](z. B. in der Datenbank) vor der Deserialisierung erforderlich sein . Das heißt, das verfolgte Ziel ist das gleiche wie das von Jackson JSON (mit Typen).
Funktionen der Bibliothekseinstellungen ...
:
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 , .
Auf geht's!

Rennen


Geschwindigkeit ist das Hauptkriterium für die Bewertung von Java-Serialisierungsbibliotheken, die an unserem spontanen Wettbewerb teilnehmen. Um objektiv zu bewerten, welche der Serialisierungsbibliotheken schneller ist, haben wir reale Daten aus den Protokollen unseres Systems entnommen und daraus synthetische Sitzungsdaten unterschiedlicher Länge zusammengestellt : von 0 bis 1 MB. Das Format der Daten war Zeichenfolgen und Byte-Arrays.
Hinweis: Mit Blick auf die Zukunft sollte gesagt werden, dass Gewinner und Verlierer bereits bei Größen von serialisierbaren Objekten von 0 bis 10 KB aufgetreten sind. Eine weitere Vergrößerung der Objekte auf 1 MB hat das Ergebnis des Wettbewerbs nicht verändert.
In dieser Hinsicht sind zur besseren Übersichtlichkeit die folgenden Diagramme der Leistung von Java-Serialisierern durch die Größe von Objekten von 10 KB begrenzt.
, :
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 .
Darüber hinaus wurde in unserem Fork die Serialisierung von Strings optimiert - Strings wurden bei der Arbeit an IBM JRE 30-40% schneller serialisiert.

In dieser Hinsicht wurden in dieser Veröffentlichung alle Ergebnisse für die One Nio-Bibliothek auf unserer eigenen Gabel und nicht auf der Originalbibliothek erhalten.
Die direkte Messung der Serialisierungs- / Deserialisierungsgeschwindigkeit wurde mit Java Microbenchmark Harness (JMH) durchgeführt - einem Tool von OpenJDK zum Erstellen und Ausführen von Benchmarks. Für jede Messung (ein Punkt in der Grafik) wurden 5 Sekunden verwendet, um die JVM aufzuwärmen, und weitere 5 Sekunden für die Zeitmessungen selbst, gefolgt von einer Mittelung.
UPD:
JMH-Benchmark-Code ohne einige Details
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() );
    }
}

Folgendes ist passiert: Zunächst stellen wir fest, dass Bibliotheksoptionen, die dem Serialisierungsergebnis zusätzliche Metadaten hinzufügen, langsamer sind als die Standardkonfigurationen derselben Bibliotheken (siehe die Konfigurationen „mit Typen“ und „für die Beibehaltung“). Unabhängig von der Konfiguration werden Jackson JSON und Bson4Jackson, die nicht im Rennen sind, nach den Ergebnissen der Serialisierung zu Außenseitern . Darüber hinaus fällt Java Standard aus dem Rennen basierend auf Deserialisierung Ergebnisse , wie Bei jeder Größe serialisierbarer Daten ist die Deserialisierung viel langsamer als bei Wettbewerbern. Schauen Sie sich die verbleibenden Teilnehmer genauer an: Nach den Ergebnissen der Serialisierung ist die FST- Bibliothek zuversichtlich

Rennen - alle Teilnehmer







Rennen - außer für Außenstehende

und mit zunehmender Größe von Objekten „tritt One Nio ihr auf die Fersen“ . Beachten Sie, dass für One Nio die Option "for persist" viel langsamer ist als die Standardkonfiguration für die Serialisierungsgeschwindigkeit.
Wenn Sie sich die Deserialisierung ansehen, sehen wir, dass One Nio FST mit zunehmender Datengröße überholen konnte . Im letzteren Fall führt die nicht standardmäßige Konfiguration "unsicher" die Deserialisierung viel schneller durch.

Um alle Punkte über AND zu setzen, schauen wir uns das Gesamtergebnis der Serialisierung und Deserialisierung an: Es wurde deutlich, dass es zwei eindeutige Führungskräfte gibt: FST (unsicher) und One Nio . Wenn auf kleinen Objekten FST (unsicher)

Rennen - außer für Außenstehende (Gesamtwertung)


 Selbstbewusst führt er dann mit der Vergrößerung serialisierbarer Objekte, dass er One Nio zugibt und letztendlich unterlegen ist .

Die dritte Position mit der Vergrößerung serialisierbarer Objekte wird von BSON MongoDb zuversichtlich eingenommen , obwohl sie den Führenden fast zweimal voraus ist .

Wiegen


Die Größe des Serialisierungsergebnisses ist das zweitwichtigste Kriterium für die Bewertung von Java-Serialisierungsbibliotheken. In gewisser Weise hängt die Geschwindigkeit der Serialisierung / Deserialisierung von der Größe des Ergebnisses ab: Es ist schneller, ein kompaktes Ergebnis zu erstellen und zu verarbeiten als ein Volumenergebnis. Um die Ergebnisse der Serialisierung zu "gewichten", wurden dieselben Java-Objekte verwendet, die aus realen Daten aus den Systemprotokollen (Zeichenfolgen und Bytearrays) gebildet wurden.

Darüber hinaus ist eine wichtige Eigenschaft des Ergebnisses der Serialisierung auch, wie stark es gut komprimiert wird (z. B. um es in der Datenbank oder in anderen Speichern zu speichern). In unserem Wettbewerb haben wir den Deflate- Komprimierungsalgorithmus verwendet , der die Grundlage für ZIP und gzip bildet.

Die Ergebnisse des "Wiegens" waren wie folgt:

Wiegen

Es wird erwartet, dass die kompaktesten Ergebnisse die Serialisierung von einem der Führer des Rennens waren: One Nio .
Der zweite Platz in Sachen Kompaktheit ging an BSON MongoDb  (das im Rennen den dritten Platz belegte).
Auf dem dritten Platz in Bezug auf die Kompaktheit „entkam“ die Kryo- Bibliothek , die sich zuvor im Rennen nicht bewährt hatte.

Die Serialisierungsergebnisse dieser 3 Leiter des "Wiegens" sind ebenfalls perfekt komprimiert (fast zwei). Es stellte sich als das unkomprimierbarste heraus: Das binäre Äquivalent von JSON ist Smile und JSON selbst.

Eine merkwürdige Tatsache: Alle Gewinner des "Wiegens" während der Serialisierung fügen kleinen und großen serialisierbaren Objekten die gleiche Menge an Servicedaten hinzu.

Flexibilität


Bevor wir eine verantwortungsvolle Entscheidung über die Auswahl eines Gewinners treffen, haben wir uns entschlossen, die Flexibilität jedes Serialisierers und seine Verwendbarkeit gründlich zu prüfen.
Dazu haben wir 20 Kriterien für die Bewertung unserer am Wettbewerb teilnehmenden Serializer zusammengestellt, damit „keine einzige Maus über unsere Augen gleitet“.

Flexibilität
Fußnoten mit Erläuterungen
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: Nach dem 13. Kriterium erhielt One Nio (für persist) einen weiteren Punkt (19.).

Diese sorgfältige „Prüfung der Bewerber“ war vielleicht die zeitaufwändigste Phase unseres „Castings“. Diese Vergleichsergebnisse eröffnen jedoch die Möglichkeit, Serialisierungsbibliotheken zu verwenden. Infolgedessen können Sie diese Ergebnisse als Referenz verwenden.

Es war eine Schande zu realisieren, aber unsere Führer nach den Ergebnissen von Rennen und Wiegen - FST (unsicher) und One Nio- stellte sich in Bezug auf Flexibilität als Außenseiter heraus ... Wir waren jedoch an einer merkwürdigen Tatsache interessiert: Ein Nio in der Konfiguration "für beständig" (nicht der schnellste und nicht der kompakteste) erzielte die meisten Punkte in Bezug auf Flexibilität - 19/20. Die Möglichkeit, die standardmäßige (schnelle und kompakte) One Nio-Konfiguration so flexibel wie möglich zu gestalten, sah ebenfalls sehr attraktiv aus - und es gab einen Weg.

Ganz am Anfang, als wir die Teilnehmer in den Wettbewerb einführten, wurde gesagt, dass One Nio (für persist) im Ergebnis der Serialisierung detaillierte Metainformationen über die Klasse des serialisierbaren Java-Objekts enthält(*). Mithilfe dieser Metainformationen zur Deserialisierung weiß die One Nio-Bibliothek genau, wie die Klasse des serialisierbaren Objekts zum Zeitpunkt der Serialisierung aussah. Auf der Grundlage dieses Wissens ist der One Nio-Deserialisierungsalgorithmus so flexibel, dass er die maximale Kompatibilität bietet, die sich aus der Serialisierung ergibt byte[].

Es stellte sich heraus, dass Metainformationen (*) für die angegebene Klasse separat abgerufen, serialisiert  byte[] und an die Seite gesendet werden können, auf der die Java-Objekte dieser Klasse deserialisiert werden:
Mit Code in Schritten ...
//  №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 );
...

Wenn Sie dieses explizite Verfahren zum Austausch von Metainformationen über Klassen zwischen verteilten Diensten ausführen, können diese Dienste serialisierte Java-Objekte unter Verwendung der standardmäßigen (schnellen und kompakten) One Nio-Konfiguration aneinander senden. Während die Dienste ausgeführt werden, bleiben die Versionen der Klassen auf ihren Seiten unverändert, was bedeutet, dass es keinen Grund gibt, die konstanten Metainformationen in jedem Serialisierungsergebnis während jeder Interaktion hin und her zu ziehen. Nachdem Sie am Anfang etwas mehr Action ausgeführt haben, können Sie die Geschwindigkeit und Kompaktheit von One Nio gleichzeitig mit der Flexibilität von One Nio nutzen (um zu bestehen) . Genau das, was benötigt wird!

Infolgedessen für die Übertragung von Java-Objekten zwischen verteilten Diensten in serialisierter Form (dafür haben wir diesen Wettbewerb organisiert) Ein Nio war der Gewinner in Sachen Flexibilität  (19/20).
Unter den Java-Serialisierern, die sich früher im Rennsport und beim Wiegen auszeichneten, wurde keine schlechte Flexibilität nachgewiesen:

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

Sockel


Erinnern Sie sich an die Ergebnisse früherer Java-Serialisierungswettbewerbe:

  • Bei Rennen wurden die ersten beiden Zeilen der Wertung durch FST (unsicher) und One Nio geteilt , und BSON MongoDb belegte den dritten Platz .
  • Ein Nio besiegte das Wiegen , gefolgt von BSON MongoDb und Kryo .
  • In Bezug auf die Flexibilität stand One Nio nur für unsere Aufgabe, den Sitzungskontext zwischen verteilten Anwendungen auszutauschen, erneut an erster Stelle  , und BSON MongoDb und Kryo waren herausragend .

In Bezug auf die Gesamtheit der erzielten Ergebnisse ist der Sockel, den wir erhalten haben, wie folgt:

  1. Ein Nio
    Im Hauptwettbewerb - Rennen - teilte sich FST den ersten Platz (unsicher) , wog aber den Konkurrenten beim Wiegen und Testen der Flexibilität erheblich.
  2. FST (unsicher)
    Auch eine sehr schnelle Java-Serialisierungsbibliothek, der jedoch die direkte und Abwärtskompatibilität der aus der Serialisierung resultierenden Byte-Arrays fehlt.
  3. BSON MongoDB + Kryo
    2 3- Java-, . 2- , . Collection Map, BSON MongoDB custom- / (Externalizable ..).

In der Sberbank haben wir in unserem Sitzungsdatendienst die One Nio- Bibliothek verwendet , die den ersten Platz in unserem Wettbewerb gewonnen hat. Mit dieser Bibliothek wurden Java-Sitzungskontextdaten serialisiert und zwischen Anwendungen übertragen. Dank dieser Überarbeitung hat sich die Geschwindigkeit des Sitzungstransports um ein Vielfaches beschleunigt. Lasttests haben gezeigt, dass in Szenarien, die dem tatsächlichen Verhalten der Benutzer in Sberbank Online nahe kommen, eine Beschleunigung von bis zu 40% nur aufgrund dieser Verbesserung erzielt wurde. Ein solches Ergebnis bedeutet eine Verkürzung der Reaktionszeit des Systems auf Benutzeraktionen, was die Zufriedenheit unserer Kunden erhöht.

Im nächsten Artikel werde ich versuchen, die zusätzliche Beschleunigung von One Nio in Aktion zu demonstrierenabgeleitet von der Verwendung der Klasse sun.reflect.MagicAccessorImpl. Leider unterstützt die IBM JRE nicht die wichtigsten Eigenschaften dieser Klasse, was bedeutet, dass das volle Potenzial von One Nio für diese Version der JRE noch nicht offengelegt wurde. Fortsetzung folgt.

All Articles