Container nội bộ đo thật: cgroups, namespace và giới hạn tài nguyên khi app chạy trong Docker
Điều gì thật sự xảy ra khi một ứng dụng backend chạy trong container Linux: namespace cô lập cái gì, cgroups v2 giới hạn CPU, bộ nhớ, I/O và số tiến trình ra sao, OOM killer chọn ai, OverlayFS tốn dung lượng thế nào, capabilities và seccomp cho phép gì. Mỗi phần dựng container thật với cờ giới hạn cụ thể rồi đo bằng /proc, /sys/fs/cgroup, strace và các công cụ Linux, không trích từ tài liệu.
7/12 phần đã đăng
Lập trình
1
Namespace đo thật: cùng một tiến trình sleep là PID 1 bên trong container nhưng PID 90064 trên host, hai container cô lập năm trục bằng năm inode kernel khác nhau, nhưng dùng chung user namespace nên root trong container là uid 0 thật
Container không phải máy ảo mà là nhóm tiến trình Linux bị cô lập cái nó nhìn thấy. Dựng hai container debian và đo: cùng một sleep là PID 1 bên trong nhưng 90064 trên host, mỗi container chỉ thấy tiến trình của mình. So inode namespace qua readlink /proc/1/ns: pid, net, mnt, uts, ipc đều khác giữa hai container; nhưng user namespace cùng inode 4026531837, nghĩa là Docker mặc định không bật user namespace và root trong container là uid 0 thật trên host. docker exec chui đúng vào namespace của container, và shared memory tạo ở container này container kia không thấy.
22/09/2026
· 10 phút đọc
2
Network namespace đo thật: mỗi container một IP riêng 172.17.0.6 và .7 trên cầu docker0, eth0 là một đầu cặp veth nối sang host, hai container ping nhau 0,09 ms, và dựng lại toàn bộ bằng năm lệnh ip netns
Mạng của container không có gì huyền bí. Dựng hai container debian: mỗi cái có eth0 với IP riêng 172.17.0.6 và 172.17.0.7 trên cầu docker0 subnet 172.17.0.0/16 gateway 172.17.0.1. eth0 là một đầu của cặp veth, đầu kia là veth ifindex 798 trên host cắm vào docker0. Hai container cùng cầu ping nhau 0,094 ms. Port mapping -p 49231:8000 cho curl từ host vào qua iptables DNAT, không có -p thì cổng chỉ sống trong container. Cuối cùng dựng lại một network namespace bằng tay với ip netns và cặp veth, chứng minh Docker chỉ làm đúng năm lệnh đó.
22/09/2026
· 9 phút đọc
3
cgroups v2 giới hạn CPU đo thật: --cpus=0.5 đặt cpu.max 50000/100000 chứ không giấu bớt lõi, một khối việc 6 giây phình thành 77 giây ở --cpus=0.2, nproc vẫn báo đủ 10 lõi host, cpu.stat cho thấy luồng bị ghìm 50 trên 51 chu kỳ
cgroups giới hạn cái container dùng, khác namespace cô lập cái nó thấy. --cpus=X đặt cpu.max = quota/period: --cpus=0.5 là 50 ms chạy mỗi 100 ms rồi bị ghìm. Nhưng nproc vẫn báo đủ 10 lõi host, nên Go đặt GOMAXPROCS=10 và Java tính pool theo 10 lõi. Một luồng busy 5 giây ở --cpus=0.5 chỉ chạy 2,55 giây, bị ghìm 50 trên 51 chu kỳ, chờ 2,5 giây dù host rảnh, đọc qua cpu.stat. Cùng khối việc 6 giây chạy như nhau ở 1, 2 hay unlimited CPU nhưng thành 12 giây ở 0.5 và 77 giây ở 0.2. Một request 1 giây CPU biến thành 12 giây.
22/09/2026
· 10 phút đọc
4
cgroups v2 giới hạn bộ nhớ đo thật: --memory=128m đặt memory.max 128 MiB, vượt trần là OOM killer giết chứ không chạy chậm, container chết với exit 137 và OOMKilled=true, nhưng OOM chỉ giết tiến trình ngốn RAM nên PID 1 sống thì container sống
Khác CPU bị ghìm, bộ nhớ vượt trần thì OOM killer giết tiến trình. --memory=128m đặt memory.max 134217728 byte. Tắt swap rồi cho một tiến trình ngốn 10 MB mỗi vòng: memory.current leo 95, 105, 115, 125 MB rồi vượt 128 MB là bị SIGKILL, container exit 137 và OOMKilled=true. memory.events đếm max là số lần chạm trần và oom_kill là số tiến trình bị giết. Điểm quan trọng: OOM giết tiến trình ngốn RAM, không nhất thiết cả container, nên nếu PID 1 không phải kẻ ngốn thì container vẫn Running. memory.high ghìm mềm thì Docker không phơi ra.
22/09/2026
· 10 phút đọc
5
cgroups v2 giới hạn I/O và số tiến trình đo thật: --pids-limit chặn fork bomb đúng tại trần 50 tiến trình, --device-write-bps bóp ghi đĩa từ 1,7 GB/s xuống 10,6 MB/s, nhưng ghi buffered lách qua page cache với 3,3 GB/s và chỉ bị bóp khi fsync
Hai lưới an toàn ít ai đặt. pids.max là trần cho tổng số tiến trình: fork bomb bằng python os.fork() dừng đúng tại 50 tiến trình với --pids-limit=50, pids.current chạm 50, pids.events đếm fork bị từ chối. io.max là trần băng thông đĩa: --device-write-bps /dev/vda:10mb cho io.max wbps 10485760, ghi direct bị bóp xuống 10,6 MB/s so với 1,7 GB/s không giới hạn. Nhưng io.max chỉ chặn I/O thật xuống thiết bị: ghi buffered vào page cache đạt 3,3 GB/s vì chưa chạm đĩa, chỉ bị bóp xuống 10,4 MB/s khi fsync ép writeback. App ghi rồi fsync như database cảm nhận io.max, ghi buffered rồi quên thì không.
22/09/2026
· 9 phút đọc
6
OverlayFS đo thật: rootfs của container là ba lớp lowerdir, upperdir, workdir, ba container cùng image chia sẻ lớp chỉ đọc nên mỗi cái chỉ thêm 4 kB, nhưng ghi một byte vào file 50 MB của image tốn nguyên 50 MB vì copy-on-write ở mức file
rootfs của container không phải một đĩa mà là mount overlay ghép từ lowerdir (các lớp image, chỉ đọc, dùng chung), upperdir (lớp ghi riêng) và workdir. Ba container từ cùng debian:12 mỗi cái chỉ thêm 4,1 kB vì image 155 MB được chia sẻ một lần. Ghi file mới 30 MB chỉ cộng 30 MB vào upperdir. Nhưng copy-on-write ở mức file: ghi một byte vào file 50 MB có sẵn trong image copy cả file lên upperdir, lớp ghi riêng phình từ 4,1 kB lên 52,4 MB. docker diff cho C với file copy-up và A với file mới. Lớp ghi của container là tạm, mất khi container bị xoá, nên dữ liệu cần giữ phải để trong volume.
22/09/2026
· 9 phút đọc
7
Capabilities đo thật: root trong container không phải root đầy đủ, Docker mặc định chỉ cấp 14 trong hơn 40 capability, nên chown và bind cổng thấp chạy được nhưng date -s, mount và ip addr add bị chặn, và --cap-drop ALL đưa CapEff về 0
Linux chia quyền root thành hơn 40 capability riêng. Docker mặc định chỉ cấp 14 cho root trong container, bỏ khoảng 26 cái nguy hiểm. Đo bằng CapEff trong /proc/self/status và test từng syscall bằng exit code: chown, tạo device node, raw socket, bind cổng dưới 1024 chạy được; date -s, mount tmpfs, ip addr add bị chặn vì thiếu SYS_TIME, SYS_ADMIN, NET_ADMIN. --cap-add NET_ADMIN đổi CapEff từ a80425fb thành a80435fb và bật ip addr add; --cap-drop CHOWN chặn chown; --cap-drop ALL đưa CapEff về 0 chặn tất cả, rồi --cap-add một cái cho CapEff chỉ bit đó.
22/09/2026
· 9 phút đọc
Còn 5 phần nữa sẽ lần lượt được đăng.