gosuda.org/zstd: Drop-In DataDog/zstd Compression with Zero CGO and Crash Isolation
gosuda.org/zstd ist eine Go-Komprimierungsbibliothek mit CGO_ENABLED=0, die die offizielle C-Zstandard-Engine (libzstd 1.5.7) in einem isolierten Worker-Prozess über den HQQ-Gemeinsamen-Speicher (shared memory) ausführt. Sie stellt eine nahtlose Ersatz-API (drop-in replacement API) für github.com/DataDog/zstd bereit (einschließlich einmaligem Compress/Decompress, Streaming von io.Writer/io.Reader, vorab aufbereiteten Wörterbüchern sowie wiederverwendbaren Kontexten ohne Heap-Allokationen), während die C-Ausführung vollständig außerhalb des Adressraums der Go-Laufzeitumgebung gehalten wird.
1go get gosuda.org/zstd@v0.3.10507
Warum Dekompression außerhalb Ihres Go-Heaps gehört
In Produktions-Backends befinden sich Kompression und Dekompression direkt an der Grenze für nicht vertrauenswürdige Eingaben. Protokollsammler (log collectors), Nachrichten-Broker (message brokers), RPC-Gateways und Speicher-Engines dekomprimieren jede Minute Gigabytes an extern bereitgestellten Frames.
Viele Go-Teams wählen github.com/DataDog/zstd, weil sie direkt an das Upstream-C von libzstd anbinden: Die C-Implementierung liefert einen hohen Single-Core-Durchsatz, enge Wörterbuch-Komprimierungsraten bei kleinen Datensätzen (records) und C-Bereiche außerhalb des Heaps (ZSTD_CCtx und ZSTD_DCtx), die die Scan-Zeiten der Go-Garbage-Collection nicht aufblähen.
Das Einlinken von libzstd in einen Go-Dienst über cgo bringt jedoch zwei betriebliche Reibungspunkte mit sich:
- Build- und Bereitstellungs-Reibung: Genau wie
go-sqlite3erfordertDataDog/zstdCGO_ENABLED=1und eine Ziel-C-Compiler-Toolchain. Das Cross-Kompilieren von Diensten oder das Erstellen statischer Binärdateien fürlinux/arm64oderwindows/arm64erfordert externe C-Sysroots. - Gemeinsamer Adressraum bei nicht vertrauenswürdigen Eingaben: Das Dekomprimieren beschädigter oder feindseliger Nutzdaten innerhalb desselben virtuellen Adressraums wie Ihr Go-Server bedeutet, dass jeder Fehler im nativen C-Code oder bei der benutzerdefinierten 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 github.com/DataDog/zstd-API bei, verlagert die C-Ausführung jedoch in einen verifizierten Kindprozess (zstd_server), der über einen Cache-Line-Gemeinsamen-Speicher mit Ihrem Go-Programm verbunden ist.
Nahtloser Ersatz für github.com/DataDog/zstd
Die Migration von github.com/DataDog/zstd erfordert lediglich die Änderung des Importpfads zu "gosuda.org/zstd". Es ist keine externe Binärinstallation oder Einrichtungssoftware erforderlich: Beim ersten Aufruf materialisiert gosuda.org/zstd eine eingebettete SLSA-attestierte zstd_server-Binärdatei für Ihre Plattform, verifiziert deren SHA-256-Prüfsumme über gosuda.org/hqq/secureexec und öffnet einen Sitzungs-Pool für den Gemeinsamen-Speicher.
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 festgesandten (pinned) Worker-Binärdateien ausgeliefert, die über plattformspezifische Build-Tags eingebettet sind (internal/embedded/embed_<goos>_<goarch>.go):
| Ziel-OS | Ziel-Architektur (GOARCH) | Eingebettete Binärdatei | Binärgröße | Build-Toolchain & Provenienz |
|---|---|---|---|---|
| Linux | amd64, arm64 | zstd_server_linux_<arch> | ~770 - 880 KiB | GCC -O3, SLSA Build L3 Provenienz |
| macOS | arm64, amd64 | zstd_server_darwin_<arch> | ~660 - 795 KiB | AppleClang -O3, SLSA Build L3 Provenienz |
| Windows | amd64 | zstd_server_windows_amd64.exe | ~818 KiB | MSVC -O2, SLSA Build L3 Provenienz |
| Windows | arm64 | zstd_server_windows_arm64.exe | ~551 KiB | zig cc -target aarch64-windows-gnu -O3, SLSA L3 |
Im Datenpfad des Gemeinsamen-Speichers
Um den Durchsatz im Vergleich zum prozessinternen cgo wettbewerbsfähig zu halten, vermeidet gosuda.org/zstd Socket-Serialisierung und Heap-Allokationen auf dem kritischen Pfad (hot path).
1. Slot-Kodierung ohne Allokationen (zstd_ctx.go und c/zstd_hooks.c)
Wenn Sie einen wiederverwendbaren zstd.Ctx verwenden (oder wenn die paketeigenen Compress- und Decompress-Funktionen einen Ctx aus dem internen Sitzungs-Pool ausleihen), 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
- Der generierte
hqq-bindgen-Client kopiertsrcdirekt in den im Speicher abgebildeten (memory-mapped) Anforderungs-Slot (request slot) und schaltet dasSeqLock-Sequenzwort um. - Der C-Worker liest
srcan Ort und Stelle aus dem Anforderungs-Slot und übergibt den Nutzdaten-Zeiger des Antwort-Slots direkt als Zielpuffer anZSTD_compressCCtxoderZSTD_decompressDCtx. - Der Go-Client kopiert das resultierende Slice aus dem Antwort-Slot in das vom Aufrufer bereitgestellte
dst-Slice. Im stabilen Zustand (steady state) führenctx.CompressTo(dst, src, level)undctx.DecompressInto(dst, src)0 allocs/opaus.
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) analysiert die Wörterbuchtabellen innerhalb des C-Workers und gibt ein leichtgewichtigen Handle zurück, das über Tausende von Aufrufen hinweg wiederverwendet werden kann, ohne die Wörterbuch-Bytes erneut zu senden.
2. Amortisierung des IPC-Overheads durch Bulk-Framing (zstd_bulk.go)
Bei großen Nutzdaten (16 KiB und mehr) ist der Hin- und Rücklauf (round trip) im Gemeinsamen-Speicher von ca. 200 ns im Vergleich zu der in libzstd verbrauchten CPU-Zeit vernachlässigbar. Bei Arbeitslasten, die Tausende kleiner Nachrichten von 100 Byte bis 1 KiB auf einmal komprimieren, summiert sich jedoch selbst das Bezahlen von 200 ns pro Element.
gosuda.org/zstd stellt CompressBulk und DecompressBulk für BulkCtx (zstd_bulk.go) bereit, um Hin- und Rückläufe 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-Batch in ein einzelnes längenpräfixiertes Anforderungs-Frame im Gemeinsamen-Speicher. Der C-Worker durchläuft jedes Element in C (compress_bulk in c/zstd_hooks.c), schreibt alle komprimierten Frames lückenlos (contiguously) in den Antwort-Slot und gibt den gesamten Batch in einem einzigen Hin- und Rücklauf im Gemeinsamen-Speicher zurück.
3. Überleben von Worker-Beendigungen ohne Absturz des Aufrufers
Da zstd_server in einem separaten Betriebssystemprozess läuft, bringt ein schwerwiegender Fehler innerhalb des C-Workers Ihre Go-Anwendung niemals zum Absturz:
- Wenn
zstd_serverextern beendet wird (zum Beispiel durchSIGKILLoder ein OOM-Ereignis), erkennt die HQQ-Transportebene den unterbrochenen Peer sofort und gibt einen typisierten*rt.TransportErroraus dem laufendenCompress- oderDecompress-Aufruf zurück. - Paketeigene Funktionen (
zstd.Compress,zstd.Decompress,zstd.NewWriter,zstd.NewReader) erkennen automatisch, dass der gemeinsame Standard-Worker verschwunden ist, starten beim nächsten Aufruf eine neue verifiziertezstd_server-Instanz und nehmen den Normalbetrieb wieder auf.
Repository-Layout
Die Codebasis trennt die nahtlose DataDog/zstd-Fassade von den Low-Level-Sitzungs- 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
- GitHub-Repository: https://github.com/gosuda/hqq-zstd
- Architektur & Benchmarks: docs/ARCHITECTURE.md
- HQQ Core Runtime: https://gosuda.org/hqq