Chạy 100 container từ cùng một image 200MB — chúng có tốn 20GB đĩa không? Không, chúng tốn khoảng 200MB cộng một chút cho mỗi container. Đây là một trong những phần khéo léo nhất của Docker: image layers và overlayfs. Image không phải một khối đơn mà là chồng nhiều lớp chỉ-đọc dùng chung được, còn mỗi container chỉ thêm một lớp ghi mỏng chồng lên trên. Bài này (phần 6 loạt Docker) chạy thật để phơi bày các layer, tự dựng overlayfs, và đo mức tiết kiệm đĩa.

Image = chồng nhiều layer chỉ-đọc

Mỗi lệnh trong Dockerfile (RUN, COPY, ADD) tạo ra một layer — một tập hợp các thay đổi của hệ thống file. Image cuối là các layer đó xếp chồng lên nhau. docker history phơi bày chúng cùng kích thước:

docker history python:3.11-alpine
# 55.6MB  RUN apk add python...    (layer cài Python)
# 2.94MB  RUN apk add ca-certs...
# 0B      ENV/CMD ...              (metadata, không tốn đĩa)

Điểm mấu chốt: layer được chia sẻ. Image python:3.11-alpine dựa trên alpine, nên layer alpine của nó là chính layer của image alpine — đo thật: python:3.11-alpine có 4 layer, alpine có 1 layer, và layer alpine đó không bị nhân đôi trên đĩa. Mọi image dựa trên alpine chia sẻ chung một bản.

Ảnh chụp đoạn mã nền tối minh hoạ image layers và overlayfs vì sao 100 container cùng image không tốn 100 lần đĩa, image bằng chồng nhiều layer chỉ đọc mỗi lệnh trong Dockerfile RUN COPY ADD tạo một layer python alpine bằng layer alpine cộng layer cài python xếp chồng docker history python 3.11-alpine xem từng layer cộng kích thước layer alpine dùng chung cho mọi image dựa trên alpine, overlayfs gộp nhiều layer thành một cây file lowerdir các layer image chỉ đọc dùng chung upperdir layer ghi của container riêng mỗi container merged cây file container thấy bằng lower cộng upper chồng lên mount -t overlay -o lowerdir upperdir workdir merged, copy-on-write sửa file mới copy sang upper đọc file lấy từ lower không tốn thêm đĩa ghi sửa file copy-up bản sao sang upper sửa ở đó lower image không bao giờ đổi mọi container chia sẻ được, hệ quả tiết kiệm 100 container cùng image bằng 1 bản layer chỉ đọc dùng chung cộng 100 layer ghi nhỏ chỉ chứa thay đổi của từng cái khởi động container bằng chỉ tạo một upperdir rỗng tức thì

Hình 1: Image là chồng layer chỉ-đọc (mỗi lệnh Dockerfile một layer); overlayfs gộp lowerdir (layer image chỉ đọc, dùng chung) + upperdir (lớp ghi riêng) thành merged; copy-on-write khiến lower không bao giờ đổi.

overlayfs và copy-on-write, dựng bằng tay

Cơ chế gộp các layer là overlayfs — một union filesystem. Nó xếp chồng:

  • lowerdir: các layer image (chỉ đọc, dùng chung giữa mọi container).
  • upperdir: lớp ghi của container (riêng mỗi container).
  • merged: cây file container thấy — là lower và upper chồng lên nhau.

Mình tự dựng một overlay để thấy copy-on-write hoạt động:

Ảnh chụp bảng kết quả chạy thật docker layers cộng overlayfs output thật, một docker history image là nhiều layer cộng kích thước python 3.11-alpine 55.6MB RUN apk add python layer cài python 2.94MB RUN apk add ca-certs 0B ENV CMD metadata python có 4 layer alpine có 1 layer dùng chung, hai overlayfs copy-on-write tự dựng lower cộng upper merged đọc base.txt ra tu-image-goc từ lower chỉ đọc ghi sửa qua merged lower base.txt bằng tu-image-goc gốc không đổi upper base.txt bằng sua-boi-container bản copy đã sửa upper chứa base.txt new.txt merged thấy sua-boi-container, ba 3 container cùng image layer ghi tí hon phần lớn dùng chung c1 4.1kB virtual 67.9MB c2 4.1kB virtual 67.9MB c3 4.1kB virtual 67.9MB mỗi container chỉ thêm 4.1kB 67.9MB image dùng chung, kết image chỉ đọc dùng chung cộng lớp ghi riêng nhỏ bằng tiết kiệm đĩa lớn và khởi động container gần như tức thì chỉ tạo upperdir rỗng

Hình 2: Chạy thật — docker history phơi bày layer (55.6MB cài Python...), python 4 layer / alpine 1 layer; overlay tự dựng: đọc base.txt lấy từ lower ("tu-image-goc"), sửa qua merged thì lower giữ nguyên còn upper có bản copy ("sua-boi-container"); ba container cùng image mỗi cái thêm 4.1kB, 67.9MB dùng chung.

  • Đọc lấy từ lower: cat merged/base.txt trả về tu-image-goc — file nằm ở lower (chỉ đọc), không tốn thêm đĩa.
  • Ghi thì copy-up sang upper: sửa merged/base.txt thành sua-boi-container. Kết quả đo thật: lower/base.txt vẫn là tu-image-goc (không đổi!), còn upper/base.txt là bản copy đã sửa. File mới new.txt cũng nằm ở upper. merged thấy phiên bản đã sửa. Đây là copy-on-write: chỉ khi ghi, một bản sao mới được tạo ở lớp trên; lớp dưới (image) không bao giờ thay đổi — nên nhiều container chia sẻ được nó an toàn.

Đo thật mức tiết kiệm

Chạy ba container từ cùng python:3.11-alpine, xem kích thước lớp ghi của từng cái: mỗi container SizeRw = 4.1kB, với virtual = 67.9MB (kích thước "ảo" gồm cả image dùng chung). Nghĩa là ba container không tốn 3 × 67.9MB; chúng dùng chung một bản 67.9MB và mỗi cái chỉ thêm 4.1kB cho những gì nó ghi. Với 100 container, tiết kiệm còn ngoạn mục hơn.

Đây cũng là lý do container khởi động gần như tức thì: tạo một container mới chỉ cần tạo một upperdir rỗng rồi overlay lên các layer image có sẵn — không copy gì cả.

Đánh đổi cần cân nhắc

Ghi nhiều vào lớp container thì mất lợi thế — dùng volume. Copy-on-write rẻ khi container chủ yếu đọc image và ghi ít. Nhưng nếu app ghi khối lượng lớn (database, log) vào lớp ghi của container, mỗi lần sửa file lớn phải copy-up cả file, và dữ liệu đó mất khi container bị xoá. Với dữ liệu bền/ghi nhiều, dùng volume hoặc bind mount (chủ đề bài sau) — chúng nằm ngoài overlayfs.

Thứ tự và cách gộp layer ảnh hưởng kích thước image. Vì mỗi lệnh là một layer cộng dồn, xoá file ở một layer sau không làm image nhỏ đi — file vẫn nằm ở layer trước. Ví dụ RUN apt install ... rồi RUN apt clean ở hai layer để lại rác ở layer đầu. Gộp vào một RUN (dùng &&) hoặc dùng multi-stage build (bài sau) mới giảm được kích thước thật.

overlayfs có giới hạn và điểm lạ. Đổi tên file lớn qua ranh giới lower/upper, một số thao tác trên thư mục có thể chậm hoặc có hành vi khác hệ thống file thường. Với hầu hết ứng dụng không sao, nhưng workload I/O đặc thù (database) nên đặt dữ liệu trên volume thay vì lớp overlay để có hiệu năng và ngữ nghĩa đĩa chuẩn.

Ba ý mang về

  1. Image là chồng layer chỉ-đọc dùng chung: đo thật docker history phơi bày từng layer + kích thước; python:3.11-alpine 4 layer, alpine 1 layer, và layer alpine dùng chung cho mọi image dựa trên nó — không nhân đôi trên đĩa.
  2. overlayfs + copy-on-write: đo thật tự dựng overlay, sửa file qua merged thì lower (image) giữ nguyên còn bản copy nằm ở upper — image không bao giờ đổi nên nhiều container chia sẻ an toàn.
  3. Tiết kiệm đĩa và khởi động tức thì: đo thật 3 container cùng image mỗi cái chỉ thêm 4.1kB (67.9MB dùng chung); tạo container = tạo upperdir rỗng — nhưng ghi nhiều nên dùng volume, và gộp layer để giảm kích thước image.

Nguồn

Phần sau ta xem cách image được dựng: Dockerfile và cache layer — vì sao thứ tự các lệnh quyết định tốc độ build, và đặt COPY . . sai chỗ làm hỏng cache khiến build lại từ đầu mỗi lần.