왜 gzip이 병목이 되는가

Docker 이미지 레이어는 기본적으로 gzip으로 압축되어 레지스트리에 저장·전송된다. gzip은 단일 스레드에 압축률과 속도의 균형도 오래된 알고리즘이라, 수 GB짜리 ML 이미지나 빌드 캐시를 push/pull 할 때 CPU 한 코어가 포화되며 파이프라인이 대기한다. 특히 CI에서 매 빌드마다 캐시 레이어를 밀고 당기는 구조라면 이 압축·해제 시간이 전체 잡 시간의 상당 부분을 차지한다.

zstd(Zstandard)는 gzip 대비 비슷하거나 더 높은 압축률을 훨씬 빠른 속도로 달성하고, 멀티스레드 압축과 세밀한 레벨 조정을 지원한다. OCI 이미지 스펙과 containerd, BuildKit이 zstd 미디어 타입을 정식 지원하면서 프로덕션에서도 안전하게 쓸 수 있게 됐다.

gzip vs zstd 특성 비교

항목gzipzstd
멀티스레드미지원지원 (--threads)
레벨 범위1~91~19 (+ ultra 22)
동일 압축률 시 속도기준수 배 빠름
OCI 미디어 타입tar+gziptar+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 캐시는 중간 레벨로 반복 압축 시간을 줄이는 식으로 용도별 레벨을 분리하고, 외부 배포 경로의 호환성만 사전에 확인하면 안전하게 도입할 수 있다.