Ba phần vừa rồi đo các giới hạn cgroups quen thuộc: CPU và bộ nhớ, hai thứ mọi người đặt khi triển khai container. Phần này đo hai giới hạn còn lại mà rất ít người đặt, nhưng lại là lưới an toàn quan trọng nhất khi có sự cố: số tiến trình và băng thông đĩa. Thiếu chúng, một container lỗi không chỉ tự hại mình mà có thể kéo sập cả host: một vòng lặp đẻ tiến trình vô hạn làm kiệt bảng PID của toàn máy, hay một container ghi log ồ ạt bóp nghẹt đĩa khiến mọi container khác chậm theo. Bài này đo chính xác pids.max chặn một fork bomb ra sao và io.max bóp băng thông ghi đĩa thế nào, cùng một điểm bất ngờ về page cache khiến giới hạn I/O không chặt như tưởng.
Hai lưới an toàn: pids.max và io.max

Hình 1: pids.max chặn fork bomb và io.max bóp băng thông đĩa, cùng cách đo và lý do dùng python để đo fork bomb.
pids.max, đặt qua cờ --pids-limit, là trần cho tổng số tiến trình và luồng trong một container. Khi số tiến trình đã chạm trần, mọi lời gọi fork() hay clone() tiếp theo thất bại với lỗi EAGAIN. Công dụng chính là chặn một fork bomb: một vòng lặp đẻ tiến trình không ngừng, cổ điển là đoạn shell :(){ :|:& };:, vốn có thể làm kiệt bảng PID của cả host chứ không chỉ container, vì PID là tài nguyên dùng chung ở tầng kernel. Một container không đặt pids.max, nếu chạy phải một đoạn mã lỗi hay một đầu vào độc hại gây đẻ tiến trình, có thể treo toàn máy. io.max, đặt qua --device-write-bps hay --device-read-bps, là trần băng thông đĩa theo từng thiết bị, ghi dưới dạng major:minor wbps=số-byte, ví dụ 254:0 wbps=10485760 là 10 MB/s. Nó ngăn một container ghi ồ ạt bóp nghẹt I/O của container khác trên cùng ổ đĩa. Một điểm quan trọng sẽ đo được: io.max chỉ chặn I/O thật xuống thiết bị; ghi vào page cache, tức ghi buffered thông thường, chưa chạm đĩa nên không bị bóp cho tới khi kernel writeback.
Cách đo cần một mẹo cho fork bomb. Đo bằng shell rất khó: khi container chạm trần pids.max, chính shell cũng không fork nổi một lệnh ngoài như cat hay echo để in kết quả, nên báo cáo bị mất. Giải pháp là dùng python, một tiến trình đơn: nó gọi os.fork() để đẻ con trong vòng lặp cho tới khi os.fork() ném OSError, bắt lỗi đó, rồi đọc file cgroup bình thường vì bản thân nó không cần fork để open() một file. Nhờ vậy đo được chính xác: tạo được bao nhiêu tiến trình con, pids.current chạm trần ở đâu, và pids.events đếm bao nhiêu lần fork bị từ chối. Cho I/O, dùng dd ghi một lượng cố định với các cờ khác nhau: oflag=direct để ghi thẳng xuống thiết bị, ghi buffered thường, và buffered cộng fsync để ép writeback, rồi so tốc độ với lần không giới hạn.
Đo thật: fork bomb dừng tại 50, ghi đĩa còn 10,6 MB/s

Hình 2: pids.max chặn fork bomb đúng tại trần, io.max bóp ghi đĩa xuống đúng giới hạn, và page cache lách qua io.max.
| phép đo | kết quả |
|---|---|
| --pids-limit=50, fork bomb python | 49 con + 1 python = 50, pids.current=50 |
| pids.events sau khi chạm trần | max 1 (fork bị từ chối) |
| --pids-limit=20 | dừng đúng tại 20 |
| --device-write-bps /dev/vda:10mb → io.max | 254:0 wbps=10485760 (10 MB/s) |
| dd 30 MB direct, có io.max | 10,6 MB/s |
| dd 30 MB direct, không giới hạn | 1,7 GB/s |
| ghi buffered, có io.max | 3,3 GB/s (vào page cache) |
| ghi buffered + fsync, có io.max | 10,4 MB/s (writeback bị bóp) |
pids.max dừng fork bomb đúng tại trần. Với --pids-limit=50, chương trình python gọi os.fork() tới 300 lần nhưng chỉ thành công 49; lần thứ 50 thất bại, vì 49 con cộng với chính tiến trình python là 50, đúng bằng pids.max. pids.current chạm 50, và pids.events cho max 1, nghĩa là có một lần fork bị từ chối trước khi chương trình dừng thử. Đặt --pids-limit=20 cho kết quả tương tự, dừng ở 20. Ý nghĩa thực tế rất lớn: một fork bomb thật vốn đẻ hàng nghìn, hàng triệu tiến trình trong vài giây, làm kiệt bảng PID của host và treo toàn máy, bị chặn đứng tại một con số nhỏ và không lan ra ngoài container. Đây là lý do mọi container nhận đầu vào không tin cậy, hoặc chạy mã có thể lỗi, nên có --pids-limit; chi phí bằng không và nó biến một sự cố toàn hệ thống thành một container chết đơn lẻ.
io.max cắt băng thông ghi đĩa xuống đúng trần. --device-write-bps /dev/vda:10mb đặt io.max thành 254:0 rbps=max wbps=10485760, tức giới hạn ghi 10 MB mỗi giây trên thiết bị, không giới hạn đọc. Ghi 30 MB bằng dd với oflag=direct, ghi thẳng xuống thiết bị bỏ qua cache, mất 2,98 giây, tức 10,6 MB/s, đúng bằng trần đặt ra. So với lần không giới hạn, cùng lệnh ghi đạt 1,7 GB/s, tốc độ của đĩa ảo trên máy này: io.max đã cắt tốc độ xuống hơn 160 lần. Con số này là công cụ cô lập: khi nhiều container chia một ổ đĩa vật lý, một container ghi backup hay log ồ ạt có thể làm đĩa bão hoà và mọi container khác chờ I/O; đặt io.max cho container đó giữ băng thông của nó trong giới hạn và các container khác vẫn chạy.
Nhưng page cache lách qua io.max, và đây là điểm dễ hiểu sai. Cùng giới hạn 10 MB/s, ghi 30 MB bằng ba cách cho ba kết quả rất khác nhau. Ghi direct: 10,6 MB/s, bị bóp như trên. Ghi buffered thông thường: 3,3 GB/s, nhanh hơn trần 330 lần, vì dữ liệu chỉ được chép vào page cache trong RAM và dd trả về ngay, chưa có byte nào thật sự chạm đĩa, nên io.max chưa áp vào đâu cả. Ghi buffered cộng fsync để ép kernel đẩy cache xuống đĩa: 10,4 MB/s, bị bóp trở lại, vì lúc này I/O thật mới xảy ra. Hệ quả thực tế rất quan trọng cho người vận hành: io.max chỉ giới hạn I/O thật xuống thiết bị, nên một ứng dụng ghi-rồi-fsync, điển hình là database khi commit, sẽ cảm nhận đầy đủ giới hạn và chậm đi; còn một ứng dụng ghi buffered rồi quên, như ghi log không fsync, trông như không bị giới hạn, cho tới khi page cache đầy và kernel buộc phải writeback, lúc đó độ trễ bất ngờ dồn lại. Nói cách khác, io.max không phải một cái van đều đặn mà là một giới hạn áp lên writeback, và hành vi quan sát được phụ thuộc hoàn toàn vào việc ứng dụng có fsync hay không.
Đánh đổi cần cân nhắc
Đặt --pids-limit cho mọi container, nó gần như miễn phí. Khác với CPU và bộ nhớ cần đo đỉnh thật để chọn, pids.max chỉ cần đặt một con số đủ rộng cho hoạt động bình thường của ứng dụng, thường vài trăm tới vài nghìn, và nó chặn được cả một lớp sự cố nghiêm trọng. Ứng dụng đơn luồng cần ít; ứng dụng nhiều worker hay nhiều kết nối cần nhiều hơn vì mỗi luồng tính một pid. Đặt quá chặt làm ứng dụng không tạo nổi worker khi tải cao; đặt rộng vừa phải là an toàn. Nhiều hướng dẫn bảo mật container khuyên luôn bật, và Kubernetes có thể đặt pids.max mặc định cho mọi pod ở cấp node.
io.max hữu ích khi nhiều container chia đĩa, nhưng nhớ hiệu ứng page cache. Với một container độc chiếm đĩa, io.max ít cần; với nhiều container cùng ổ, nó ngăn một kẻ ghi ồ ạt làm đói I/O các container khác. Nhưng vì page cache lách qua nó, đừng kỳ vọng io.max làm độ trễ đồng đều: ứng dụng không fsync sẽ dồn I/O thành đợt khi writeback. cgroups v2 còn có io.weight để chia I/O theo tỉ lệ khi tranh chấp, mềm hơn trần cứng io.max, thường phù hợp hơn cho việc cân bằng giữa các container bình thường.
Phạm vi đo. Các số io là của đĩa ảo trên Docker Desktop, tốc độ tuyệt đối sẽ khác trên SSD NVMe thật hay HDD; tỉ lệ trước và sau io.max là thứ mang đi được. Chưa đo giới hạn IOPS (riops, wiops) vốn quan trọng với workload nhiều thao tác nhỏ như database hơn là băng thông tuần tự; chưa đo io.weight; và chưa đo tương tác giữa io.max với page cache chung của nhiều cgroup, một chủ đề tinh tế liên quan tới phần 11 về RSS và cache.
Ba ý mang về
- pids.max chặn fork bomb đúng tại trần, gần như miễn phí.
--pids-limit=50dừng việc đẻ tiến trình ở 50; một fork bomb vốn treo cả host bị cô lập thành một container chết. Đặt--pids-limitcho mọi container, nhất là khi nhận đầu vào không tin cậy. - io.max bóp băng thông ghi đĩa xuống đúng giới hạn.
--device-write-bps 10mbcắt ghi direct từ 1,7 GB/s còn 10,6 MB/s; hữu ích để một container không bóp nghẹt đĩa của container khác. - Page cache lách qua io.max. Ghi buffered đạt 3,3 GB/s vì chưa chạm đĩa; chỉ
fsynchay writeback mới bị bóp. App ghi-rồi-fsync cảm nhận io.max, app ghi buffered rồi quên thì không, tới khi cache đầy.
Nguồn
- Linux kernel — Control Group v2, mục PID controller (
pids.max,pids.events): https://docs.kernel.org/admin-guide/cgroup-v2.html#pid - Linux kernel — Control Group v2, mục IO controller (
io.max,io.stat,io.weight): https://docs.kernel.org/admin-guide/cgroup-v2.html#io - Docker — Runtime options (
--pids-limit,--device-write-bps,--device-read-bps): https://docs.docker.com/engine/containers/resource_constraints/ - Linux
fork(2)— lỗiEAGAINkhi chạmRLIMIT_NPROChoặcpids.max: https://man7.org/linux/man-pages/man2/fork.2.html - Linux
dd(1)vàopen(2)—O_DIRECT,fsync: https://man7.org/linux/man-pages/man2/open.2.html - OWASP / Docker CIS Benchmark — khuyến nghị đặt
--pids-limit: https://www.cisecurity.org/benchmark/docker
Phần sau chuyển sang tầng lưu trữ: OverlayFS, cơ chế làm rootfs của container. Đo lớp image chỉ đọc chồng với lớp ghi riêng, vì sao ghi một byte vào một file lớn phải copy cả file (copy-on-write), và dung lượng thật mỗi container chiếm thêm so với image gốc.