JavaのシリアライザであるKryoを使う

エキサイト株式会社エンジニアの佐々木です。弊社では結構なリクエストがあるので、キャッシュをいたるところに使用しております。キャッシュサーバ等に一時的にデータを避難するために、シリアライズ・デシリアライズ処理が必須となります。一般的には、Protobuf、MessagePack、Avroなどが有名ですが、JVMのみのやりとりであればKryo(クライオ)というものが選択肢に入ります。こちらのご紹介になります。

前提

$ java --version
openjdk 17.0.2 2022-01-18
OpenJDK Runtime Environment Temurin-17.0.2+8 (build 17.0.2+8)
OpenJDK 64-Bit Server VM Temurin-17.0.2+8 (build 17.0.2+8, mixed mode)

$ ./gradlew --version

------------------------------------------------------------
Gradle 7.6.1
------------------------------------------------------------

Build time:   2023-02-24 13:54:42 UTC
Revision:     3905fe8ac072bbd925c70ddbddddf4463341f4b4

Kotlin:       1.7.10
Groovy:       3.0.13
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          17.0.2 (Eclipse Adoptium 17.0.2+8)
OS:           Mac OS X 12.5 aarch64

Gradle Dependencies

dependencies {
    implementation 'com.esotericsoftware:kryo:5.5.0'
}

コード

Kryoでシリアライズするコードは下記になります。

    private static byte[] serializeByKryo(SampleData data) {

        Kryo kryo = new Kryo();
        kryo.setRegistrationRequired(false);   // class情報が設定されていなくてもシリアライズ・デシリアライズができるようにする(パフォーマンスは落ちる)

        try (
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                Output output = new Output(outputStream)
        ) {

            kryo.writeClassAndObject(output, data);  // シリアライズ化
            return output.toBytes();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static byte[] deserializeByKryo(byte[] data) {

        Kryo kryo = new Kryo();
        kryo.setRegistrationRequired(false);   // class情報が設定されていなくてもシリアライズ・デシリアライズができるようにする(パフォーマンスは落ちる)

        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
            Input input = new Input(inputStream)) {
            return (SampleData) kryo.readClassAndObject(input);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }    
    }

## JMHによるベンチマークテスト
Benchmark                                Mode  Cnt         Score          Error  Units
RedisMainTest.JacksonSerialize          thrpt    5     39046.433 ±     7585.141  ops/s
RedisMainTest.kryoSerialize             thrpt    5   3448556.879 ±    56029.843  ops/s

簡単に書くと上記でKryoを使用できていることになります。 OutputクラスはAutoClosableインターフェースを実装していますので、try-with-resourcesの中で宣言すれば、終了時に自動的にクローズされメモリリークがなくなります。こういうところは大変便利かなと思います。

結果

JDKシリアライズ、Jackson(JSON)で比較した結果だと下記のような結果になります。

## シリアライズ後のサイズ
Kryo binary size: 211
JDK binary size: 630
Jackson(json) binary size: 214

JDKシリアライズとは3倍くらいサイズが異なります。現時点でJDK標準のSerializerを使用しているところはあまりないと思いますが、切り替えることでネットワークコストやシリアライズ・デシリアライズにかかるコストを軽減できるかと思います。今回の結果だと、Jacksonを使用したJSONへのシリアライズの結果だとバイナリサイズでの差分はほとんどありませんでした。

まとめ

Kryoは、Java環境のみで使用可能ですが簡単にコンパクトなシリアライズが可能なライブラリとなります。Javaで利用できる様々なシリアライザ比較の参考サイトをリンクしておきます。

Home · eishay/jvm-serializers Wiki · GitHub

Kryoは、他のシリアライザと比較しても結構成績がよくアプリケーションのパフォーマンス向上にも寄与してくれそうです。しかし、Kryoは単体ではスレッドセーフではないので、実装する際には注意が必要です。(別記事で対応方法を投稿します)

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com