GoSuda

gosuda.org/zstd: Drop-In DataDog/zstd Compression with Zero CGO and Crash Isolation

By Lemon Mint
views ...

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

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