gosuda.org/zstd: Совместимая замена DataDog/zstd с сжатием без CGO и изоляцией сбоев

gosuda.org/zstd — это библиотека сжатия на Go с CGO_ENABLED=0, которая запускает официальный C-движок Zstandard (libzstd 1.5.7) внутри изолированного рабочего процесса через разделяемую память HQQ. Она предоставляет API-замену с прямой совместимостью для github.com/DataDog/zstd (включая однократные Compress/Decompress, потоковые io.Writer/io.Reader, предварительно обработанные словари и переиспользуемые контексты с нулевым количеством аллокаций), при этом полностью выводя выполнение C-кода за пределы адресного пространства среды выполнения Go.
1go get gosuda.org/zstd@v0.3.10507
Почему распаковка должна выполняться за пределами кучи Go
В промышленных бэкендах сжатие и распаковка находятся непосредственно на границе недоверенных входных данных. Коллекторы логов, брокеры сообщений, RPC-шлюзы и системы хранения распаковывают гигабайты внешне поступающих фреймов каждую минуту.
Многие команды Go выбирают github.com/DataDog/zstd, поскольку он напрямую связывается с исходной C-библиотекой libzstd: C-реализация обеспечивает высокую пропускную способность на одном ядре, эффективные коэффициенты сжатия со словарями для небольших записей и внешние по отношению к куче C-арены (ZSTD_CCtx и ZSTD_DCtx), которые не увеличивают время сканирования сборщиком мусора Go.
Однако подключение libzstd к сервису на Go через cgo создаёт две эксплуатационные проблемы:
- Трудности со сборкой и развёртыванием: Так же, как
go-sqlite3,DataDog/zstdтребуетCGO_ENABLED=1и целевой инструментарий C-компилятора. Кросс-компиляция сервисов или сборка статических бинарных файлов дляlinux/arm64илиwindows/arm64требует внешних C-sysroot. - Общее адресное пространство при недоверенных входных данных: Распаковка повреждённых или враждебных полезных нагрузок в том же виртуальном адресном пространстве, что и ваш Go-сервер, означает, что любая ошибка в нативном C-коде или в обработке пользовательских словарей может завершить весь процесс Go.
gosuda.org/zstd сохраняет точный исходный движок libzstd 1.5.7 и знакомый API github.com/DataDog/zstd, но переносит выполнение C-кода в проверенный дочерний процесс (zstd_server), соединённый с вашей программой на Go через разделяемую память, выровненную по строкам кэша.
Прямая замена github.com/DataDog/zstd
Для перехода с github.com/DataDog/zstd достаточно изменить только путь импорта на "gosuda.org/zstd". Никакой внешней установки бинарных файлов или кода настройки не требуется: при первом вызове gosuda.org/zstd извлекает встроенный бинарный файл zstd_server, прошедший аттестацию SLSA, для вашей платформы, проверяет его хеш SHA-256 через gosuda.org/hqq/secureexec и открывает пул сессий разделяемой памяти.
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}
Поддерживаемые платформы и встроенные бинарные файлы
Все шесть целевых платформ поставляются с предварительно собранными рабочими бинарными файлами с закреплённым SHA-256, встроенными через платформенно-специфичные теги сборки (internal/embedded/embed_<goos>_<goarch>.go):
| Целевая ОС | Целевая архитектура (GOARCH) | Встроенный бинарный файл | Размер файла | Инструментарий сборки и происхождение |
|---|---|---|---|---|
| Linux | amd64, arm64 | zstd_server_linux_<arch> | ~770 - 880 KiB | GCC -O3, SLSA Build L3 Provenance |
| macOS | arm64, amd64 | zstd_server_darwin_<arch> | ~660 - 795 KiB | AppleClang -O3, SLSA Build L3 Provenance |
| Windows | amd64 | zstd_server_windows_amd64.exe | ~818 KiB | MSVC -O2, SLSA Build L3 Provenance |
| Windows | arm64 | zstd_server_windows_arm64.exe | ~551 KiB | zig cc -target aarch64-windows-gnu -O3, SLSA L3 |
Внутри пути данных через разделяемую память
Чтобы сохранить пропускную способность, сопоставимую с in-process cgo, gosuda.org/zstd избегает сериализации через сокеты и аллокаций в куче на горячем пути.
1. Кодирование слотов без аллокаций (zstd_ctx.go и c/zstd_hooks.c)
При использовании переиспользуемого zstd.Ctx (или когда функции уровня пакета Compress и Decompress заимствуют Ctx из внутреннего пула сессий), данные передаются непосредственно через отображённый слот 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
- Сгенерированный клиент
hqq-bindgenкопируетsrcнепосредственно в отображённый в память слот запроса и переключает слово последовательностиSeqLock. - C-воркер читает
srcна месте из слота запроса и передаёт указатель на полезную нагрузку слота ответа непосредственно как буфер назначения вZSTD_compressCCtxилиZSTD_decompressDCtx. - Go-клиент копирует результирующий срез из слота ответа в переданный вызывающей стороной срез
dst. В установившемся режимеctx.CompressTo(dst, src, level)иctx.DecompressInto(dst, src)выполняют0 allocs/op.
Предварительно обработанные словари (CDict и DDict) хранятся на C-воркере за 64-битными дескрипторами с проверкой поколения (handle CDict, handle DDict в zstd.idl). Однократное создание словаря через ctx.NewCDict(rawDict, level) разбирает таблицы словаря внутри C-воркера и возвращает легковесный дескриптор, который можно повторно использовать в тысячах вызовов без повторной передачи байтов словаря.
2. Амортизация накладных расходов IPC с помощью пакетного фреймирования (zstd_bulk.go)
Для крупных полезных нагрузок (от 16 KiB и выше) задержка обращения к разделяемой памяти около 200 нс незначительна по сравнению с процессорным временем, затрачиваемым внутри libzstd. Однако для рабочих нагрузок, которые сжимают тысячи небольших сообщений размером от 100 байт до 1 KiB за один раз, даже 200 нс на элемент накапливаются в существенную величину.
gosuda.org/zstd предоставляет CompressBulk и DecompressBulk в BulkCtx (zstd_bulk.go) для устранения обращений на каждый элемент:
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 упаковывает весь пакет [][]byte в один кадр запроса с префиксами длины в разделяемой памяти. C-воркер проходит по каждому элементу в C (compress_bulk в c/zstd_hooks.c), записывает все сжатые фреймы непрерывно в слот ответа и возвращает весь пакет за одно обращение к разделяемой памяти.
3. Выживание при завершении воркера без падения вызывающей стороны
Поскольку zstd_server работает в отдельном процессе ОС, фатальный сбой внутри C-воркера никогда не приводит к падению вашего приложения на Go:
- Если
zstd_serverзавершён извне (например, сигналомSIGKILLили событием OOM), транспортный уровень HQQ немедленно обнаруживает разорванного партнёра и возвращает типизированную ошибку*rt.TransportErrorиз выполняющегося вызоваCompressилиDecompress. - Функции уровня пакета (
zstd.Compress,zstd.Decompress,zstd.NewWriter,zstd.NewReader) автоматически определяют, что общий рабочий процесс по умолчанию отсутствует, запускают новый проверенный экземплярzstd_serverпри следующем вызове и возобновляют нормальную работу.
Структура репозитория
Кодовая база отделяет фасад DataDog/zstd для прямой замены от низкоуровневых примитивов сессий и пакетной обработки 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)
Начало работы
Установите gosuda.org/zstd с помощью go get и используйте его с CGO_ENABLED=0:
1go get gosuda.org/zstd@v0.3.10507
- Репозиторий GitHub: https://github.com/gosuda/hqq-zstd
- Архитектура и бенчмарки: docs/ARCHITECTURE.md
- HQQ Core Runtime: https://gosuda.org/hqq