GoSuda

gosuda.org/zstd: Zero CGO 및 Crash Isolation을 지원하는 Drop-In DataDog/zstd Compression

By Lemon Mint
views ...

gosuda.org/zstd는 HQQ 공유 메모리를 통해 격리된 워커 프로세스 내부에서 공식 C Zstandard(libzstd 1.5.7) 엔진을 실행하는 CGO_ENABLED=0 Go 압축 라이브러리입니다. 이 라이브러리는 C 실행 환경을 Go 런타임 주소 공간으로부터 완전히 분리하면서도, github.com/DataDog/zstd를 위한 드롭인 교체 API(단발성 Compress/Decompress, 스트리밍 io.Writer/io.Reader, 사전 미리 압축된 딕셔너리, 재사용 가능한 제로 allocation 컨텍스트 포함)를 제공합니다.

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

디압축이 Go 힙 외부에 위치해야 하는 이유

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

많은 Go 팀이 github.com/DataDog/zstd를 선택하는 이유는 업스트림 C libzstd에 직접 바인딩되기 때문입니다. 즉, C 구현체는 높은 단일 코어 처리량, 소규모 레코드에 대한 엄격한 딕셔너리 압축률, 그리고 Go 가비지 컬렉션 스캔 시간을 증가시키지 않는 힙 외(off-heap) 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. 단발성 Compress / Decompress (github.com/DataDog/zstd 미러링)
14	src := []byte("cgo 없는 공유 메모리 zstd 페이로드")
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("단발성: %d 바이트 -> %d 바이트 (%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("스트리밍됨: %s\n", streamed)
42}

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

6개의 대상 플랫폼 모두 플랫폼별 빌드 태그(internal/embedded/embed_<goos>_<goarch>.go)를 통해 내장된, 미리 빌드되고 SHA-256이 고정된 워커 바이너리와 함께 제공됩니다.

대상 OS대상 아키텍처 (GOARCH)내장 바이너리바이너리 크기빌드 툴체인 및 Provenance
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

공유 메모리 데이터 경로 내부

인프로세스 cgo와 경쟁력 있는 처리량을 유지하기 위해 gosuda.org/zstd는 핫 패스에서 소켓 직렬화와 힙 allocation을 방지합니다.

1. 제로 Allocation 슬롯 인코딩 (zstd_ctx.go 및 c/zstd_hooks.c)

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

1Go 호출자 (zstd.Ctx)                    공유 메모리 슬롯                     C11 워커 (zstd_hooks.c)
2--------------------                    ------------------                    -------------------------
3src []byte  ------------------------->  요청 슬롯 페이로드  ------------->  ZSTD_compressCCtx(
4                                                                                cctx, resp_slot, src, level)
5dst []byte  <-------------------------  응답 슬롯 페이로드  <-------------  압축된 프레임을 공유 응답 슬롯에
6                                                                              직접 기록함
  1. 생성된 hqq-bindgen 클라이언트는 src를 메모리 맵핑된 요청 슬롯으로 직접 복사하고 SeqLock 시퀀스 워드를 전환합니다.
  2. C 워커는 요청 슬롯에서 src를 제자리에서 읽고, 응답 슬롯의 페이로드 포인터를 대상 버퍼로서 ZSTD_compressCCtx 또는 ZSTD_decompressDCtx에 직접 전달합니다.
  3. Go 클라이언트는 결과 슬롯에서 호출자가 제공한 dst 슬롯으로 결과 슬라이스를 복사합니다. 정상 상태(steady state)에서 ctx.CompressTo(dst, src, level) 및 ctx.DecompressInto(dst, src)는 0 allocs/op를 수행합니다.

미리 사전 컴파일된 딕셔너리(CDict 및 DDict)는 세대 검증(generation-checked)된 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 기반의 제로 allocation 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/Arch 타겟)
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