cgroups — từ Linux đến Kubernetes
Cơ chế kernel sau mọi container, OOMKilled và CPU throttle — cgroup là gì, v1 vs v2, và đường đi từ Linux → Docker → Kubernetes.
cgroups
giải thích từ gốc
Mọi container bạn từng chạy, mọi pod bị OOMKilled, mọi service bị CPU throttle — tất cả đều quy về một cơ chế kernel duy nhất. Hiểu nó một lần, không cần đoán nữa.
cgroup là gì?
cgroup (control group) là một tính năng của Linux kernel để giới hạn, ưu tiên và theo dõi tài nguyên (CPU, memory, I/O, số process...) của một nhóm process.
Điểm bất ngờ với newbie: cgroup không phải một câu lệnh hay một daemon. Nó là một virtual filesystem. Bạn điều khiển nó bằng cách read / write file trong thư mục /sys/fs/cgroup. Không có syscall riêng — chỉ là đọc ghi file thông thường.
Hai khái niệm cốt lõi cần phân biệt:
| Thuật ngữ | Là gì | Ví dụ |
|---|---|---|
| cgroup | một nhóm process (= một thư mục trong cây) | thư mục /sys/fs/cgroup/myapp |
| controller (subsystem) | module kernel quản lý một loại tài nguyên | cpu, memory, io, pids |
cgroups vs namespaces
cgroups gắn liền với namespaces — nhưng hai cơ chế này giải quyết hai bài toán hoàn toàn khác nhau. Chúng trực giao (orthogonal): độc lập với nhau, và chỉ khi gộp lại mới tạo thành một container đúng nghĩa.
namespaces
- pid — process nghĩ nó là PID 1, không thấy process host
- net — card mạng, IP, routing riêng
- mnt — cây filesystem riêng
- uts / ipc / user / cgroup — hostname, IPC, UID, view cgroup
cgroups
- cpu — giới hạn hoặc tỉ trọng CPU
- memory — trần RAM, OOM khi vượt
- io — giới hạn băng thông / IOPS đĩa
- pids — số process tối đa
Có thể dùng độc lập
cgroup không cần namespace: systemd-run -p MemoryMax=500M ... giới hạn một service nhưng nó vẫn chạy trên host bình thường, thấy mọi thứ.
namespace không cần cgroup: một process bị cô lập tầm nhìn nhưng không bị giới hạn tài nguyên — vẫn ngốn hết RAM host được. Container chỉ ra đời khi runtime gọi cả hai.
Bản thân cgroup là namespace thứ 8. Nó ảo hóa tầm nhìn vào cây cgroup: process trong container thấy cgroup của mình như root / thay vì đường dẫn đầy đủ /kubepods.slice/.../scope.
Đây chính là lý do cat /sys/fs/cgroup/memory.max từ trong container trả về limit của container chứ không phải của host.
cgroups nằm ở đâu?
Hiểu lầm phổ biến: nghĩ cgroup tự nó "giới hạn" tài nguyên. Thực ra cgroup chỉ là lớp kế toán và policy — nó hook vào các cơ chế lõi của kernel, và chính những cơ chế đó mới thực thi việc giới hạn.
Cụ thể từng controller hook vào đâu:
| cgroup file | Hook vào | Hành vi thực thi |
|---|---|---|
cpu.max | CFS scheduler | Không cho process chạy tiếp khi hết quota trong chu kỳ → CPU throttling |
memory.max | memory manager / OOM killer | Giết process khi cgroup vượt trần RAM → OOMKilled |
io.max | block layer | Làm chậm I/O của cgroup khi vượt IOPS/bandwidth |
Hai nhánh — cgroup (xanh lá) và namespace (tím) — chạy song song và độc lập trong kernel. Chúng chỉ gặp nhau ở chỗ runtime gọi cả hai để dựng container.
cgroup v1 vs v2
Có hai phiên bản cgroup tồn tại song song. Khác biệt lớn nhất nằm ở cách tổ chức cây thư mục. Hầu hết hệ thống mới (Ubuntu 22.04+, RHEL 9, systemd hiện đại, EKS AL2023) đã mặc định v2.
cgroup v1
- Mỗi controller có hierarchy riêng: cpu một cây, memory một cây khác
- Mount tại
/sys/fs/cgroup/cpu/,/sys/fs/cgroup/memory/... - Một process có thể nằm ở vị trí khác nhau trong mỗi cây → khó suy luận
- Linh hoạt nhưng phức tạp, dễ mâu thuẫn
cgroup v2
- Unified hierarchy: tất cả controller chung một cây duy nhất
- Mount tại
/sys/fs/cgroup/(một chỗ duy nhất) - Một process nằm ở đúng một cgroup → dễ suy luận
- Có quy tắc chặt chẽ hơn (xem bên dưới)
Hai quy tắc quan trọng của v2
1. "No internal process" rule: một cgroup không thể vừa chứa process vừa có controller bật cho cgroup con. Process chỉ được nằm ở leaf node (nút lá). Đây là lý do systemd tổ chức cây theo slice/scope.
2. Top-down enabling: controller phải được bật từ parent xuống thông qua file cgroup.subtree_control. Con không thể có controller mà cha chưa cấp.
Chạy stat -fc %T /sys/fs/cgroup/ — kết quả cgroup2fs là v2, tmpfs là v1.
Tự tay tạo một cgroup
Cách tốt nhất để hiểu là làm thủ công. Ví dụ dưới đây (cgroup v2) tạo một group giới hạn 100MB RAM và 50% một CPU core, không cần Docker hay K8s gì cả.
# Tạo một cgroup con (chỉ là mkdir một thư mục) mkdir /sys/fs/cgroup/demo # Bật controller cho cgroup con (ghi vào subtree_control của CHA) echo "+cpu +memory" > /sys/fs/cgroup/cgroup.subtree_control # Giới hạn memory 100MB echo 104857600 > /sys/fs/cgroup/demo/memory.max # Giới hạn CPU: 50% của 1 core → "quota period" (đơn vị micro giây) # 50000 / 100000 = 50% echo "50000 100000" > /sys/fs/cgroup/demo/cpu.max # Đưa shell hiện tại vào cgroup echo $$ > /sys/fs/cgroup/demo/cgroup.procs # Giờ mọi process spawn từ shell này đều bị giới hạn stress --vm 1 --vm-bytes 200M # → bị OOMKilled vì vượt 100M
Các file điều khiển hay dùng (v2)
| Controller | File | Ý nghĩa |
|---|---|---|
| memory | memory.max | Hard limit. Vượt → OOM kill trong cgroup |
| memory | memory.high | Soft limit. Vượt → kernel throttle + reclaim mạnh (KHÔNG kill) |
| memory | memory.current | Đang dùng bao nhiêu (đọc-only) |
| cpu | cpu.max | Hard cap. quota period — giới hạn cứng CPU |
| cpu | cpu.weight | Tỉ trọng. Chỉ tác động khi tranh chấp (1–10000, mặc định 100) |
| cpu | cpu.stat | nr_throttled, throttled_usec — đo throttling |
| pids | pids.max | Số process tối đa (chống fork bomb) |
systemd quản lý cgroup thế nào
Trên hệ thống hiện đại, systemd là cgroup manager mặc định. Mọi process chạy đều nằm trong cây này, tổ chức theo 3 loại đơn vị:
| Đơn vị | Là gì |
|---|---|
| slice | Nhóm logic (nhánh cây), không chứa process trực tiếp. VD system.slice |
| scope | Nhóm process do bên ngoài tạo (VD container runtime) |
| service | Process do systemd start (VD nginx.service) |
# Tạo limit nhanh không cần đụng cgroupfs thủ công systemd-run --scope -p MemoryMax=500M -p CPUQuota=50% stress --vm 1 --vm-bytes 600M # Xem cây cgroup realtime (như top nhưng cho cgroup) systemd-cgtop
Nếu systemd quản lý cây cgroup mà runtime lại ghi trực tiếp vào cgroupfs, sẽ có hai manager tranh nhau cùng một cây → trạng thái không nhất quán. Đây chính là lý do Docker và kubelet cần config cgroup driver = systemd để khớp.
Docker flags map xuống cgroup
Docker chỉ là wrapper. Khi bạn chạy docker run --memory 100m, runc tạo một scope trong system.slice rồi ghi đúng các file cgroup bạn vừa thấy ở tầng Linux — giống hệt lúc bạn mkdir + echo thủ công.
Bảng quy đổi flag → file
| Docker flag | cgroup file | Giá trị |
|---|---|---|
--memory 100m | memory.max | 104857600 |
--memory-reservation 80m | memory.high | soft, throttle reclaim |
--cpus 0.5 | cpu.max | 50000 100000 |
--cpu-shares 512 | cpu.weight | tỉ trọng tương đối |
--pids-limit 50 | pids.max | 50 |
Trước khi JVM / Node trở nên cgroup-aware, chúng đọc /proc/meminfo và nproc → thấy RAM/CPU của host, không phải limit của cgroup.
Một JVM trong container 512MB nhưng tự tính heap theo host 64GB → OOMKilled ngay. Cách xử lý: JVM 11+ tự nhận cgroup limit (hoặc dùng -XX:MaxRAMPercentage); Node dùng --max-old-space-size set tay.
requests vs limits
Đây là phần quan trọng nhất — và là nguồn gốc của hầu hết hiểu lầm. Mấu chốt: requests và limits KHÔNG map xuống cùng một loại file cgroup.
requests
- CPU →
cpu.weight(tỉ trọng khi tranh chấp) - Memory → KHÔNG thành
memory.max - Scheduler dùng để chọn node đủ chỗ
limits
- CPU →
cpu.max(hard cap → throttle) - Memory →
memory.max(vượt → OOMKilled) - Kernel dùng để thực thi giới hạn cứng
Hệ quả thực tế
CPU limit thấp → throttle dù node còn rảnh. Vì cpu.max là hard cap theo chu kỳ. Nhiều team bỏ CPU limit hẳn (chỉ giữ request) để tránh throttle oan với service nhạy độ trễ.
Memory không có "throttle". Chỉ có dùng được hoặc bị giết. Vượt memory.max = OOMKilled, không có vùng đệm như CPU.
QoS class quyết định thứ tự bị giết
K8s dựng cây 3 tầng dưới kubepods.slice. Khi node hết RAM (memory pressure), thứ tự eviction + OOM là: besteffort chết trước → burstable → guaranteed chết cuối.
# Trên node: xem cây cgroup của pods systemd-cgls /kubepods.slice # Đường cgroup thật của một container (v2) # /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/ # kubepods-burstable-pod<uid>.slice/cri-containerd-<id>.scope/ cat .../cpu.max # 50000 100000 ← limit 500m cat .../cpu.stat # nr_throttled ← bằng chứng throttling cat .../memory.max # 536870912 ← limit 512Mi cat .../memory.current # đang dùng bao nhiêu
Tỉ lệ cao = nên nới CPU limit:
rate(container_cpu_cfs_throttled_periods_total[5m]) / rate(container_cpu_cfs_periods_total[5m])
Một cơ chế, ba tầng
Cùng một file cgroup xuyên suốt cả ba tầng — chỉ khác lớp abstraction phía trên. Hiểu tầng dưới cùng thì mọi hành vi lạ ở tầng trên đều giải thích được mà không cần đoán.
Checklist debug nhanh
| Triệu chứng | Kiểm tra |
|---|---|
| Pod bị OOMKilled | So memory.current vs memory.max; tăng limit hoặc fix leak; check app có cgroup-aware không |
| Service chậm, latency spike | cpu.stat → nr_throttled cao? Nới CPU limit hoặc bỏ limit |
| Pod không schedule được | requests quá cao so với node; không liên quan limit |
| Pod bị evict ngẫu nhiên | QoS class? besteffort/burstable bị giết trước khi node memory pressure |
| Node EKS hành vi lạ | stat -fc %T /sys/fs/cgroup/ — v1 hay v2? Đường dẫn cgroup khác nhau |