GoSuda

gosuda.org/zstd : Compression DataDog/zstd intégrée sans modification (Drop-In) avec zéro CGO et isolation des plantages

By Lemon Mint
views ...

gosuda.org/zstd est une bibliothèque de compression Go CGO_ENABLED=0 qui exécute le moteur C officiel de Zstandard (libzstd 1.5.7) au sein d'un processus ouvrier isolé via la mémoire partagée HQQ. Elle fournit une API de remplacement direct pour github.com/DataDog/zstd (comprenant Compress/Decompress en un seul appel, io.Writer/io.Reader en flux, dictionnaires pré-analysés et contextes réutilisables sans allocation) tout en maintenant l'exécution du code C totalement en dehors de l'espace d'adressage du runtime Go.

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

Pourquoi la décompression doit résider en dehors de votre tas Go

Dans les environnements de production, la compression et la décompression se situent directement à la frontière des entrées non fiables. Les collecteurs de journaux, les courtiers de messages, les passerelles RPC et les moteurs de stockage décompressent des gigaoctets de trames fournies de l'extérieur à chaque minute.

De nombreuses équipes Go choisissent github.com/DataDog/zstd parce qu'elle s'lie directement à la bibliothèque amont en C libzstd : l'implémentation en C offre un débit unicœur élevé, des taux de compression par dictionnaire serrés sur les petits enregistrements, ainsi que des zones mémoire C hors tas (ZSTD_CCtx et ZSTD_DCtx) qui n'augmentent pas les temps de balayage du ramasse-miettes de Go.

L'liaison de libzstd dans un service Go via cgo, cependant, entraîne deux difficultés opérationnelles majeures :

  1. Friction de compilation et de déploiement : Tout comme go-sqlite3, DataDog/zstd nécessite CGO_ENABLED=1 et une chaîne d'outils de compilation C cible. La compilation croisée de services ou la construction de binaires statiques pour linux/arm64 ou windows/arm64 requiert des environnements système C externes.
  2. Espace d'adressage partagé sur des charges utiles non fiables : Décompresser des charges utiles corrompues ou malveillantes dans le même espace d'adressage virtuel que votre serveur Go signifie que tout bogue dans le code C natif ou dans la gestion personnalisée des dictionnaires peut interrompre l'ensemble du processus Go.

gosuda.org/zstd conserve le moteur C amont exact libzstd 1.5.7 ainsi que l'API familière de github.com/DataDog/zstd, mais déplace l'exécution du code C vers un processus enfant vérifié (zstd_server) connecté à votre programme Go via une mémoire partagée alignée sur les lignes de cache.

Remplacement direct pour github.com/DataDog/zstd

La migration depuis github.com/DataDog/zstd ne nécessite que le changement du chemin d'importation vers "gosuda.org/zstd". Aucune installation de binaire externe ou code de configuration n'est nécessaire : lors du premier appel, gosuda.org/zstd matérialise un binaire embarqué zstd_server attesté SLSA pour votre plateforme, vérifie son condensé SHA-256 via gosuda.org/hqq/secureexec, et ouvre un pool de sessions en mémoire partagée.

 1package main
 2
 3import (
 4	"bytes"
 5	"fmt"
 6	"io"
 7	"log"
 8
 9	"gosuda.org/zstd"
10)
11
12func main() {
13	// 1. One-shot Compress / Decompress (mirrors 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. Streaming Writer and Reader with Dictionary Support
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}

Plateformes prises en charge et binaires embarqués

Les six plateformes cibles intègrent des binaires ouvriers précompilés et épinglés par SHA-256 via des balises de compilation spécifiques à la plateforme (internal/embedded/embed_<goos>_<goarch>.go) :

Target OSTarget Arch (GOARCH)Embedded BinaryBinary SizeBuild Toolchain & 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

Au cœur du chemin de données en mémoire partagée

Afin de maintenir un débit compétitif par rapport au cgo intraprocessus, gosuda.org/zstd évite la sérialisation par socket et les allocations sur le tas dans le chemin critique.

1. Encodage d'emplacements sans allocation (zstd_ctx.go et c/zstd_hooks.c)

Lorsque vous utilisez un objet zstd.Ctx réutilisable (ou lorsque les fonctions de niveau paquet Compress et Decompress empruntent un Ctx du pool de sessions interne), les données transitent directement par l'emplacement mappé 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. Le client généré par hqq-bindgen copie src directement dans l'emplacement de requête mappé en mémoire et bascule le mot de séquence SeqLock.
  2. L'ouvrier C lit src directement depuis l'emplacement de requête et transmet le pointeur de charge utile de l'emplacement de réponse directement en tant que tampon de destination à ZSTD_compressCCtx ou ZSTD_decompressDCtx.
  3. Le client Go copie la tranche résultante de l'emplacement de réponse vers la tranche dst fournie par l'appelant. En régime permanent, ctx.CompressTo(dst, src, level) et ctx.DecompressInto(dst, src) n'effectuent 0 allocs/op.

Les dictionnaires pré-analysés (CDict et DDict) résident sur l'ouvrier C derrière des descripteurs 64 bits vérifiés par génération (handle CDict, handle DDict dans zstd.idl). La création d'un dictionnaire une seule fois via ctx.NewCDict(rawDict, level) analyse les tables du dictionnaire à l'intérieur de l'ouvrier C et renvoie un descripteur léger qui peut être réutilisé sur des milliers d'appels sans retransmettre les octets du dictionnaire.

2. Amortissement du coût de communication inter-processus (IPC) avec le tramage par lots (zstd_bulk.go)

Pour les charges utiles volumineuses (16 Ko et plus), l'aller-retour en mémoire partagée d'environ 200 ns est négligeable par rapport au temps processeur passé au sein de libzstd. Pour les charges de travail qui compressent simultanément des milliers de petits messages de 100 octets à 1 Ko, cependant, payer ne serait-ce que 200 ns par élément s'accumule rapidement.

gosuda.org/zstd fournit CompressBulk et DecompressBulk sur BulkCtx (zstd_bulk.go) pour éliminer les allers-retours par élément :

 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 regroupe l'ensemble du lot [][]byte en une seule trame de requête préfixée par la longueur dans la mémoire partagée. L'ouvrier C boucle sur chaque élément en C (compress_bulk dans c/zstd_hooks.c), écrit toutes les trames compressées de manière contiguë dans l'emplacement de réponse, et renvoie le lot entier en un seul aller-retour en mémoire partagée.

3. Survie à la fermeture de l'ouvrier sans faire planter l'appelant

Puisque zstd_server s'exécute dans un processus système séparé, une défaillance fatale au sein de l'ouvrier C ne provoque jamais la chute de votre application Go :

  • Si zstd_server est arrêté de l'extérieur (par exemple, par un signal SIGKILL ou un événement OOM), la couche de transport HQQ détecte immédiatement le pair rompu et renvoie une erreur typée *rt.TransportError de l'appel Compress ou Decompress en cours.
  • Les fonctions de niveau paquet (zstd.Compress, zstd.Decompress, zstd.NewWriter, zstd.NewReader) détectent automatiquement que l'ouvrier par défaut partagé a disparu, lancent une nouvelle instance vérifiée de zstd_server lors du prochain appel, et reprennent un fonctionnement normal.

Organisation du dépôt

La base de code sépare la façade de remplacement direct DataDog/zstd des primitives de session et de traitement par lots de bas niveau hqq-bindgen :

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)

Prise en main

Installez gosuda.org/zstd avec go get et utilisez-le avec CGO_ENABLED=0 :

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