[Container] Docker와 containerd 이미지 관리 비교 - 3. 같은 이미지가 중복 저장된 이유
TL;DR
k3s 클러스터에서 컨테이너 런타임을 dockerd에서 containerd로 변경한 뒤, 동일한 이미지가 두 벌 저장되어 디스크가 낭비되고 있었다. 같은 이미지인데 docker로 보면 20.3GB, crictl로 보면 10.4GB로 표시되었다.
$ docker images | grep co-detr
co-detr-coco-app 1.0 f05ffb0c16af 20.3GB
$ sudo crictl images | grep co-detr
docker.io/co-detr-coco-app 1.0 f05ffb0c16af 10.4GB
분석 결과는 다음과 같다.
- 중복 저장: 각 런타임이 독립적인 경로에 이미지를 저장하기 때문
dockerd:/var/lib/docker/overlay2containerd:/var/lib/containerd또는/var/lib/rancher/k3s/agent/containerd(k3s)
- 이름 차이: dockerd는
docker.io를 생략하고, containerd는 fully qualified name으로 표시 - 크기 차이: 런타임별 계산 방식이 다름
dockerd: 압축 해제된 총 크기containerd: 압축된 blob 크기 기반
- 권장 사항: k8s/k3s 환경에서는
crictl로 확인하는 것이 적합
이전 글에서 다룬 컨테이너 이미지와 런타임, 컨테이너 파일 시스템과 CLI에 대한 배경 지식을 바탕으로, 이 글에서는 실제 분석 과정을 다룬다.
문제
이미지 ID가 f05ffb0c16af로 같은 두 이미지를 각자 다른 CLI로 확인했는데, 이미지 크기가 다르다.
$ sudo crictl images | grep co-detr
docker.io/co-detr-coco-app 1.0 f05ffb0c16af 10.4GB
$ docker images | grep co-detr
co-detr-coco-app 1.0 f05ffb0c16af 8 months ago 20.3GB
하나의 노드에 같은 이미지가 두 벌있는 것은 목격하기 쉽지 않은 일이다.
- 보통 로컬에서 이미지를 빌드하게 되면, 동일한 Dockerfile을 이용해 빌드하더라도 이미지 ID가 달라진다.
- 레지스트리에서 이미지를 pull하게 되더라도, 컨테이너 런타임이 같은 이미지가 있는지 확인한다. 같은 이미지가 있을 경우, 다운로드하지 않는다.
기존 레거시 k3s 클러스터에서 컨테이너 런타임으로 dockerd를 이용하고 있다가, k3s에 내장되어 있는 containerd를 이용하도록 변경하면서 발생한 일이다. k3s 클러스터가 프라이빗 레지스트리를 이용하도록 미러링 설정되어 있기 때문에, 두 이미지는 모두 프라이빗 레지스트리에서 pull해온 것이다.
co-detr-coco-app:1.0: 기존에 dockerd 런타임을 사용할 때 pull한 이미지docker.io/co-detr-coco-app:1.0: containerd 런타임을 사용하도록 변경하면서 pull한 이미지
노드에 장애가 생기지는 않았다. 다만, 습관처럼 루트 파티션 용량을 검사하다 70% 정도가 찬 것을 통해, 어디 크기가 큰 파일이 있는지 확인하다가 발견한 문제다. 결과적으로는 같은 이미지가 중복 저장되어 디스크가 낭비되고 있던 셈이다.
# docker 이미지 용량 확인
$ sudo du -sh /var/lib/docker/overlay2/
38G /var/lib/docker/overlay2/
# k3s 내장 containerd 이미지 용량 확인
$ sudo du -sh /var/lib/rancher/k3s/agent/containerd/
73G /var/lib/rancher/k3s/agent/containerd/
그런데 여기서 아래와 같은 의문이 발생한다.
- 어떻게 동일한 이미지가 중복 저장될 수 있었는가?
- 어떻게 동일한 이미지의 이름이 다르게 나타날 수 있는가?
- 어떻게 동일한 이미지의 크기가 다르게 나타날 수 있는가?
분석
서버 환경 설명
dockerd와 k3s가 설치되어 있기 때문에, 이 서버에는 두 개의 containerd 인스턴스가 실행 중이다.
1. dockerd
└── dockerd (/var/run/docker.sock)
└── containerd (/run/containerd/containerd.sock) # dockerd가 실행
2. k3s
└── k3s 내장 containerd (/run/k3s/containerd/containerd.sock) # k3s가 내장
그리고 이 서버에는 Docker와 k3s가 모두 설치되어 있기 때문에, 각각의 설치 과정에서 다른 CLI 도구들이 함께 설치되었다.
- Docker 설치 시:
dockerCLI와containerd(그리고ctrCLI)가 함께 설치됨 - k3s 설치 시:
crictl이 k3s 바이너리에 내장되어 설치됨
각 CLI가 어떤 소켓에 연결되는지 정리하면 다음과 같다.
| CLI | 기본 소켓 | 연결 대상 |
|---|---|---|
| docker | /var/run/docker.sock | dockerd |
| ctr | /run/containerd/containerd.sock | dockerd가 사용하는 containerd |
| crictl | /run/k3s/containerd/containerd.sock | k3s 내장 containerd |
참고: k3s는 crictl을 자체 바이너리에 내장하고, 기본값으로 k3s containerd 소켓을 사용하도록 설정되어 있다. ctr은
--address옵션으로 소켓을 변경할 수 있다.
동일한 이미지가 중복 저장된 이유
각각의 컨테이너 런타임이 독립적으로 이미지를 관리하기 때문이다. 이전 글에서 살펴보았듯이 dockerd와 containerd는 서로 다른 스토리지 드라이버 구현체를 사용하기 때문에 저장 구조도 다른데, 이미지 저장 경로가 다른 것이 직접적인 원인이다.
레지스트리 (원본)
├── manifest
├── config.json ← sha256 = 이미지 ID (동일)
└── layers (tar.gz) ← 압축된 레이어들 (동일)
│
├──→ docker pull ──→ /var/lib/docker/overlay2/
│ (overlay2 스토리지 드라이버로 관리)
│
└──→ k3s pull ──→ /var/lib/rancher/k3s/agent/containerd/
(overlayfs snapshotter로 관리)
각 런타임의 상세한 디렉토리 구조는 이전 글에서 확인할 수 있다.
같은 이미지를 dockerd와 containerd에서 각각 pull했을 때, 무엇이 같고 무엇이 다른지 정리하면 다음과 같다.
| 항목 | 동일 여부 |
|---|---|
| 이미지 ID (config sha256) | 동일 |
| 레이어 내용 (파일들) | 동일 |
| 저장 경로 | 다름 |
| 디렉토리 구조/메타데이터 | 다름 |
이미지의 본질(ID, 레이어 내용)은 동일하지만, 각 런타임이 이를 저장하고 관리하는 방식이 다르다. 결과적으로, 같은 이미지를 dockerd로도 pull하고 k3s(containerd)로도 pull하면, 디스크에는 동일한 레이어가 두 벌 존재하게 된다.
동일한 이미지의 이름이 다르게 나타나는 이유
런타임별로 이미지 이름을 표시하는 방식이 다르다.
| 런타임 | 표시 방식 | 예시 |
|---|---|---|
| dockerd | 기본 레지스트리(docker.io) 생략 | co-detr-coco-app:1.0 |
| containerd | fully qualified name으로 정규화 | docker.io/co-detr-coco-app:1.0 |
containerd는 이미지를 fully qualified name(전체 경로)으로 저장한다.
[registry]/[namespace]/[repository]:[tag]
예: docker.io/library/nginx:latest
또한, containerd 미러링 설정을 사용하는 경우에도, 실제 다운로드는 미러 레지스트리에서 진행하지만 이미지 이름은 원래 요청한 형식으로 유지한다.
요청: co-detr-coco-app:1.0
↓(정규화)
docker.io/co-detr-coco-app:1.0
↓ (미러 설정 확인)
실제 다운로드: private.registry.com:5000/co-detr-coco-app:1.0
↓
저장되는 이름: docker.io/co-detr-coco-app:1.0 ← 원래 요청 유지
이는 containerd 미러링이 투명하게 동작하도록 설계되었기 때문이다. 클라이언트 입장에서 미러 존재 여부와 관계없이 동일한 이름으로 이미지를 참조할 수 있다.
동일한 이미지의 크기가 다르게 나타나는 이유
이미지 크기 차이는 런타임 계산 방식 차이와 CLI 단위 표시 방식 차이가 모두 영향을 준다.
실제 검증 결과
동일한 이미지를 각 CLI로 확인한 결과이다.
| 명령어 | 소켓 | 이미지 크기 |
|---|---|---|
| docker images | /var/run/docker.sock | 20.3GB |
| crictl images | /run/k3s/containerd/containerd.sock | 10.4GB |
| ctr images | /run/containerd/containerd.sock | (없음) |
| ctr -n k8s.io images | /run/k3s/containerd/containerd.sock | 9.7GiB |
# 1. docker로 확인
$ docker images | grep co-detr
co-detr-coco-app 1.0 f05ffb0c16af 20.3GB
# 2. crictl로 확인
$ sudo crictl images | grep co-detr
docker.io/co-detr-coco-app 1.0 f05ffb0c16aff 10.4GB
# 3. ctr로 확인 (Docker의 containerd, default 네임스페이스)
$ sudo ctr images ls
(비어있음 - Docker는 containerd가 아닌 /var/lib/docker에서 이미지 관리)
# 4. ctr로 확인 (k3s containerd, k8s.io 네임스페이스)
$ sudo ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep co-detr
docker.io/co-detr-coco-app:1.0 ... 9.7 GiB linux/amd64 ...
실제 검증 결과 분석
1 vs 2: 런타임 간 계산 방식 차이
| 런타임 | 크기 계산 방식 |
|---|---|
| dockerd | 모든 레이어의 압축 해제된(uncompressed) 총 크기 |
| containerd | Content Store에 저장된 레이어 blob의 압축된(compressed) 크기 기반 |
dockerd의 20.3GB와 containerd의 10.4GB 차이(약 2배)는 압축 상태의 차이가 주요 원인이다.
레지스트리에서 이미지를 pull할 때 레이어는 압축된 상태(tar.gz)로 전송되며, dockerd는 이를 압축 해제한 크기를, containerd는 압축된 blob 크기를 기준으로 계산한다.
이 구조 덕분에 containerd는:
- 디스크 저장 시에는 압축된 상태로 공간 효율적으로 저장하고,
- 컨테이너 실행 시에만 필요한 레이어를 압축 해제하여 제공할 수 있다.
참고: containerd의 이미지 크기와 Content Store
1편에서 다룬 것처럼, containerd는 이미지를 두 단계로 관리한다:
- Content Store: 레이어 blob을 압축 상태로 저장 (
io.containerd.content.v1.content/blobs/sha256/)- Snapshotter: 컨테이너 실행 시 압축을 해제하여 파일시스템으로 마운트 (
io.containerd.snapshotter.v1.overlayfs/snapshots/)
crictl images에서 보이는 크기는 Content Store의 압축된 blob 크기를 기반으로 한다. 반면, 컨테이너가 실제로 실행될 때는 Snapshotter가 이를 압축 해제하므로 실행 중인 컨테이너의 파일시스템 크기는 더 커진다.
2 vs 4: 같은 런타임, CLI 단위 표시 차이
crictl과 ctr은 같은 k3s containerd를 바라보지만, 표시 단위가 다르다. 표시 단위를 맞춰 보면, 같은 크기임을 확인할 수 있다.
- crictl: 10.4 GB (10^9 bytes 기준, SI 단위)
- ctr: 9.7 GiB (2^30 bytes 기준, 이진 단위)
- 환산: 9.7 GiB × 1.073741824 ≈ 10.4 GB
3이 비어있는 이유
ctr의 기본 네임스페이스는 default인데, dockerd는 이미지를 containerd에 저장하지 않고 /var/lib/docker/에서 자체 관리한다. 따라서 dockerd의 containerd에 연결해도 이미지가 보이지 않는다.
정리
dockervscrictl: 런타임 간 크기 계산 방식 차이 (본질적 차이)crictlvsctr: CLI 간 단위 표시 방식 차이 (같은 값, 다른 표현)
결론
처음에 제시한 의문에 대한 답을 정리하면 다음과 같다.
| 의문 | 답변 |
|---|---|
| 동일한 이미지가 중복 저장된 이유 | 각 런타임이 독립적인 스토리지 드라이버로 이미지를 관리하기 때문 |
| 동일한 이미지의 이름이 다르게 나타나는 이유 | dockerd는 docker.io를 생략하고, containerd는 fully qualified name으로 표시하기 때문 |
| 동일한 이미지의 크기가 다르게 나타나는 이유 | 런타임별 크기 계산 방식 차이 + CLI별 단위 표시 방식 차이 |
배운 점
숫자를 그대로 믿지 말 것
docker images에서 20GB로 보이던 이미지가 crictl images에서는 10GB로 보였다. 같은 이미지인데 숫자가 다르다고 당황할 필요 없다. 어떤 CLI로 어떤 런타임을 바라보느냐에 따라 계산 방식과 표시 단위가 다를 수 있다.
환경을 이해할 것
이번 문제를 해결하면서 컨테이너 런타임의 구조에 대해 이해하게 되었다. dockerd와 containerd가 어떻게 다른지, 이미지가 어디에 저장되는지, 각 CLI가 어떤 소켓으로 통신하는지를 알아야 문제 상황을 정확히 파악할 수 있다.
적절한 도구를 사용할 것
k8s/k3s 환경에서는 crictl, dockerd 환경에서는 docker를 사용하는 것이 맞다. 쿠버네티스가 보는 것과 동일한 뷰를 보려면, 쿠버네티스가 사용하는 CRI 인터페이스를 통해 확인해야 한다.
디스크 관리에 주의할 것
런타임이 여러 개 설치된 환경에서는 같은 이미지가 중복 저장될 수 있다. 여태까지 살펴본 것처럼, 같은 이미지가 두 벌 저장되어 디스크가 낭비될 수 있다. 주기적으로 사용하지 않는 런타임의 이미지를 정리하고, 가능하다면 하나의 런타임으로 통일하는 것이 좋다.
# Docker 이미지 정리
docker image prune -a
# containerd 이미지 정리 (k3s)
sudo crictl rmi --prune
k8s/k3s 환경에서의 권장 사항
k8s/k3s 환경에서는 crictl을 사용하는 것이 적합하다.
- 쿠버네티스가 CRI를 통해 containerd와 통신하므로, crictl을 사용하면 쿠버네티스가 보는 것과 동일한 뷰를 볼 수 있다.
dockerCLI(dockerd)로 이미지 크기를 확인할 경우 실제보다 크게 보일 수 있으니 당황하지 말자.
| 환경 | 권장 CLI | 이유 |
|---|---|---|
| k3s/K8s | crictl | CRI 인터페이스, 쿠버네티스 관점 |
| dockerd 단독 | docker | dockerd가 관리하는 전체 정보 |
| containerd 직접 | ctr | 네이티브 API, 디버깅용 |
장애가 발생한 것은 아니었지만, 디스크 용량을 확인하다 우연히 발견한 현상 덕분에 컨테이너 런타임의 동작 방식을 깊이 이해할 수 있었다. 때로는 사소한 의문이 가장 좋은 학습 기회가 된다
댓글남기기