💹

データ圧縮でElastiCacheの料金を節約する

2024/09/30に公開

はじめに

Redisはインメモリデータベースでありメモリ(RAM)上でデータを管理します。HDD(ハードディスク)やSSD(ソリッドステートドライブ)に比べてデータの読み取り書き込みの待ち時間は大幅に短いです。

が、同じ容量でもHDDやSSDのコストよりRAMの方がはるかに高いです。本記事では大量のキャッシュデータを保存しながらElastiCache料金を抑えることができる方法をご紹介します。

データ圧縮とは

Wikipediaによるとデータ圧縮とはあるデータを、そのデータの実質的な内容(情報、あるいはその情報量)を可能な限り保ったまま、データ量を減らした別のデータに変換すること。 データ圧縮には主に次の2つの種類があります。

  1. 可逆圧縮(ロスレス圧縮)
    データを圧縮後、元のデータが完全に復元できる圧縮方法です。例: ZIPファイル、PNG画像、FLAC音声

  2. 非可逆圧縮(ロッシー圧縮):
    圧縮後にデータの一部が失われるため、元のデータを完全には復元できませんが、その分サイズを大幅に削減できます。画像、音声、動画などでよく使われている圧縮方法です。 例: JPEG画像、MP3音声、MP4動画

データを圧縮することでデータサイズが小さくなり、同じ容量でももっと多くのデータが保存できるようにするだけでなくネットワーク上でのデータ転送の効率を向上させることが可能になります。

Goでデータ圧縮を実装する

圧縮アルゴリズム

データ圧縮アルゴリズムは色々ありますが、どのアルゴリズムを選定するかは目的によります。基本的に待ち時間が短いほど圧縮率が低くなります。代わりに圧縮率が高いほど待ち時間が長くなります。

今回ご紹介したいアルゴリズムはGoogleが開発したsnappy圧縮アルゴリズムです。このアルゴリズムはgzipアルゴリズムより圧縮率は低いですが、snappyのパーフォーマンスの方が何倍も速いです。要するにsnappyはバランスがいいアルゴリズムです。

Goでsnappy圧縮アルゴリズムを使用したい場合はgolang/snappyライブラリを使用できます。

go get github.com/golang/snappy

使いかたは以下の通り

func main() {
	src := []byte("Hello, 世界")

	// 圧縮
	encoded := snappy.Encode(nil, src)

	// 解凍
	decoded, err := snappy.Decode(nil, encoded)
	if err != nil {
    	log.Fatal(err)
	}

	fmt.Println(string(decoded))
}

データ圧縮でどのくらい容量を節約できるかは元データしだいです。たとえば以下のダミーテキストsnappyにかけると6.5%バイトを減らしました。

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

	str := []byte(`Lorem ipsum dolor sit amet...`)
	fmt.Printf("圧縮前: %d\n", len(str)) // 445

	encoded := snappy.Encode(nil, str)
	fmt.Printf("圧縮後: %d\n", len(encoded)) // 416

シリアライズフォーマット

バイトデータの場合はすぐに圧縮できますが配列や構造体(struct)の場合シリアル化する必要があります。一般的に使われているシリアル化する方法はJSONシリアライズです。他のシリアライズフォーマットはGobMessagePackです。

それぞれシリアル化のライブラリは以下になります。

  • JSON: encoding/json Go標準ライブラリ)
  • Gob: encoding/gob(Go標準ライブラリ)
  • MessagePack: github.com/vmihailenco/msgpack/v5

この3つのライブラリをベンチマークしてみました。

BenchmarkJSONMarshal-12                  	 45304     	    25614 ns/op   	17984 B/op      	35 allocs/op
BenchmarkJSONUnmarshal-12                	 14763     	    81072 ns/op   	31334 B/op      	74 allocs/op
BenchmarkGobMarshal-12                  	120536     	    10011 ns/op   	29876 B/op      	51 allocs/op
BenchmarkGobUnmarshal-12                 	 56109     	    21446 ns/op   	36464 B/op             243 allocs/op
BenchmarkMsgPackMarshal-12              	373802     	     2888 ns/op   	28005 B/op       	 5 allocs/op
BenchmarkMsgPackUnmarshal-12            	369746     	     3261 ns/op   	15286 B/op      	34 allocs/op

ベンチマークの結果に基づいて以下をまとめました。

  • MessagePackは一番速いだけでなくメモリ使用率も一番低い
  • JSONの処理速度は一番低い
  • GobでのUnmarshal処理のメモリ使用率はJSONに比べて若干高いですが、Gobのほうが4倍速い

シリアル化のバイトサイズも比較してみました。

フォーマット バイトサイズ
JSON 14602
Gob 13441
MessagePack 13304

MessagePack + snappy

MessagePackとsnappyの組み合わせも検証してみました。

BenchmarkMsgPackSnappyMarshal-12         	 55236     	    21330 ns/op   	44386 B/op       	 5 allocs/op
BenchmarkMsgPackSnappyUnmarshal-12      	140824     	     8507 ns/op   	28951 B/op      	35 allocs/op

GobでのMarshall処理に比べて若干パフォーマンスが低くなりますが、JSONより変わらずパーフォーマンスが良いです。 さらにバイトサイズは7789(JSONとGobの1/2)になりました。

注意点

上記の結果を見ると「すべてのデータを圧縮すればいいんじゃない」と思ってしまいますね。が、データ圧縮にもデメリットがあります。

  1. CPU使用率の増加
    データ圧縮またはデータ解凍にはCPUを使います。データのサイズが大きいほどCPU使用率も上がります。

  2. 圧縮できるサイズには限界がある
    元データのサイズがすでに小さいであれば圧縮するとファイルサイズが大きくなる可能性があります。 なぜかと言うとデータを圧縮するとき解凍に使われる情報などが追加されます。追加された情報が圧縮された分を上回るとファイルサイズが大きくなってしまいます。 同じ理由で一度圧縮されたデータを何回も圧縮するのは逆効果になるだけです。

まとめ

キャッシュデータを圧縮することでElasticCacheのメモリ使用容量を減らすことができます。より小さいスペックにダウングレードすることでコストセービングに繋がります。

とはいえデータ圧縮のデメリットも忘れないでくださいね。

しっかりベンチマークを行って、その結果を比較してからデータ圧縮が必要かどうかご確認ください。

Discussion