gosuda.org/zstd: Drop-In DataDog/zstd Compression fără CGO și cu izolarea erorilor fatale

gosuda.org/zstd este o bibliotecă de compresie Go cu CGO_ENABLED=0 care rulează motorul oficial C Zstandard (libzstd 1.5.7) într-un proces worker izolat, peste memoria partajată HQQ. Oferă o API de înlocuire directă pentru github.com/DataDog/zstd (inclusiv Compress/Decompress one-shot, io.Writer/io.Reader pentru streaming, dicționare pre-procesate și contexte reutilizabile fără alocări), păstrând în același timp execuția C complet în afara spațiului de adrese al runtime-ului Go.
1go get gosuda.org/zstd@v0.3.10507
De ce decompresia trebuie să rămână în afara heap-ului Go
În backend-urile de producție, compresia și decompresia se află direct la granița intrărilor nesigure. Colectoarele de log-uri, brokerii de mesaje, gateway-urile RPC și motoarele de stocare decompresează, în fiecare minut, gigabytes de cadre furnizate din exterior.
Multe echipe Go aleg github.com/DataDog/zstd deoarece se leagă direct de libzstd upstream scris în C: implementarea C oferă un randament ridicat pe un singur nucleu, rapoarte de compresie strânse cu dicționare pentru înregistrări mici și arene C off-heap (ZSTD_CCtx și ZSTD_DCtx) care nu măresc timpii de scanare ai colectorului de gunoi Go.
Totuși, legarea libzstd într-un serviciu Go prin cgo aduce două puncte dureroase operaționale:
- Fricțiune la build și deployment: la fel ca
go-sqlite3,DataDog/zstdnecesităCGO_ENABLED=1și un toolchain de compilare C pentru ținta respectivă. Compilarea încrucișată a serviciilor sau construirea de binare statice pentrulinux/arm64oriwindows/arm64necesită sysroot-uri C externe. - Spațiu de adrese partajat la intrări nesigure: decompresarea unor payload-uri corupte sau adversariale în același spațiu de adrese virtual cu serverul Go înseamnă că orice eroare din codul C nativ sau din gestionarea dicționarelor personalizate poate termina întregul proces Go.
gosuda.org/zstd păstrează motorul upstream exact libzstd 1.5.7 și API-ul familiar github.com/DataDog/zstd, dar mută execuția C într-un proces copil verificat (zstd_server), conectat programului Go prin memorie partajată aliniată la linia de cache.
Înlocuire directă pentru github.com/DataDog/zstd
Migrarea de la github.com/DataDog/zstd necesită modificarea doar a calea de import către "gosuda.org/zstd". Nu este necesară instalarea unui binar extern sau cod de configurare: la primul apel, gosuda.org/zstd materializează un binar zstd_server embedded, atestat SLSA, pentru platforma ta, îi verifică digestul SHA-256 prin gosuda.org/hqq/secureexec și deschide un pool de sesiuni cu memorie partajată.
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}
Platforme suportate și binare embedded
Toate cele șase platforme țintă sunt livrate cu binare worker precompilate, fixate prin SHA-256, încorporate prin etichete de build specifice platformei (internal/embedded/embed_<goos>_<goarch>.go):
| OS țintă | Arhitectură țintă (GOARCH) | Binar embedded | Dimensiune binar | Toolchain de build și proveniență |
|---|---|---|---|---|
| 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 |
În interiorul căii de date din memoria partajată
Pentru a păstra randamentul competitiv cu cgo în proces, gosuda.org/zstd evită serializarea prin socket-uri și alocările de heap pe calea critică.
1. Codificare de slot fără alocări (zstd_ctx.go și c/zstd_hooks.c)
Când folosiți un zstd.Ctx reutilizabil (sau când funcțiile de nivel pachet Compress și Decompress împrumută un Ctx din pool-ul de sesiuni intern), datele circulă direct prin slotul StandardLink mapat în memorie:
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
- Clientul
hqq-bindgengenerat copiazăsrcdirect în slotul de cerere mapat în memorie și inversează cuvântul de secvențăSeqLock. - Worker-ul C citește
srcîn loc din slotul de cerere și transmite pointerul payload-ului din slotul de răspuns direct ca buffer de destinație cătreZSTD_compressCCtxsauZSTD_decompressDCtx. - Clientul Go copiază sliceul rezultat din slotul de răspuns în sliceul
dstfurnizat de apelant. În regim staționar,ctx.CompressTo(dst, src, level)șictx.DecompressInto(dst, src)realizează0 allocs/op.
Dicționarele pre-procesate (CDict și DDict) trăiesc pe worker-ul C în spatele handle-urilor pe 64 de biți verificate prin generație (handle CDict, handle DDict în zstd.idl). Crearea unui dicționar o singură dată prin ctx.NewCDict(rawDict, level) analizează tabelele dicționarului în interiorul worker-ului C și returnează un handle ușor, care poate fi reutilizat în mii de apeluri fără retrimiterea octeților dicționarului.
2. Amortizarea overhead-ului IPC prin încadrare în bloc (zstd_bulk.go)
Pentru payload-uri mari (16 KiB și mai mult), round trip-ul de memorie partajată de ~200 ns este neglijabil în comparație cu timpul CPU petrecut în libzstd. Pentru sarcinile care comprimă mii de mesaje mici, de 100 de octeți până la 1 KiB, totuși, plătirea a chiar 200 ns per element se adună.
gosuda.org/zstd oferă CompressBulk și DecompressBulk pe BulkCtx (zstd_bulk.go) pentru a elimina round trip-urile per element:
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 împachetează întregul lot [][]byte într-un singur cadru de cerere cu prefix de lungime în memoria partajată. Worker-ul C parcurge fiecare element în C (compress_bulk în c/zstd_hooks.c), scrie toate cadrele comprimate contiguu în slotul de răspuns și returnează întregul lot într-un singur round trip de memorie partajată.
3. Supraviețuirea terminării worker-ului fără prăbușirea apelantului
Deoarece zstd_server rulează într-un proces OS separat, o eroare fatală în worker-ul C nu îți doboară niciodată aplicația Go:
- Dacă
zstd_servereste oprit din exterior (de exemplu, prinSIGKILLsau un eveniment OOM), stratul de transport HQQ detectează imediat peer-ul defect și returnează un*rt.TransportErrortipizat din apelulCompresssauDecompressaflat în curs. - Funcțiile de nivel pachet (
zstd.Compress,zstd.Decompress,zstd.NewWriter,zstd.NewReader) detectează automat că worker-ul implicit partajat a dispărut, pornesc o nouă instanțăzstd_serververificată la următorul apel și reiau funcționarea normală.
Structura depozitului
Codul sursă separă fațada de înlocuire directă DataDog/zstd de primitivele de nivel scăzut hqq-bindgen pentru sesiuni și operații în bloc:
1gosuda.org/zstd
2├── autolaunch.go / hashes.go # API Compress/Decompress de înlocuire directă, pool cu recuperare automată și hash-uri SHA-256
3├── zstd_stream.go # Writer/Reader de streaming (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go # Ctx fără alocări, CDict și DDict, wrappere peste hqq-bindgen
5├── zstd_bulk.go # Încadrare CompressBulk și DecompressBulk cu un singur round trip
6├── zstd.go # Clientul Go hqq-bindgen generat pentru serviciul IDL zstd
7├── zstd.idl # Schema IDL HQQ care definește Context, CDict, DDict și metodele RPC
8├── internal/embedded/ # Binare zstd_server //go:embed cu etichete de platformă (6 ținte OS/arhitectură)
9└── c/ # Implementarea serverului C11 (zstd_server.c, zstd_hooks.c, main.c)
Noțiuni de bază
Instalați gosuda.org/zstd cu go get și utilizați-l cu CGO_ENABLED=0:
1go get gosuda.org/zstd@v0.3.10507
- Repository GitHub: https://github.com/gosuda/hqq-zstd
- Arhitectură și benchmark-uri: docs/ARCHITECTURE.md
- HQQ Core Runtime: https://gosuda.org/hqq