GoSuda

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

By Lemon Mint views ...

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 создаёт две эксплуатационные проблемы:

  1. Трудности со сборкой и развёртыванием: Так же, как go-sqlite3, DataDog/zstd требует CGO_ENABLED=1 и целевой инструментарий C-компилятора. Кросс-компиляция сервисов или сборка статических бинарных файлов для linux/arm64 или windows/arm64 требует внешних C-sysroot.
  2. Общее адресное пространство при недоверенных входных данных: Распаковка повреждённых или враждебных полезных нагрузок в том же виртуальном адресном пространстве, что и ваш 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)Встроенный бинарный файлРазмер файлаИнструментарий сборки и происхождение
Linuxamd64, arm64zstd_server_linux_<arch>~770 - 880 KiBGCC -O3, SLSA Build L3 Provenance
macOSarm64, amd64zstd_server_darwin_<arch>~660 - 795 KiBAppleClang -O3, SLSA Build L3 Provenance
Windowsamd64zstd_server_windows_amd64.exe~818 KiBMSVC -O2, SLSA Build L3 Provenance
Windowsarm64zstd_server_windows_arm64.exe~551 KiBzig 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
  1. Сгенерированный клиент hqq-bindgen копирует src непосредственно в отображённый в память слот запроса и переключает слово последовательности SeqLock.
  2. C-воркер читает src на месте из слота запроса и передаёт указатель на полезную нагрузку слота ответа непосредственно как буфер назначения в ZSTD_compressCCtx или ZSTD_decompressDCtx.
  3. 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