GoSuda

gosuda.org/zstd: Drop-In náhrada DataDog/zstd s kompresí bez CGO a izolací pádů

By Lemon Mint views ...

gosuda.org/zstd je knihovna pro kompresi v Go s CGO_ENABLED=0, která spouští oficiální C engine Zstandard (libzstd 1.5.7) uvnitř izolovaného pracovního procesu přes sdílenou paměť HQQ. Poskytuje API jako náhradu za github.com/DataDog/zstd (včetně jednorázových Compress/Decompress, streamovacích io.Writer/io.Reader, předem zpracovaných slovníků a znovupoužitelných kontextů s nulovou alokací), přičemž provádění C kódu zůstává zcela mimo adresní prostor běhového prostředí Go.

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

Proč by dekomprese měla probíhat mimo haldu Go

V produkčních backendech se komprese a dekomprese nacházejí přímo na hranici nedůvěryhodného vstupu. Sběrače logů, message brokery, RPC brány a úložné enginy dekomprimují každou minutu gigabajty externě dodaných rámců.

Mnoho týmů v Go volí github.com/DataDog/zstd, protože se váže přímo na upstream C libzstd: C implementace poskytuje vysokou propustnost na jednom jádře, těsné poměry komprese se slovníkem u malých záznamů a off-heap C arény (ZSTD_CCtx a ZSTD_DCtx), které nezvyšují dobu prohledávání paměti garbage collectorem v Go.

Propojení libzstd s Go službou přes cgo však přináší dva provozní problémy:

  1. Obtíže při sestavení a nasazení: Stejně jako go-sqlite3 vyžaduje DataDog/zstd CGO_ENABLED=1 a cílový toolchain C kompilátoru. Křížová kompilace služeb nebo sestavení statických binárních souborů pro linux/arm64 či windows/arm64 vyžaduje externí C sysroot.
  2. Sdílený adresní prostor při nedůvěryhodných vstupech: Dekomprese poškozených nebo protivníkových datových částí ve stejném virtuálním adresním prostoru jako váš Go server znamená, že jakákoli chyba v nativním C kódu nebo ve vlastní práci se slovníky může ukončit celý proces Go.

gosuda.org/zstd zachovává přesný upstream C engine libzstd 1.5.7 a známé API github.com/DataDog/zstd, ale přesouvá provádění C kódu do ověřeného child procesu (zstd_server), který je s vaším Go programem propojen přes sdílenou paměť zarovnanou na cache line.

Náhrada za github.com/DataDog/zstd bez úprav kódu

Migrace z github.com/DataDog/zstd vyžaduje změnit pouze importní cestu na "gosuda.org/zstd". Není potřeba instalovat žádný externí binární soubor ani provádět nastavení: při prvním volání gosuda.org/zstd vytvoří z vestavěného binárního souboru zstd_server s attestací SLSA pro vaši platformu, ověří jeho digest SHA-256 pomocí gosuda.org/hqq/secureexec a otevře fond relací se sdílenou pamětí.

 1package main
 2
 3import (
 4	"bytes"
 5	"fmt"
 6	"io"
 7	"log"
 8
 9	"gosuda.org/zstd"
10)
11
12func main() {
13	// 1. Jednorázové Compress / Decompress (kopíruje 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. Streamovací Writer a Reader s podporou slovníků
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}

Podporované platformy a vestavěné binární soubory

Všech šest cílových platforem obsahuje předkompilované pracovní binární soubory s pevným SHA-256, vestavěné pomocí specifických build tagů pro platformu (internal/embedded/embed_<goos>_<goarch>.go):

Cílový OSCílová architektura (GOARCH)Vestavěný binární souborVelikost binárního souboruBuild toolchain a provenience
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

Uvnitř datové cesty se sdílenou pamětí

Aby gosuda.org/zstd udržel propustnost konkurenceschopnou vůči procesu cgo běžícímu v rámci stejného procesu, vyhýbá se serializaci přes sockety a alokacím na heapu v kritické cestě.

1. Kódování slotů bez alokací (zstd_ctx.go a c/zstd_hooks.c)

Při použití znovupoužitelného zstd.Ctx (nebo když funkce na úrovni balíčku Compress a Decompress vypůjčí Ctx z interního fondu relací) data proudí přímo přes namapovaný slot StandardLink:

1Go volající (zstd.Ctx)                  Slot sdílené paměti                   C11 worker (zstd_hooks.c)
2--------------------                    -------------------                   -------------------------
3src []byte  ------------------------->  Payload požadavkového slotu  ------->  ZSTD_compressCCtx(
4                                                                                cctx, resp_slot, src, level)
5dst []byte  <-------------------------  Payload odpovědního slotu  <---------  Zapisuje komprimovaný rámec přímo
6                                                                              do sdíleného odpovědního slotu
  1. Generovaný klient hqq-bindgen zkopíruje src přímo do namapovaného požadavkového slotu a přepne sekvenční slovo SeqLock.
  2. C worker čte src na místě z požadavkového slotu a předává ukazatel na payload odpovědního slotu přímo jako cílový buffer do ZSTD_compressCCtx nebo ZSTD_decompressDCtx.
  3. Go klient zkopíruje výsledný slice z odpovědního slotu do volajícím poskytnutého slice dst. V ustáleném stavu ctx.CompressTo(dst, src, level) a ctx.DecompressInto(dst, src) provádějí 0 allocs/op.

Předem zpracované slovníky (CDict a DDict) žijí v C workeru za 64bitovými handly s kontrolou generace (handle CDict, handle DDict v zstd.idl). Jednorázové vytvoření slovníku pomocí ctx.NewCDict(rawDict, level) naparsuje tabulky slovníku uvnitř C workeru a vrátí lehký handle, který lze znovu použít v tisících volání bez opětovného odesílání bajtů slovníku.

2. Amortizace režie IPC pomocí hromadného rámování (zstd_bulk.go)

U velkých datových částí (16 KiB a více) je round trip sdílené paměti přibližně 200 ns zanedbatelný oproti času CPU strávenému uvnitř libzstd. U úloh, které komprimují tisíce malých zpráv o velikosti 100 bajtů až 1 KiB najednou, však i 200 ns na položku se sčítá.

gosuda.org/zstd poskytuje CompressBulk a DecompressBulk na BulkCtx (zstd_bulk.go), aby odstranil round trip pro každou položku:

 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 zabalí celou dávku [][]byte do jediného rámce požadavku s předponou délky ve sdílené paměti. C worker prochází každou položku v C (compress_bulk v c/zstd_hooks.c), zapisuje všechny komprimované rámce souvisle do odpovědního slotu a vrací celou dávku v jediném round tripu sdílené paměti.

3. Přežití ukončení workeru bez pádu volajícího

Protože zstd_server běží v samostatném procesu operačního systému, fatální selhání uvnitř C workeru nikdy neshodí vaši Go aplikaci:

  • Pokud je zstd_server ukončen zvenčí (například signálem SIGKILL nebo událostí OOM), transportní vrstva HQQ okamžitě detekuje rozbitého partnera a vrátí typovanou chybu *rt.TransportError z probíhajícího volání Compress nebo Decompress.
  • Funkce na úrovni balíčku (zstd.Compress, zstd.Decompress, zstd.NewWriter, zstd.NewReader) automaticky zjistí, že sdílený výchozí worker zmizel, při dalším volání spustí novou ověřenou instanci zstd_server a obnoví normální provoz.

Rozložení repozitáře

Kódová základna odděluje facade DataDog/zstd pro drop-in použití od nízkoúrovňových primitiv relací a hromadných operací hqq-bindgen:

1gosuda.org/zstd
2├── autolaunch.go / hashes.go  # Drop-in API Compress/Decompress, fond s automatickým obnovením a hashe SHA-256
3├── zstd_stream.go             # Streamovací Writer/Reader (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go                # Ctx bez alokací, obaly CDict a DDict nad hqq-bindgen
5├── zstd_bulk.go               # Rámování CompressBulk a DecompressBulk v jediném round tripu
6├── zstd.go                    # Generovaný klient hqq-bindgen pro službu zstd IDL
7├── zstd.idl                   # Schéma HQQ IDL definující Context, CDict, DDict a RPC metody
8├── internal/embedded/         # Binární soubory zstd_server s tagy platforem pro //go:embed (6 cílů OS/architektura)
9└── c/                         # Implementace serveru v C11 (zstd_server.c, zstd_hooks.c, main.c)

Začínáme

Nainstalujte gosuda.org/zstd pomocí go get a používejte jej s CGO_ENABLED=0:

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