GoSuda

gosuda.org/zstd: Drop-In DataDog/zstd Compression with Zero CGO and Crash Isolation

By Lemon Mint
views ...

A gosuda.org/zstd egy CGO_ENABLED=0 Go tömörítési könyvtár, amely a hivatalos C Zstandard (libzstd 1.5.7) motort futtatja egy izolált munkafolyamaton belül, a HQQ megosztott memóriát használva. Egy közvetlenül behelyettesíthető API-t biztosít a github.com/DataDog/zstd könyvtárhoz (beleértve az egyszeri Compress/Decompress műveleteket, afolyamatos io.Writer/io.Reader interfészeket, az előre feldolgozott szótárakat, valamint az újrahasznosítható, memóriafoglalás-mentes kontextusokat), miközben a C kód végrehajtását teljes egészében a Go futtatókörnyezet címtartományán kívül tartja.

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

Miért tartozik a kitömörítés a Go heapen kívülre?

A termelési háttérrendszerekben a tömörítés és a kitömörítés közvetlenül a nem megbízható bemeneti határon helyezkedik el. Naplógyűjtők, üzenetközvetítők, RPC-átjárók és tárolómotorok külsőleg biztosított keretek gigabájtjait tömörítik ki percenként.

Számos Go csapat választja a github.com/DataDog/zstd könyvtárat, mivel az közvetlenül kapcsolódik az upstream C libzstd könyvtárhoz: a C implementáció magas egyszálas teljesítményt, szoros szótártömörítési arányt nyújt kis rekordok esetén, valamint olyan heapen kívüli C arénákat (ZSTD_CCtx és ZSTD_DCtx) használ, amelyek nem növelik a Go szemétgyűjtési (garbage-collection) vizsgálati időket.

A libzstd C-szolgáltatásba való becsatolása a cgo révén azonban két üzemeltetési nehézséget von maga után:

  1. Fordítási és telepítési súrlódás: A go-sqlite3 könyvtárhoz hasonlóan a DataDog/zstd is megköveteli a CGO_ENABLED=1 beállítást és egy C fordító láncot. A szolgáltatások keresztfordítása vagy a statikus binárisok építése linux/arm64 vagy windows/arm64 rendszerekhez külső C sysrootokat igényel.
  2. Megosztott címtartomány a nem megbízható bemeneteken: A sérült vagy ellenséges rakományok kitömörítése 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ármilyen hibája leállíthatja az egész Go folyamatot.

A gosuda.org/zstd megőrzi a pontos 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 át, amely a gyorsítótár-sor megosztott memóriáján keresztül kapcsolódik a Go programhoz.

Behelyettesíthető helyettesítő a github.com/DataDog/zstd könyvtárhoz

A github.com/DataDog/zstd könyvtárról való áttérés kizárólag az importálási útvonal "gosuda.org/zstd" értékre történő módosítását igényli. Nincs szükség külső bináris telepítésre vagy beállítási kódra: az első hívás alkalmával a gosuda.org/zstd létrehoz egy beágyazott, SLSA-hitelesített zstd_server binárist az adott platformhoz, ellenőrzi annak SHA-256 kivonatát a gosuda.org/hqq/secureexec segítségével, és 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 (tükrözi a github.com/DataDog/zstd működését)
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. Folyamatos (Streaming) író és olvasó 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árisok

Mind a hat célplatform előre elkészített, SHA-256 kivonattal rögzített munkafolyamat-binárisokat tartalmaz, amelyek platformspecifikus fordítási címkékkel (internal/embedded/embed_<goos>_<goarch>.go) vannak beágyazva:

Cél OSCél architektúra (GOARCH)Beágyazott binárisBináris méretFordítási lánc és eredet (Provenance)
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

A megosztott memóriás adatútvonal belsejében

Annak érdekében, hogy az áteresztőképesség versenyképes maradjon a folyamaton belüli cgo megoldással, a gosuda.org/zstd elkerüli a socket sorosítást és a heap memóriafoglalásokat a kritikus útvonalon.

1. Memóriafoglalás-mentes slot kódolás (zstd_ctx.go és c/zstd_hooks.c)

Amikor egy újrahasznosítható zstd.Ctx kontextust használ (vagy amikor a csomagszintű Compress és Decompress függvények kölcsönöznek egy Ctx kontextust a belső munkamenet-készletből), az adatok közvetlenül a leképezett StandardLink sloton 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 közvetlenül a memóriába leképezett kérésslotba másolja az src adatokat, és átbillenti a SeqLock szekvenciaszót.
  2. A C munkafolyamat a kérésslotból helyben olvassa be az src adatokat, és a válaszslot hasznos teher (payload) mutatóját közvetlenül célpufferként adja át a ZSTD_compressCCtx vagy ZSTD_decompressDCtx függvényeknek.
  3. A Go kliens átmásolja a kapott szeletet a válaszslotból a hívó által megadott dst szeletbe. Stabil állapotban a ctx.CompressTo(dst, src, level) és a ctx.DecompressInto(dst, src) függvények 0 allocs/op mutatóval hajthatók végre.

Az előre feldolgozott szótárak (CDict és DDict) a C munkafolyamaton belül, generációellenőrzött 64 bites leírók (handle CDict, handle DDict a zstd.idl fájlban) mögött élnek. A szótár egyszeri létrehozása a ctx.NewCDict(rawDict, level) révén elemzi a szótártáblákat a C munkafolyamaton belül, és egy könnyűsúlyú leírót ad vissza, amely ezrekre rúgó hívások során újra felhasználható anélkül, hogy újra el kellene küldeni a szótárbájtokat.

2. Az IPC-fejlesztési költségek amortizálása tömeges keretezéssel (zstd_bulk.go)

Nagyobb adatsorok (16 KiB és felette) esetén a ~200 ns-os megosztott memóriás oda-vissza út elhanyagolható a libzstd belsejében eltöltött CPU-időhöz képest. Azoknál a munkaterheléseknél azonban, amelyek egyszerre több ezer kis méretű, 100 bájt és 1 KiB közötti üzenetet tömörítenek, a tételenkénti 200 ns megfizetése is számottevővé válik.

A gosuda.org/zstd a CompressBulk és DecompressBulk függvényeket biztosítja a BulkCtx (zstd_bulk.go) keretében a tételenkénti oda-vissza utak megszüntetésére:

 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, hosszprefixszel ellátott kéréskeretté csomagolja a megosztott memóriában. A C munkafolyamat végigiterál minden egyes tételen C-ben (compress_bulk a c/zstd_hooks.c fájlban), az összes tömörített keretet folytonosan írja be a válaszslotba, és a teljes köteget egyetlen megosztott memóriás oda-vissza út alatt adja vissza.

3. Túlélés a munkafolyamat leállásakor a hívó összeomlása nélkül

Mivel a zstd_server egy külön operációsrendszer-folyamatban fut, a C munkafolyamaton belüli végzetes hiba soha nem dönti romba a Go alkalmazást:

  • Ha a zstd_server folyamatot külsőleg kényszerítik ki (például SIGKILL vagy OOM esemény által), a HQQ szállítási réteg azonnal érzékeli a megszakadt partnert, és egy típusos *rt.TransportError hibát ad vissza az aktuális 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 munkafolyamat megszűnt, a következő híváskor létrehoznak egy friss, ellenőrzött zstd_server példányt, és folytatják a normál működést.

Adattár elrendezése

A kódbázis elválasztja a behelyettesíthető DataDog/zstd homlokzatot az alacsony szintű hqq-bindgen munkamenettől és tömeges primitívektől:

1gosuda.org/zstd
2├── autolaunch.go / hashes.go  # Behelyettesíthető Compress/Decompress API, automatikus helyreállítási készlet és SHA-256 kivonatok
3├── zstd_stream.go             # Folyamatos (Streaming) Író/Olvasó (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go                # Memóriafoglalás-mentes Ctx, CDict és DDict burkolók a hqq-bindgen felett
5├── zstd_bulk.go               # Egyszeri oda-vissza utas CompressBulk és DecompressBulk keretezés
6├── zstd.go                    # Generált hqq-bindgen Go kliens a zstd IDL szolgáltatáshoz
7├── zstd.idl                   # HQQ IDL séma, amely definiálja a Context, CDict, DDict és RPC metódusokat
8├── internal/embedded/         # Platformcímkékkel ellátott //go:embed zstd_server binárisok (6 OS/Arch cél)
9└── c/                         # C11 szerver implementáció (zstd_server.c, zstd_hooks.c, main.c)

Kezdő lépések

Telepítse a gosuda.org/zstd könyvtárat a go get paranccsal, és használja a CGO_ENABLED=0 beállítással:

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