GoSuda

gosuda.org/zstd: CGO 없이 구현된 DataDog/zstd 호환 압축 라이브러리와 크래시 격리

By Lemon Mint views ...

gosuda.org/zstd는 CGO_ENABLED=0 Go 압축 라이브러리로, HQQ 공유 메모리를 통해 격리된 워커 프로세스 내부에서 공식 C Zstandard(libzstd 1.5.7) 엔진을 실행합니다. github.com/DataDog/zstd를 위한 드롭인 대체 API를 제공하며(일회성 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 구현은 높은 단일 코어 처리량, 작은 레코드에 대한 우수한 사전 압축률, 그리고 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 다이제스트를 검증한 후 공유 메모리 세션 풀을 엽니다.

 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 (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. 사전 지원이 있는 스트리밍 Writer 및 Reader
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}

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

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

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

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)은 세대 검사가 된 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를 제공합니다.

 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에서 모든 항목을 순회하며(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 세션 및 대량 처리 프리미티브와 분리합니다.

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

시작하기

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

1go get gosuda.org/zstd@v0.3.10507