Requests và limits trong Kubernetes: vì sao Pod mới dùng 40% CPU đã bị throttle Trung cấp
Thời gian đọc ước tính: 8 phút
Vấn đề
Phần tiêu đề “Vấn đề”checkout-api là service NestJS chạy Node 24, 6 replica. Mỗi container đặt cpu: 1 và memory: 768Mi cho cả request lẫn limit. Trong đợt khuyến mãi, p99 của POST /checkout tăng từ 180 ms lên 1,2 s.
Dashboard cho thấy mỗi Pod chỉ dùng 40% CPU limit. Không có OOMKilled, HPA không scale, database vẫn nhàn. (Mọi con số trong kịch bản này là minh hoạ.)
Câu hỏi của team: Pod dùng chưa tới nửa CPU sao vẫn thiếu CPU, và nên chỉnh thông số nào?
Đặt gì? (tóm tắt trước)
Phần tiêu đề “Đặt gì? (tóm tắt trước)”| Tình huống | Đặt | Điều kiện |
|---|---|---|
| Mọi workload production | Request CPU và memory theo usage đo được | Scheduler và HPA chỉ nhìn request |
| Memory | limit = request | Memory không throttle được; overcommit kết thúc bằng OOM kill hoặc eviction |
| API nhạy latency, CPU theo đợt (hash, gzip, JSON) | CPU limit cao hơn hẳn request, hoặc bỏ CPU limit | Chỉ khi cluster cho phép và có theo dõi throttling |
| Cần cô lập chặt, benchmark, core riêng | CPU limit = request (Guaranteed) | Chấp nhận throttling; đặt limit theo burst đỉnh |
| Node.js trong container | --max-old-space-size ≈ 75% memory limit |
Chừa chỗ cho Buffer, native memory và thread |
| HPA theo CPU | Target utilization tính trên request | Mọi container trong Pod đều có CPU request |
Khái niệm cốt lõi
Phần tiêu đề “Khái niệm cốt lõi”| Request | Limit | |
|---|---|---|
| Ai dùng | kube-scheduler, HPA, kubelet khi xếp hạng eviction | kubelet và container runtime, kernel thực thi qua cgroup |
| CPU | Trọng số tương đối khi node tranh chấp | Quota cứng mỗi period: throttling |
| Memory | Scheduling; gợi ý memory.min/memory.low trên cgroup v2 |
Vượt thì kernel OOM-kill một process trong container |
| Nếu bỏ trống | Lấy bằng limit khi chỉ đặt limit | Không có trần: container dùng được phần trống của node |
Scheduler chỉ đặt Pod lên node mà allocatable trừ tổng request hiện có còn đủ cho request của Pod. Limit do kernel thực thi về sau, bất kể scheduler đã tính gì.
Tổ hợp request và limit quyết định QoS class của Pod, thứ quan trọng khi hết memory:
Đang tải diagram…
checkout-api với cpu: 1 và memory: 768Mi cho cả request lẫn limit là Guaranteed. Guaranteed không có nghĩa “không bao giờ bị throttle”; nó chỉ có nghĩa CPU limit bằng request.
Cách hoạt động
Phần tiêu đề “Cách hoạt động”CPU limit = CFS quota mỗi 100 ms
Phần tiêu đề “CPU limit = CFS quota mỗi 100 ms”Kubelet và container runtime đổi CPU limit thành CFS quota: quota = limit × period, period mặc định 100 ms. cpu: 1 thành cpu.max = 100000 100000 trên cgroup v2: 100 ms thời gian CPU cho mỗi 100 ms, cộng dồn mọi thread của container.
Process Node không chỉ có một thread. Các lệnh async của crypto, zlib, fs chạy trên libuv threadpool, mặc định 4 thread. Bốn lệnh hash song song có thể tiêu hết quota trong một phần tư period:
t=0: bốn request checkout, mỗi request chạy một hash 40 ms trên bốn thread của threadpool, dùng bốn core cùng lúc.
Trạng thái sau từng bước:
- t=0: quota 100 ms, bốn hash chạy trên bốn core.
- t=25 ms: 4 × 25 ms = 100 ms đã dùng. Mọi thread trong cgroup bị throttle, mỗi hash còn thiếu 15 ms.
- t=30 ms:
GET /carttới. Event loop thuộc cùng cgroup nên cũng không chạy được. - t=100 ms: quota được nạp lại. Các thread bị throttle chạy tiếp.
- t=115 ms: hash xong. Latency là 115 ms thay vì 40 ms, trong khi CPU trung bình một giây chỉ bằng 16% limit.
Vì vậy “40% limit” không chứng minh được gì. Dashboard lấy trung bình CPU trong 30 s đến 5 phút; throttling xảy ra bên trong 100 ms. Metric nhìn thấy nó là container_cpu_cfs_throttled_periods_total chia cho container_cpu_cfs_periods_total.
Memory limit: OOMKilled khác eviction
Phần tiêu đề “Memory limit: OOMKilled khác eviction”Memory có hai đường hỏng độc lập:
cấp phátĐường 1: V8 heap và Buffer của checkout-api tăng; working set chạm 768Mi.
| OOMKilled | Node-pressure eviction | |
|---|---|---|
| Kích hoạt | Container vượt memory limit của chính nó | Tín hiệu của node, ví dụ memory.available<100Mi (hard threshold mặc định trên Linux) |
| Ai ra tay | OOM killer của kernel trong cgroup | kubelet, trước khi node cạn hẳn |
| Kết quả | Container restart trong cùng Pod, exit 137 |
Pod chuyển phase Failed; controller tạo Pod mới |
| Thứ tự nạn nhân | Container chạm limit | Pod dùng vượt request trước, rồi theo priority, rồi theo mức vượt request |
Thực tế Pod BestEffort (request 0) đi trước, Guaranteed đi sau cùng vì Pod Guaranteed không thể dùng vượt request. Nếu node OOM thật trước khi kubelet kịp phản ứng, kernel chọn theo oom_score_adj: −997 cho Guaranteed, 1000 cho BestEffort.
Chi tiết vận hành
Phần tiêu đề “Chi tiết vận hành”Node.js trong container (đo với Node 24.22 trong Docker):
os.availableParallelism()trả 1 với--cpus=1, nhưngos.cpus().lengthvẫn liệt kê mọi core của host. Threadpool vẫn 4 thread, không phụ thuộc CPU limit.UV_THREADPOOL_SIZEphải đặt trước khi process khởi động (biến môi trường), không đặt từ code.- Không truyền flag, heap limit của V8 đi theo container limit: 432 MB ở 768Mi, 560 MB ở 1Gi. Phần còn lại của RSS (Buffer, native code, stack của thread) nằm ngoài heap.
--max-old-space-size=576ở 768Mi cho heap limit 624 MB (old space cộng young generation), nên Node dừng vớiJavaScript heap out of memorykèm GC trace trước khi kernel gửi SIGKILL im lặng.
Chọn số từ metric (ít nhất một tuần, gồm ngày bận nhất):
- CPU request ≈ p90–p95 của
rate(container_cpu_usage_seconds_total[5m])mỗi Pod lúc cao điểm. - Memory request = limit ≈ max
container_memory_working_set_bytescộng 20–30% dư. - Theo dõi tỷ lệ throttling sau mỗi lần đổi; dưới 5% số period là điểm xuất phát hay dùng, không phải chuẩn.
HPA theo CPU:
desired = ceil(current × currentUtilization / target), trong đó utilization là usage chia cho request, lấy trung bình các Pod.- Đồng bộ mỗi 15 s, bỏ qua thay đổi trong tolerance 10%, chờ 5 phút trước khi scale down.
- Container nào thiếu CPU request thì HPA không làm gì với metric đó.
Tranh luận về CPU limit:
- Không CPU limit: container dùng CPU rảnh của node và chỉ được chia theo request khi tranh chấp. Không throttling, latency kém đoán trước hơn.
- Có CPU limit: hành vi dễ đoán, nhưng burst phải trả giá bằng throttling.
- Memory thì khác: không thể thu hồi memory bằng cách cho process chạy chậm lại, nên giữ limit.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Demo chạy 20 đợt, mỗi đợt bốn lệnh pbkdf2 song song, cách nhau 900 ms, và đọc cpu.stat của cgroup:
docker run --rm --cpus=1 -v "$PWD/examples/src/kubernetes-resources-limits:/demo" \ node:24-alpine node /demo/throttle-demo.ts| CPU limit Docker | p50 | p99 | CPU trung bình | Period bị throttle |
|---|---|---|---|---|
--cpus=1 |
105–189 ms | 221–299 ms | 19–22% | 31–39 trên 72–81 |
--cpus=1, UV_THREADPOOL_SIZE=1 |
78 ms | 180 ms | 12% | 3 trên 66 |
--cpus=2 |
53 ms | 171 ms | 23% | 12 trên 57 |
--cpus=4 |
51 ms | 83 ms | 23% | 0 trên 53 |
| không limit | 55 ms | 82 ms | 23% | — |
Đo trên Docker Desktop (cgroup v2, 10 core); số thay đổi giữa các lần chạy. CPU trung bình chưa tới một phần tư core, vậy mà với --cpus=1 khoảng một nửa số period có hoạt động bị throttle.
Trade-offs
Phần tiêu đề “Trade-offs”CPU limit chặt (limit = request)
- Hiệu năng dễ đoán; test trên staging khớp production.
- Pod chạy loạn không lấy được CPU của Pod hàng xóm.
- Có QoS Guaranteed; bắt buộc nếu muốn core riêng với static CPU manager policy.
Cái giá
- Burst ngắn bị throttle dù node còn core rảnh.
- Latency spike không hiện trên dashboard CPU trung bình.
- Cần thêm replica để hấp thụ burst, tốn tiền hơn.
Checklist production
Phần tiêu đề “Checklist production”- Mọi container đặt request CPU và memory từ ít nhất một tuần metric.
- Memory limit bằng memory request;
--max-old-space-sizekhoảng 75% con số đó. - Nếu giữ CPU limit, cảnh báo khi tỷ lệ throttling trên 5% kéo dài 10 phút.
UV_THREADPOOL_SIZEchọn theo CPU limit và lượng việc hash, gzip, fs.- Target HPA chỉnh theo request, và mọi container trong Pod đều có CPU request.
- Load test với burst sát thực tế chạy trước khi đổi limit, đo p99 và throttling cùng lúc.
- Kiểm tra allocatable của node và tổng request mỗi node trước khi tăng request hoặc limit.
- Có cảnh báo cho restart do OOMKilled và cho Pod bị evict.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Bỏ memory limit để “hết OOMKilled” chỉ đẩy rủi ro sang node:
Memory không có limit
Hay gặp
resources: requests: { cpu: 500m, memory: 256Mi } # usage thật 700Mi, không limitNên viết
resources: requests: { cpu: "1", memory: 768Mi } limits: { cpu: "2", memory: 768Mi } # memory limit = requestVì sao
Usage vượt xa request khiến Pod này là ứng viên bị evict đầu tiên khi node thiếu memory, và lúc phình ra nó có thể đẩy Pod khác ra ngoài.
| Cách hiểu sai | Hệ quả | Cần làm |
|---|---|---|
| QoS Guaranteed thì không bị throttle | Burst vẫn chạm CPU quota | Theo dõi tỷ lệ throttling cho mọi class |
| Pod bị throttle thì HPA sẽ scale | Usage trung bình vẫn dưới target, HPA không làm gì | Đặt request theo usage; cảnh báo theo throttling |
| Thêm replica là hết throttle | Mỗi Pod vẫn burst trong quota của chính nó | Nâng limit hoặc giảm burst song song |
Tự kiểm tra
Phần tiêu đề “Tự kiểm tra”- Vì sao container
cpu: 1có thể bị throttle khi trung bình chỉ dùng 0,2 core? - Pod bị OOMKilled trong khi node còn memory trống. Limit nào bị chạm, và Pod ra sao?
- Khi node thiếu memory, kubelet evict Pod nào trước, và vì sao Pod Guaranteed đi sau cùng?
- Request 200m, usage thật 600m: HPA thấy gì?
Tóm tắt
Phần tiêu đề “Tóm tắt”Tham khảo
Phần tiêu đề “Tham khảo”- Resource Management for Pods and Containers — Kubernetes: request và limit được áp dụng, thực thi ra sao.
- Pod Quality of Service Classes — Kubernetes: điều kiện chính xác của Guaranteed, Burstable, BestEffort.
- Node-pressure Eviction — Kubernetes: ngưỡng, thứ tự eviction và
oom_score_adj. - Horizontal Pod Autoscaling — Kubernetes: công thức scale, tolerance và stabilization.
- CFS Bandwidth Control — Linux kernel: quota, period và thống kê throttling.
- Command-line options — Node.js v24:
UV_THREADPOOL_SIZEvà các flag heap. - Prometheus metrics — cAdvisor: tên các counter throttling của CFS.