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

Ảnh chụp đoạn mã nền tối OverlayFS rootfs của container là nhiều lớp chồng lên nhau với Docker 29 overlayfs container tạm debian 12 đo bằng proc mounts docker diff docker ps -s docker system df, rootfs không phải một đĩa mà là các lớp phủ mount gạch chéo của container là overlay ghép từ ba phần lowerdir các lớp của image chỉ đọc dùng chung cho mọi container từ image đó upperdir lớp ghi riêng của container này mọi thay đổi nằm ở đây workdir vùng tạm để overlay làm việc nội bộ, đọc một file lấy từ upperdir nếu có không thì rơi xuống lowerdir ghi một file từ image copy-on-write copy cả file lên upperdir rồi mới sửa ở mức file không phải block docker diff A bằng thêm mới C bằng đổi đã copy-up D bằng xoá docker ps -s kích thước lớp ghi riêng, cách đo A đọc proc self mounts của gạch chéo thấy lowerdir upperdir workdir B copy-on-write image có big.bin 50 MB ghi 1 byte vào nó đo lớp ghi riêng phình bao nhiêu C 3 container cùng image debian 12 mỗi lớp ghi riêng bao nhiêu image dùng chung không nhân lên D ghi file mới 30 MB không có trong image chỉ cộng vào upperdir docker diff bằng A, vì sao đo docker diff và ps -s thay vì du trong container du trong container đếm cả file từ lowerdir image không tách được cái gì thật sự tốn chỗ mới docker ps -s cho Size lớp ghi riêng và virtual gồm cả image tách bạch rõ docker diff liệt kê đúng file nào đã copy-up C hay thêm mới A thấy copy-on-write xảy ra ở đâu

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

Ảnh chụp output thật nền tối ghi 1 byte vào file 50 MB tốn 50 MB đĩa với overlayfs docker diff docker ps -s Size lớp ghi virtual gồm image debian 12, A rootfs là overlay ba lớp mount gạch chéo bằng overlay lowerdir snapshots 16439 fs và 16297 fs 2 lớp image chỉ đọc upperdir snapshots 16440 fs lớp ghi riêng của container workdir snapshots 16440 work vùng tạm overlay, B copy-on-write sửa 1 byte bằng copy cả file image cow-test có big.bin 50 MB container vừa chạy docker diff trống chưa đổi gì lớp ghi riêng Size 4.1 kB virtual 208 MB gồm image ghi 1 byte vào big.bin printf X dd conv notrunc docker diff C big.bin C bằng changed đã copy-up lớp ghi riêng Size 52.4 MB virtual 260 MB sửa 1 byte tốn 50 MB đĩa OverlayFS copy ở mức file đụng 1 byte của file lớn là nhân đôi cả file lên lớp ghi, C image dùng chung D file mới chỉ cộng vào upperdir 3 container từ cùng debian 12 s1 s2 s3 mỗi cái lớp ghi 4.1 kB virtual 155 MB 155 MB image được tính một lần lowerdir chỉ đọc dùng chung không nhân 3 docker system df Images 24,85 GB với 12,73 GB reclaimable lớp trùng dùng chung ghi file mới 30 MB newfile.bin không có trong image lớp ghi riêng s1 bằng 31.5 MB docker diff A newfile.bin A bằng added file mới chỉ cộng đúng kích thước của nó không có copy-on-write, nói thẳng lớp ghi của container là tạm mất khi container bị xoá dữ liệu cần giữ phải để trong volume phần riêng và tránh sửa file lớn nằm sẵn trong image đụng 1 byte là copy cả file phình lớp ghi và chậm lần ghi đầu

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ề

  1. 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.
  2. Copy-on-write ở mức file: sửa một byte của file 50 MB tốn 50 MB. docker diff cho C khi 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.
  3. 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

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ả.