왜 gzip이 병목이 되는가
Docker 이미지 레이어는 기본적으로 gzip으로 압축되어 레지스트리에 저장·전송된다. gzip은 단일 스레드에 압축률과 속도의 균형도 오래된 알고리즘이라, 수 GB짜리 ML 이미지나 빌드 캐시를 push/pull 할 때 CPU 한 코어가 포화되며 파이프라인이 대기한다. 특히 CI에서 매 빌드마다 캐시 레이어를 밀고 당기는 구조라면 이 압축·해제 시간이 전체 잡 시간의 상당 부분을 차지한다.
zstd(Zstandard)는 gzip 대비 비슷하거나 더 높은 압축률을 훨씬 빠른 속도로 달성하고, 멀티스레드 압축과 세밀한 레벨 조정을 지원한다. OCI 이미지 스펙과 containerd, BuildKit이 zstd 미디어 타입을 정식 지원하면서 프로덕션에서도 안전하게 쓸 수 있게 됐다.
gzip vs zstd 특성 비교
| 항목 | gzip | zstd |
|---|---|---|
| 멀티스레드 | 미지원 | 지원 (--threads) |
| 레벨 범위 | 1~9 | 1~19 (+ ultra 22) |
| 동일 압축률 시 속도 | 기준 | 수 배 빠름 |
| OCI 미디어 타입 | tar+gzip | tar+zstd |
| 구형 클라이언트 호환 | 모두 지원 | containerd/BuildKit 필요 |
BuildKit으로 zstd 이미지 빌드하기
BuildKit은 --output 또는 --push 시 압축 타입을 지정할 수 있다. 레지스트리에 zstd 레이어로 직접 push 하는 예시다.
docker buildx build \
--output type=image,name=registry.example.com/app:latest,push=true,\
compression=zstd,compression-level=15,force-compression=true \
.
force-compression=true는 기존 gzip 레이어까지 zstd로 재압축한다. 이를 빼면 새로 빌드된 레이어만 zstd로 저장되고 베이스 이미지 레이어는 원본 압축을 유지한다.
CI 캐시에 적용하기
가장 효과가 큰 곳은 인라인 캐시나 레지스트리 캐시다. GitHub Actions 예시에서 캐시 export에도 zstd를 적용한다.
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: registry.example.com/app:latest
outputs: type=image,compression=zstd,compression-level=12
cache-from: type=registry,ref=registry.example.com/app:buildcache
cache-to: type=registry,ref=registry.example.com/app:buildcache,mode=max,compression=zstd,compression-level=12
mode=max로 모든 중간 레이어를 캐시하면 캐시 크기가 커지는데, zstd가 이 비용을 상쇄해 전송·압축 시간을 함께 줄여준다.
압축 레벨을 어떻게 고를까
레벨을 무조건 올리는 건 답이 아니다. CI 캐시처럼 매번 새로 압축하고 버려지는 데이터는 압축 시간이 곧 비용이므로 중간 레벨(9~12)이 유리하다. 반대로 한 번 빌드해 수백 번 pull 하는 배포 이미지는 높은 레벨(15~19)로 저장해 pull 대역폭을 아끼는 편이 낫다.
# 동일 레이어 tar를 레벨별로 비교
for L in 3 9 12 19; do
/usr/bin/time -v zstd -T0 -$L -q layer.tar -o /dev/null 2>&1 \
| awk -v l=$L '/Elapsed/{t=$NF} END{print "level "l" time "t}'
echo " size: $(zstd -T0 -$L -c layer.tar | wc -c) bytes"
done
-T0은 가용 코어 전부를 쓴다. 실제 레이어로 위 스크립트를 돌려 시간 대비 절감 크기의 무릎 지점을 찾은 뒤 레벨을 고정하는 것이 가장 확실하다.
주의점
- 클라이언트 호환성: zstd 레이어는 containerd 기반 런타임과 최신 BuildKit이 필요하다. 오래된 Docker 데몬이나 일부 서드파티 스캐너·admission 컨트롤러가 zstd 미디어 타입을 못 읽을 수 있으니, 외부 배포용 태그는 사전 검증한다.
- 레지스트리 지원: 대부분의 OCI 호환 레지스트리는 문제없지만, 미러·프록시 캐시 계층이 미디어 타입을 재작성하는 경우 확인이 필요하다.
- 재현성:
force-compression으로 재압축하면 레이어 digest가 원본과 달라져 다른 이미지와 레이어를 공유하지 못한다. 스토리지 절감을 노린다면 오히려 역효과일 수 있다. - 단일 코어 러너: zstd의 최대 강점은 멀티스레드다. vCPU 1개짜리 러너에서는 이득이 작으니 러너 스펙과 함께 판단한다.
정리
zstd 전환은 코드 변경 없이 빌드 출력 옵션만 바꿔 적용할 수 있는 저비용 개선이다. 배포 이미지는 높은 레벨로 pull 대역폭을, CI 캐시는 중간 레벨로 반복 압축 시간을 줄이는 식으로 용도별 레벨을 분리하고, 외부 배포 경로의 호환성만 사전에 확인하면 안전하게 도입할 수 있다.