gosuda.org/zstd: Compressione compatibile con DataDog/zstd come drop-in replacement, senza CGO e con isolamento dei crash

gosuda.org/zstd è una libreria di compressione Go con CGO_ENABLED=0 che esegue il motore C Zstandard ufficiale (libzstd 1.5.7) all'interno di un processo worker isolato su memoria condivisa HQQ. Essa fornisce un'API di sostituzione diretta per github.com/DataDog/zstd (includendo Compress/Decompress one-shot, io.Writer/io.Reader in streaming, dizionari pre-elaborati e contesti riutilizzabili a allocazione zero), mantenendo l'esecuzione del codice C completamente al di fuori dello spazio di indirizzamento del runtime Go.
1go get gosuda.org/zstd@v0.3.10507
Perché la decompressione deve trovarsi al di fuori dell'heap Go
Nei backend di produzione, la compressione e la decompressione si collocano direttamente sul confine di input non attendibile. Log collector, message broker, gateway RPC e storage engine decomprimono ogni minuto gigabyte di frame forniti dall'esterno.
Molti team Go scelgono github.com/DataDog/zstd perché si collega direttamente alla libreria C libzstd upstream: l'implementazione C offre un'elevata throughput su singolo core, rapporti di compressione dei dizionari precisi su record di piccole dimensioni e arene C off-heap (ZSTD_CCtx e ZSTD_DCtx) che non aumentano i tempi di scansione del garbage collector di Go.
Il collegamento di libzstd a un servizio Go tramite cgo, tuttavia, comporta due criticità operative:
- Attrito in fase di build e deployment: proprio come
go-sqlite3,DataDog/zstdrichiedeCGO_ENABLED=1e una toolchain del compilatore C per la piattaforma di destinazione. La cross-compilazione dei servizi o la creazione di binari statici perlinux/arm64owindows/arm64richiede sysroot C esterni. - Spazio di indirizzamento condiviso con input non attendibili: la decompressione di payload corrotti o ostili all'interno dello stesso spazio di indirizzamento virtuale del proprio server Go implica che qualsiasi bug nel codice C nativo o nella gestione di dizionari personalizzati possa terminare l'intero processo Go.
gosuda.org/zstd mantiene l'esatto motore upstream libzstd 1.5.7 in C e la familiare API di github.com/DataDog/zstd, ma sposta l'esecuzione del C in un processo figlio verificato (zstd_server) collegato al programma Go tramite memoria condivisa allineata alle linee di cache.
Sostituzione diretta per github.com/DataDog/zstd
La migrazione da github.com/DataDog/zstd richiede di modificare esclusivamente il percorso di importazione in "gosuda.org/zstd". Non sono necessari l'installazione di binari esterni né codice di configurazione: alla prima chiamata, gosuda.org/zstd materializza un binario zstd_server incorporato, certificato SLSA, per la propria piattaforma, ne verifica il digest SHA-256 tramite gosuda.org/hqq/secureexec e apre un pool di sessioni su memoria condivisa.
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 one-shot (equivalente a 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 e Reader in streaming con supporto ai dizionari
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}
Piattaforme supportate e binari incorporati
Tutte e sei le piattaforme di destinazione includono binari worker precompilati con digest SHA-256 fissato, incorporati tramite tag di build specifici per piattaforma (internal/embedded/embed_<goos>_<goarch>.go):
| Sistema operativo | Architettura di destinazione (GOARCH) | Binario incorporato | Dimensione del binario | Toolchain di build e provenienza |
|---|---|---|---|---|
| Linux | amd64, arm64 | zstd_server_linux_<arch> | ~770 - 880 KiB | GCC -O3, SLSA Build L3 Provenance |
| macOS | arm64, amd64 | zstd_server_darwin_<arch> | ~660 - 795 KiB | AppleClang -O3, SLSA Build L3 Provenance |
| Windows | amd64 | zstd_server_windows_amd64.exe | ~818 KiB | MSVC -O2, SLSA Build L3 Provenance |
| Windows | arm64 | zstd_server_windows_arm64.exe | ~551 KiB | zig cc -target aarch64-windows-gnu -O3, SLSA L3 |
All'interno del percorso dati su memoria condivisa
Per mantenere una throughput competitiva rispetto al cgo in-process, gosuda.org/zstd evita la serializzazione su socket e le allocazioni heap nel percorso critico.
1. Codifica degli slot a allocazione zero (zstd_ctx.go e c/zstd_hooks.c)
Quando si utilizza un zstd.Ctx riutilizzabile (oppure quando le funzioni Compress e Decompress a livello di package prendono in prestito un Ctx dal pool di sessioni interno), i dati fluiscono direttamente attraverso lo slot StandardLink mappato in memoria:
1Caller Go (zstd.Ctx) Slot in memoria condivisa Worker C11 (zstd_hooks.c)
2-------------------- ------------------------- -------------------------
3src []byte -------------------------> Payload dello slot di richiesta ---> ZSTD_compressCCtx(
4 cctx, resp_slot, src, level)
5dst []byte <------------------------- Payload dello slot di risposta <--- Scrive il frame compresso direttamente
6 nello slot di risposta condiviso
- Il client
hqq-bindgengenerato copiasrcdirettamente nello slot di richiesta mappato in memoria e modifica la parola di sequenzaSeqLock. - Il worker C legge
srcin place dallo slot di richiesta e passa il puntatore al payload dello slot di risposta direttamente come buffer di destinazione aZSTD_compressCCtxoZSTD_decompressDCtx. - Il client Go copia la slice risultante dallo slot di risposta nella slice
dstfornita dal chiamante. In regime stazionario,ctx.CompressTo(dst, src, level)ectx.DecompressInto(dst, src)eseguono0 allocs/op.
I dizionari pre-elaborati (CDict e DDict) risiedono nel worker C dietro handle a 64 bit con controllo di generazione (handle CDict, handle DDict in zstd.idl). La creazione di un dizionario una sola volta tramite ctx.NewCDict(rawDict, level) analizza le tabelle del dizionario all'interno del worker C e restituisce un handle leggero riutilizzabile per migliaia di chiamate senza reinviare i byte del dizionario.
2. Ammortizzare il sovraccarico IPC con il framing in blocco (zstd_bulk.go)
Per payload di grandi dimensioni (16 KiB e oltre), il round trip su memoria condivisa di circa 200 ns è trascurabile rispetto al tempo di CPU impiegato all'interno di libzstd. Per i carichi di lavoro che comprimono migliaia di messaggi piccoli, da 100 byte a 1 KiB, in un'unica operazione, tuttavia, pagare anche solo 200 ns per elemento si accumula.
gosuda.org/zstd fornisce CompressBulk e DecompressBulk su BulkCtx (zstd_bulk.go) per eliminare i round trip per elemento:
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 impacchetta l'intero batch [][]byte in un singolo frame di richiesta con prefisso di lunghezza nella memoria condivisa. Il worker C itera su ogni elemento in C (compress_bulk in c/zstd_hooks.c), scrive tutti i frame compressi in modo contiguo nello slot di risposta e restituisce l'intero batch in un'unica operazione di round trip su memoria condivisa.
3. Sopravvivere alla terminazione del worker senza causare il crash del chiamante
Poiché zstd_server viene eseguito in un processo del sistema operativo separato, un errore fatale all'interno del worker C non compromette mai l'applicazione Go:
- Se
zstd_serverviene terminato dall'esterno (ad esempio tramiteSIGKILLo un evento OOM), il livello di trasporto HQQ rileva immediatamente il peer interrotto e restituisce un*rt.TransportErrortipizzato dalla chiamataCompressoDecompressin corso. - Le funzioni a livello di package (
zstd.Compress,zstd.Decompress,zstd.NewWriter,zstd.NewReader) rilevano automaticamente che il worker predefinito condiviso non è più disponibile, avviano una nuova istanza verificata dizstd_serveralla chiamata successiva e riprendono il funzionamento normale.
Struttura del repository
La codebase separa la facciata drop-in DataDog/zstd dalle primitive di sessione e di bulk di basso livello hqq-bindgen:
1gosuda.org/zstd
2├── autolaunch.go / hashes.go # API drop-in Compress/Decompress, pool con auto-recupero e hash SHA-256
3├── zstd_stream.go # Writer/Reader in streaming (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go # Ctx a allocazione zero, wrapper CDict e DDict su hqq-bindgen
5├── zstd_bulk.go # Framing CompressBulk e DecompressBulk con singolo round trip
6├── zstd.go # Client Go hqq-bindgen generato per il servizio IDL zstd
7├── zstd.idl # Schema IDL HQQ che definisce Context, CDict, DDict e i metodi RPC
8├── internal/embedded/ # Binari zstd_server con //go:embed e tag di piattaforma (6 target OS/Arch)
9└── c/ # Implementazione del server C11 (zstd_server.c, zstd_hooks.c, main.c)
Per iniziare
Installa gosuda.org/zstd con go get e utilizzalo con CGO_ENABLED=0:
1go get gosuda.org/zstd@v0.3.10507
- Repository GitHub: https://github.com/gosuda/hqq-zstd
- Architettura e benchmark: docs/ARCHITECTURE.md
- HQQ Core Runtime: https://gosuda.org/hqq