gosuda.org/zstd: Zero CGO 및 Crash Isolation을 지원하는 Drop-In DataDog/zstd Compression
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 서비스에 링크하는 것은 두 가지 운영상의 어려움을 동반합니다.
- 빌드 및 배포 마찰:
go-sqlite3와 마찬가지로DataDog/zstd는CGO_ENABLED=1과 대상 C 컴파일러 툴체인을 요구합니다. 서비스 교차 컴파일이나linux/arm64또는windows/arm64용 정적 바이너리 빌드에는 외부 C sysroot가 필요합니다. - 신뢰할 수 없는 입력에 대한 공유 주소 공간: 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 |
|---|---|---|---|---|
| Linux | amd64, arm64 | zstd_server_linux_<arch> | ~770 - 880 KiB | GCC -O3, SLSA Build L3 Provenance |
| macOS | arm64, amd64 | zstd_server_darwin_<arch> | ~660 - 795 KiB | AppleClang -O3, SLSA Build L3 Provenance |
| Windows | amd64 | zstd_server_windows_amd64.exe | ~818 KiB | MSVC -O2, SLSA Build L3 Provenance |
| Windows | arm64 | zstd_server_windows_arm64.exe | ~551 KiB | zig 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 직접 기록함
- 생성된
hqq-bindgen클라이언트는src를 메모리 맵핑된 요청 슬롯으로 직접 복사하고SeqLock시퀀스 워드를 전환합니다. - C 워커는 요청 슬롯에서
src를 제자리에서 읽고, 응답 슬롯의 페이로드 포인터를 대상 버퍼로서ZSTD_compressCCtx또는ZSTD_decompressDCtx에 직접 전달합니다. - 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
- GitHub 저장소: https://github.com/gosuda/hqq-zstd
- 아키텍처 및 벤치마크: docs/ARCHITECTURE.md
- HQQ 코어 런타임: https://gosuda.org/hqq