스토리지 드라이버
스토리지 드라이버 (Storage drivers)
도커가 이미지 레이어를 어디에 어떻게 쌓아 두고, 컨테이너가 그 위에 쓰는 데이터를 어떤 식으로 저장하는지 궁금했던 적 있죠. 그 뒷일을 담당하는 게 스토리지 드라이버예요. 이미지와 컨테이너가 디스크를 어떻게 쓰는지 이해하면, 애플리케이션 데이터를 어디에 저장하는 게 좋은지, 성능 문제는 왜 생기는지 훨씬 명확해져요.
본문
스토리지 드라이버와 볼륨의 역할 구분
도커는 스토리지 드라이버로 이미지 레이어를 저장하고, 컨테이너의 쓰기 가능한(writable) 레이어에 데이터를 쌓아요. 컨테이너의 쓰기 레이어는 컨테이너가 삭제되면 함께 사라지기 때문에, 런타임에 생성되는 임시 데이터를 담기에 적합해요. 다만 스토리지 드라이버는 공간 효율에 최적화되어 있고, 특히 copy-on-write 파일시스템을 쓰는 드라이버는 네이티브 파일시스템보다 쓰기 속도가 느릴 수 있어요. 데이터베이스처럼 쓰기가 많은 애플리케이션은 성능 오버헤드의 영향을 받을 수 있죠. 쓰기가 많거나, 컨테이너보다 오래 살아야 하거나, 여러 컨테이너가 공유해야 하는 데이터는 볼륨을 쓰는 게 정답이에요.
이미지와 레이어
도커 이미지는 일련의 레이어로 구성돼요. 각 레이어는 이미지의 Dockerfile 지시어 하나를 나타내고, 마지막 레이어를 제외한 모든 레이어는 읽기 전용이에요. 아래 Dockerfile을 볼게요.
# syntax=docker/dockerfile:1
FROM ubuntu:22.04
LABEL org.opencontainers.image.authors="[email protected]"
COPY . /app
RUN make /app
RUN rm -r $HOME/.cache
CMD python /app/app.py
파일시스템을 건드리는 명령이 새 레이어를 만들어요. FROM은 ubuntu:22.04 이미지에서 레이어를 하나 만들어 내고, LABEL은 메타데이터만 바꾸므로 레이어를 만들지 않아요. COPY는 현재 디렉터리의 파일을 추가하고, 첫 RUN은 make로 빌드한 결과를 새 레이어에 써요. 두 번째 RUN은 캐시 디렉터리를 지우고 그 결과를 새 레이어에 써요. 마지막 CMD는 컨테이너에서 실행할 명령을 지정하는데, 이것 역시 메타데이터만 바꾸므로 이미지 레이어를 만들지 않아요.
여기서 중요한 점이 있어요. 각 레이어는 바로 앞 레이어와의 '차이'일 뿐인데, 파일을 추가하든 삭제하든 둘 다 새 레이어를 만든다는 거예요. 위 예시에서 $HOME/.cache를 지웠지만 그 디렉터리는 여전히 이전 레이어에 남아 있어서 이미지 전체 크기에 더해져요. 그래서 효율적인 이미지를 만들려면 Dockerfile 모범 사례나 멀티스테이지 빌드를 참고하는 게 좋아요.
이 레이어들은 서로 쌓여요. 새 컨테이너를 만들면 그 위에 얇은 쓰기 가능한 레이어('컨테이너 레이어')가 하나 더 올라가요. 실행 중인 컨테이너의 모든 변경(새 파일 쓰기, 기존 파일 수정, 삭제)은 전부 이 얇은 컨테이너 레이어에 기록돼요. 스토리지 드라이버는 이런 레이어들이 서로 어떻게 상호작용하는지를 담당해요.
컨테이너와 레이어
컨테이너와 이미지의 가장 큰 차이는 맨 위에 있는 쓰기 가능한 레이어예요. 컨테이너가 데이터를 추가하거나 수정하면 그 데이터는 전부 이 쓰기 레이어에 저장되고, 컨테이너가 삭제되면 이 레이어도 함께 삭제돼요. 아래에 있는 이미지는 그대로 남아요.
각 컨테이너는 자신만의 컨테이너 레이어를 갖기 때문에, 여러 컨테이너가 같은 이미지를 공유하면서도 각자 고유한 데이터 상태를 가질 수 있어요. 도커의 모든 스토리지 드라이버는 쌓을 수 있는 이미지 레이어와 copy-on-write(CoW) 전략을 사용해요. 만약 여러 컨테이너가 정확히 같은 데이터를 공유해야 한다면 볼륨을 써야 하고요.
컨테이너의 디스크 사용량 확인하기
실행 중인 컨테이너의 대략적인 크기는 docker ps -s로 확인할 수 있어요. 두 개의 열이 눈에 들어올 거예요.
size: 각 컨테이너의 쓰기 레이어가 (디스크에서) 사용하는 데이터 양.virtual size: 컨테이너가 쓰는 읽기 전용 이미지 데이터 + 쓰기 레이어size의 합.
참고로 여러 컨테이너가 읽기 전용 이미지 데이터의 일부 또는 전부를 공유할 수 있어요. 같은 이미지에서 만든 두 컨테이너는 읽기 전용 데이터를 100% 공유하고, 공통 레이어가 있는 서로 다른 이미지라면 그 레이어만 공유해요. 그래서 virtual size를 단순히 전부 더하면 실제 디스크 사용량을 과대평가하게 돼요.
docker ps -s에 잡히지 않는 디스크 사용도 있어요. 로깅 드라이버가 기록한 로그 파일(로테이션 미설정 시 크게 늘어날 수 있어요), 컨테이너가 쓰는 볼륨·바인드 마운트, 컨테이너 설정 파일, 스왑 활성화 시 메모리가 디스크에 내려간 양, 실험 기능인 체크포인트 등이 그 대상이에요.
copy-on-write(CoW) 전략
copy-on-write는 파일을 최대 효율로 공유·복사하는 전략이에요. 이미지의 낮은 레이어에 파일이 있고 다른 레이어(쓰기 레이어 포함)가 읽기만 하면 기존 파일을 그대로 써요. 그런데 다른 레이어가 그 파일을 수정하려는 순간에만 그 파일을 해당 레이어로 복사해 수정해요. 이렇게 하면 I/O가 줄고 이후 레이어들의 크기도 작아져요.
docker pull로 이미지를 받아오면 각 레이어가 따로 받아지고, 리눅스 호스트의 로컬 저장 영역(보통 /var/lib/docker/)에 저장돼요. 예를 들어 overlay2 드라이버라면 /var/lib/docker/overlay2 디렉터리 안에 레이어들이 들어 있어요. 디렉터리 이름은 레이어 ID와 대응하지 않아요.
CoW 덕분에 컨테이너가 효율적이라는 점을 다시 짚어 볼게요. 공간을 아낄 뿐 아니라 컨테이너 시작 시간도 단축돼요. 같은 이미지에서 컨테이너를 만들 때 도커는 얇은 쓰기 레이어만 만들면 되거든요. 만약 컨테이너를 만들 때마다 아래 이미지 스택 전체를 통째로 복사해야 한다면, 컨테이너 생성 시간과 디스크 사용량이 크게 늘어났을 거예요. 단, vfs 스토리지 드라이버는 CoW 파일시스템을 제공하지 않아서 컨테이너마다 이미지 데이터를 통째로 복사해요.
버전별 참고
이 페이지는 overlay2 같은 클래식 스토리지 드라이버를 설명해요. Docker Engine 29.0 이상은 신규 설치 시 기본적으로 containerd 이미지 저장소를 쓰는데, 여기선 클래식 드라이버 대신 스냅샷터(snapshotters)를 사용해요. 신규 설치거나 containerd 저장소로 마이그레이션했다면, 이 페이지의 레이어 개념은 여전히 유효하지만 명령어·예시가 실제 저장 방식과 다를 수 있어요. 운영 관점의 안내는 containerd 이미지 저장소를 보면 돼요.
더 알아보기
- 공식 원문: Storage drivers
- 관련: 볼륨으로 데이터 저장하기
- 관련: 스토리지 드라이버 선택하기
- 관련: containerd 이미지 저장소