GoSuda

gosuda.org/zstd: Drop-In-Ersatz für DataDog/zstd mit Zero CGO und Crash-Isolation

By Lemon Mint views ...

gosuda.org/zstd ist eine CGO_ENABLED=0-Go-Kompressionsbibliothek, die die offizielle C-Zstandard-Engine (libzstd 1.5.7) innerhalb eines isolierten Worker-Prozesses über gemeinsamen Speicher von HQQ ausführt. Sie bietet eine Drop-in-Replacement-API für github.com/DataDog/zstd (einschließlich einmaliger Compress/Decompress-Aufrufe, streamender io.Writer/io.Reader, vorab aufbereiteter Wörterbücher und wiederverwendbarer Kontexte ohne Allokationen), während die C-Ausführung vollständig außerhalb des Adressraums der Go-Laufzeit bleibt.

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

Warum die Dekompression außerhalb Ihres Go-Heaps stattfinden sollte

In produktiven Backend-Systemen liegen Kompression und Dekompression unmittelbar an der Grenze zu nicht vertrauenswürdigen Eingaben. Log-Collectors, Message-Broker, RPC-Gateways und Speicher-Engines dekomprimieren jede Minute Gigabyte extern bereitgestellter Frames.

Viele Go-Teams wählen github.com/DataDog/zstd, weil die Bibliothek direkt an das upstream-C-libzstd gebunden ist: Die C-Implementierung liefert einen hohen Durchsatz pro Kern, wirksame Wörterbuch-Kompressionsraten bei kleinen Datensätzen sowie außerhalb des Heaps liegende C-Arenen (ZSTD_CCtx und ZSTD_DCtx), die die Scan-Zeiten der Go-Garbage-Collection nicht erhöhen.

Die Einbindung von libzstd in einen Go-Dienst über cgo bringt jedoch zwei betriebliche Schwachstellen mit sich:

  1. Erschwernisse bei Build und Deployment: Ebenso wie go-sqlite3 erfordert DataDog/zstd CGO_ENABLED=1 sowie eine Ziel-C-Compiler-Toolchain. Das Cross-Compiling von Diensten oder das Erstellen statischer Binärdateien für linux/arm64 oder windows/arm64 erfordert externe C-Sysroots.
  2. Gemeinsamer Adressraum bei nicht vertrauenswürdigen Eingaben: Die Dekompression beschädigter oder feindseliger Nutzdaten innerhalb desselben virtuellen Adressraums wie Ihr Go-Server bedeutet, dass jeder Fehler im nativen C-Code oder in der eigenen Wörterbuchverarbeitung den gesamten Go-Prozess beenden kann.

gosuda.org/zstd behält die exakte upstream-C-Engine libzstd 1.5.7 und die vertraute API von github.com/DataDog/zstd bei, verlagert die C-Ausführung jedoch in einen verifizierten Kindprozess (zstd_server), der über cache-line-ausgerichteten gemeinsamen Speicher mit Ihrem Go-Programm verbunden ist.

Drop-in-Replacement für github.com/DataDog/zstd

Für die Migration von github.com/DataDog/zstd muss lediglich der Importpfad auf "gosuda.org/zstd" geändert werden. Es ist weder eine externe Binärinstallation noch Setup-Code erforderlich: Beim ersten Aufruf erzeugt gosuda.org/zstd eine eingebettete, per SLSA attestierte zstd_server-Binärdatei für Ihre Plattform, verifiziert deren SHA-256-Digest über gosuda.org/hqq/secureexec und öffnet einen Pool gemeinsam genutzter Speicher-Sitzungen.

 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}

Unterstützte Plattformen und eingebettete Binärdateien

Alle sechs Zielplattformen werden mit vorgefertigten, per SHA-256 fixierten Worker-Binärdateien ausgeliefert, die über plattformspezifische Build-Tags (internal/embedded/embed_<goos>_<goarch>.go) eingebettet werden:

ZielbetriebssystemZielarchitektur (GOARCH)Eingebettete BinärdateiBinärgrößeBuild-Toolchain und Herkunftsnachweis
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

Im gemeinsamen Speicher-Datenpfad

Um den Durchsatz im Vergleich zu prozessinternem cgo konkurrenzfähig zu halten, vermeidet gosuda.org/zstd Socket-Serialisierung und Heap-Allokationen auf dem Hot Path.

1. Allokationsfreie Slot-Kodierung (zstd_ctx.go und c/zstd_hooks.c)

Wenn Sie einen wiederverwendbaren zstd.Ctx verwenden (oder wenn die Paketfunktionen Compress und Decompress einen Ctx aus dem internen Sitzungspool entnehmen), fließen die Daten direkt durch den abgebildeten StandardLink-Slot:

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. Der generierte hqq-bindgen-Client kopiert src direkt in den speicherabgebildeten Request-Slot und setzt das SeqLock-Sequenzwort um.
  2. Der C-Worker liest src direkt aus dem Request-Slot und übergibt den Payload-Zeiger des Response-Slots unmittelbar als Zielpuffer an ZSTD_compressCCtx bzw. ZSTD_decompressDCtx.
  3. Der Go-Client kopiert das resultierende Slice aus dem Response-Slot in das vom Aufrufer bereitgestellte dst-Slice. Im stationären Betrieb führen ctx.CompressTo(dst, src, level) und ctx.DecompressInto(dst, src) 0 allocs/op aus.

Vorab aufbereitete Wörterbücher (CDict und DDict) befinden sich auf dem C-Worker hinter generationsgeprüften 64-Bit-Handles (handle CDict, handle DDict in zstd.idl). Das einmalige Erstellen eines Wörterbuchs über ctx.NewCDict(rawDict, level) parst die Wörterbuchtabellen innerhalb des C-Workers und liefert ein leichtgewichtiges Handle zurück, das über Tausende von Aufrufen hinweg wiederverwendet werden kann, ohne die Wörterbuchbytes erneut zu übertragen.

2. Amortisierung des IPC-Overheads durch Bulk-Framing (zstd_bulk.go)

Bei großen Nutzdaten (16 KiB und darüber) ist der Round Trip über gemeinsamen Speicher von etwa 200 ns im Vergleich zur CPU-Zeit innerhalb von libzstd vernachlässigbar. Bei Arbeitslasten, die Tausende kleiner Nachrichten von 100 Byte bis 1 KiB auf einmal komprimieren, summieren sich jedoch selbst 200 ns pro Element.

gosuda.org/zstd stellt CompressBulk und DecompressBulk auf BulkCtx (zstd_bulk.go) bereit, um Round Trips pro Element zu eliminieren:

 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 packt den gesamten [][]byte-Stapel in einen einzigen längenpräfixierten Request-Frame im gemeinsamen Speicher. Der C-Worker durchläuft jedes Element in C (compress_bulk in c/zstd_hooks.c), schreibt alle komprimierten Frames zusammenhängend in den Response-Slot und liefert den gesamten Stapel in einem einzigen Round Trip über gemeinsamen Speicher zurück.

3. Überleben des Worker-Abbruchs ohne Absturz des Aufrufers

Da zstd_server in einem separaten Betriebssystemprozess läuft, bringt ein fataler Fehler im C-Worker Ihre Go-Anwendung nie zum Absturz:

  • Wird zstd_server extern beendet (etwa durch SIGKILL oder ein OOM-Ereignis), erkennt die HQQ-Transportschicht den unterbrochenen Peer sofort und gibt einen typisierten *rt.TransportError aus dem laufenden Compress- oder Decompress-Aufruf zurück.
  • Paketweite Funktionen (zstd.Compress, zstd.Decompress, zstd.NewWriter, zstd.NewReader) erkennen automatisch, dass der gemeinsam genutzte Standard-Worker nicht mehr vorhanden ist, starten beim nächsten Aufruf eine neue verifizierte zstd_server-Instanz und nehmen den regulären Betrieb wieder auf.

Repository-Struktur

Die Codebasis trennt die Drop-in-Fassade DataDog/zstd von den low-level Session- und Bulk-Primitiven von 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)

Erste Schritte

Installieren Sie gosuda.org/zstd mit go get und verwenden Sie es mit CGO_ENABLED=0:

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