---
id: e52d68870be3de9f644fcfc584bd8aec
author: Lemon Mint
title: 'gosuda.org/zstd: CGO 없이 구현된 DataDog/zstd 호환 압축 라이브러리와 크래시 격리'
description: CGO_ENABLED=0 환경에서 동작하며, 공유 메모리 워커에서 업스트림 C 라이브러리 libzstd 1.5.7을 실행하는 Go Zstandard 라이브러리로, DataDog/zstd 호환 API, 재사용 가능한 컨텍스트, 대량 일괄 처리(bulk batching)를 지원합니다.
language: ko
date: 2026-10-18T14:07:11.281986997Z
path: /zstd
go_package: gosuda.org/zstd
go_repourl: https://github.com/gosuda/hqq-zstd.git
category: lowlevel
---

`gosuda.org/zstd`는 `CGO_ENABLED=0` Go 압축 라이브러리로, [HQQ](https://gosuda.org/hqq) 공유 메모리를 통해 격리된 워커 프로세스 내부에서 공식 C Zstandard(`libzstd 1.5.7`) 엔진을 실행합니다. `github.com/DataDog/zstd`를 위한 드롭인 대체 API를 제공하며(일회성 `Compress`/`Decompress`, 스트리밍 `io.Writer`/`io.Reader`, 사전 처리된 사전, 재사용 가능한 제로 할당 컨텍스트 포함), C 실행을 Go 런타임 주소 공간 외부에 완전히 유지합니다.

```bash
go get gosuda.org/zstd@v0.3.10507
```

## 디코딩을 Go 힙 외부에 두어야 하는 이유

프로덕션 백엔드에서 압축 및 압축 해제는 신뢰할 수 없는 입력 경계에 직접 위치합니다. 로그 수집기, 메시지 브로커, RPC 게이트웨이, 스토리지 엔진은 매분 외부에서 공급된 기가바이트 단위의 프레임을 압축 해제합니다.

많은 Go 팀이 `github.com/DataDog/zstd`를 선택하는 이유는 상위 C `libzstd`에 직접 바인딩되기 때문입니다. C 구현은 높은 단일 코어 처리량, 작은 레코드에 대한 우수한 사전 압축률, 그리고 Go 가비지 컬렉션 스캔 시간을 증가시키지 않는 힙 외부 C 아레나(`ZSTD_CCtx` 및 `ZSTD_DCtx`)를 제공합니다.

그러나 `cgo`를 통해 `libzstd`를 Go 서비스에 링크하면 두 가지 운영상의 문제가 발생합니다.

1. **빌드 및 배포 마찰**: `go-sqlite3`와 마찬가지로 `DataDog/zstd`는 `CGO_ENABLED=1`과 대상 C 컴파일러 툴체인을 필요로 합니다. `linux/arm64` 또는 `windows/arm64`용 서비스를 교차 컴파일하거나 정적 바이너리를 빌드하려면 외부 C sysroot가 필요합니다.
2. **신뢰할 수 없는 입력에 대한 공유 주소 공간**: 손상되었거나 적대적인 페이로드를 Go 서버와 동일한 가상 주소 공간 내에서 압축 해제하면, 네이티브 C 코드 또는 사용자 정의 사전 처리의 모든 버그가 전체 Go 프로세스를 종료시킬 수 있습니다.

`gosuda.org/zstd`는 정확한 상위 C `libzstd 1.5.7` 엔진과 익숙한 `github.com/DataDog/zstd` API를 유지하면서, C 실행을 캐시 라인 공유 메모리를 통해 Go 프로그램과 연결된 검증된 자식 프로세스(`zstd_server`)로 이동시킵니다.

## `github.com/DataDog/zstd`의 드롭인 대체

`github.com/DataDog/zstd`에서 마이그레이션하려면 가져오기 경로를 `"gosuda.org/zstd"`로 변경하기만 하면 됩니다. 외부 바이너리 설치나 설정 코드는 필요하지 않습니다. 첫 번째 호출 시 `gosuda.org/zstd`는 플랫폼에 맞는 내장 SLSA 증명 `zstd_server` 바이너리를 구체화하고, `gosuda.org/hqq/secureexec`를 통해 SHA-256 다이제스트를 검증한 후 공유 메모리 세션 풀을 엽니다.

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"

	"gosuda.org/zstd"
)

func main() {
	// 1. One-shot Compress / Decompress (github.com/DataDog/zstd 미러링)
	src := []byte("shared-memory zstd payload without cgo")
	compressed, err := zstd.CompressLevel(nil, src, zstd.BestSpeed)
	if err != nil {
		log.Fatal(err)
	}
	plain, err := zstd.Decompress(nil, compressed)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("one-shot: %d bytes -> %d bytes (%s)\n", len(src), len(compressed), plain)

	// 2. 사전 지원이 있는 스트리밍 Writer 및 Reader
	var buf bytes.Buffer
	w := zstd.NewWriterLevel(&buf, zstd.DefaultCompression)
	if _, err := w.Write(src); err != nil {
		log.Fatal(err)
	}
	if err := w.Close(); err != nil {
		log.Fatal(err)
	}

	r := zstd.NewReader(&buf)
	defer r.Close()
	streamed, err := io.ReadAll(r)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("streamed: %s\n", streamed)
}
```

### 지원 플랫폼 및 내장 바이너리

여섯 개의 모든 대상 플랫폼은 플랫폼별 빌드 태그(`internal/embedded/embed_<goos>_<goarch>.go`)를 통해 내장된, 사전 빌드되고 SHA-256으로 고정된 워커 바이너리를 제공합니다.

| 대상 OS     | 대상 아키텍처 (`GOARCH`) | 내장 바이너리                   |       바이너리 크기 | 빌드 툴체인 및 출처                               |
| :---------- | :----------------------- | :------------------------------ | ------------------: | :------------------------------------------------ |
| **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`는 핫 패스에서 소켓 직렬화와 힙 할당을 피합니다.

### 1. 제로 할당 슬롯 인코딩 (`zstd_ctx.go` 및 `c/zstd_hooks.c`)

재사용 가능한 `zstd.Ctx`를 사용하거나 패키지 수준의 `Compress` 및 `Decompress` 함수가 내부 세션 풀에서 `Ctx`를 빌릴 때, 데이터는 매핑된 `StandardLink` 슬롯을 통해 직접 흐릅니다.

```text
Go Caller (zstd.Ctx)                    Shared-Memory Slot                    C11 Worker (zstd_hooks.c)
--------------------                    ------------------                    -------------------------
src []byte  ------------------------->  Request Slot Payload  ------------->  ZSTD_compressCCtx(
                                                                                cctx, resp_slot, src, level)
dst []byte  <-------------------------  Response Slot Payload <-------------  Writes compressed frame directly
                                                                              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`)은 세대 검사가 된 64비트 핸들(`zstd.idl`의 `handle CDict`, `handle DDict`) 뒤의 C 워커에 존재합니다. `ctx.NewCDict(rawDict, level)`로 사전을 한 번 생성하면 C 워커 내부에서 사전 테이블을 파싱하고, 사전 바이트를 다시 전송하지 않고도 수천 번의 호출에 걸쳐 재사용할 수 있는 경량 핸들을 반환합니다.

### 2. 대량 프레이밍을 통한 IPC 오버헤드 분산 (`zstd_bulk.go`)

큰 페이로드(16 KiB 이상)의 경우 약 200 ns의 공유 메모리 왕복 시간은 `libzstd` 내부에서 소요되는 CPU 시간에 비해 무시할 만합니다. 그러나 100바이트에서 1 KiB 사이의 작은 메시지 수천 개를 한 번에 압축하는 작업에서는 항목당 200 ns라도 누적됩니다.

`gosuda.org/zstd`는 항목별 왕복을 제거하기 위해 `BulkCtx`(`zstd_bulk.go`)에 `CompressBulk` 및 `DecompressBulk`를 제공합니다.

```go
bctx, err := client.NewBulkCtx(1 << 20)
if err != nil {
	log.Fatal(err)
}
defer bctx.Close()

items := [][]byte{
	[]byte(`{"event":"login","user":101}`),
	[]byte(`{"event":"purchase","user":101,"amount":4200}`),
	[]byte(`{"event":"logout","user":101}`),
}
compressedItems, err := bctx.CompressBulk(nil, items, zstd.DefaultCompression)
```

`CompressBulk`는 전체 `[][]byte` 배치를 공유 메모리 내의 단일 길이 접두사 요청 프레임으로 패킹합니다. C 워커는 C에서 모든 항목을 순회하며(`c/zstd_hooks.c`의 `compress_bulk`), 압축된 모든 프레임을 응답 슬롯에 연속으로 기록하고, 단일 공유 메모리 왕복으로 전체 배치를 반환합니다.

### 3. 호출자를 충돌시키지 않고 워커 종료를 견디기

`zstd_server`는 별도의 OS 프로세스에서 실행되므로, C 워커 내부의 치명적인 실패가 Go 애플리케이션을 중단시키지 않습니다.

- `zstd_server`가 외부에서 종료되면(예: `SIGKILL` 또는 OOM 이벤트), HQQ 전송 계층은 끊어진 피어를 즉시 감지하고, 진행 중인 `Compress` 또는 `Decompress` 호출에서 타입이 지정된 `*rt.TransportError`를 반환합니다.
- 패키지 수준 함수(`zstd.Compress`, `zstd.Decompress`, `zstd.NewWriter`, `zstd.NewReader`)는 공유 기본 워커가 사라졌음을 자동으로 감지하고, 다음 호출 시 새로운 검증된 `zstd_server` 인스턴스를 생성하여 정상 동작을 재개합니다.

## 저장소 구조

코드베이스는 드롭인 `DataDog/zstd` 파사드를 저수준 `hqq-bindgen` 세션 및 대량 처리 프리미티브와 분리합니다.

```text
gosuda.org/zstd
├── autolaunch.go / hashes.go  # 드롭인 Compress/Decompress API, 자동 복구 풀, SHA-256 해시
├── zstd_stream.go             # 스트리밍 Writer/Reader (NewWriterLevelDict, NewReaderDict)
├── zstd_ctx.go                # hqq-bindgen 위에 구축된 제로 할당 Ctx, CDict, DDict 래퍼
├── zstd_bulk.go               # 단일 왕복 CompressBulk 및 DecompressBulk 프레이밍
├── zstd.go                    # zstd IDL 서비스용으로 생성된 hqq-bindgen Go 클라이언트
├── zstd.idl                   # Context, CDict, DDict 및 RPC 메서드를 정의하는 HQQ IDL 스키마
├── internal/embedded/         # 플랫폼 태그가 지정된 //go:embed zstd_server 바이너리 (6개 OS/아키텍처 대상)
└── c/                         # C11 서버 구현 (zstd_server.c, zstd_hooks.c, main.c)
```

## 시작하기

`go get`으로 `gosuda.org/zstd`를 설치하고 `CGO_ENABLED=0`으로 사용합니다.

```bash
go get gosuda.org/zstd@v0.3.10507
```

- **GitHub 저장소**: [https://github.com/gosuda/hqq-zstd](https://github.com/gosuda/hqq-zstd)
- **아키텍처 및 벤치마크**: [docs/ARCHITECTURE.md](https://github.com/gosuda/hqq-zstd/blob/main/docs/ARCHITECTURE.md)
- **HQQ 코어 런타임**: [https://gosuda.org/hqq](https://gosuda.org/hqq)
