gosuda.org/zstd: Compresión compatible con DataDog/zstd sin CGO y con aislamiento de fallos

gosuda.org/zstd es una biblioteca de compresión en Go con CGO_ENABLED=0 que ejecuta el motor oficial de Zstandard en C (libzstd 1.5.7) dentro de un proceso trabajador aislado sobre memoria compartida de HQQ. Ofrece una API de reemplazo directo para github.com/DataDog/zstd (incluyendo Compress/Decompress de una sola llamada, io.Writer/io.Reader en modo streaming, diccionarios precalculados y contextos reutilizables sin asignaciones de memoria) y mantiene la ejecución de C completamente fuera del espacio de direcciones del runtime de Go.
1go get gosuda.org/zstd@v0.3.10507
Por qué la descompresión debe ubicarse fuera del heap de Go
En los backends de producción, la compresión y la descompresión se encuentran directamente en el límite de entrada no confiable. Los colectores de logs, los brokers de mensajes, las puertas de enlace RPC y los motores de almacenamiento descomprimen gigabytes de tramas suministradas externamente cada minuto.
Muchos equipos de Go eligen github.com/DataDog/zstd porque se enlaza directamente con la libzstd original escrita en C: la implementación en C ofrece un alto rendimiento por núcleo, relaciones de compresión con diccionario muy ajustadas en registros pequeños y arenas de C fuera del heap (ZSTD_CCtx y ZSTD_DCtx) que no incrementan los tiempos de escaneo del recolector de basura de Go.
No obstante, enlazar libzstd en un servicio Go mediante cgo introduce dos problemas operativos:
- Fricción en la compilación y el despliegue: al igual que
go-sqlite3,DataDog/zstdrequiereCGO_ENABLED=1y una cadena de herramientas de compilación de C para el objetivo. La compilación cruzada de servicios o la generación de binarios estáticos paralinux/arm64owindows/arm64exige sysroots de C externos. - Espacio de direcciones compartido con entradas no confiables: descomprimir cargas corruptas o adversarias dentro del mismo espacio de direcciones virtual que su servidor Go implica que cualquier error en el código nativo de C o en el manejo de diccionarios personalizados puede terminar todo el proceso Go.
gosuda.org/zstd conserva el motor exacto libzstd 1.5.7 original en C y la API familiar de github.com/DataDog/zstd, pero traslada la ejecución de C a un proceso hijo verificado (zstd_server) conectado a su programa Go mediante memoria compartida alineada a líneas de caché.
Reemplazo directo de github.com/DataDog/zstd
Migrar desde github.com/DataDog/zstd requiere cambiar únicamente la ruta de importación a "gosuda.org/zstd". No se necesita instalar ningún binario externo ni código de configuración: en la primera llamada, gosuda.org/zstd extrae un binario zstd_server incrustado con atestación SLSA para su plataforma, verifica su digest SHA-256 mediante gosuda.org/hqq/secureexec y abre un pool de sesiones de memoria compartida.
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 de una sola llamada (replica 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 y Reader en streaming con soporte de diccionarios
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}
Plataformas compatibles y binarios incrustados
Las seis plataformas objetivo incluyen binarios de trabajador preconstruidos y fijados por SHA-256, incrustados mediante etiquetas de compilación específicas de plataforma (internal/embedded/embed_<goos>_<goarch>.go):
| Sistema objetivo | Arquitectura objetivo (GOARCH) | Binario incrustado | Tamaño del binario | Cadena de compilación y procedencia |
|---|---|---|---|---|
| Linux | amd64, arm64 | zstd_server_linux_<arch> | ~770 - 880 KiB | GCC -O3, procedencia SLSA Build L3 |
| macOS | arm64, amd64 | zstd_server_darwin_<arch> | ~660 - 795 KiB | AppleClang -O3, procedencia SLSA Build L3 |
| Windows | amd64 | zstd_server_windows_amd64.exe | ~818 KiB | MSVC -O2, procedencia SLSA Build L3 |
| Windows | arm64 | zstd_server_windows_arm64.exe | ~551 KiB | zig cc -target aarch64-windows-gnu -O3, SLSA L3 |
Dentro de la ruta de datos en memoria compartida
Para mantener un rendimiento competitivo frente a cgo en proceso, gosuda.org/zstd evita la serialización por sockets y las asignaciones de heap en la ruta crítica.
1. Codificación de slots sin asignaciones (zstd_ctx.go y c/zstd_hooks.c)
Cuando utiliza un zstd.Ctx reutilizable (o cuando las funciones Compress y Decompress a nivel de paquete toman prestado un Ctx del pool de sesiones interno), los datos fluyen directamente a través del slot StandardLink mapeado:
1Llamador Go (zstd.Ctx) Slot de memoria compartida Trabajador C11 (zstd_hooks.c)
2-------------------- -------------------------- -----------------------------
3src []byte -------------------------> Carga del slot de solicitud ------> ZSTD_compressCCtx(
4 cctx, resp_slot, src, level)
5dst []byte <------------------------- Carga del slot de respuesta <------ Escribe la trama comprimida directamente
6 en el slot de respuesta compartido
- El cliente
hqq-bindgengenerado copiasrcdirectamente en el slot de solicitud mapeado en memoria e invierte la palabra de secuenciaSeqLock. - El trabajador C lee
srcen el lugar desde el slot de solicitud y pasa el puntero de carga del slot de respuesta directamente como búfer de destino aZSTD_compressCCtxoZSTD_decompressDCtx. - El cliente Go copia el slice resultante desde el slot de respuesta al slice
dstproporcionado por el llamador. En estado estacionario,ctx.CompressTo(dst, src, level)yctx.DecompressInto(dst, src)realizan0 allocs/op.
Los diccionarios precalculados (CDict y DDict) residen en el trabajador C detrás de manejadores de 64 bits con verificación de generación (handle CDict, handle DDict en zstd.idl). Crear un diccionario una vez mediante ctx.NewCDict(rawDict, level) analiza las tablas del diccionario dentro del trabajador C y devuelve un manejador ligero que puede reutilizarse en miles de llamadas sin reenviar los bytes del diccionario.
2. Amortización de la sobrecarga de IPC con empaquetado por lotes (zstd_bulk.go)
Para cargas grandes (16 KiB o más), el viaje de ida y vuelta por memoria compartida de ~200 ns es despreciable frente al tiempo de CPU dedicado dentro de libzstd. Sin embargo, para cargas de trabajo que comprimen miles de mensajes pequeños de entre 100 bytes y 1 KiB de una sola vez, pagar incluso 200 ns por elemento se acumula.
gosuda.org/zstd proporciona CompressBulk y DecompressBulk sobre BulkCtx (zstd_bulk.go) para eliminar los viajes de ida y vuelta por elemento:
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 empaqueta el lote completo [][]byte en una única trama de solicitud con prefijos de longitud en memoria compartida. El trabajador C recorre cada elemento en C (compress_bulk en c/zstd_hooks.c), escribe todas las tramas comprimidas de forma contigua en el slot de respuesta y devuelve el lote completo en un único viaje de ida y vuelta por memoria compartida.
3. Sobrevivir a la terminación del trabajador sin bloquear al llamador
Dado que zstd_server se ejecuta en un proceso del sistema operativo separado, un fallo fatal dentro del trabajador C nunca derriba su aplicación Go:
- Si
zstd_serverse termina externamente (por ejemplo, medianteSIGKILLo un evento de OOM), la capa de transporte HQQ detecta de inmediato el par roto y devuelve un*rt.TransportErrortipado desde la llamadaCompressoDecompressen curso. - Las funciones a nivel de paquete (
zstd.Compress,zstd.Decompress,zstd.NewWriter,zstd.NewReader) detectan automáticamente que el trabajador predeterminado compartido ya no existe, lanzan una nueva instancia verificada dezstd_serveren la siguiente llamada y reanudan la operación normal.
Estructura del repositorio
La base de código separa la fachada de reemplazo directo de DataDog/zstd de las primitivas de sesión y de lotes de bajo nivel de hqq-bindgen:
1gosuda.org/zstd
2├── autolaunch.go / hashes.go # API Compress/Decompress de reemplazo directo, pool de auto-recuperación y hashes SHA-256
3├── zstd_stream.go # Writer/Reader en streaming (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go # Ctx sin asignaciones, y envoltorios CDict y DDict sobre hqq-bindgen
5├── zstd_bulk.go # Framing CompressBulk y DecompressBulk en un único viaje de ida y vuelta
6├── zstd.go # Cliente Go de hqq-bindgen generado para el servicio IDL de zstd
7├── zstd.idl # Esquema IDL de HQQ que define Context, CDict, DDict y los métodos RPC
8├── internal/embedded/ # Binarios zstd_server incrustados con //go:embed y etiquetas por plataforma (6 objetivos OS/Arch)
9└── c/ # Implementación del servidor en C11 (zstd_server.c, zstd_hooks.c, main.c)
Primeros pasos
Instale gosuda.org/zstd con go get y úselo con CGO_ENABLED=0:
1go get gosuda.org/zstd@v0.3.10507
- Repositorio de GitHub: https://github.com/gosuda/hqq-zstd
- Arquitectura y benchmarks: docs/ARCHITECTURE.md
- HQQ Core Runtime: https://gosuda.org/hqq