GoSuda

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

By Lemon Mint
views ...

gosuda.org/zstd jest biblioteką kompresji Go z ustawieniem CGO_ENABLED=0, która uruchamia oficjalny silnik C Zstandard (libzstd 1.5.7) wewnątrz odizolowanego procesu roboczego za pośrednictwem pamięci współdzielonej HQQ. Zapewnia ona w pełni kompatybilne API zastępcze (drop-in replacement) dla github.com/DataDog/zstd (w tym operacje jednorazowe Compress/Decompress, strumieniowe io.Writer/io.Reader, wstępnie przetworzone słowniki oraz wielokrotnego użytku konteksty bez alokacji), utrzymując jednocześnie wykonanie kodu w języku C całkowicie poza przestrzenią adresową środowiska uruchomieniowego Go.

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

Dlaczego dekompresja powinna znajdować się poza stosem Go

W produkcyjnych systemach backendowych kompresja i dekompresja znajdują się bezpośrednio na granicy danych niezweryfikowanych. Kolektory logów, brokery wiadomości, bramy RPC oraz silniki pamięci masowej dekompresują co minutę gigabajty zewnętrznie dostarczanych ramek.

Wiele zespołów korzystających z Go wybiera github.com/DataDog/zstd, ponieważ wiąże się ona bezpośrednio z nadrzędnym kodem C libzstd: implementacja w języku C zapewnia wysoką przepustowość jednordzeniową, wysokie współczynniki kompresji słownikowej dla małych rekordów oraz areny C poza stosem (ZSTD_CCtx i ZSTD_DCtx), które nie zwiększają czasu skanowania przez odśmiacz pamięci Go (garbage collector).

Dołączenie libzstd do usługi Go za pomocą cgo niesie jednak ze sobą dwa operacyjne problemy:

  1. Konflikty kompilacji i wdrażania: Podobnie jak go-sqlite3, pakiet DataDog/zstd wymaga ustawienia CGO_ENABLED=1 oraz docelowego łańcucha narzędzi kompilatora C. Skompilowanie skrośne usług lub budowanie statycznych plików binarnych dla architektury linux/arm64 lub windows/arm64 wymaga zewnętrznych sysrootów C.
  2. Współdzielona przestrzeń adresowa dla niezweryfikowanych danych wejściowych: Dekompresja uszkodzonych lub wrogich ładunków w tej samej wirtualnej przestrzeni adresowej, co serwer Go, oznacza, że jakikolwiek błąd w natywnym kodzie C lub niestandardowej obsłudze słownika może spowodować zakończenie działania całego procesu Go.

gosuda.org/zstd zachowuje dokładny nadrzędny silnik C libzstd 1.5.7 oraz znane API github.com/DataDog/zstd, ale przenosi wykonywanie kodu C do zweryfikowanego procesu potomnego (zstd_server), połączonego z programem Go za pośrednictwem pamięci współdzielonej opartej na liniach pamięci podręcznej.

Zamiennik typu drop-in dla github.com/DataDog/zstd

Migracja z github.com/DataDog/zstd wymaga jedynie zmiany ścieżki importu na "gosuda.org/zstd". Żadna zewnętrzna instalacja plików binarnych ani kod konfiguracyjny nie są potrzebne: przy pierwszym wywołaniu gosuda.org/zstd materializuje osadzony binarny plik zstd_server z poświadczeniem SLSA dla danej platformy, weryfikuje jego skrót SHA-256 za pomocą gosuda.org/hqq/secureexec i otwiera pulę sesji pamięci współdzielonej.

 1package main
 2
 3import (
 4	"bytes"
 5	"fmt"
 6	"io"
 7	"log"
 8
 9	"gosuda.org/zstd"
10)
11
12func main() {
13	// 1. Jednorazowa kompresja / dekompresja (odzwierciedla 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. Strumieniowy zapis i odczyt z obsługą słowników
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}

Obsługiwane platformy i osadzone pliki binarne

Wszystkie sześć docelowych platform zawiera wstępnie skompilowane pliki binarne procesów roboczych ze sprawdzonymi sumami kontrolnymi SHA-256, osadzone za pomocą znaczników kompilacji specyficznych dla platformy (internal/embedded/embed_<goos>_<goarch>.go):

Docelowy OSDocelowa architektura (GOARCH)Osadzony plik binarnyRozmiar plikuŁańcuch narzędzi kompilacji i pochodzenie
Linuxamd64, arm64zstd_server_linux_<arch>~770 - 880 KiBGCC -O3, pochodzenie SLSA Build L3
macOSarm64, amd64zstd_server_darwin_<arch>~660 - 795 KiBAppleClang -O3, pochodzenie SLSA Build L3
Windowsamd64zstd_server_windows_amd64.exe~818 KiBMSVC -O2, pochodzenie SLSA Build L3
Windowsarm64zstd_server_windows_arm64.exe~551 KiBzig cc -target aarch64-windows-gnu -O3, SLSA L3

Analiza ścieżki danych w pamięci współdzielonej

Aby utrzymać przepustowość konkurencyjną wobec mechanizmu cgo działającego wewnątrz procesu, gosuda.org/zstd unika serializacji przez gniazda oraz alokacji na stosie na ścieżce krytycznej.

1. Kodowanie gniazd bez alokacji (zstd_ctx.go oraz c/zstd_hooks.c)

Gdy używasz wielokrotnego użytku zstd.Ctx (lub gdy funkcje Compress i Decompress poziomu pakietu pobierają Ctx z wewnętrznej puli sesji), dane przepływają bezpośrednio przez odwzorowane gniazdo StandardLink:

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. Wygenerowany klient hqq-bindgen kopiuje src bezpośrednio do odwzorowanego w pamięci gniazda żądania i zmienia słowo sekwencji SeqLock.
  2. Proces roboczy C odczytuje src na miejscu z gniazda żądania i przekazuje wskaźnik ładunku gniazda odpowiedzi bezpośrednio jako bufor docelowy do funkcji ZSTD_compressCCtx lub ZSTD_decompressDCtx.
  3. Klient Go kopiuje wynikowy wycinek z gniazda odpowiedzi do dostarczonego przez wywołującego wycinka dst. W stanie ustabilizowanym metody ctx.CompressTo(dst, src, level) oraz ctx.DecompressInto(dst, src) wykonują operacje typu 0 allocs/op.

Wstępnie przetworzone słowniki (CDict and DDict) znajdują się w procesie roboczym C za 64-bitowymi uchwytami weryfikowanymi pod kątem generacji (handle CDict, handle DDict w zstd.idl). Jednorazowe utworzenie słownika za pomocą ctx.NewCDict(rawDict, level) analizuje tablice słownika wewnątrz procesu roboczego C i zwraca lekki uchwyt, który może być wielokrotnie używany w tysiącach wywołań bez ponownego wysyłania bajtów słownika.

2. Zamortyzowanie narzutu IPC za pomocą ramek zbiorczych (zstd_bulk.go)

W przypadku dużych ładunków (16 KiB i więcej) czas rundy pamięci współdzielonej wynoszący około 200 ns jest znikomy w porównaniu z czasem procesora zużywanym wewnątrz libzstd. Jednak w przypadku obciążeń, które kompresują tysiące małych wiadomości o wielkości od 100 bajtów do 1 KiB naraz, koszt nawet 200 ns na element sumuje się.

gosuda.org/zstd udostępnia funkcje CompressBulk oraz DecompressBulk w obiekcie BulkCtx (zstd_bulk.go) w celu wyeliminowania rund dla pojedynczych elementów:

 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 pakuje całą partię [][]byte w pojedynczą ramkę żądania poprzedzoną długością w pamięci współdzielonej. Proces roboczy C wykonuje pętlę po każdym elemencie w języku C (compress_bulk w c/zstd_hooks.c), zapisuje wszystkie skompresowane ramki w sposób ciągły do gniazda odpowiedzi i zwraca całą partię w ramach jednej rundy pamięci współdzielonej.

3. Przetrwanie zakończenia procesu roboczego bez awarii wywołującego

Ponieważ zstd_server działa w osobnym procesie systemu operacyjnego, błąd krytyczny wewnątrz procesu roboczego C nigdy nie powoduje awarii aplikacji Go:

  • Jeśli zstd_server zostanie zewnętrznie zamknięty (na przykład przez sygnał SIGKILL lub zdarzenie OOM), warstwa transportowa HQQ natychmiast wykrywa uszkodzenie powiązanego elementu i zwraca typowany błąd *rt.TransportError z trwającego wywołania Compress lub Decompress.
  • Funkcje poziomu pakietu (zstd.Compress, zstd.Decompress, zstd.NewWriter, zstd.NewReader) automatycznie wykrywano, że współdzielony domyślny proces roboczy przestał istnieć, uruchamiają nową, zweryfikowaną instancję zstd_server przy następnym wywołaniu i wznawiają normalne działanie.

Układ repozytorium

Baza kodu oddziela fasadę zastępczą DataDog/zstd od niskopoziomowej sesji hqq-bindgen oraz prymitywów zbiorczych:

1gosuda.org/zstd
2├── autolaunch.go / hashes.go  # Drop-in Compress/Decompress API, auto-recovery pool, and SHA-256 hashes
3├── zstd_stream.go             # Streaming Writer/Reader (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go                # Zero-allocation Ctx, CDict, and DDict wrappers over hqq-bindgen
5├── zstd_bulk.go               # Single-round-trip CompressBulk and DecompressBulk framing
6├── zstd.go                    # Generated hqq-bindgen Go client for the zstd IDL service
7├── zstd.idl                   # HQQ IDL schema defining Context, CDict, DDict, and RPC methods
8├── internal/embedded/         # Platform-tagged //go:embed zstd_server binaries (6 OS/Arch targets)
9└── c/                         # C11 server implementation (zstd_server.c, zstd_hooks.c, main.c)

Rozpoczęcie pracy

Zainstaluj gosuda.org/zstd za pomocą polecenia go get i używaj go z ustawieniem CGO_ENABLED=0:

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