gosuda.org/zstd: Drop-In DataDog/zstd Compression with Zero CGO and Crash Isolation
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:
- Konflikty kompilacji i wdrażania: Podobnie jak
go-sqlite3, pakietDataDog/zstdwymaga ustawieniaCGO_ENABLED=1oraz docelowego łańcucha narzędzi kompilatora C. Skompilowanie skrośne usług lub budowanie statycznych plików binarnych dla architekturylinux/arm64lubwindows/arm64wymaga zewnętrznych sysrootów C. - 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 OS | Docelowa architektura (GOARCH) | Osadzony plik binarny | Rozmiar pliku | Łańcuch narzędzi kompilacji i pochodzenie |
|---|---|---|---|---|
| Linux | amd64, arm64 | zstd_server_linux_<arch> | ~770 - 880 KiB | GCC -O3, pochodzenie SLSA Build L3 |
| macOS | arm64, amd64 | zstd_server_darwin_<arch> | ~660 - 795 KiB | AppleClang -O3, pochodzenie SLSA Build L3 |
| Windows | amd64 | zstd_server_windows_amd64.exe | ~818 KiB | MSVC -O2, pochodzenie SLSA Build L3 |
| Windows | arm64 | zstd_server_windows_arm64.exe | ~551 KiB | zig 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
- Wygenerowany klient
hqq-bindgenkopiujesrcbezpośrednio do odwzorowanego w pamięci gniazda żądania i zmienia słowo sekwencjiSeqLock. - Proces roboczy C odczytuje
srcna miejscu z gniazda żądania i przekazuje wskaźnik ładunku gniazda odpowiedzi bezpośrednio jako bufor docelowy do funkcjiZSTD_compressCCtxlubZSTD_decompressDCtx. - Klient Go kopiuje wynikowy wycinek z gniazda odpowiedzi do dostarczonego przez wywołującego wycinka
dst. W stanie ustabilizowanym metodyctx.CompressTo(dst, src, level)orazctx.DecompressInto(dst, src)wykonują operacje typu0 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_serverzostanie zewnętrznie zamknięty (na przykład przez sygnałSIGKILLlub zdarzenie OOM), warstwa transportowa HQQ natychmiast wykrywa uszkodzenie powiązanego elementu i zwraca typowany błąd*rt.TransportErrorz trwającego wywołaniaCompresslubDecompress. - 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_serverprzy 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
- Repozytorium GitHub: https://github.com/gosuda/hqq-zstd
- Architektura i testy wydajnościowe: docs/ARCHITECTURE.md
- Główne środowisko uruchomieniowe HQQ: https://gosuda.org/hqq