GoSuda

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

By Lemon Mint views ...

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:

  1. Attrito in fase di build e deployment: proprio come go-sqlite3, DataDog/zstd richiede CGO_ENABLED=1 e una toolchain del compilatore C per la piattaforma di destinazione. La cross-compilazione dei servizi o la creazione di binari statici per linux/arm64 o windows/arm64 richiede sysroot C esterni.
  2. 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 operativoArchitettura di destinazione (GOARCH)Binario incorporatoDimensione del binarioToolchain di build e provenienza
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

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
  1. Il client hqq-bindgen generato copia src direttamente nello slot di richiesta mappato in memoria e modifica la parola di sequenza SeqLock.
  2. Il worker C legge src in place dallo slot di richiesta e passa il puntatore al payload dello slot di risposta direttamente come buffer di destinazione a ZSTD_compressCCtx o ZSTD_decompressDCtx.
  3. Il client Go copia la slice risultante dallo slot di risposta nella slice dst fornita dal chiamante. In regime stazionario, ctx.CompressTo(dst, src, level) e ctx.DecompressInto(dst, src) eseguono 0 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_server viene terminato dall'esterno (ad esempio tramite SIGKILL o un evento OOM), il livello di trasporto HQQ rileva immediatamente il peer interrotto e restituisce un *rt.TransportError tipizzato dalla chiamata Compress o Decompress in 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 di zstd_server alla 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