Một image Docker không phải một tệp. Nó là một chồng layer, mỗi layer là một tập thay đổi so với layer dưới nó. Ý tưởng đó nghe đơn giản, nhưng nó kéo theo ba hệ quả mà lệnh docker images không cho bạn thấy — và một trong ba khiến bạn có thể nhầm dung lượng tới mười sáu lần.
Image thật có bao nhiêu layer
alpine:3.20 1 layer | 3,9 MB
debian:12-slim 1 layer | 26,8 MB
eclipse-temurin:21-jre-alpine 5 layer | 70,0 MB
postgres:16 14 layer | 151,5 MB
Số layer không tỉ lệ với dung lượng: debian:12-slim chỉ một layer mà nặng gấp bảy alpine.
Lệnh nào sinh ra layer
Hai Dockerfile làm đúng một việc, viết hai cách:
# a: ba lenh RUN rieng
FROM alpine:3.20
RUN echo mot > /a
RUN echo hai > /b
RUN echo ba > /c
ENV X=1
WORKDIR /app
LABEL vi.du=1
# b: gop lam mot
FROM alpine:3.20
RUN echo mot > /a && echo hai > /b && echo ba > /c
ENV X=1
WORKDIR /app
LABEL vi.du=1
Layer trong RootFS |
Dòng trong history |
Dung lượng | |
|---|---|---|---|
a — ba RUN |
5 | 8 | 3,91 MB |
b — gộp RUN |
3 | 6 | 3,91 MB |
Ba điều đọc được từ bảng này:
Không phải lệnh nào cũng sinh layer. history có 8 dòng nhưng RootFS chỉ có 5 layer. Nhìn kỹ hơn:
0B LABEL vi.du=1
8.19kB WORKDIR /app
0B ENV X=1
8.19kB RUN /bin/sh -c echo ba > /c
8.19kB RUN /bin/sh -c echo hai > /b
8.19kB RUN /bin/sh -c echo mot > /a
0B CMD ["/bin/sh"]
9.49MB ADD alpine-minirootfs...
ENV, LABEL, CMD là 0 B — chúng chỉ sửa metadata của image, không tạo layer filesystem. WORKDIR thì có, vì nó tạo thư mục.
Gộp RUN giảm số layer nhưng ở đây không giảm dung lượng. Cả hai đều 3,91 MB. Lời khuyên "gộp RUN để image nhỏ hơn" chỉ đúng khi bạn xoá thứ gì đó trong cùng lệnh — ví dụ apt-get install && rm -rf /var/lib/apt/lists/*. Xoá ở một RUN sau thì tệp vẫn nằm trong layer trước và vẫn tính dung lượng; layer chỉ ghi thêm dấu "đã xoá". Đây là điểm đáng nhớ nhất về mô hình layer: layer chỉ cộng thêm, không bao giờ trừ đi.
8,19 kB cho một tệp ba chữ cái. Đó là kích thước khối nhỏ nhất của filesystem. Mỗi layer đều có phần dôi cố định, nên hàng trăm layer tí hon vẫn tốn thật.
Ba lệnh, ba con số
Hỏi ba lệnh khác nhau xem alpine:3.20 to bao nhiêu:
docker image inspect .Size : 3,91 MB
docker history (dong nen) : 9,49 MB
docker images bao : 13,7 MB
Ba con số cho cùng một image. Câu trả lời nằm ở docker image ls --tree:
IMAGE ID DISK USAGE CONTENT SIZE
alpine:3.20 d9e853e87e55 13.7MB 4.17MB
├─ linux/amd64 c64c687cbea9 0B 0B
├─ linux/arm64/v8 45e09956dc66 13.6MB 4.09MB
├─ linux/arm/v6 37753b296504 0B 0B
...
- CONTENT SIZE (4,17 MB) — dữ liệu đã nén, tức lượng byte bạn tải qua mạng.
- DISK USAGE (13,7 MB) — chỗ nó chiếm trên đĩa sau khi bung ra, cộng cả bản nén còn giữ lại.
inspect .Size(3,91 MB) — tổng kích thước layer theo cấu hình image, cho một nền tảng.history9,49 MB — kích thước tệp tar gốc lúc image nền được dựng.
Bảng cây cũng cho thấy alpine:3.20 thật ra là bảy nền tảng trong một tên; máy tôi chỉ tải về linux/arm64/v8, sáu cái kia là 0 B.
Cái bẫy nén: image 4,0 MB chiếm 66,1 MB
Đây là chỗ ba con số kia không còn là chuyện học thuật. Hai image, mỗi cái ghi 50 MB dữ liệu, chỉ khác nội dung:
RUN dd if=/dev/zero of=/f bs=1M count=50 # 50 MB so 0
RUN dd if=/dev/urandom of=/f bs=1M count=50 # 50 MB ngau nhien
inspect .Size |
CONTENT SIZE | DISK USAGE | |
|---|---|---|---|
| 50 MB số 0 | 4,0 MB | 4,15 MB | 66,1 MB |
| 50 MB ngẫu nhiên | 53,9 MB | 56,5 MB | 118 MB |
Image chứa 50 megabyte dữ liệu báo cáo là 4,0 MB và chiếm 66,1 MB đĩa. Lệch mười sáu lần.
Lý do là số 0 nén lại gần như bằng không, còn inspect .Size cùng docker images báo theo kích thước đã nén. Dữ liệu thật của bạn không phải toàn số 0, nhưng log, JSON, mã nguồn, tệp locale đều nén rất tốt — nên khoảng cách giữa "image của tôi 200 MB" và chỗ nó thật sự chiếm trên đĩa luôn lớn hơn bạn nghĩ.
Muốn biết image chiếm bao nhiêu đĩa thì dùng docker image ls --tree và đọc cột DISK USAGE, đừng đọc cột SIZE quen thuộc.
Mười image chung một nền tốn bao nhiêu
Layer được chia sẻ giữa các image, nên tổng dung lượng không phải phép cộng. Tôi dựng 10 image, mỗi cái thêm một tệp 5 MB ngẫu nhiên lên cùng alpine:3.20, rồi lấy hiệu docker system df trước và sau:
tong docker images bao cao : 241,0 MB
dia that su tang : 163,8 MB
=> chia se tiet kiem : 77,2 MB (32%)
Cộng dồn cột SIZE cho ra con số cao hơn thực tế 47%, vì layer alpine chung được đếm mười lần.
Và còn một khoản nữa mà docker images không hề nhắc tới:
Images +163,8 MB
Build Cache +109,1 MB
Riêng cache build đã bằng hai phần ba dung lượng image. Trên máy tôi lúc đo, docker system df báo tổng 66 GB image với 37,31 GB thu hồi được — con số đó không xuất hiện ở bất cứ đâu trong docker images.
Nên có hai cách docker images làm bạn hiểu sai, và chúng ngược chiều nhau: nó cộng thừa layer dùng chung, và bỏ sót hoàn toàn cache build.
Ba luật rút ra
- Xoá tệp ở layer sau không làm image nhỏ đi. Phải xoá trong cùng
RUNđã tạo ra nó, hoặc dùng multi-stage — chủ đề của phần 9. docker imageskhông phải nguồn tin về dung lượng đĩa. Dùngdocker image ls --treecho từng image vàdocker system dfcho toàn máy.- Sắp xếp Dockerfile theo tần suất thay đổi, vì layer nào đổi thì mọi layer sau nó mất cache. Đó là chủ đề của phần sau.
Thử ba mươi giây
Xem Docker đang thật sự chiếm bao nhiêu đĩa trên máy bạn:
docker system df
docker image ls --tree | head -20
Nếu cột RECLAIMABLE của bạn lớn hơn vài GB — của tôi là 37 GB — thì phần lớn dung lượng đó là layer mồ côi và cache build, và docker images chưa bao giờ cho bạn thấy chúng.
Phần sau đo vòng đời container và những gì docker run thật sự làm trong 260 mili giây đó.