GoSuda

gosuda.org/zstd: Drop-In DataDog/zstd tömörítés nulla CGO-val és összeomlás-elkülönítéssel

By Lemon Mint views ...

gosuda.org/zstd egy CGO_ENABLED=0 Go tömörítőkönyvtár, amely a hivatalos C Zstandard (libzstd 1.5.7) motort egy izolált worker folyamatban futtatja, HQQ megosztott memórián keresztül. A github.com/DataDog/zstd csomaghoz drop-in helyettesítő API-t biztosít (beleértve az egyszeri Compress/Decompress függvényeket, a streaming io.Writer/io.Reader típusokat, az előre feldolgozott szótárakat és az újrafelhasználható, nulla allokációs kontextusokat), miközben a C végrehajtás teljes egészében a Go futtatókörnyezet címtartományán kívül marad.

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

Miért tartozik a dekompresszió a Go heapen kívülre

Éles backend rendszerekben a tömörítés és a kicsomagolás közvetlenül a nem megbízható bemeneti határon történik. A naplógyűjtők, üzenetközvetítők, RPC-átjárók és tárolómotorok percenként gigabájtnyi külsőleg megadott keretet bontanak ki.

Sok Go csapat a github.com/DataDog/zstd csomagot választja, mert közvetlenül a felső szintű C libzstd könyvtárhoz kapcsolódik: a C implementáció nagy egymagos átviteli sebességet, szoros szótáras tömörítési arányt kis rekordokon, valamint heapen kívüli C memóriaterületeket (ZSTD_CCtx és ZSTD_DCtx) biztosít, amelyek nem növelik a Go szemétgyűjtés bejárási idejét.

A libzstd cgo segítségével történő Go szolgáltatásba integrálása azonban két üzemeltetési problémát okoz:

  1. Build- és telepítési súrlódás: A go-sqlite3-hoz hasonlóan a DataDog/zstd is megköveteli a CGO_ENABLED=1 beállítást és a céloldali C fordítóeszköztárat. A szolgáltatások keresztfordítása vagy statikus bináris fájlok építése linux/arm64 vagy windows/arm64 célra külső C sysroot-okat igényel.
  2. Közös címtartomány nem megbízható bemeneteken: A sérült vagy ellenséges adatok kicsomagolása ugyanabban a virtuális címtartományban, mint a Go szerver, azt jelenti, hogy a natív C kód vagy az egyéni szótárkezelés bármely hibája leállíthatja a teljes Go folyamatot.

A gosuda.org/zstd megtartja a pontosan azonos upstream C libzstd 1.5.7 motort és az ismerős github.com/DataDog/zstd API-t, de a C végrehajtást egy ellenőrzött gyermekfolyamatba (zstd_server) helyezi, amely a Go programhoz cache-line méretű megosztott memórián keresztül kapcsolódik.

Drop-in csere a github.com/DataDog/zstd csomagra

A github.com/DataDog/zstd csomagról történő áttéréshez csak az importútvonalat kell "gosuda.org/zstd" értékre cserélni. Nincs szükség külső bináris telepítésre vagy beállító kódra: az első hívásnál a gosuda.org/zstd kibont egy beágyazott, SLSA-attesztált zstd_server binárist az adott platformra, ellenőrzi annak SHA-256 digesztjét a gosuda.org/hqq/secureexec segítségével, majd megnyit egy megosztott memóriás munkamenet-készletet.

 1package main
 2
 3import (
 4	"bytes"
 5	"fmt"
 6	"io"
 7	"log"
 8
 9	"gosuda.org/zstd"
10)
11
12func main() {
13	// 1. Egyszeri Compress / Decompress hívások (a github.com/DataDog/zstd API-t tükrözi)
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 és Reader szótártámogatással
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}

Támogatott platformok és beágyazott bináris fájlok

Mind a hat célplatform előre lefordított, SHA-256 digesztjével rögzített worker binárisokat tartalmaz, amelyek platformspecifikus build tageken keresztül vannak beágyazva (internal/embedded/embed_<goos>_<goarch>.go):

Céloperációs rendszerCélarchitektúra (GOARCH)Beágyazott bináris fájlBináris méretBuild-eszköztár és eredet
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

Megosztott memórián belüli adatút

Ahhoz, hogy az átviteli sebesség versenyképes maradjon a folyamatba ágyazott cgo megoldással szemben, a gosuda.org/zstd elkerüli a socket-alapú szerializációt és a heap-allokációkat a kritikus úton.

1. Nulla allokációs slot-kódolás (zstd_ctx.go és c/zstd_hooks.c)

Amikor újrafelhasználható zstd.Ctx objektumot használ (vagy amikor a csomagszintű Compress és Decompress függvények egy Ctx objektumot kölcsönöznek a belső munkamenet-készletből), az adatok közvetlenül a leképezett StandardLink slotokon keresztül áramlanak:

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
  1. A generált hqq-bindgen kliens a src adatot közvetlenül a memórián leképezett kérési slotba másolja, és megfordítja a SeqLock sorszámszót.
  2. A C worker a src adatot helyben olvassa a kérési slotból, és a válasz slot payload mutatóját közvetlenül célpufferként adja át a ZSTD_compressCCtx vagy a ZSTD_decompressDCtx függvénynek.
  3. A Go kliens az eredményül kapott szeletet a válasz slotból a hívó által megadott dst szeletbe másolja. Állandósult állapotban a ctx.CompressTo(dst, src, level) és a ctx.DecompressInto(dst, src) 0 allocs/op teljesítményt nyújt.

Az előre feldolgozott szótárak (CDict és DDict) a C workeren élnek, generáció-ellenőrzött 64 bites handle-ök mögött (handle CDict, handle DDict a zstd.idl fájlban). Egyszer létrehozva a szótárat a ctx.NewCDict(rawDict, level) hívással, a szótártáblák a C workeren kerülnek feldolgozásra, és egy könnyű handle-t kap vissza, amely a szótár bájtjainak újraküldése nélkül többezer hívás során újrafelhasználható.

2. Az IPC-költség amortizálása kötegelt keretezéssel (zstd_bulk.go)

Nagy adatmennyiségek (16 KiB és afölött) esetén a körülbelül 200 ns-os megosztott memórián belüli körforgási idő elhanyagolható a libzstd belsejében töltött CPU-időhöz képest. Azonban olyan munkaterhelések esetén, amelyek egyszerre több ezer kis, 100 bájttól 1 KiB-ig terjedő üzenetet tömörítenek, elemenként akár 200 ns-os költség is összeadódik.

A gosuda.org/zstd a BulkCtx típuson (zstd_bulk.go) keresztül biztosítja a CompressBulk és a DecompressBulk függvényeket, amelyek kiküszöbölik az elemenkénti körforgási időket:

 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)

A CompressBulk a teljes [][]byte köteget egyetlen, hosszúság-előtagos kérési keretbe csomagolja a megosztott memóriában. A C worker C nyelven iterál minden elemen (compress_bulk a c/zstd_hooks.c fájlban), a tömörített kereteket folytonosan írja a válasz slotba, és a teljes köteget egyetlen megosztott memórián belüli körforgással adja vissza.

3. A worker leállásának túlélése a hívó összeomlása nélkül

Mivel a zstd_server külön operációs rendszer-folyamatban fut, a C workeren belüli végzetes hiba soha nem állítja le a Go alkalmazást:

  • Ha a zstd_server külső beavatkozás miatt leáll (például SIGKILL jelzés vagy OOM esemény következtében), a HQQ szállítási réteg azonnal észleli a megszakadt partnert, és egy típusos *rt.TransportError hibát ad vissza a folyamatban lévő Compress vagy Decompress hívásból.
  • A csomagszintű függvények (zstd.Compress, zstd.Decompress, zstd.NewWriter, zstd.NewReader) automatikusan észlelik, hogy a megosztott alapértelmezett worker megszűnt, a következő hívásnál új, ellenőrzött zstd_server példányt indítanak, és visszaáll a normál működés.

Tárolóstruktúra

A kódbázis elválasztja a DataDog/zstd drop-in homlokzatot az alacsony szintű hqq-bindgen munkamenet- és kötegelt primitívektől:

1gosuda.org/zstd
2├── autolaunch.go / hashes.go  # Drop-in Compress/Decompress API, automatikus helyreállítási készlet és SHA-256 hash-ek
3├── zstd_stream.go             # Streaming Writer/Reader (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go                # Nulla allokációs Ctx, CDict és DDict burkolók a hqq-bindgen felett
5├── zstd_bulk.go               # Egyetlen körforgású CompressBulk és DecompressBulk keretezés
6├── zstd.go                    # A zstd IDL szolgáltatáshoz generált hqq-bindgen Go kliens
7├── zstd.idl                   # HQQ IDL séma, amely a Context, CDict, DDict és RPC metódusokat definiálja
8├── internal/embedded/         # Platformcímkézett //go:embed zstd_server binárisok (6 OS/architektúra célplatform)
9└── c/                         # C11 szerverimplementáció (zstd_server.c, zstd_hooks.c, main.c)

Első lépések

Telepítse a gosuda.org/zstd csomagot a go get paranccsal, és használja CGO_ENABLED=0 mellett:

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