以下の内容はhttps://tech.askul.co.jp/entry/2025/09/16/185103より取得しました。


Kotlinで可逆圧縮アルゴリズムを使う(gzip、Brotli、Zstd、LZ4)

はじめまして。アスクルのたかせです。
BtoB領域の商品情報の管理を担うシステムを担当しています。
今回は、Kotlin(Spring Boot)環境で動作する代表的な可逆圧縮アルゴリズム(gzip、Brotli、Zstd、LZ4)の実装や特徴比較、ベンチマークをまとめました。

可逆圧縮処理が必要になった背景

アスクルのBtoB領域の商品情報管理のAPIでは、DB負荷やコストを考慮してキャッシュの導入を進めています。
しかし、キャッシュの利用が増えてきたことで、ネットワーク帯域不足の問題が発生したため、キャッシュに保存するデータの圧縮を実装する運びになりました。

可逆圧縮アルゴリズムの選定基準

可逆圧縮アルゴリズムを選定する上で比較する要素は次になるかと思います。

  • 圧縮率: どの程度サイズを削減できるか
  • 圧縮速度: エンコード処理の高速性
  • 展開速度: デコード処理の高速性
  • メモリ使用量: 処理に必要なメモリ量
  • ライブラリのサポート状況: Kotlinでの利用しやすさ

用途によってどれを重視するかが変わってくるので、それぞれの特徴を把握しておくことが大切ですね!

各アルゴリズムの紹介と特徴比較

アルゴリズム 圧縮率 圧縮速度 展開速度 特徴メモ
gzip 登場時期が古く、採用例が多い。互換性・安定性が抜群
Brotli ◎(テキスト) Googleが開発。Webコンテンツ向けに最適化されている。圧縮は重めだがサイズ削減大
Zstd ○〜◎ ○〜◎ ○〜◎ Facebook(現Meta)が開発。圧縮レベル幅が広くバランス最適化しやすい。後発でモダンなシステムへの採用が増えている。
LZ4 とにかく速さ重視。サイズよりレイテンシ/スループット優先

先発のgzipに対して、Brotliは事前辞書を活用したテキスト向け最適化、Zstdは圧縮率と速度のバランスを強みとしています。 LZ4は圧縮率より低レイテンシを最重視した設計で、リアルタイム性の高い場面で活躍しています。

各アルゴリズムのサンプル実装

Kotlinでの各アルゴリズム(gzip、Brotli、Zstd、LZ4)のサンプル実装です。
今回は大きめ/逐次データも扱えるように、ストリームAPIで実装しています。
いずれも入出力はバイト配列になります。
あくまで簡易的なコードで、オブジェクトの生成回数や例外処理は考慮していない点にご注意ください。

gzip

gzipはJavaの標準ライブラリに含まれているため、JVM上で動作するKotlinでは追加の依存関係は不要です!
圧縮レベルは、1(高速/低圧縮)〜9(低速/高圧縮)を指定できます。
デフォルト値は6です。

import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream

// 圧縮
fun compress(data: ByteArray): ByteArray {
  val outputStream = ByteArrayOutputStream()
  val gzipOutputStream = object : GZIPOutputStream(outputStream) {
      // 圧縮レベルを指定(省略可)
      init { def.setLevel(6) }
  }
  gzipOutputStream.use {
      it.write(data)
  }
  return outputStream.toByteArray()
}

// 展開
fun decompress(compressedData: ByteArray): ByteArray {
    val inputStream = ByteArrayInputStream(compressedData)
    GZIPInputStream(inputStream).use { gzipStream ->
        return gzipStream.readBytes()
    }
}

参考

Brotli

Brotliを利用するためには、次の依存関係の追加が必要です。 プラットフォームによって異なる依存関係を利用する点に注意です。

// build.gradle.kts
dependencies {
    implementation("com.aayushatharva.brotli4j:brotli4j:$brotliVersion")
    // 以下の依存関係は、プラットフォームごとに用意されている点に注意
    runtimeOnly("com.aayushatharva.brotli4j:native-osx-aarch64:$brotliVersion")
}

圧縮レベルは、1(高速/低圧縮)〜11(低速/高圧縮)を指定できます。
デフォルト値は4です。

import com.aayushatharva.brotli4j.Brotli4jLoader
import com.aayushatharva.brotli4j.decoder.BrotliInputStream
import com.aayushatharva.brotli4j.encoder.Encoder
import com.aayushatharva.brotli4j.encoder.BrotliOutputStream
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream

Brotli4jLoader.ensureAvailability()

// 圧縮
fun compress(data: ByteArray): ByteArray {
    // 圧縮品質を指定(省略可能)
    val params = Encoder.Parameters().setQuality(4)
    val baos = ByteArrayOutputStream()
    BrotliOutputStream(baos, params).use { it.write(data) }
    return baos.toByteArray()
}

// 展開
fun decompress(compressedData: ByteArray): ByteArray {
    val inputStream = ByteArrayInputStream(compressedData)
    BrotliInputStream(inputStream).use { brotliStream ->
        return brotliStream.readBytes()
    }
}

参考

Zstd

Zstdを利用するためには、次の依存関係の追加が必要です。

// build.gradle.kts
dependencies {
    implementation("com.github.luben:zstd-jni:1.5.5-11")
}

圧縮レベルは1(高速/低圧縮)〜22(低速/高圧縮)と、調整段階が非常に多いです!
デフォルト値は3です。
スライドウィンドウのサイズも指定できます。

import com.github.luben.zstd.ZstdInputStream
import com.github.luben.zstd.ZstdOutputStream
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream

// 圧縮
fun compress(data: ByteArray): ByteArray {
    val outputStream = ByteArrayOutputStream()
    // 圧縮レベルを指定(省略可能)
    ZstdOutputStream(outputStream, 3).use { zstdOutputStream ->
        zstdOutputStream.write(data)
        zstdOutputStream.flush()
    }
    return outputStream.toByteArray()
}

// 展開
fun decompress(compressedData: ByteArray): ByteArray {
    val inputStream = ByteArrayInputStream(compressedData)
    val outputStream = ByteArrayOutputStream()

    ZstdInputStream(inputStream).use { zstdInputStream ->
        val buffer = ByteArray(1024)
        var len: Int
        while (zstdInputStream.read(buffer).also { len = it } != -1) {
            outputStream.write(buffer, 0, len)
        }
    }

    return outputStream.toByteArray()
}

参考

LZ4

LZ4を利用するためには、次の依存関係の追加が必要です。

// build.gradle.kts
dependencies {
    implementation("org.lz4:lz4-java:1.8.0")
}

速度最優先のfastCompressorと圧縮率を意識したhighCompressorから選択できます。
バッファの割当サイズも指定可能です。

import net.jpountz.lz4.LZ4BlockInputStream
import net.jpountz.lz4.LZ4BlockOutputStream
import net.jpountz.lz4.LZ4Factory
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream

// 圧縮
fun compress(data: ByteArray): ByteArray {
    val outputStream = ByteArrayOutputStream()
    // fastCompressorかhighCompressorを選択
    LZ4BlockOutputStream(outputStream, 1024, LZ4Factory.fastestInstance().fastCompressor()).use { lz4OutputStream ->
        lz4OutputStream.write(data)
        lz4OutputStream.finish()
    }
    return outputStream.toByteArray()
}

// 展開
fun decompress(compressedData: ByteArray): ByteArray {
    val inputStream = ByteArrayInputStream(compressedData)
    val outputStream = ByteArrayOutputStream()

    LZ4BlockInputStream(inputStream, LZ4Factory.fastestInstance().fastDecompressor()).use { lz4InputStream ->
        val buffer = ByteArray(65536) // 64KB
        var len: Int
        while (lz4InputStream.read(buffer).also { len = it } != -1) {
            outputStream.write(buffer, 0, len)
        }
    }

    return outputStream.toByteArray()
}

参考

ベンチマーク

JMHを使って各アルゴリズムのベンチマークを取っていきます。
圧縮レベルは各アルゴリズムのデフォルト値で検証します。

テスト環境

- CPU: Apple M3
- メモリ: 16GB
- JVM: 17
- Kotlin: 2.1.21
- ベンチマークライブラリ: JMH 1.37

テストデータ

(1)JSONデータ(17,353バイト)

繰り返しが多い構造的なデータとして、itemを100件もつJSONを使用します。

{
  "items" : [
    {"id":1, "name":"", "description":"澄んだ山の湧き水から採取された天然ミネラルウォーター。日常の水分補給とリフレッシュに最適です。"},
    {"id":2, "name":"コーヒー", "description":"完璧に焙煎されたプレミアムアラビカコーヒー豆。チョコレートとキャラメルのヒントを含む豊かで芳香なブレンド。"},
    {"id":3, "name":"牛乳", "description":"草で育てられた牛からの新鮮な全乳。カルシウムとタンパク質が豊富で、飲用や料理に理想的です。"},
    ...
    {"id":100, "name":"豆腐", "description":"中性の風味と密な食感のしっかり豆腐。ベジタリアン料理に最適な大豆タンパク質。"}
  ]
}

(2)テキストデータ(10081文字、30,320バイト)

繰り返しが限定的な文章データとして、青空文庫より太宰治の 走れメロス を使用します。

走れメロス
太宰治

 メロスは激怒した。必ず、かの邪智暴虐じゃちぼうぎゃくの王を除かなければならぬと決意した。メロスには政治がわからぬ。メロスは、村の牧人である。笛を吹き...

「出典:青空文庫『走れメロス』」

JMHのベンチマーク設定

@Fork(value = 1)  // フォーク数を1に設定
@Warmup(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)  // ウォームアップを3回、各5秒
@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)  // 測定を5回、各10秒

結果

(1)JSONデータ(17,353バイト)

アルゴリズム 圧縮レベル(デフォ値) 圧縮後サイズ【bytes】 圧縮率(圧縮後サイズ ÷ 元サイズ × 100)【%】 圧縮速度【ms】 展開速度【ms】
gzip 6 5,040 29.04 0.157 0.032
Brotli 4 5,289 30.48 0.096 0.030
Zstd 3 5,418 31.22 0.105 0.057
LZ4 fast 11,595 66.82 0.026 0.014

gzip、Brotli、Zstdの3つは圧縮率と速度ともに大きな差がありませんでした。
LZ4は期待どおり非常に高速ですが、圧縮率は他3つに比べて大きく離されています。

(2)テキストデータ(30,320バイト)

アルゴリズム 圧縮レベル(デフォ値) 圧縮後サイズ【bytes】 圧縮率(圧縮後サイズ ÷ 元サイズ × 100)【%】 圧縮速度【ms】 展開速度【ms】
gzip 6 11,162 36.81 0.866 0.059
Brotli 4 11,715 38.64 0.179 0.058
Zstd 3 11,934 39.36 0.133 0.121
LZ4 fast 24,085 79.44 0.050 0.023

BrotliとZstdの圧縮速度が非常によい結果でした。
LZ4は圧縮率80%程度で大きく伸び悩みました。

まとめ

今回はKotlin環境で動作する代表的な可逆圧縮アルゴリズム(gzip、Brotli、Zstd、LZ4)の特徴と実装サンプル、ベンチマークを紹介しました。
個人的には、速度重視ならLZ4、圧縮率と速度のバランスを取りたい場合は導入コストを考えてgzipやZstdをまずは試してみて、圧縮レベルを調整していくとよいかと思います。

最後までお読みいただき、ありがとうございました!




以上の内容はhttps://tech.askul.co.jp/entry/2025/09/16/185103より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14