gosuda.org/zstd: Compresión DataDog/zstd sin inconvenientes con CGO nulo y aislamiento de fallos
gosuda.org/zstd es una librería de compresión en Go con CGO_ENABLED=0 que ejecuta el motor oficial en C de Zstandard (libzstd 1.5.7) dentro de un proceso trabajador aislado mediante memoria compartida HQQ. Proporciona una API de reemplazo directo para github.com/DataDog/zstd (que incluye Compress/Decompress de una sola ejecución, io.Writer/io.Reader en streaming, diccionarios pre-digeridos y contextos reutilizables sin asignación) mientras mantiene la ejecución en C completamente fuera del espacio de direcciones del tiempo de ejecución de Go.
1go get gosuda.org/zstd@v0.3.10507
Por qué la descompresión pertenece fuera de su heap de Go
En los backends de producción, la compresión y la descompresión se ubican directamente en el límite de entrada no confiable. Los recolectores de registros, los intermediarios de mensajes, las pasarelas 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 el libzstd en C ascendente: la implementación en C ofrece un alto rendimiento en un solo núcleo, relaciones estrictas de compresión de diccionarios en registros pequeños y áreas de C fuera del heap (ZSTD_CCtx y ZSTD_DCtx) que no inflan los tiempos de análisis de la recolección de basura de Go.
Sin embargo, vincular libzstd a un servicio de Go mediante cgo conlleva dos problemas operativos:
- Fricción de construcción y despliegue: Al igual que
go-sqlite3,DataDog/zstdrequiereCGO_ENABLED=1y una cadena de herramientas de compilación de C de destino. La compilación cruzada de servicios o la construcción de binarios estáticos paralinux/arm64owindows/arm64requiere sysroots de C externos. - Espacio de direcciones compartido en entradas no confiables: Descomprimir cargas útiles corruptas o adversarias dentro del mismo espacio de direcciones virtual que su servidor Go significa que cualquier error en el código nativo de C o en el manejo de diccionarios personalizados puede terminar todo el proceso de Go.
gosuda.org/zstd conserva el motor exacto de C libzstd 1.5.7 ascendente y la conocida API de github.com/DataDog/zstd, pero traslada la ejecución de C a un proceso secundario verificado (zstd_server) conectado a su programa de Go a través de memoria compartida de línea de caché.
Reemplazo directo para 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 ninguna instalación de binarios externos ni código de configuración: en la primera llamada, gosuda.org/zstd materializa un binario incrustado zstd_server con atestación SLSA para su plataforma, verifica su resumen SHA-256 mediante gosuda.org/hqq/secureexec y abre un grupo 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. 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}
Plataformas soportadas y binarios incrustados
Las seis plataformas de destino se entregan con binarios de trabajadores preconstruidos y con hash SHA-256 fijado, incrustados mediante etiquetas de compilación específicas de la plataforma (internal/embedded/embed_<goos>_<goarch>.go):
| SO de destino | Arquitectura de destino (GOARCH) | Binario incrustado | Tamaño del binario | Cadena de herramientas 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 de memoria compartida
Para mantener el rendimiento competitivo con cgo en el mismo proceso, gosuda.org/zstd evita la serialización de sockets y las asignaciones en el heap en la ruta crítica.
1. Codificación de ranuras sin asignación (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 grupo de sesiones interno), los datos fluyen directamente a través de la ranura mapeada de 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
- El cliente generado por
hqq-bindgencopiasrcdirectamente en la ranura de solicitud mapeada en memoria e invierte la palabra de secuenciaSeqLock. - El trabajador en C lee
srcen el lugar desde la ranura de solicitud y pasa el puntero de carga útil de la ranura de respuesta directamente como búfer de destino aZSTD_compressCCtxoZSTD_decompressDCtx. - El cliente de Go copia el segmento resultante de la ranura de respuesta en el segmento
dstproporcionado por el invocador. En estado estacionario,ctx.CompressTo(dst, src, level)yctx.DecompressInto(dst, src)realizan0 allocs/op.
Los diccionarios pre-digeridos (CDict y DDict) residen en el trabajador de C detrás de identificadores de 64 bits verificados por generación (handle CDict, handle DDict en zstd.idl). Crear un diccionario una sola vez mediante ctx.NewCDict(rawDict, level) analiza las tablas de diccionarios dentro del trabajador de C y devuelve un identificador ligero que se puede reutilizar en miles de llamadas sin volver a enviar los bytes del diccionario.
2. Amortización de la sobrecarga de IPC con marcos por lotes (zstd_bulk.go)
Para cargas útiles grandes (16 KiB en adelante), el viaje de ida y vuelta de memoria compartida de ~200 ns es insignificante en comparación con el tiempo de CPU consumido dentro de libzstd. Sin embargo, para cargas de trabajo que comprimen miles de mensajes pequeños de 100 bytes a 1 KiB a la vez, pagar incluso 200 ns por elemento se acumula.
gosuda.org/zstd proporciona CompressBulk y DecompressBulk en 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 todo el lote de [][]byte en una sola trama de solicitud con prefijo de longitud en memoria compartida. El trabajador de C buclea sobre cada elemento en C (compress_bulk en c/zstd_hooks.c), escribe todas las tramas comprimidas de forma contigua en la ranura de respuesta y devuelve todo el lote en un solo viaje de ida y vuelta de memoria compartida.
3. Sobrevivir a la terminación del trabajador sin bloquear al invocador
Debido a que zstd_server se ejecuta en un proceso de SO separado, un fallo fatal dentro del trabajador de C nunca derriba su aplicación de Go:
- Si
zstd_serverse elimina externamente (por ejemplo, medianteSIGKILLo un evento de OOM), la capa de transporte HQQ detecta al par roto inmediatamente y devuelve un*rt.TransportErrortipado desde la llamada aCompressoDecompressen curso. - Las funciones a nivel de paquete (
zstd.Compress,zstd.Decompress,zstd.NewWriter,zstd.NewReader) detectan automáticamente que el trabajador predeterminado compartido ha desaparecido, generan una nueva instancia verificada dezstd_serveren la siguiente llamada y reanudan la operación normal.
Disposición del repositorio
El código base separa la fachada de reemplazo directo de DataDog/zstd de la sesión de bajo nivel de hqq-bindgen y las primitivas por lotes:
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)
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 pruebas de rendimiento: docs/ARCHITECTURE.md
- Entorno de ejecución principal de HQQ: https://gosuda.org/hqq