Năm phần đầu đo cách container bị cô lập và giới hạn: namespace cho cái nó thấy, cgroups cho cái nó dùng. Phần này chuyển sang tầng lưu trữ: cái filesystem mà một container nhìn thấy khi mở / đến từ đâu. Câu trả lời giải thích nhiều điều lập trình viên backend gặp hằng ngày: vì sao một image 155 MB chạy thành mười container không tốn 1,5 GB, vì sao container to lên bất ngờ khi ứng dụng sửa một file lớn, và vì sao dữ liệu ghi trong container biến mất khi container bị xoá. Tất cả đến từ một cơ chế: OverlayFS, hệ thống file phủ lớp làm rootfs của container. Bài này tháo nó ra bằng cách đo từng lớp, rồi đo chi phí thật của việc ghi.
Ba lớp chồng lên nhau và copy-on-write

Hình 1: rootfs của container là overlay ghép ba lớp, cơ chế copy-on-write, và cách đo bằng docker diff và ps -s.
Khi một container mở /, cái nó thấy là một mount kiểu overlay, không phải một phân vùng đĩa. OverlayFS ghép rootfs từ ba phần. lowerdir là các lớp của image, chỉ đọc, và quan trọng nhất, dùng chung cho mọi container tạo từ cùng một image; mười container chạy từ một image cùng trỏ vào một bộ lowerdir, không nhân bản. upperdir là lớp ghi riêng của từng container, nơi chứa mọi thay đổi container đó tạo ra. workdir là một vùng tạm để overlay thực hiện các thao tác nội bộ một cách nguyên tử. Khi đọc một file, overlay lấy từ upperdir nếu có, không thì rơi xuống lowerdir; container thấy một cây thư mục liền mạch mà không biết file đến từ lớp nào.
Cơ chế quyết định nhất là ghi. Khi container sửa một file vốn nằm trong lowerdir, tức một file đến từ image, overlay không thể ghi đè lớp chỉ đọc, nên nó thực hiện copy-on-write: copy toàn bộ file đó lên upperdir rồi mới sửa bản copy. Điều cốt lõi là copy xảy ra ở mức file, không phải mức block: sửa một byte của một file lớn cũng kéo theo copy cả file. Để đo, dùng ba công cụ tách bạch thay vì du bên trong container, vì du đếm cả file từ lowerdir nên không cho biết cái gì thật sự tốn chỗ mới. docker ps -s cho hai số, Size là lớp ghi riêng và virtual là tổng gồm cả image; docker diff liệt kê đúng file nào đã bị copy-up, đánh dấu C cho changed, A cho added, D cho deleted. Cách đo: đọc mount / để thấy ba lớp; tạo một image có file 50 MB rồi ghi một byte vào nó để xem lớp ghi phình bao nhiêu; chạy ba container từ một image để xem image có bị nhân lên không; và ghi một file mới để so với trường hợp copy-on-write.
Đo thật: ghi một byte vào file 50 MB tốn 50 MB

Hình 2: overlay ba lớp, copy-on-write làm sửa một byte tốn 50 MB, image dùng chung không nhân lên, và file mới chỉ cộng vào upperdir.
| phép đo | kết quả |
|---|---|
mount / |
overlay với lowerdir, upperdir, workdir |
| lớp ghi riêng lúc vừa chạy | 4,1 kB (virtual 208 MB gồm image) |
| ghi 1 byte vào file 50 MB của image → lớp ghi | 52,4 MB, docker diff C /big.bin |
| 3 container từ cùng image, mỗi lớp ghi | 4,1 kB mỗi cái, image 155 MB tính 1 lần |
| ghi file mới 30 MB → lớp ghi | 31,5 MB, docker diff A /newfile.bin |
rootfs là một mount overlay ghép ba lớp. Đọc mount / của container cho thấy rõ cấu trúc: lowerdir trỏ vào hai snapshot của image, chỉ đọc; upperdir trỏ vào một snapshot riêng, nơi ghi; workdir là vùng tạm cạnh đó. Đây không phải một đĩa ảo mà container độc chiếm, mà là một khung nhìn ghép lại từ các lớp, phần lớn là của image và dùng chung. Khi container vừa chạy và chưa làm gì, docker ps -s cho Size là 4,1 kB, tức lớp ghi riêng gần như trống, và virtual là 208 MB, gồm cả các lớp image. docker diff trống: container chưa thay đổi gì so với image.
Ghi một byte vào một file 50 MB của image tốn nguyên 50 MB. Dùng một image có sẵn file /big.bin 50 MB, rồi ghi đúng một byte vào giữa file đó mà không cắt ngắn: docker diff lập tức cho C /big.bin, đánh dấu file đã bị copy-up, và Size của lớp ghi riêng nhảy từ 4,1 kB lên 52,4 MB. Sửa một byte đã tốn hơn 50 MB đĩa, vì OverlayFS copy ở mức file: để sửa dù chỉ một byte của một file nằm trong lớp chỉ đọc, nó phải copy toàn bộ file lên upperdir trước. Hệ quả thực tế đáng nhớ: một ứng dụng ghi vào một file lớn vốn đóng gói sẵn trong image, ví dụ một database bundled hay một file cấu hình lớn, sẽ nhân đôi dung lượng file đó ngay lần ghi đầu, và lần ghi đầu đó cũng chậm bất thường vì phải copy cả file. Đây là lý do nên tránh để dữ liệu hay giữ file sẽ bị sửa nằm trong image; chúng thuộc về volume, chủ đề sẽ gặp trong các phần vận hành.
Ba container từ cùng image chia sẻ lớp chỉ đọc, không nhân lên. Chạy ba container s1, s2, s3 từ cùng debian:12: mỗi cái có Size 4,1 kB và virtual 155 MB. Ba container, nhưng 155 MB của image được tính một lần, vì cả ba cùng trỏ vào một bộ lowerdir chỉ đọc; chỉ các lớp ghi riêng, mỗi cái 4,1 kB, là riêng biệt. docker system df xác nhận ở quy mô lớn: tổng image là 24,85 GB nhưng 12,73 GB là reclaimable, phần lớn là các lớp trùng được chia sẻ giữa nhiều image và container. Đây là lợi ích cốt lõi của mô hình lớp: chạy một trăm container từ một image không tốn một trăm lần dung lượng image, chỉ tốn một lần image cộng với các lớp ghi nhỏ. Đó cũng là lý do image được xây theo lớp và các lớp cơ sở như debian hay alpine được tái dùng rộng rãi: một lớp đã tải về được mọi image và container dùng chung lớp đó chia sẻ.
Ghi một file mới chỉ cộng đúng kích thước nó, không copy-on-write. Ghi một file hoàn toàn mới 30 MB, không có trong image: docker diff cho A /newfile.bin, đánh dấu added, và lớp ghi riêng tăng lên 31,5 MB, đúng bằng kích thước file cộng chút ít. Không có copy-on-write ở đây vì không có file gốc nào để copy; file mới đơn giản được ghi thẳng vào upperdir. Khác biệt giữa trường hợp này và copy-on-write là mấu chốt: ghi dữ liệu mới vào container tốn đúng kích thước dữ liệu, nhưng sửa một file có sẵn từ image tốn kích thước cả file đó. Hiểu điều này giúp đoán được dung lượng lớp ghi sẽ phình ra sao: một ứng dụng chủ yếu tạo file mới thì lớp ghi lớn theo dữ liệu; một ứng dụng sửa file lớn có sẵn thì lớp ghi phình theo kích thước file bị đụng.
Đánh đổi cần cân nhắc
Dữ liệu cần giữ phải nằm trong volume, không trong lớp ghi. Lớp ghi riêng của container là tạm: khi container bị xoá, cả upperdir biến mất cùng nó. Mọi thứ ghi vào filesystem container, log, file upload, dữ liệu database, sẽ mất khi container được tạo lại, chuyện xảy ra mỗi lần deploy phiên bản mới. Dữ liệu cần tồn tại qua vòng đời container phải để trong volume hoặc bind mount, thứ nằm ngoài OverlayFS và không bị xoá theo container. Đây là một trong những bài học vận hành quan trọng nhất về container, và nguồn của nhiều sự cố mất dữ liệu khi người ta tưởng filesystem container là bền.
Tránh sửa file lớn đóng sẵn trong image. Vì copy-on-write ở mức file, mọi lần ghi đầu tiên vào một file lớn từ image đều kéo theo copy cả file, tốn dung lượng và chậm. Thiết kế image để file sẽ bị sửa không nằm sẵn trong lớp image: để chúng trong volume, hoặc tạo mới lúc chạy. Các workload ghi nhiều như database không nên lưu dữ liệu trên lớp overlay cả vì lý do dung lượng lẫn vì OverlayFS không tối ưu cho ghi ngẫu nhiên nhiều; chúng cần volume trên filesystem thật.
Phạm vi đo. Các số là của driver overlay2 trên Docker Desktop; các storage driver khác như btrfs hay zfs có cơ chế copy-on-write ở mức block, nên sửa một byte của file lớn không copy cả file, một khác biệt quan trọng cho workload ghi ngẫu nhiên. Chưa đo chi phí đọc khi một file nằm sâu trong nhiều lớp lowerdir, chưa đo giới hạn số lớp, và chưa đo docker build cache theo lớp, chủ đề riêng về tối ưu image.
Ba ý mang về
- rootfs của container là overlay ba lớp: lowerdir chỉ đọc dùng chung, upperdir ghi riêng, workdir tạm. Nhiều container từ một image chia sẻ lowerdir, nên mỗi cái chỉ thêm lớp ghi vài kB; image không bị nhân lên.
- Copy-on-write ở mức file: sửa một byte của file 50 MB tốn 50 MB.
docker diffchoCkhi file bị copy-up; tránh để dữ liệu hay file sẽ sửa nằm sẵn trong image vì mỗi lần đụng là copy cả file. - Lớp ghi là tạm, mất khi container bị xoá. File mới chỉ cộng đúng kích thước nó vào upperdir, nhưng tất cả biến mất khi container được tạo lại; dữ liệu cần giữ phải để trong volume.
Nguồn
- Linux kernel — Overlay Filesystem (lowerdir, upperdir, workdir, copy-up): https://docs.kernel.org/filesystems/overlayfs.html
- Docker — Use the OverlayFS storage driver: https://docs.docker.com/engine/storage/drivers/overlayfs-driver/
- Docker — About storage drivers và mô hình lớp copy-on-write: https://docs.docker.com/engine/storage/drivers/
- Docker —
docker container diffvàdocker system df: https://docs.docker.com/reference/cli/docker/container/diff/ - Docker — Volumes, lưu dữ liệu bền ngoài lớp ghi: https://docs.docker.com/engine/storage/volumes/
- Linux
mount(8)— tùy chọn overlay: https://man7.org/linux/man-pages/man8/mount.8.html
Phần sau quay lại chuyện bảo mật đã mở ra ở phần 1: root trong container. Đo chính xác tiến trình root trong container làm được gì và bị chặn gì qua Linux capabilities, --cap-drop và --cap-add thay đổi những syscall nào, và vì sao bỏ bớt capability là cách siết quyền container rẻ và hiệu quả.