Bài 2
Xoá file rồi ảnh vẫn không nhỏ lại
Một ảnh Docker là chồng các layer chỉ đọc xếp lên nhau. Layer sau che được file của layer trước,
nhưng không sửa được layer trước — layer trước đã đóng gói và đã có digest. Nghĩa là
rm ở một lệnh RUN sau chỉ giấu file đi, còn byte thì vẫn nằm trong ảnh
và vẫn được tải về khi ai đó docker pull.
Ba ảnh, một nội dung
Ba Dockerfile dưới đây cùng dựa trên alpine:3.22, cùng ghi ra 20 MB dữ liệu ngẫu
nhiên. Chỉ khác nhau ở chỗ xoá lúc nào, hoặc có xoá không:
# giu — ghi ra roi de do
RUN dd if=/dev/urandom of=/big bs=1M count=20
# hai — xoa o mot lenh RUN khac
RUN dd if=/dev/urandom of=/big bs=1M count=20
RUN rm -f /big
# mot — ghi va xoa trong CUNG mot lenh RUN
RUN dd if=/dev/urandom of=/big bs=1M count=20 && rm -f /big
| Ảnh | inspect .Size | Số layer | Có /big trong container? |
|---|---|---|---|
alpine:3.22 (nền) | 4 133 954 | 1 | — |
giu | 25 103 572 | 2 | có, 20 971 520 byte |
hai | 25 104 035 | 3 | không |
mot | 4 125 528 | 2 | không |
hai đo được 25 104 035 byte, còn giu — bản giữ nguyên file 20 MB — đo
được 25 103 572 byte. Chênh lệch là 463 byte, và nó nghiêng về phía bản "đã xoá". Lệnh
rm không lấy lại được gì; nó chỉ thêm một layer ghi rằng file đã bị xoá.
Gộp hai thao tác vào một lệnh RUN thì khác hẳn: mot đo được 4 125 528
byte, tức là gấp 6,09 lần nhỏ hơn hai, trong khi cả hai đều không có
/big khi chạy.
docker history nói ra chỗ mất byte
docker history in kích thước từng layer, nên nhìn là thấy ngay 21 MB nằm ở đâu:
$ docker history dsize:hai --format '{{.Size}}\t{{.CreatedBy}}'
4.1kB RUN /bin/sh -c rm -f /big
21MB RUN /bin/sh -c dd if=/dev/urandom of=/big bs=1M count=20
0B CMD ["/bin/sh"]
9.2MB ADD alpine-minirootfs-3.22.6-aarch64.tar.gz /
$ docker history dsize:mot --format '{{.Size}}\t{{.CreatedBy}}'
4.1kB RUN /bin/sh -c dd if=/dev/urandom of=/big bs=1M count=20 && rm -f /big
0B CMD ["/bin/sh"]
9.2MB ADD alpine-minirootfs-3.22.6-aarch64.tar.gz /
Ở hai, layer 21 MB vẫn còn nguyên và layer rm chỉ nặng 4,1 kB. Ở
mot, chính lệnh ghi 20 MB lại chỉ tạo ra một layer 4,1 kB — vì khi layer được đóng
gói, file đã bị xoá rồi.
docker history lại được 9,2 + 21 = 30,2 MB, còn
docker image inspect .Size báo 25 104 035 byte, và
docker image ls báo 55,3 MB — cả ba đều đúng theo cách chúng đếm, vì kho ảnh
containerd giữ cả blob nén lẫn bản giải nén. Khi trích một con số về kích thước ảnh, nên nói kèm
lệnh đã sinh ra nó. Bài này dùng docker image inspect --format '{{.Size}}'.
Vì sao layer sau không xoá được layer trước
Mỗi layer là một tarball các thay đổi so với layer dưới, và có một digest SHA-256 tính trên nội dung đó. Digest ấy là thứ để chia sẻ layer giữa các ảnh và để kiểm tra khi tải về. Nếu một lệnh ở layer sau sửa được layer trước thì digest đổi, và mọi ảnh khác đang dùng chung layer đó sẽ hỏng.
Nên việc xoá được ghi lại bằng một cách khác: hệ thống file chồng lớp tạo một whiteout —
với overlayfs là một node thiết bị ký tự rỗng mang đúng tên file bị xoá. Tiến trình trong container
nhìn thấy file biến mất; byte của layer dưới vẫn nằm nguyên trong ảnh. Đó chính là 4,1 kB mà
docker history ghi cho lệnh rm.
Phòng thí nghiệm
Ghép các lệnh RUN của bạn, đặt số MB ghi ra và xoá đi, và đánh dấu bước nào nằm chung
một lệnh RUN với bước trên. Mô hình lệch tối đa 12 552 byte so với
docker image inspect trên ba ảnh đã dựng thật ở bảng trên.
Các lệnh RUN của bạn
Các ảnh đã dựng thật:
Ảnh dựng ra
Ba cách tránh
-
Gộp vào một
RUN. Tải, giải nén, build, dọn — tất cả nối bằng&&trong một lệnh. Đây là cách cho ra ảnhmot4 125 528 byte ở bảng trên. Giá phải trả là cả khối đó là một mắt cache duy nhất: đổi một chữ trong đó thì chạy lại toàn bộ. -
Build nhiều stage. Stage đầu có đủ trình biên dịch và thư viện; stage cuối
COPY --fromđúng thứ cần. Mọi layer của stage đầu không đi vào ảnh cuối, nên thứ không đượcCOPYsang thì không tồn tại trong ảnh. -
Đừng ghi ra thứ mình sẽ xoá. Ví dụ hay gặp là cache của trình quản lý gói:
apk add --no-cache,apt-get … && rm -rf /var/lib/apt/lists/*trong cùng mộtRUN, hoặc--mount=type=cachecủa BuildKit để cache nằm ngoài ảnh.
Tự kiểm tra
Ảnh của bạn 1,2 GB. Bạn thêm RUN rm -rf /root/.cache ở cuối Dockerfile và build lại. Ảnh còn bao nhiêu?
Vẫn khoảng 1,2 GB, cộng thêm vài kB cho layer ghi việc xoá. Đúng trường hợp hai trong bảng đo: xoá ở layer sau không lấy lại được byte nào. Phải xoá trong cùng lệnh RUN đã tạo ra cache đó, hoặc dùng multi-stage.
Vì sao hai lại lớn hơn giu, chứ không bằng nhau?
Vì hai có thêm một layer thứ ba chỉ để ghi lại việc xoá. Layer đó đo được 4,1 kB theo docker history; phần chênh trong inspect .Size là 463 byte. Dù tính theo cách nào thì xoá cũng chỉ làm ảnh to thêm.
Bạn muốn ảnh nhỏ nhưng cũng muốn cache build tốt. Hai mục tiêu đó có mâu thuẫn không?
Có, nếu chỉ dùng cách gộp mọi thứ vào mộtRUN: khối càng lớn thì cache càng thô. Multi-stage giải được cả hai — stage build giữ nhiều bước nhỏ để cache mịn, stage cuối chỉ COPY --from kết quả, nên kích thước ảnh không phụ thuộc số bước ở stage build.