gosuda.org/zstd: Drop-In DataDog/zstd Compression with Zero CGO and Crash Isolation
gosuda.org/zstd — это библиотека сжатия на языке Go с установленным параметром CGO_ENABLED=0, которая запускает официальный C-модуль Zstandard (libzstd 1.5.7) внутри изолированного рабочего процесса (worker process) через разделяемую память HQQ. Она предоставляет заменяемый «drop-in» API для github.com/DataDog/zstd (включая однократные операции Compress/Decompress, потоковые io.Writer/io.Reader, предварительно скомпилированные словари и переиспользуемые контексты без аллокаций), при этом выполняя код на языке C полностью за пределами адресного пространства среды выполнения Go.
1go get gosuda.org/zstd@v0.3.10507
Почему декомпрессия должна находиться вне вашей кучи Go (Go Heap)
В производственных бэкендах операции сжатия и распаковки располагаются непосредственно на границе ненадежных входных данных. Коллекторы логов, брокеры сообщений, шлюзы RPC и движки баз данных распаковывают гигабайты внешних кадров каждую минуту.
Многие команды, использующие Go, выбирают github.com/DataDog/zstd, поскольку эта библиотека напрямую связывается с upstream-модулем libzstd на языке C: реализация на C обеспечивает высокую пропускную способность на одном ядре, высокие коэффициенты словарного сжатия на небольших записях, а также внекучевые области памяти C (ZSTD_CCtx и ZSTD_DCtx), которые не увеличивают время сканирования сборщиком мусора Go.
Однако связывание libzstd с сервисом Go через cgo влечет за собой две операционные проблемы:
- Трудности при сборке и развертывании: Точно так же, как и
go-sqlite3, библиотекаDataDog/zstdтребует установкиCGO_ENABLED=1и наличия целевой цепочки инструментов компилятора C. Кросс-компиляция сервисов или сборка статических бинарных файлов дляlinux/arm64илиwindows/arm64требует внешних системных корневых каталогов (sysroots) C. - Общее адресное пространство для ненадежных входных данных: Распаковка поврежденных или созданных злоумышленником полезных данных (payloads) в том же виртуальном адресном пространстве, где работает ваш сервер Go, означает, что любая ошибка в нативном коде C или в пользовательской обработке словарей может привести к завершению всего процесса Go.
Библиотека gosuda.org/zstd сохраняет оригинальный upstream-модуль libzstd 1.5.7 на языке C и привычный API github.com/DataDog/zstd, но переносит выполнение кода C в верифицируемый дочерний процесс (zstd_server), связанный с вашей программой на Go через разделяемую память на уровне строк кэша (cache-line).
Прямозаменяемая альтернатива (Drop-In Replacement) для 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. Однократные операции сжатия / распаковки (аналогичны 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. Потоковые запись и чтение с поддержкой словарей
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) | Встроенный бинарный файл | Размер бинарного файла | Цепочка сборки и происхождение (Provenance) |
|---|---|---|---|---|
| Linux | amd64, arm64 | zstd_server_linux_<arch> | ~770 - 880 KiB | GCC -O3, происхождение SLSA Build L3 |
| macOS | arm64, amd64 | zstd_server_darwin_<arch> | ~660 - 795 KiB | AppleClang -O3, происхождение SLSA Build L3 |
| Windows | amd64 | zstd_server_windows_amd64.exe | ~818 KiB | MSVC -O2, происхождение SLSA Build L3 |
| Windows | arm64 | zstd_server_windows_arm64.exe | ~551 KiB | zig cc -target aarch64-windows-gnu -O3, SLSA L3 |
Архитектура передачи данных в разделяемой памяти
Чтобы поддерживать пропускную способность на уровне внутрипроцессорного вызова cgo, библиотека gosuda.org/zstd избегает сериализации через сокеты и аллокаций в куче на критическом пути (hot path).
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 копирует полученный срез (slice) из слота ответа в срез
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 и выше) круговое обращение к разделяемой памяти (round trip), занимающее около 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 # Встроенный API Compress/Decompress, пул автовосстановления и хэши SHA-256
3├── zstd_stream.go # Потоковые Writer/Reader (NewWriterLevelDict, NewReaderDict)
4├── zstd_ctx.go # Контекст без аллокаций Ctx, обертки CDict и DDict поверх hqq-bindgen
5├── zstd_bulk.go # Форматирование кадров CompressBulk и DecompressBulk за один круговой запрос
6├── zstd.go # Сгенерированный клиент Go через hqq-bindgen для службы zstd IDL
7├── zstd.idl # Схема HQQ IDL, определяющая методы Context, CDict, DDict и RPC
8├── internal/embedded/ # Бинарные файлы zstd_server с тегами ОС/архитектуры //go:embed (6 целевых ОС/архитектур)
9└── c/ # Реализация сервера C11 (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: https://gosuda.org/hqq