Serializujte optimálně skrze Kryo

kryoJe zajímavé, že tak základní věc, jako serializace objektů do binárního streamu je v Javě implementovaná neoptimálně - a to jak z hlediska velikosti výsledné binární podoby, tak i rychlosti s jakou je vytvořena. Míst, kde se serializace objektů hodí je celá řada, a proto je určitě v zájmu každého kvalitního vývojáře zamyslet se, jestli to nejde dělat líp.

Ono totiž jde :)

Pokusů o reimplementaci Java serializace je na webu mnoho, ale mezi ty nejlepší zcela jistě patří knihovna Kryo. Dá se použít jako plnohodnotná náhrada Java serializace, je jednoduchá na konfiguraci a má klony pro alternativní jazyky nad JVM (Scala, Clojure) ale má také port pro Objective-C.

Kdy byste měli Kryo použít místo běžné Java Serializace?

Vždy, když serializujete velké množství objektů a tudíž vám záleží na výkonnosti a velikosti výsledného pole. Že rozdíly nejsou malé, se můžete přesvědčit na následujícím grafu (zdroj: Java Serializers Benchmark Group):

V grafu je kombinován čas na serializaci a deserializaci běžných POJO objektů a jak je vidět, oproti standardní Java serializaci je zde 95% časová úspora.

Když se podíváme na velikost vygenerovaného binárního pole docházíme k podobně zásadní (76%) úspoře místa:

Jak složitá je náhrada za standardní serializaci?

Vcelku jednoduchá - nevyžaduje aby objekty implementovaly rozhranní ani umístění speciálních anotací nad vlastnosti třídy. Do svého existujícího doménového modelu tak nemusíte, pokud nepotřebujete nějaké speciality, nijak zásadně zasahovat (což považuji za další velkou výhodu).

Ukázka serializace:

Kryo kryo = new Kryo();
kryo.addDefaultSerializer(Trida.class, new TridaSerializer()));
final ByteArrayOutputStream bos = new ByteArrayOutputStream(32768);
try (Output output = new Output(bos)) {
   kryo.writeObject(output, clusteredResult);
}
return bos.toByteArray();

Ukázka deserializace:

Kryo kryo = new Kryo();
kryo.addDefaultSerializer(Trida.class, new TridaSerializer()));
try (Input input = new Input(stream)) {
   return kryo.readObject(input, resultType);
}

Pokud budete chtít navíc nad výsledným streamem provést kompresi stačí stream obalit do DeflaterStreamu.

Kde jsem Kryo použil já a s jakým úspěchem?

Kryo jsem použil v souvislosti se službou MonkeyTracker pro serializaci map s objekty reprezentující kliknutí na konkrétní souřadnici před uložením do MongoDB databáze. Mongo je totiž poměrně dost známé tím, že (díky neexistenci schématu) ke každému záznamu opakovaně ukládá jak hodnoty, tak i názvy polí (problém platí ještě i pro současnou GA verzi 2.6). V rozsáhlém objektu může být pak místo pro uložení názvů polí klidně stejně tak veliké, jako místo pro hodnoty.

Základní představu o tom, jak vypadá můj dokument si můžete udělat z tohoto Gistu.

Vzhledem k tomu, že data kliknutí neslouží k dotazování a potřebuji je vždy nahrát / uložit jako celek, mohu s nimi v I/O na úrovni MongoDB klidně nakládat jako s binárními daty a opustit "drahý" JSON formát. V hostingu využíváme drahé SSD disky a proto má úspora místa přímé finanční úspory.

Výsledky testů na datech

Abych si hypotézu ověřil (podle úsloví - nikdy nevěř statistikám, které si nezfalšuješ sám) vytvořil jsem jednoduchý test. Vzal jsem reálná data z měření domény www.stava.cz za poslední rok a dávkově jsem nad nimi provedl serializaci a deserializaci v následujících formátech:

  1. v plném JSON formátu (použil jsem Jackson)
  2. v BSON formátu, který pro ukládání používá MongoDB (použil jsem knihovnu Bson4Jackson)
  3. jako binární data přes Java serializaci (standardní ObjectStream)
  4. jako binární data přes Kryo serializaci
  5. jako komprimovaná (deflater) data přes Kryo serializaci

Jednalo se celkem o 176 tisíc záznamů podobných tomu, který je uveden v Gistu. Výsledky mého měření jsou zde:

Rychlost serializace pomocí Kryo (bez komprimace) je více než 3x rychlejší oproti nativní Java serializaci, rychlost deserializace dokonce 40x rychlejší! I v případě, že zapojíme komprimaci dat dosáhneme v součtu lepších výsledků (serializace je sice pomalejší, ale deserializace je i tak 9x rychlejší oproti Java deserializaci).

Když se potom podívám na velikost výstupního streamu, Kryo je v případě použití komprimovaného formátu šetří 11x úspornější a v případě nekomprimovaného 5x úspornější než Java serializace. Pokud porovnám velikost BSON formátku, který používá Mongo - ušetří mi komprimované Kryo přes 70% místa na disku.

Použití Kryo knihovny má v mém případě jednoznačně smysl.