gosuda.org/zstd: CGO不要かつクラッシュ隔離を備えたDataDog/zstdの完全互換ドロップイン置換

gosuda.org/zstd は CGO_ENABLED=0 で動作する Go 圧縮ライブラリであり、HQQ 共有メモリを介して、公式の C Zstandard (libzstd 1.5.7) エンジンを隔離されたワーカープロセス内で実行します。github.com/DataDog/zstd に対するドロップイン置換 API を提供し(一回限りの Compress/Decompress、ストリーミング io.Writer/io.Reader、事前処理済み辞書、再利用可能なゼロアロケーションのコンテキストを含む)、C による実行を Go ランタイムのアドレス空間から完全に分離した状態に保ちます。
1go get gosuda.org/zstd@v0.3.10507
展開処理を Go ヒープの外に置く理由
本番環境のバックエンドでは、圧縮と展開は信頼されていない入力の境界に直接位置します。ログコレクター、メッセージブローカー、RPC ゲートウェイ、ストレージエンジンは、外部から供給されたフレームを毎分ギガバイト単位で展開しています。
多くの Go チームは github.com/DataDog/zstd を選択しています。これは上流の C libzstd に直接バインドしているためです。C 実装は、単一コアでの高いスループット、小さなレコードに対する高密度な辞書圧縮率、そして Go のガベージコレクションのスキャン時間を増大させないオフヒープの C アリーナ (ZSTD_CCtx および ZSTD_DCtx) を提供します。
しかしながら、cgo を介して libzstd を Go サービスにリンクすると、運用上の問題が二つ生じます。
- ビルドおよびデプロイの困難:
go-sqlite3と同様に、DataDog/zstdはCGO_ENABLED=1と対象となる C コンパイラツールチェーンを必要とします。linux/arm64やwindows/arm64向けにサービスをクロスコンパイルしたり、静的バイナリをビルドしたりするには、外部の C sysroot が必要になります。 - 信頼されていない入力に対する共有アドレス空間: 破損した、あるいは悪意のあるペイロードを Go サーバーと同じ仮想アドレス空間内で展開すると、ネイティブ C コードやカスタム辞書処理のバグによって Go プロセス全体が終了する可能性があります。
gosuda.org/zstd は、上流の C libzstd 1.5.7 エンジンそのものと、慣れ親しんだ github.com/DataDog/zstd API を維持しつつ、C による実行を、キャッシュライン単位の共有メモリで Go プログラムに接続された検証済みの子プロセス (zstd_server) に移します。
github.com/DataDog/zstd のドロップイン置換
github.com/DataDog/zstd から移行するには、インポートパスを "gosuda.org/zstd" に変更するだけで十分です。外部バイナリのインストールやセットアップコードは不要です。最初の呼び出し時に、gosuda.org/zstd は、お使いのプラットフォーム向けの SLSA 証明付きで埋め込まれた zstd_server バイナリを展開し、gosuda.org/hqq/secureexec を通じてその SHA-256 ダイジェストを検証し、共有メモリセッションプールを開きます。
1package main
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "log"
8
9 "gosuda.org/zstd"
10)
11
12func main() {
13 // 1. One-shot Compress / Decompress (mirrors github.com/DataDog/zstd)
14 src := []byte("shared-memory zstd payload without cgo")
15 compressed, err := zstd.CompressLevel(nil, src, zstd.BestSpeed)
16 if err != nil {
17 log.Fatal(err)
18 }
19 plain, err := zstd.Decompress(nil, compressed)
20 if err != nil {
21 log.Fatal(err)
22 }
23 fmt.Printf("one-shot: %d bytes -> %d bytes (%s)\n", len(src), len(compressed), plain)
24
25 // 2. Streaming Writer and Reader with Dictionary Support
26 var buf bytes.Buffer
27 w := zstd.NewWriterLevel(&buf, zstd.DefaultCompression)
28 if _, err := w.Write(src); err != nil {
29 log.Fatal(err)
30 }
31 if err := w.Close(); err != nil {
32 log.Fatal(err)
33 }
34
35 r := zstd.NewReader(&buf)
36 defer r.Close()
37 streamed, err := io.ReadAll(r)
38 if err != nil {
39 log.Fatal(err)
40 }
41 fmt.Printf("streamed: %s\n", streamed)
42}
サポートされるプラットフォームと埋め込みバイナリ
six つの対象プラットフォームすべてに、プラットフォーム固有のビルドタグ (internal/embedded/embed_<goos>_<goarch>.go) を介して埋め込まれた、ビルド済みで SHA-256 により固定されたワーカーバイナリが含まれています。
| 対象 OS | 対象アーキテクチャ (GOARCH) | 埋め込みバイナリ | バイナリサイズ | ビルドツールチェーンと来歴 |
|---|---|---|---|---|
| Linux | amd64, arm64 | zstd_server_linux_<arch> | 約 770 - 880 KiB | GCC -O3、SLSA Build L3 来歴 |
| macOS | arm64, amd64 | zstd_server_darwin_<arch> | 約 660 - 795 KiB | AppleClang -O3、SLSA Build L3 来歴 |
| Windows | amd64 | zstd_server_windows_amd64.exe | 約 818 KiB | MSVC -O2、SLSA Build L3 来歴 |
| Windows | arm64 | zstd_server_windows_arm64.exe | 約 551 KiB | zig cc -target aarch64-windows-gnu -O3、SLSA L3 |
共有メモリのデータパスの内部
cgo によるインプロセス実行に匹敵するスループットを維持するため、gosuda.org/zstd はホットパスにおけるソケットによるシリアライズとヒープアロケーションを回避しています。
1. ゼロアロケーションのスロットエンコーディング (zstd_ctx.go と c/zstd_hooks.c)
再利用可能な zstd.Ctx を使用する場合、またはパッケージレベルの Compress と Decompress 関数が内部セッションプールから Ctx を借用する場合、データはマッピングされた StandardLink スロットを通じて直接流れます。
1Go Caller (zstd.Ctx) Shared-Memory Slot C11 Worker (zstd_hooks.c)
2-------------------- ------------------ -------------------------
3src []byte -------------------------> Request Slot Payload -------------> ZSTD_compressCCtx(
4 cctx, resp_slot, src, level)
5dst []byte <------------------------- Response Slot Payload <------------- Writes compressed frame directly
6 into shared response slot
- 生成された
hqq-bindgenクライアントは、srcをメモリマップされたリクエストスロットに直接コピーし、SeqLockのシーケンスワードを反転させます。 - C ワーカーは、リクエストスロットから
srcをその場で読み取り、レスポンススロットのペイロードポインタを、送信先バッファとしてZSTD_compressCCtxまたはZSTD_decompressDCtxにそのまま渡します。 - Go クライアントは、結果のスライスをレスポンススロットから呼び出し元が指定した
dstスライスにコピーします。定常状態において、ctx.CompressTo(dst, src, level)とctx.DecompressInto(dst, src)は0 allocs/opで動作します。
事前処理済みの辞書 (CDict および DDict) は、zstd.idl の handle CDict、handle DDict で定義された、世代検査付きの 64 ビットハンドルの背後で C ワーカー上に存在します。ctx.NewCDict(rawDict, level) で辞書を一度作成すると、C ワーカー内で辞書テーブルが解析され、辞書のバイト列を再送することなく、数千回の呼び出しで再利用できる軽量なハンドルが返されます。
2. バルクフレーミングによる IPC オーバーヘッドの償却 (zstd_bulk.go)
大きなペイロード(16 KiB 以上)の場合、約 200 ns の共有メモリのラウンドトリップは、libzstd 内で費やされる CPU 時間に比べて無視できる程度です。しかしながら、100 バイトから 1 KiB の小さなメッセージを数千個まとめて圧縮するワークロードでは、項目ごとに 200 ns を費やすだけでも累積します。
gosuda.org/zstd は、項目ごとのラウンドトリップを排除するために、BulkCtx(zstd_bulk.go)上に CompressBulk と DecompressBulk を提供します。
1bctx, err := client.NewBulkCtx(1 << 20)
2if err != nil {
3 log.Fatal(err)
4}
5defer bctx.Close()
6
7items := [][]byte{
8 []byte(`{"event":"login","user":101}`),
9 []byte(`{"event":"purchase","user":101,"amount":4200}`),
10 []byte(`{"event":"logout","user":101}`),
11}
12compressedItems, err := bctx.CompressBulk(nil, items, zstd.DefaultCompression)
CompressBulk は、[][]byte のバッチ全体を、共有メモリ内の単一の長さ接頭辞付きリクエストフレームにパックします。C ワーカーは、C 内(c/zstd_hooks.c の compress_bulk)ですべての項目をループ処理し、圧縮されたフレームをすべて連続してレスポンススロットに書き込み、バッチ全体を単一の共有メモリのラウンドトリップで返します。
3. ワーカーの終了を生き延び、呼び出し元をクラッシュさせない
zstd_server は独立した OS プロセスで実行されるため、C ワーカー内で発生した致命的な障害が Go アプリケーション全体を停止させることはありません。
zstd_serverが外部から(例えばSIGKILLや OOM イベントによって)強制終了された場合、HQQ トランスポート層は切断されたピアを即座に検出し、進行中のCompressまたはDecompress呼び出しから型付きの*rt.TransportErrorを返します。- パッケージレベルの関数(
zstd.Compress、zstd.Decompress、zstd.NewWriter、zstd.NewReader)は、共有デフォルトワーカーが存在しないことを自動的に検出し、次の呼び出し時に検証済みの新しいzstd_serverインスタンスを起動して、通常の動作を再開します。
リポジトリの構成
コードベースは、ドロップインの DataDog/zstd ファサードと、低レベルの hqq-bindgen セッションおよびバルクプリミティブとを分離しています。
1gosuda.org/zstd
2├── autolaunch.go / hashes.go # Drop-in Compress/Decompress API, auto-recovery pool, and SHA-256 hashes
3├── zstd_stream.go # Streaming Writer/Reader (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go # Zero-allocation Ctx, CDict, and DDict wrappers over hqq-bindgen
5├── zstd_bulk.go # Single-round-trip CompressBulk and DecompressBulk framing
6├── zstd.go # Generated hqq-bindgen Go client for the zstd IDL service
7├── zstd.idl # HQQ IDL schema defining Context, CDict, DDict, and RPC methods
8├── internal/embedded/ # Platform-tagged //go:embed zstd_server binaries (6 OS/Arch targets)
9└── c/ # C11 server implementation (zstd_server.c, zstd_hooks.c, main.c)
はじめに
go get で gosuda.org/zstd をインストールし、CGO_ENABLED=0 を指定して使用します。
1go get gosuda.org/zstd@v0.3.10507
- GitHub リポジトリ: https://github.com/gosuda/hqq-zstd
- アーキテクチャとベンチマーク: docs/ARCHITECTURE.md
- HQQ コアランタイム: https://gosuda.org/hqq