はじめまして。アスクルのたかせです。
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() } }
参考
- https://docs.oracle.com/javase/jp/8/docs/api/java/util/zip/GZIPInputStream.html
- https://docs.oracle.com/javase/jp/8/docs/api/java/util/zip/GZIPOutputStream.html
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をまずは試してみて、圧縮レベルを調整していくとよいかと思います。
最後までお読みいただき、ありがとうございました!