GoSuda

gosuda.org/zstd : compression compatible avec DataDog/zstd sans CGO et avec isolation des pannes

By Lemon Mint views ...

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

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

Pourquoi la décompression doit se situer en dehors du tas Go

Dans les backends 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 chaque minute des gigaoctets de trames fournies de l'extérieur.

De nombreuses équipes Go choisissent github.com/DataDog/zstd parce qu'il se lie directement à la bibliothèque C libzstd amont : l'implémentation C offre un haut débit sur un seul cœur, des taux de compression par dictionnaire élevés sur de petits enregistrements, et des arènes C hors tas (ZSTD_CCtx et ZSTD_DCtx) qui n'augmentent pas la durée des analyses du ramasse-miettes Go.

Toutefois, lier libzstd à un service Go via cgo entraîne deux difficultés opérationnelles :

  1. Frictions de compilation et de déploiement : à l'instar de go-sqlite3, DataDog/zstd exige CGO_ENABLED=1 ainsi qu'une chaîne d'outils de compilation C cible. La compilation croisée des services ou la production de binaires statiques pour linux/arm64 ou windows/arm64 nécessitent des sysroots C externes.
  2. Espace d'adressage partagé pour les entrées non fiables : la décompression de 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 de dictionnaires personnalisés peut interrompre l'intégralité du processus Go.

gosuda.org/zstd conserve l'implémentation C amont exacte libzstd 1.5.7 et l'API familière de github.com/DataDog/zstd, mais déplace l'exécution du code C dans un processus enfant vérifié (zstd_server) relié à votre programme Go par une mémoire partagée alignée sur les lignes de cache.

Remplacement direct de github.com/DataDog/zstd

La migration depuis github.com/DataDog/zstd nécessite uniquement de modifier le chemin d'importation vers "gosuda.org/zstd". Aucune installation de binaire externe ni code de configuration n'est requis : au premier appel, gosuda.org/zstd matérialise un binaire zstd_server embarqué et attesté SLSA pour votre plateforme, vérifie son empreinte SHA-256 via gosuda.org/hqq/secureexec, puis 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. Compress / Decompress en un seul appel (reproduit 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. Writer et Reader en flux avec prise en charge des dictionnaires
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 sont livrées avec des binaires de travail précompilés et épinglés par SHA-256, intégrés via des balises de compilation propres à chaque plateforme (internal/embedded/embed_<goos>_<goarch>.go) :

Système cibleArchitecture cible (GOARCH)Binaire embarquéTaille du binaireChaîne de compilation et provenance
Linuxamd64, arm64zstd_server_linux_<arch>~770 - 880 KiBGCC -O3, provenance SLSA Build L3
macOSarm64, amd64zstd_server_darwin_<arch>~660 - 795 KiBAppleClang -O3, provenance SLSA Build L3
Windowsamd64zstd_server_windows_amd64.exe~818 KiBMSVC -O2, provenance SLSA Build L3
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

Pour maintenir un débit compétitif face au cgo en processus, gosuda.org/zstd évite la sérialisation par sockets et les allocations de tas sur le chemin critique.

1. Encodage de slots sans allocation (zstd_ctx.go et c/zstd_hooks.c)

Lorsque vous utilisez un zstd.Ctx réutilisable (ou lorsque les fonctions Compress et Decompress au niveau du paquet empruntent un Ctx au pool de sessions interne), les données circulent directement par le slot StandardLink mappé en mémoire :

1Appelant Go (zstd.Ctx)                  Slot de mémoire partagée              Worker C11 (zstd_hooks.c)
2----------------------                  ------------------------              -------------------------
3src []byte  ------------------------->  Charge utile du slot requête  ------>  ZSTD_compressCCtx(
4                                                                                cctx, resp_slot, src, level)
5dst []byte  <-------------------------  Charge utile du slot réponse  <-----  Écrit la trame compressée directement
6                                                                              dans le slot de réponse partagé
  1. Le client hqq-bindgen généré copie src directement dans le slot de requête mappé en mémoire et inverse le mot de séquence SeqLock.
  2. Le worker C lit src sur place depuis le slot de requête et transmet le pointeur de charge utile du slot de réponse directement comme tampon de destination à ZSTD_compressCCtx ou ZSTD_decompressDCtx.
  3. Le client Go copie la tranche résultante du slot 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) réalisent 0 allocs/op.

Les dictionnaires pré-traités (CDict et DDict) résident sur le worker C derrière des handles de 64 bits à contrôle de 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 au sein du worker C et renvoie un handle léger, réutilisable pour des milliers d'appels sans renvoyer à nouveau les octets du dictionnaire.

2. Amortir le surcoût IPC grâce à l'encapsulation en lot (zstd_bulk.go)

Pour les charges utiles volumineuses (16 KiB et plus), l'aller-retour en mémoire partagée d'environ 200 ns est négligeable comparé au temps CPU consommé à l'intérieur de libzstd. En revanche, pour les charges de travail qui compressent des milliers de petits messages de 100 octets à 1 KiB en une seule fois, payer ne serait-ce que 200 ns par élément s'accumule.

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

3. Survivre à l'arrêt du worker sans faire planter l'appelant

Comme zstd_server s'exécute dans un processus système distinct, une défaillance fatale à l'intérieur du worker C ne met jamais en péril votre application Go :

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

Organisation du dépôt

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

1gosuda.org/zstd
2├── autolaunch.go / hashes.go  # API Compress/Decompress de remplacement direct, pool à récupération automatique et empreintes SHA-256
3├── zstd_stream.go             # Writer/Reader en flux (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go                # Wrappers Ctx, CDict et DDict sans allocation au-dessus de hqq-bindgen
5├── zstd_bulk.go               # Encapsulation CompressBulk et DecompressBulk en un seul aller-retour
6├── zstd.go                    # Client Go hqq-bindgen généré pour le service IDL zstd
7├── zstd.idl                   # Schéma IDL HQQ définissant Context, CDict, DDict et les méthodes RPC
8├── internal/embedded/         # Binaires zstd_server intégrés via //go:embed, balisés par plateforme (6 cibles OS/Arch)
9└── c/                         # Implémentation du serveur C11 (zstd_server.c, zstd_hooks.c, main.c)

Premiers pas

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

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