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

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 :
- Frictions de compilation et de déploiement : à l'instar de
go-sqlite3,DataDog/zstdexigeCGO_ENABLED=1ainsi qu'une chaîne d'outils de compilation C cible. La compilation croisée des services ou la production de binaires statiques pourlinux/arm64ouwindows/arm64nécessitent des sysroots C externes. - 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 cible | Architecture cible (GOARCH) | Binaire embarqué | Taille du binaire | Chaîne de compilation et provenance |
|---|---|---|---|---|
| Linux | amd64, arm64 | zstd_server_linux_<arch> | ~770 - 880 KiB | GCC -O3, provenance SLSA Build L3 |
| macOS | arm64, amd64 | zstd_server_darwin_<arch> | ~660 - 795 KiB | AppleClang -O3, provenance SLSA Build L3 |
| Windows | amd64 | zstd_server_windows_amd64.exe | ~818 KiB | MSVC -O2, provenance SLSA Build L3 |
| Windows | arm64 | zstd_server_windows_arm64.exe | ~551 KiB | zig 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é
- Le client
hqq-bindgengénéré copiesrcdirectement dans le slot de requête mappé en mémoire et inverse le mot de séquenceSeqLock. - Le worker C lit
srcsur 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_compressCCtxouZSTD_decompressDCtx. - Le client Go copie la tranche résultante du slot de réponse vers la tranche
dstfournie par l'appelant. En régime permanent,ctx.CompressTo(dst, src, level)etctx.DecompressInto(dst, src)réalisent0 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_serverest tué de l'extérieur (par exemple parSIGKILLou un événement OOM), la couche de transport HQQ détecte immédiatement le pair défaillant et renvoie un*rt.TransportErrortypé depuis l'appelCompressouDecompressen 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 dezstd_serverlors 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
- Dépôt GitHub : https://github.com/gosuda/hqq-zstd
- Architecture et benchmarks : docs/ARCHITECTURE.md
- Runtime central HQQ : https://gosuda.org/hqq