gosuda.org/zstd: Zamiennik DataDog/zstd z kompresją bez CGO i izolacją awarii

gosuda.org/zstd to biblioteka kompresji dla Go działająca z CGO_ENABLED=0, która uruchamia oficjalny silnik C Zstandard (libzstd 1.5.7) w odizolowanym procesie roboczym za pośrednictwem pamięci współdzielonej HQQ. Udostępnia ona zamiennik API typu drop-in dla github.com/DataDog/zstd (obejmujący jednorazowe Compress/Decompress, strumieniowe io.Writer/io.Reader, wstępnie przetworzone słowniki oraz wielokrotnego użytku konteksty bez alokacji), przy zachowaniu pełnego oddzielenia wykonania kodu C od przestrzeni adresowej środowiska uruchomieniowego Go.
1go get gosuda.org/zstd@v0.3.10507
Dlaczego dekompresja powinna znajdować się poza heapem Go
W środowiskach produkcyjnych backendów kompresja i dekompresja znajdują się bezpośrednio na granicy niezaufanych danych wejściowych. Kolektory logów, brokery komunikatów, bramy RPC oraz silniki pamięci masowej dekompresują co minutę gigabajty zewnętrznie dostarczonych ramek.
Wiele zespołów korzystających z Go wybiera github.com/DataDog/zstd, ponieważ bezpośrednio wiąże się ono z oryginalną biblioteką C libzstd: implementacja w C zapewnia wysoką przepustowość na pojedynczym rdzeniu, wysoki stopień kompresji słownikowej dla małych rekordów oraz pozaheapowe areny C (ZSTD_CCtx i ZSTD_DCtx), które nie zwiększają czasu skanowania przez garbage collector Go.
Połączenie libzstd z usługą Go za pośrednictwem cgo wiąże się jednak z dwoma problemami operacyjnymi:
- Utrudnienia w budowaniu i wdrażaniu: podobnie jak
go-sqlite3,DataDog/zstdwymagaCGO_ENABLED=1oraz docelowego zestawu narzędzi kompilatora C. Kompilacja krzyżowa usług lub tworzenie statycznych plików binarnych dlalinux/arm64lubwindows/arm64wymaga zewnętrznych sysroot C. - Wspólna przestrzeń adresowa dla niezaufanych danych wejściowych: dekompresja uszkodzonych lub przygotowanych w sposób wrogi ładunków w tej samej przestrzeni adresowej co serwer Go oznacza, że każdy błąd w natywnym kodzie C lub w obsłudze niestandardowych słowników może zakończyć cały proces Go.
gosuda.org/zstd zachowuje dokładny, oryginalny silnik libzstd 1.5.7 w C oraz znane API github.com/DataDog/zstd, lecz przenosi wykonanie kodu C do zweryfikowanego procesu potomnego (zstd_server) połączonego z programem Go za pośrednictwem pamięci współdzielonej wyrównanej do linii cache.
Zamiennik typu drop-in dla github.com/DataDog/zstd
Migracja z github.com/DataDog/zstd wymaga zmiany wyłącznie ścieżki importu na "gosuda.org/zstd". Nie jest potrzebna instalacja zewnętrznych plików binarnych ani kod konfiguracyjny: przy pierwszym wywołaniu gosuda.org/zstd udostępnia osadzony plik binarny zstd_server dla danej platformy, poświadczony w standardzie SLSA, 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. Jednorazowe Compress / Decompress (odpowiada 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. Strumieniowe Writer i Reader 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 przygotowane wstępnie, przypięte skrótem SHA-256 pliki binarne procesu roboczego, osadzone za pomocą specyficznych dla platformy znaczników budowania (internal/embedded/embed_<goos>_<goarch>.go):
| System docelowy | Architektura docelowa (GOARCH) | Osadzony plik binarny | Rozmiar pliku | Łańcuch narzędzi budowania i pochodzenie |
|---|---|---|---|---|
| Linux | amd64, arm64 | zstd_server_linux_<arch> | ~770 - 880 KiB | GCC -O3, SLSA Build L3 Provenance |
| macOS | arm64, amd64 | zstd_server_darwin_<arch> | ~660 - 795 KiB | AppleClang -O3, SLSA Build L3 Provenance |
| Windows | amd64 | zstd_server_windows_amd64.exe | ~818 KiB | MSVC -O2, SLSA Build L3 Provenance |
| Windows | arm64 | zstd_server_windows_arm64.exe | ~551 KiB | zig cc -target aarch64-windows-gnu -O3, SLSA L3 |
Wnętrze ścieżki danych we pamięci współdzielonej
Aby utrzymać przepustowość porównywalną z cgo działającym w tym samym procesie, gosuda.org/zstd unika serializacji przez gniazda oraz alokacji sterty na gorącej ścieżce.
1. Kodowanie slotów bez alokacji (zstd_ctx.go i c/zstd_hooks.c)
Gdy używany jest wielokrotnego użytku zstd.Ctx (lub gdy funkcje pakietowe Compress i Decompress pożyczają Ctx z wewnętrznej puli sesji), dane przepływają bezpośrednio przez zmapowany slot StandardLink:
1Wywołujący Go (zstd.Ctx) Slot pamięci współdzielonej Worker C11 (zstd_hooks.c)
2------------------------ --------------------------- -------------------------
3src []byte -------------------------> Ładunek slotu żądania -------------> ZSTD_compressCCtx(
4 cctx, resp_slot, src, level)
5dst []byte <------------------------- Ładunek slotu odpowiedzi <------------ Zapisuje skompresowaną ramkę bezpośrednio
6 do slotu odpowiedzi we pamięci współdzielonej
- Wygenerowany klient
hqq-bindgenkopiujesrcbezpośrednio do zmapowanego w pamięci slotu żądania i przełącza słowo sekwencjiSeqLock. - Worker C odczytuje
srcw miejscu z slotu żądania i przekazuje wskaźnik ładunku slotu odpowiedzi bezpośrednio jako bufor docelowy doZSTD_compressCCtxlubZSTD_decompressDCtx. - Klient Go kopiuje wynikowy wycinek z slotu odpowiedzi do dostarczonego przez wywołującego wycinka
dst. W stanie ustalonymctx.CompressTo(dst, src, level)orazctx.DecompressInto(dst, src)wykonują0 allocs/op.
Wstępnie przetworzone słowniki (CDict i DDict) znajdują się po stronie workera C za uchwytami 64-bitowymi kontrolowanymi pod kątem generacji (handle CDict, handle DDict w zstd.idl). Jednorazowe utworzenie słownika za pomocą ctx.NewCDict(rawDict, level) przetwarza tabele słownika wewnątrz workera C i zwraca lekki uchwyt, który może być wielokrotnie używany w tysiącach wywołań bez ponownego przesyłania bajtów słownika.
2. Amortyzacja narzutu IPC dzięki framingowi zbiorczemu (zstd_bulk.go)
Dla dużych ładunków (16 KiB i większych) około 200 ns opóźnienia pełnej rundy przez pamięć współdzieloną jest pomijalne w porównaniu z czasem procesora spędzonym wewnątrz libzstd. Jednak dla obciążeń kompresujących tysiące małych komunikatów (od 100 bajtów do 1 KiB) jednocześnie, nawet 200 ns na element sumuje się do znaczącego kosztu.
gosuda.org/zstd udostępnia CompressBulk i DecompressBulk w BulkCtx (zstd_bulk.go), aby wyeliminować rundy na każdy element:
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 z prefiksem długości w pamięci współdzielonej. Worker C przechodzi w pętli przez każdy element w kodzie C (compress_bulk w c/zstd_hooks.c), zapisuje wszystkie skompresowane ramki w sposób ciągły do slotu odpowiedzi i zwraca całą partię w ramach jednej rundy przez pamięć współdzieloną.
3. Przetrwanie zakończenia workera bez awarii wywołującego
Ponieważ zstd_server działa w osobnym procesie systemowym, krytyczna awaria wewnątrz workera C nigdy nie zakańcza aplikacji Go:
- Jeśli
zstd_serverzostanie zabity z zewnątrz (na przykład przezSIGKILLlub zdarzenie OOM), warstwa transportu HQQ natychmiast wykrywa uszkodzonego partnera i zwraca typowany błąd*rt.TransportErrorz trwającego wywołaniaCompresslubDecompress. - Funkcje pakietowe (
zstd.Compress,zstd.Decompress,zstd.NewWriter,zstd.NewReader) automatycznie wykrywają, że współdzielony domyślny worker zniknął, uruchamiają nową, zweryfikowaną instancjęzstd_serverprzy następnym wywołaniu i wznawiają normalne działanie.
Układ repozytorium
Kod źródłowy rozdziela fasadę typu drop-in DataDog/zstd od niskopoziomowych prymitywów sesji i pakietowania zbiorczego hqq-bindgen:
1gosuda.org/zstd
2├── autolaunch.go / hashes.go # API Compress/Decompress typu drop-in, pula z automatycznym odzyskiwaniem oraz skróty SHA-256
3├── zstd_stream.go # Strumieniowe Writer/Reader (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go # Ctx bez alokacji oraz opakowania CDict i DDict nad hqq-bindgen
5├── zstd_bulk.go # Framing CompressBulk i DecompressBulk w jednej rundzie
6├── zstd.go # Wygenerowany klient Go hqq-bindgen dla usługi zstd IDL
7├── zstd.idl # Schemat HQQ IDL definiujący Context, CDict, DDict oraz metody RPC
8├── internal/embedded/ # Osadzone za pomocą //go:embed pliki binarne zstd_server ze znacznikami platform (6 celów OS/Arch)
9└── c/ # Implementacja serwera C11 (zstd_server.c, zstd_hooks.c, main.c)
Pierwsze kroki
Zainstaluj gosuda.org/zstd za pomocą go get i używaj go przy CGO_ENABLED=0:
1go get gosuda.org/zstd@v0.3.10507
- Repozytorium GitHub: https://github.com/gosuda/hqq-zstd
- Architektura i benchmarki: docs/ARCHITECTURE.md
- Główne środowisko uruchomieniowe HQQ: https://gosuda.org/hqq