Bỏ qua đến nội dung
DevOps Lab

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.

Linux internals → containers → 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.

01 / KHÁI NIỆM

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.

Một container chỉ là process bình thường bị nhốt bởi namespaces (cô lập tầm nhìn) và giới hạn bởi cgroups (giới hạn tài nguyên).

Hai khái niệm cốt lõi cần phân biệt:

Thuật ngữLà gìVí dụ
cgroupmộ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êncpu, memory, io, pids
Container = Process + 2 cơ chế kernel namespaces cô lập tầm nhìn cgroups giới hạn tài nguyên namespaces: pid, net, mnt, uts, ipc, user → "thấy gì" cgroups: cpu, memory, io, pids → "dùng được bao nhiêu" Linux kernel /sys/fs/cgroup/ virtual filesystem — điều khiển bằng cách đọc / ghi file
Container không huyền bí — chỉ là process + namespaces + cgroups
02 / CƠ CHẾ ANH EM

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

"thấy được gì" — ảo hóa tầm nhìn
  • 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

"dùng được bao nhiêu" — giới hạn tài nguyên
  • 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
namespaces "thấy được gì" pid — chỉ thấy process của mình net — card mạng, IP riêng mnt — filesystem riêng cgroups "dùng được bao nhiêu" cpu — giới hạn / tỉ trọng memory — trần RAM io, pids — đĩa, số process cô lập + giới hạn = container hai trục độc lập, gộp lại mới thành container
Hai cơ chế trực giao — namespace giấu, cgroup hạn chế

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.

Chi tiết thú vị: cgroup CŨNG là một namespace

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.

03 / VỊ TRÍ TRONG KERNEL

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.

User space process / container runtime docker · runc · kubelet · systemd · stress cgroupfs /sys/fs/cgroup — đọc/ghi file clone() / unshare() syscall tạo namespace Kernel subsystems cgroup controllers cpu · memory · io · pids namespace subsystem pid · net · mnt · uts · ipc · user · cgroup hook vào ↓ Cơ chế lõi kernel (nơi THỰC THI) CFS scheduler cpu.max throttle ở đây memory manager OOM killer ở đây block / VFS layer io throttle ở đây Hardware CPU cores · RAM · disk · NIC
cgroup là lớp policy — scheduler / memory manager / block layer mới thực thi
cgroup không tự giới hạn gì. Nó là bộ quy định ngân sách gắn vào scheduler, memory manager và block layer — những phòng ban đó mới là người thực thi.

Cụ thể từng controller hook vào đâu:

cgroup fileHook vàoHành vi thực thi
cpu.maxCFS schedulerKhông cho process chạy tiếp khi hết quota trong chu kỳ → CPU throttling
memory.maxmemory manager / OOM killerGiết process khi cgroup vượt trần RAM → OOMKilled
io.maxblock layerLà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.

04 / HAI PHIÊN BẢN

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

Legacy — mỗi controller một cây riêng
  • 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

Hiện đại — một cây thống nhất
  • 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)
cgroup v1 — nhiều cây tách rời cpu/ appA memory/ appA pids/ appA Cùng một app "appA" xuất hiện rời rạc ở 3 cây khác nhau cgroup v2 — một cây thống nhất /sys/fs/cgroup/ appA (cpu+mem+pids) appA nằm ở đúng MỘT chỗ, mọi controller gắn vào cùng node
v1 phân mảnh theo controller — v2 gom tất cả vào một cây

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.

Cách kiểm tra version

Chạy stat -fc %T /sys/fs/cgroup/ — kết quả cgroup2fs là v2, tmpfs là v1.

05 / TẦNG LINUX

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 RAM50% 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)

ControllerFileÝ nghĩa
memorymemory.maxHard limit. Vượt → OOM kill trong cgroup
memorymemory.highSoft limit. Vượt → kernel throttle + reclaim mạnh (KHÔNG kill)
memorymemory.currentĐang dùng bao nhiêu (đọc-only)
cpucpu.maxHard cap. quota period — giới hạn cứng CPU
cpucpu.weightTỉ trọng. Chỉ tác động khi tranh chấp (1–10000, mặc định 100)
cpucpu.statnr_throttled, throttled_usec — đo throttling
pidspids.maxSố process tối đa (chống fork bomb)
max là giới hạn cứng (cap hoặc kill). weight / high là mềm (chỉ tác động khi tranh chấp hoặc cảnh báo trước). Hiểu sai cặp này là nguồn gốc rất nhiều sự cố.

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ì
sliceNhóm logic (nhánh cây), không chứa process trực tiếp. VD system.slice
scopeNhóm process do bên ngoài tạo (VD container runtime)
serviceProcess 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
Bẫy: cgroup driver

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.

06 / TẦNG DOCKER

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.

docker run --memory 100m giới hạn RAM --cpus 0.5 nửa core --pids-limit 50 chống fork bomb containerd / runc tạo namespaces + ghi vào cgroupfs /sys/fs/cgroup/system.slice/docker-<id>.scope/ leaf node chứa process của container memory.max = 104857600 vượt → OOMKilled cpu.max = 50000 100000 vượt → throttle pids.max = 50 vượt → EAGAIN Quan sát từ TRONG container: cat /sys/fs/cgroup/memory.max
Mỗi flag Docker = một dòng ghi vào file cgroup tương ứng

Bảng quy đổi flag → file

Docker flagcgroup fileGiá trị
--memory 100mmemory.max104857600
--memory-reservation 80mmemory.highsoft, throttle reclaim
--cpus 0.5cpu.max50000 100000
--cpu-shares 512cpu.weighttỉ trọng tương đối
--pids-limit 50pids.max50
Bẫy kinh điển: app không cgroup-aware

Trước khi JVM / Node trở nên cgroup-aware, chúng đọc /proc/meminfonproc → 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.

07 / TẦNG KUBERNETES

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: requestslimits KHÔNG map xuống cùng một loại file cgroup.

requests

Dùng cho scheduling
  • CPUcpu.weight (tỉ trọng khi tranh chấp)
  • Memory → KHÔNG thành memory.max
  • Scheduler dùng để chọn node đủ chỗ

limits

Dùng để throttle / kill
  • CPUcpu.max (hard cap → throttle)
  • Memorymemory.max (vượt → OOMKilled)
  • Kernel dùng để thực thi giới hạn cứng
Pod spec (YAML) requests cpu: 250m mem: 256Mi → dùng cho scheduling limits cpu: 500m mem: 512Mi → dùng để throttle / kill kubelet → CRI → containerd → runc tính toán rồi ghi vào cgroupfs requests → cpu.weight tỉ trọng khi tranh CPU mem request KHÔNG thành max limits → cpu.max / memory.max CPU vượt → throttle mem vượt → OOMKilled Cây cgroup phân tầng theo QoS class: kubepods.slice/ guaranteed — req = limit burstable — req < limit besteffort — không khai báo Thứ tự bị giết khi node hết RAM: besteffort → burstable → guaranteed
Hai con đường tách biệt: requests cho scheduler, limits cho kernel

Hệ quả thực tế

CPU limit thấp → throttle dù node còn rảnh.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
PromQL: đo CPU throttling

Tỉ lệ cao = nên nới CPU limit:

rate(container_cpu_cfs_throttled_periods_total[5m]) / rate(container_cpu_cfs_periods_total[5m])

08 / TỔNG KẾT

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.

Linux
echo 104857600 > memory.max  — bạn ghi tay vào file
Docker
--memory 100m  — runc ghi file đó giúp bạn
Kubernetes
limits.memory: 100Mi  — kubelet → CRI → runc ghi file đó
Throttling, OOMKill, QoS eviction — tất cả chỉ là memory.max, cpu.max, cpu.weight đang làm đúng việc của chúng.

Checklist debug nhanh

Triệu chứngKiểm tra
Pod bị OOMKilledSo memory.current vs memory.max; tăng limit hoặc fix leak; check app có cgroup-aware không
Service chậm, latency spikecpu.statnr_throttled cao? Nới CPU limit hoặc bỏ limit
Pod không schedule đượcrequests quá cao so với node; không liên quan limit
Pod bị evict ngẫu nhiênQoS 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

cgroups — Linux → Docker → Kubernetes  ·  một cơ chế duy nhất