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

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 서비스에 링크하면 두 가지 운영상의 문제가 발생합니다.
- 빌드 및 배포 마찰:
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. 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) | 내장 바이너리 | 바이너리 크기 | 빌드 툴체인 및 출처 |
|---|---|---|---|---|
| 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 슬롯을 통해 직접 흐릅니다.
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
- 생성된
hqq-bindgen클라이언트는src를 메모리 매핑된 요청 슬롯에 직접 복사하고SeqLock시퀀스 워드를 뒤집습니다. - C 워커는 요청 슬롯에서
src를 제자리에서 읽고, 응답 슬롯의 페이로드 포인터를 대상 버퍼로ZSTD_compressCCtx또는ZSTD_decompressDCtx에 직접 전달합니다. - 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
- GitHub 저장소: https://github.com/gosuda/hqq-zstd
- 아키텍처 및 벤치마크: docs/ARCHITECTURE.md
- HQQ 코어 런타임: https://gosuda.org/hqq