Mỗi container Docker bạn chạy với --cpus=0.5 hay --memory=512m thực ra đang dùng một cơ chế nhân Linux tên là cgroups (control groups) — thứ giới hạn và kế toán tài nguyên theo nhóm tiến trình. Nó là nền tảng của mọi container và của cả systemd. Nhiều người hình dung giới hạn CPU của cgroup giống như nice ở phần 17: một kiểu "ưu tiên thấp", chỉ làm chậm khi máy bận. Tôi đo trong Docker (Docker đặt cgroup dưới nền), và con số cho thấy nó là một thứ khác hẳn — một cái trần cứng, tuyệt đối, bó bạn ngay cả khi cả máy đang rảnh.

cgroups giới hạn tài nguyên

cgroup CPU: một hạn ngạch, không phải một ưu tiên

Giới hạn CPU của cgroup hoạt động bằng hạn ngạch (quota): file điều khiển cpu.max chứa hai số "quota period". Ví dụ 50000 100000 nghĩa là "được dùng 50.000 micro-giây CPU trong mỗi 100.000 micro-giây" — tức 50% của một lõi. Cơ chế rất thẳng thừng: tiến trình chạy cho tới khi dùng hết 50ms quota trong period, rồi bị đóng băng (throttle) cho tới đầu period sau mới được chạy tiếp. Đây là hạn ngạch tuyệt đối, không phải một mức ưu tiên tương đối.

Khác biệt với nice là mấu chốt. nice chỉ ảnh hưởng khi có tranh chấp — nếu máy rảnh, một tiến trình nice 19 vẫn dùng trọn CPU. Còn cgroup quota bó bạn bất kể máy rảnh hay bận. Tôi kiểm chứng bằng cách chạy một workload CPU thuần, đơn luồng, công cố định (khoảng 1,85 giây CPU) trong các container với --cpus khác nhau, trên một máy 10 lõi mà 9 lõi ngồi không.

Đo: bó cứng dù 9 lõi rảnh

Công cố định ~1,85s CPU, đơn luồng, máy 10 lõi (9 lõi rảnh):
  cpu.max của --cpus=0.5 : "50000 100000"  (quota 50ms / period 100ms)

  --cpus=1     : 1,85 s   (baseline)
  --cpus=0.5   : 4,06 s   (~2,2 lần) — dù 9 lõi ngồi không, vẫn bị bó
  --cpus=0.25  : 12,1 s   (~6,5 lần) — tệ hơn cả 4 lần
  --cpus=2     : 1,85 s   (đơn luồng nên thêm quota vô ích)

Với --cpus=0.5, workload mất 4,06 giây thay vì 1,85 — chậm ~2,2 lần. Điều quan trọng: máy có 10 lõi và 9 lõi đang rảnh hoàn toàn, nhưng tiến trình vẫn bị bó ở nửa một lõi. Nó không "mượn tạm" lõi rảnh như nice cho phép; hết quota là bị đóng băng, chấm hết. Đây là bằng chứng trực tiếp: cgroup quota là trần cứng tuyệt đối, không phải ưu tiên.

Với --cpus=0.25, còn một bất ngờ: workload mất 12,1 giây — chậm ~6,5 lần, tệ hơn cái 4 lần mà toán học ngây thơ (1/0.25) dự đoán. Vì sao? Vì cơ chế throttle đóng băng tiến trình theo đợt dài: với quota 25ms mỗi period 100ms, tiến trình chạy 25ms rồi ngủ đông 75ms. Những đợt đóng băng dài đó không chỉ làm chậm tuyến tính — chúng thêm độ trễ (và trên môi trường ảo hóa, chi phí đánh thức lại). Nên ở hạn ngạch nhỏ, hiệu ứng còn tệ hơn tỉ lệ. Ngược lại, --cpus=2 cho workload đơn luồng chẳng giúp gì (vẫn 1,85s) — quota là trần, cho dư trần mà việc không dùng tới thì vô ích.

Một cái bẫy nữa lộ ra khi đo: bên trong container --cpus=0.5, lệnh nproc vẫn báo 10 — số lõi vật lý, không phản ánh hạn ngạch. Một chương trình dựng thread pool theo nproc (như thread pool phần 30 Series 25) sẽ tạo 10 luồng trong một container chỉ được nửa lõi — oversubscribe nặng.

Một lần tôi đo hớ: "cgroup CPU limit giống nice"

Tôi vào đo với niềm tin rằng giới hạn CPU của cgroup là một dạng "ưu tiên thấp": chỉ làm chậm khi máy bận, còn máy rảnh thì tiến trình chạy thoải mái. Đo phá tan: --cpus=0.5 bó workload xuống 2,2 lần dù 9 lõi ngồi không. Nó là hạn ngạch, không phải ưu tiên — và hai thứ này hành xử hoàn toàn khác nhau. nice là tương đối (chia phần khi tranh); cgroup quota là tuyệt đối (một cái trần bạn không vượt được, kể cả khi có thừa CPU).

Bài học đo lường: cgroup CPU quota là một trần cứng tuyệt đối — nó bó tiến trình dưới hạn ngạch ngay cả khi phần còn lại của máy hoàn toàn rảnh, nên một tiến trình bị giới hạn có thể chậm trên một máy trống; và ở hạn ngạch nhỏ, throttle theo đợt còn làm nó tệ hơn tỉ lệ. Nếu tôi tin "cgroup giống nice, máy rảnh thì không sao", tôi đã bối rối khi thấy một container chạy chậm rề trên một host gần như không tải — rồi đi tìm nút thắt ở sai chỗ (CPU host, I/O…) trong khi thủ phạm là chính cái --cpus tôi đặt. Đây cũng là lý do một service trong Kubernetes với cpu limit thấp có thể bị throttle nặng và trễ, dù cluster còn rất nhiều CPU.

Vì sao điều này quan trọng khi lập trình

Hệ quả đầu tiên: hiểu --cpus / cpu limit là hạn ngạch, không phải ưu tiên — và đặt nó cẩn thận. Một giới hạn CPU quá thấp sẽ làm dịch vụ của bạn chậm và trễ ngay cả khi host rảnh, vì nó bị đóng băng mỗi period. Nếu bạn thấy một container chậm bất thường mà host không tải, hãy kiểm cpu.max và thống kê throttle (cpu.stat) trước khi nghi ngờ code. Với tác vụ nhạy độ trễ, hạn ngạch nhỏ đặc biệt hại vì các đợt đóng băng dài.

Hệ quả thứ hai: đừng dựa vào nproc (hay số lõi hệ thống) để dựng thread pool trong container. nproc báo số lõi vật lý, không phải hạn ngạch cgroup, nên bạn dễ tạo quá nhiều luồng cho quota thực có — dẫn tới oversubscription và chuyển ngữ cảnh bắt buộc. Hãy đọc hạn ngạch thật (cpu.max, hoặc các runtime hiện đại đã tự nhận), hoặc đặt số luồng theo cấu hình rõ ràng.

Hệ quả thứ ba là tinh thần đo lường: phân biệt giới hạn (quota, tuyệt đối) với ưu tiên (tương đối) — chúng hành xử khác nhau khi máy rảnh. Con số mang theo: cgroup CPU quota là TRẦN CỨNG tuyệt đối: một workload công cố định chạy 1,85s ở --cpus=1 mất 4,06s ở --cpus=0.5 (2,2×) và 12,1s ở --cpus=0.25 (6,5×) DÙ máy 10 lõi có 9 lõi RẢNH — vì hết quota là bị đóng băng tới period sau, khác nice (phần 17) chỉ tác động khi tranh; hạn ngạch nhỏ còn tệ hơn tỉ lệ (đóng băng đợt dài); nproc vẫn báo 10 dù bị --cpus=0.5, và memory.max vượt thì OOM (phần 29). cgroup là để cách ly và chia một máy giữa nhiều nhóm cho công bằng/an toàn — không phải để tăng tốc, và một giới hạn đặt sai sẽ làm chậm chính bạn trên một máy trống.

Thử ba mươi giây

Chạy một vòng lặp tính toán ngốn CPU trong hai container Docker: một --cpus=1, một --cpus=0.5, và bấm giờ cả hai — trên máy của bạn, dù nó có bao nhiêu lõi rảnh. Bạn sẽ thấy cái --cpus=0.5 mất gấp đôi thời gian, không phải vì thiếu lõi (còn thừa), mà vì hạn ngạch đóng băng nó nửa thời gian mỗi period. Rồi docker exec vào container --cpus=0.5 và chạy cat /sys/fs/cgroup/cpu.max (thấy 50000 100000) và nproc (vẫn thấy tất cả lõi vật lý — cái bẫy). Ba mươi giây đó cho bạn thấy điều mà chữ "giới hạn CPU" hay bị hiểu lầm: nó không phải "ưu tiên thấp, máy rảnh thì thoải mái", mà là một cái trần cứng — bạn không vượt qua nó dù cả thế giới đang rảnh.