Serialisasi Java: kecepatan maksimum tanpa struktur data yang kaku

Tim kami di Sberbank sedang mengembangkan layanan data sesi yang mengatur pertukaran konteks sesi Jawa tunggal antara aplikasi terdistribusi. Layanan kami sangat membutuhkan serialisasi objek Java yang sangat cepat, karena ini adalah bagian dari tugas kritis misi kami. Awalnya, mereka muncul di benak kami: Buffer Protokol Google , Apache Thrift , Apache Avro , CBORdan lain-lain. Tiga perpustakaan pertama ini membutuhkan serialisasi objek untuk menggambarkan skema data mereka. CBOR sangat rendah sehingga hanya bisa membuat serial nilai skalar dan setnya. Apa yang kami butuhkan adalah perpustakaan serialisasi Java yang "tidak terlalu banyak bertanya" dan tidak memaksa pengurutan objek secara serial ke "atom". Kami ingin membuat serial objek Java yang sewenang-wenang tanpa mengetahui apa pun tentangnya, dan kami ingin melakukan ini secepat mungkin. Oleh karena itu, kami menyelenggarakan kompetisi untuk solusi Open Source yang tersedia untuk masalah serialisasi Java.

KDPV

Pesaing


Untuk kompetisi, kami memilih perpustakaan serialisasi Java paling populer, terutama menggunakan format biner, serta perpustakaan yang telah bekerja dengan baik dalam ulasan serialisasi Java lainnya .
1Standar 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
11FSTPustaka  RuedigerMoeller / serialisasi cepat yang mengubah objek Java ke format binernya sendiri.
12FST (tidak aman)Pustaka yang sama seperti di atas, tetapi dikonfigurasi untuk menggunakan kelas sun.misc.Unsafe untuk mempercepat serialisasi / deserialisasi.
Fitur pengaturan perpustakaan ...
:
FSTConfiguration fst = FSTConfiguration.createDefaultConfiguration();
:
FSTConfiguration fst = FSTConfiguration.createUnsafeBinaryConfiguration();
tigabelasSatu nioPustaka  odnoklassniki / one-nio yang mengubah objek Java menjadi format binernya sendiri.
14One Nio (untuk bertahan)Pustaka yang sama seperti di atas, tetapi dikonfigurasi sedemikian rupa untuk memasukkan informasi meta rinci tentang kelas objek Java serializable dalam hasil serialisasi. Ini mungkin diminati selama penyimpanan jangka panjang byte[](misalnya, dalam database) sebelum deserialisasi. Artinya, tujuan yang diupayakan sama dengan Jackson JSON (dengan tipe).
Fitur pengaturan perpustakaan ...
:
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 , .
Kita mulai!

Ras


Kecepatan adalah kriteria utama untuk mengevaluasi pustaka serialisasi Java yang merupakan peserta dalam kompetisi dadakan kami. Untuk mengevaluasi perpustakaan serialisasi mana yang lebih cepat secara objektif, kami mengambil data nyata dari log sistem kami dan menyusun data sesi sintetik dengan panjang yang berbeda : dari 0 hingga 1 MB. Format data adalah string dan byte array.
Catatan: Ke depan, harus dikatakan bahwa pemenang dan pecundang telah muncul pada ukuran objek serial dari 0 hingga 10 KB. Peningkatan lebih lanjut dalam ukuran objek menjadi 1 MB tidak mengubah hasil kompetisi.
Dalam hal ini, untuk kejelasan yang lebih baik, grafik berikut dari kinerja serializers Java dibatasi oleh ukuran objek 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 .
Selain itu, dalam garpu kami, serialisasi string dioptimalkan - string mulai diserialisasi 30-40% lebih cepat ketika bekerja pada IBM JRE.

Dalam hal ini, dalam publikasi ini semua hasil untuk perpustakaan One Nio diperoleh dari garpu kami sendiri, dan bukan pada perpustakaan asli.
Pengukuran langsung kecepatan serialisasi / deserialisasi dilakukan dengan menggunakan Java Microbenchmark Harness (JMH) - alat dari OpenJDK untuk membangun dan menjalankan benchmark-s. Untuk setiap pengukuran (satu titik pada grafik), 5 detik digunakan untuk "menghangatkan" JVM dan 5 detik lainnya untuk pengukuran waktu itu sendiri, diikuti dengan rata-rata.
UPD:
Kode benchmark JMH tanpa detail
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() );
    }
}

Inilah yang terjadi: Pertama, kami mencatat bahwa opsi pustaka yang menambahkan meta data tambahan ke hasil serialisasi lebih lambat daripada konfigurasi default pustaka yang sama (lihat konfigurasi "dengan tipe" dan "untuk bertahan"). Secara umum, terlepas dari konfigurasi, Jackson JSON dan Bson4Jackson, yang keluar dari balapan, menjadi orang luar menurut hasil serialisasi . Selain itu, Java Standard dikeluarkan dari balapan berdasarkan hasil deserialisasi , seperti untuk berbagai ukuran data serial, deserialisasi jauh lebih lambat daripada pesaing. Lihatlah lebih dekat pada peserta yang tersisa: Menurut hasil serialisasi, perpustakaan FST adalah pemimpin yang percaya diri

Balap - semua peserta







Balap - kecuali untuk orang luar

, dan dengan peningkatan ukuran objek, One Nio " menginjak tumitnya" . Perhatikan bahwa untuk One Nio, opsi "untuk bertahan" jauh lebih lambat daripada konfigurasi default untuk kecepatan serialisasi.
Jika Anda melihat deserialization, kita melihat bahwa One Nio mampu menyalip FST dengan ukuran data yang meningkat . Dalam yang terakhir, sebaliknya, konfigurasi non-standar "tidak aman" melakukan deserialisasi lebih cepat.

Untuk menempatkan semua poin di atas DAN, mari kita lihat hasil total serialisasi dan deserialisasi: Menjadi jelas bahwa ada dua pemimpin yang tegas: FST (tidak aman) dan One Nio . Jika pada benda kecil FST (tidak aman)

Balap - kecuali untuk orang luar (klasifikasi keseluruhan)


 dengan percaya diri memimpin, kemudian dengan peningkatan ukuran objek serial, ia mulai menyerah dan, akhirnya, lebih rendah dari One Nio .

Posisi ketiga dengan peningkatan ukuran objek serializable dengan percaya diri diambil oleh BSON MongoDb , meskipun hampir dua kali di depan para pemimpin.

Beratnya


Ukuran hasil serialisasi adalah kriteria terpenting kedua untuk mengevaluasi pustaka serialisasi Java. Di satu sisi, kecepatan serialisasi / deserialisasi tergantung pada ukuran hasilnya: lebih cepat untuk membentuk dan memproses hasil yang kompak daripada volume. Untuk "menimbang" hasil serialisasi, semua objek Java yang sama digunakan, dibentuk dari data nyata yang diambil dari log sistem (string dan byte array).

Selain itu, properti penting dari hasil serialisasi juga seberapa besar kompresinya (misalnya, untuk menyimpannya dalam database atau penyimpanan lain). Dalam kompetisi kami, kami menggunakan algoritma kompresi Deflate , yang merupakan dasar untuk ZIP dan gzip.

Hasil dari "penimbangan" adalah sebagai berikut:

Beratnya

Diharapkan bahwa hasil yang paling kompak adalah serialisasi dari salah satu pemimpin lomba: One Nio .
Tempat kedua dalam kekompakan pergi ke BSON MongoDb  (yang mengambil tempat ketiga dalam lomba).
Di tempat ketiga dalam hal kekompakan, perpustakaan Kryo "melarikan diri" , yang sebelumnya gagal membuktikan dirinya dalam lomba.

Hasil serialisasi dari 3 pemimpin "penimbangan" ini juga terkompresi dengan sempurna (hampir dua). Ternyata menjadi yang paling tidak terkompresi: setara biner dari JSON adalah Senyum dan JSON itu sendiri.

Fakta yang aneh - semua pemenang "penimbangan" selama serialisasi menambahkan jumlah data layanan yang sama ke objek serializable kecil dan besar.

Fleksibilitas


Sebelum membuat keputusan yang bertanggung jawab tentang memilih pemenang, kami memutuskan untuk memeriksa secara menyeluruh fleksibilitas setiap serializer dan kegunaannya.
Untuk ini, kami menyusun 20 kriteria untuk mengevaluasi serializers kami yang berpartisipasi dalam kompetisi sehingga "tidak satu pun tikus akan menyelinap" melewati mata kami.

Fleksibilitas
Catatan Kaki dengan Penjelasan
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: Menurut kriteria ke-13, One Nio (untuk bertahan) menerima poin lain (ke-19).

"Pemeriksaan pelamar" yang teliti ini mungkin merupakan tahap yang paling memakan waktu dari "casting" kami. Tetapi kemudian hasil perbandingan ini membuka dengan baik kenyamanan menggunakan perpustakaan serialisasi. Sebagai konsekuensinya, Anda dapat menggunakan hasil ini sebagai referensi.

Sangat memalukan untuk disadari, tetapi para pemimpin kami sesuai dengan hasil balapan dan penimbangan - FST (tidak aman) dan One Nio- ternyata menjadi orang luar dalam hal fleksibilitas ... Namun, kami tertarik pada fakta yang aneh: One Nio dalam konfigurasi "for persist" (bukan yang tercepat dan bukan yang paling kompak) mencetak poin terbanyak dalam hal fleksibilitas - 19/20. Kesempatan untuk membuat konfigurasi default (cepat dan ringkas) One Nio bekerja secara fleksibel juga tampak sangat menarik - dan ada caranya.

Pada awalnya, ketika kami memperkenalkan peserta ke kompetisi, dikatakan bahwa One Nio (untuk bertahan) termasuk dalam hasil serialisasi detail meta-informasi tentang kelas objek Java serialable.(*). Menggunakan informasi meta ini untuk deserialization, perpustakaan One Nio tahu persis seperti apa kelas objek serializable pada saat serialisasi. Berdasarkan pengetahuan ini bahwa algoritma deserialisasi One Nio sangat fleksibel sehingga memberikan kompatibilitas maksimum yang dihasilkan dari serialisasi byte[].

Ternyata meta-informasi (*) dapat diperoleh secara terpisah untuk kelas yang ditentukan, diserialisasi ke  byte[] dan dikirim ke sisi di mana objek Java dari kelas ini akan di-deserialisasi:
Dengan kode dalam langkah-langkah ...
//  №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 );
...

Jika Anda melakukan prosedur eksplisit ini untuk bertukar informasi meta tentang kelas antara layanan terdistribusi, maka layanan tersebut akan dapat saling mengirim objek Java bersambung satu sama lain menggunakan konfigurasi One Nio default (cepat dan ringkas). Bagaimanapun, ketika layanan sedang berjalan, versi kelas di sisi mereka tidak berubah, yang berarti tidak ada alasan untuk "menyeret maju dan mundur" informasi meta konstan dalam setiap hasil serialisasi selama setiap interaksi. Dengan demikian, setelah melakukan sedikit lebih banyak aksi di awal, maka Anda dapat menggunakan kecepatan dan kekompakan One Nio secara bersamaan dengan fleksibilitas One Nio (untuk bertahan) . Persis apa yang dibutuhkan!

Akibatnya, untuk mentransfer objek Java antara layanan terdistribusi dalam bentuk serial (inilah yang kami selenggarakan untuk kompetisi ini) One Nio adalah pemenang dalam fleksibilitas  (19/20).
Di antara serialis Java yang membedakan diri mereka sebelumnya dalam balapan dan penimbangan, tidak ada fleksibilitas buruk yang ditunjukkan:

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

Alas


Ingat hasil dari kompetisi serialisasi Java sebelumnya:

  • dalam balapan, dua baris pertama peringkat dibagi oleh FST (tidak aman) dan One Nio , dan BSON MongoDb mengambil tempat ketiga ,
  • Satu Nio mengalahkan penimbangan , diikuti oleh BSON MongoDb dan Kryo ,
  • dalam hal fleksibilitas, hanya untuk tugas kami bertukar konteks sesi antara aplikasi terdistribusi, One Nio mendapat tempat pertama lagi  , dan BSON MongoDb dan Kryo membedakan diri mereka sendiri .

Dengan demikian, dalam hal totalitas hasil yang dicapai, alas yang kami peroleh adalah sebagai berikut:

  1. One Nio
    Dalam kompetisi utama - ras - berbagi tempat pertama dengan FST (tidak aman) , tetapi secara signifikan menimbang pesaing dalam menimbang dan menguji fleksibilitas.
  2. FST (tidak aman)
    Juga perpustakaan serialisasi Java yang sangat cepat, namun, tidak memiliki kompatibilitas langsung dan mundur dari array byte yang dihasilkan dari serialisasi.
  3. BSON MongoDB + Kryo
    2 3- Java-, . 2- , . Collection Map, BSON MongoDB custom- / (Externalizable ..).

Di Sberbank, dalam layanan data sesi kami, kami menggunakan perpustakaan One Nio , yang memenangkan tempat pertama dalam kompetisi kami. Menggunakan perpustakaan ini, data konteks sesi Java diserialisasi dan ditransfer antar aplikasi. Berkat revisi ini, kecepatan transportasi sesi telah dipercepat beberapa kali. Pengujian beban menunjukkan bahwa dalam skenario yang dekat dengan perilaku pengguna aktual di Sberbank Online, akselerasi hingga 40% diperoleh hanya karena peningkatan ini saja. Hasil seperti itu berarti pengurangan waktu respons sistem terhadap tindakan pengguna, yang meningkatkan tingkat kepuasan pelanggan kami.

Pada artikel selanjutnya saya akan mencoba menunjukkan dalam akselerasi akselerasi tambahan One Nioberasal dari menggunakan kelas sun.reflect.MagicAccessorImpl. Sayangnya, JRE IBM tidak mendukung properti paling penting dari kelas ini, yang berarti bahwa potensi penuh One Nio pada versi JRE ini belum terungkap. Bersambung.

All Articles