GoSuda

gosuda.org/zstd: Zero CGOおよびクラッシュ分離を備えたDrop-InなDataDog/zstd圧縮

By Lemon Mint
views ...

gosuda.org/zstd は、分離されたワーカープロセス内で HQQ 共有メモリを介して公式の C Zstandard (libzstd 1.5.7) エンジンを実行する、CGO_ENABLED=0 の Go 圧縮ライブラリです。C の実行を Go ランタイムのアドレス空間から完全に切り離した状態で、github.com/DataDog/zstd のためのドロップイン置換 API(一括の Compress/Decompress、ストリーミングの io.Writer/io.Reader、事前解析された辞書、再利用可能なゼロアロケーションコンテキストを含む)を提供します。

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 サービスにリンクさせることには、2つの運用上の課題が生じます。

  1. ビルドとデプロイの煩雑さ: go-sqlite3 と同様に、DataDog/zstd は CGO_ENABLED=1 とターゲットの C コンパイラツールチェーンを必要とします。サービスのクロスコンパイルや、linux/arm64 または windows/arm64 向けの静的バイナリのビルドには、外部の C sysroot が必要となります。
  2. 信頼されていない入力に対する共有アドレス空間: 破損した、または悪意のあるペイロードを 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. 一括 Compress / Decompress (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. 辞書サポート付きのストリーミング Writer および Reader
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}

サポートされているプラットフォームと埋め込みバイナリ

6つのターゲットプラットフォームすべてにおいて、プラットフォーム固有のビルドタグ(internal/embedded/embed_<goos>_<goarch>.go)を介して埋め込まれた、事前ビルド済みかつ SHA-256 で固定されたワーカーバイナリが同梱されています。

ターゲット OSターゲットアーキテクチャ (GOARCH)埋め込みバイナリバイナリサイズビルドツールチェーンおよびプロベナンス
Linuxamd64, arm64zstd_server_linux_<arch>~770 - 880 KiBGCC -O3, SLSA Build L3 Provenance
macOSarm64, amd64zstd_server_darwin_<arch>~660 - 795 KiBAppleClang -O3, SLSA Build L3 Provenance
Windowsamd64zstd_server_windows_amd64.exe~818 KiBMSVC -O2, SLSA Build L3 Provenance
Windowsarm64zstd_server_windows_arm64.exe~551 KiBzig 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
  1. 生成された hqq-bindgen クライアントは、src をメモリマップされたリクエストスロットに直接コピーし、SeqLock シーケンスワードを反転させます。
  2. C ワーカーはリクエストスロットからインプレースで src を読み取り、レスポンススロットのペイロードポインタを宛先バッファとして ZSTD_compressCCtx または ZSTD_decompressDCtx に直接渡します。
  3. Go クライアントは、結果として得られるスライスをレスポンススロットから呼び出し元が提供した dst スライスへとコピーします。定常状態において、ctx.CompressTo(dst, src, level) および ctx.DecompressInto(dst, src) は 0 allocs/op を実現します。

事前解析された辞書(CDict および DDict)は、世代確認が行われる 64 ビットハンドル(zstd.idl 内の handle CDict、handle DDict)の背後にある C ワーカー上に存在します。ctx.NewCDict(rawDict, level) を介して一度辞書を作成すると、C ワーカー内部で辞書テーブルが解析され、辞書バイトを再送信することなく数千回の呼び出しにわたって再利用可能な軽量ハンドルが返されます。

2. バルクフレーミングによる IPC オーバーヘッドの均等化 (zstd_bulk.go)

大きなペイロード(16 KiB 以上)の場合、約 200 ns の共有メモリラウンドトリップは、libzstd 内部で費やされる CPU 時間と比較して無視できるほど微小です。しかし、100 バイトから 1 KiB の小さなメッセージを何千件も一度に圧縮するワークロードでは、1 アイテムあたりわずか 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  # ドロップイン Compress/Decompress API、自動復旧プール、および SHA-256 ハッシュ
3├── zstd_stream.go             # ストリーミング Writer/Reader (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go                # hqq-bindgen 上のゼロアロケーション Ctx、CDict、および DDict ラッパー
5├── zstd_bulk.go               # 単一ラウンドトリップの CompressBulk および DecompressBulk フレーミング
6├── zstd.go                    # zstd IDL サービス用に生成された hqq-bindgen Go クライアント
7├── zstd.idl                   # Context、CDict、DDict、および RPC メソッドを定義する HQQ IDL スキーマ
8├── internal/embedded/         # プラットフォームタグ付き //go:embed zstd_server バイナリ (6 つの OS/Arch ターゲット)
9└── c/                         # C11 サーバーの実装 (zstd_server.c, zstd_hooks.c, main.c)

はじめに

go get を使用して gosuda.org/zstd をインストールし、CGO_ENABLED=0 で使用してください。

1go get gosuda.org/zstd@v0.3.10507