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
Ảnhinspect .SizeSố layerCó /big trong container?
alpine:3.22 (nền)4 133 9541—
giu25 103 5722có, 20 971 520 byte
hai25 104 0353không
mot4 125 5282không
Xoá ở layer sau làm ảnh to thêm 463 byte 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.

Hai lệnh cho hai con số, dùng đúng lệnh mà nói Cộng 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

    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ột RUN: 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.
    Cùng một layer 21 MB xuất hiện trong năm ảnh. Đĩa tốn 105 MB hay 21 MB? 21 MB. Layer được chia sẻ theo digest, nên năm ảnh cùng tham chiếu một bản duy nhất. Đây cũng là lý do layer không thể bị sửa: sửa là đổi digest, và mọi ảnh khác đang dùng nó sẽ không còn khớp.