Hai khối hay bị sửa đi sửa lại nhiều nhất trong manifest của một ứng dụng mới lên Kubernetes là resources và affinity. Nguyên nhân thường giống nhau: chưa hiểu rõ requests và limits tác động thế nào tới việc pod được xếp lên node nào, bị bóp CPU ra sao và khi nào bị kill. Tài liệu này trình bày phần kiến thức đó, cách chọn con số cho một container Laravel, ba lớp QoS, và cách gán pod vào đúng node pool của LKE bằng nodeAffinity.
1. Requests và limits là gì
Requests và limits là hai con số mà mỗi container có thể khai cho từng loại tài nguyên (CPU, bộ nhớ):
- request: lượng tài nguyên container được đảm bảo. Scheduler dựa vào con số này để chọn node.
- limit: mức trần container không được vượt. Kubelet và kernel (qua cgroup) thực thi con số này khi container đang chạy.
Đơn vị CPU là core, viết 1500m nghĩa là 1,5 core. Đơn vị bộ nhớ thường dùng Mi/Gi (lũy thừa của 2). Ví dụ khối requests và limits cho một container Laravel (con số chỉ minh hoạ):
containers:
- name: web
image: <registry>/laravel-app:<tag>
resources:
requests:
memory: "450Mi"
cpu: "150m"
limits:
memory: "1800Mi"
cpu: "1500m"Đọc theo ngôn ngữ của scheduler: "hãy tìm node còn trống ít nhất 150m CPU và 450Mi RAM để đặt pod này; khi chạy, cho nó dùng tới 1,5 core và 1800Mi, không hơn". Hai con số trong requests và limits phục vụ hai việc khác nhau, nên đặt sai một trong hai gây ra hai loại sự cố khác nhau. Tài liệu đầy đủ ở trang quản lý tài nguyên cho container của Kubernetes.
Một chi tiết hay bị bỏ qua: nếu chỉ có limit mà không có request, Kubernetes tự lấy limit làm request. Container khai limit 2Gi "cho thoáng" sẽ bị scheduler coi là cần đủ 2Gi.
2. Scheduler dùng request, không nhìn mức dùng thật
Mỗi node có một con số allocatable: tổng tài nguyên trừ phần giữ lại cho hệ điều hành và kubelet. Khi xếp một pod mới, scheduler cộng request của mọi pod đang nằm trên node, cộng request của pod mới, rồi so với allocatable. Nó không nhìn CPU hay RAM đang dùng thực tế.
Hệ quả:
- Một node đang nhàn rỗi vẫn bị coi là "đầy" nếu các pod trên đó khai request quá cao. Pod mới nằm
Pendingvới sự kiệnInsufficient cpuhoặcInsufficient memory. - Ngược lại, request khai thấp hơn thực tế khiến scheduler nhồi quá nhiều pod lên một node. Lúc tải lên, các pod giành nhau tài nguyên thật.
- Trong cặp requests và limits, limit không tham gia quyết định xếp lịch. Tổng limit trên một node vượt 100% allocatable là chuyện bình thường, gọi là overcommit.
Xem node đang được "đặt chỗ" bao nhiêu, tính theo cả requests và limits:
kubectl describe node <ten-node> | grep -A 8 "Allocated resources"Với pod có initContainer, request hiệu lực của pod là giá trị lớn hơn giữa tổng request các container chính và request lớn nhất của các initContainer. Một initContainer chạy php artisan migrate --force khai request RAM cao cũng góp phần quyết định pod có lọt vào node hay không.
3. Vượt limit: CPU bị bóp, bộ nhớ thì bị kill
CPU và bộ nhớ phản ứng rất khác nhau khi container chạm limit. Đây là phần quan trọng nhất khi chọn requests và limits cho một ứng dụng PHP.
| CPU | Bộ nhớ | |
|---|---|---|
| Loại tài nguyên | Nén được: chia nhỏ theo thời gian | Không nén được: đã cấp thì không lấy lại được |
| Khi chạm limit | Bị throttle: kernel tạm dừng tiến trình tới hết chu kỳ (CFS quota, mặc định 100ms) | Bị OOMKilled: kernel kill tiến trình, container thoát với mã 137 |
| Triệu chứng | Request chậm dần, timeout, pod vẫn Running | RESTARTS tăng, Last State: Terminated, Reason: OOMKilled |
| Cách nhìn thấy | Metric container_cpu_cfs_throttled_periods_total | kubectl describe pod, sự kiện của pod |
Với Laravel, limit CPU quá thấp thường biểu hiện thành trang chậm chứ không thành lỗi rõ ràng, nên dễ đổ nhầm cho code hoặc database. Limit bộ nhớ quá thấp dễ thấy hơn vì pod khởi động lại. Tuy vậy, image all-in-one chứa nhiều tiến trình (nginx, PHP-FPM, queue worker), nên tiến trình bị kill có thể chỉ là một worker và pod vẫn sống. Ngoài requests và limits của container còn memory_limit của PHP: tổng memory_limit nhân số worker vượt limit container thì kernel sẽ kill trước khi PHP kịp báo lỗi.
4. QoS: lớp ưu tiên sinh ra từ requests và limits
Từ cách khai requests và limits, Kubernetes xếp mỗi pod vào một trong ba lớp QoS (Quality of Service). Khi node cạn bộ nhớ, kubelet đuổi (evict) pod theo thứ tự sau:
| Lớp QoS | Điều kiện | Khi node thiếu RAM |
|---|---|---|
| Guaranteed | Mọi container đều có request = limit cho cả CPU và bộ nhớ | Bị đuổi sau cùng |
| Burstable | Có ít nhất một request hoặc limit, nhưng không đạt điều kiện Guaranteed | Bị đuổi sau BestEffort, ưu tiên pod dùng vượt request nhiều nhất |
| BestEffort | Không container nào khai request hay limit | Bị đuổi đầu tiên |
Xem lớp QoS của một pod:
kubectl get pod -n app <ten-pod> -o jsonpath='{.status.qosClass}'Container Laravel trong ví dụ trên có requests và limits khác nhau nên là Burstable. Một MySQL chỉ có request mà không có limit cũng là Burstable. Muốn database được bảo vệ tốt nhất khi node thiếu RAM, hãy đặt request bằng limit để pod thành Guaranteed. Hướng dẫn chính thức về cấu hình lớp QoS cho pod có ví dụ cho cả ba lớp.
5. Chọn requests và limits cho container Laravel
Khi chọn requests và limits, nhớ rằng một container chứa cả nginx, PHP-FPM, cron và queue worker cần nhiều tài nguyên hơn trực giác. Limit CPU vài trăm millicore thường quá chật: migrate, build cache hay render trang nặng sẽ bị throttle. Cách làm hay dùng ở giai đoạn đầu là "request thấp, limit rộng":
- Request phản ánh mức nền khi site nhàn rỗi, để scheduler xếp được nhiều pod trên một node.
- Limit đủ rộng cho lúc cao điểm ngắn hạn, và đủ chặt để một site lỗi không nuốt cả node.
- Đo rồi mới chỉnh. Lấy mức dùng thật nhiều ngày trên Grafana (ví dụ phân vị 95 của RAM và CPU), đặt request gần mức nền, limit RAM trên đỉnh một khoảng an toàn.
Đổi một con số trong requests và limits, apply, xem pod có lên không rồi lại đổi là cách làm tốn thời gian và dễ sai. Gom các thay đổi về tài nguyên vào một lần, có số liệu đi kèm, và đưa vào pipeline thay vì kubectl apply bằng tay.
6. Xếp pod lên đúng node: nodeSelector, nodeAffinity và label pool
Requests và limits chỉ trả lời câu hỏi "node nào còn đủ chỗ". Muốn chỉ định pod chạy trên nhóm node nào, Kubernetes có hai cách dựa vào label của node:
- nodeSelector: đơn giản nhất, liệt kê các cặp label phải khớp chính xác.
- nodeAffinity: diễn đạt được nhiều hơn, với toán tử
In,NotIn,Exists, và hai mức: bắt buộc (requiredDuringSchedulingIgnoredDuringExecution) hoặc ưu tiên (preferredDuringSchedulingIgnoredDuringExecution).
Sai lầm phổ biến là ghim theo label kubernetes.io/hostname. Tên node trên LKE có dạng lke<cluster-id>-<pool-id>-<hậu tố ngẫu nhiên>, và phần hậu tố đổi mỗi khi node bị thay: recycle node pool, nâng cấp phiên bản Kubernetes, xoá rồi thêm node. Ghim theo hostname đồng nghĩa với việc mỗi lần LKE thay node, pod sẽ nằm Pending cho tới khi ai đó vào sửa manifest.
LKE gắn sẵn cho mỗi node label lke.linode.com/pool-id, giá trị là ID của node pool chứa nó. ID này không đổi khi node bên trong pool được thay, nên hãy ghim theo pool:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: lke.linode.com/pool-id
operator: In
values:
- "<pool-id>" # bắt buộc là chuỗi, phải có dấu nháyNếu chỉ cần khớp một giá trị, nodeSelector ngắn hơn:
nodeSelector:
lke.linode.com/pool-id: "<pool-id>"nodeAffinity tiện hơn khi sau này mở rộng sang nhiều pool: chỉ cần thêm giá trị vào values. Xem ID pool của các node:
kubectl get nodes -L lke.linode.com/pool-idChi tiết về toán tử và hai mức required/preferred nằm trong tài liệu gán pod vào node của Kubernetes. Phần tạo node pool có trong bài Dựng cluster LKE đầu tiên: node pool, kubeconfig và lựa chọn ban đầu. Áp dụng cùng cách cho stack giám sát trong bài Giám sát Kubernetes với kube-prometheus và Grafana: Prometheus, Grafana và webhook của operator nên ghim vào pool hệ thống theo pool-id, không theo hostname.
7. Lỗi thường gặp với requests và limits, affinity
- Pod
Pending, sự kiệnInsufficient memory: tổng request trên các node đã chạm allocatable, dù RAM thật còn trống. Giảm request cho đúng mức nền, hoặc thêm node vào pool. - Pod
Pending, sự kiệndidn't match Pod's node affinity/selector: label không khớp node nào, thường do gõ sai ID pool hoặc pool đã bị xoá. Kiểm tra bằngkubectl get nodes -L lke.linode.com/pool-id. - Giá trị label viết dạng số:
values: [12345]không hợp lệ vì trường này là danh sách chuỗi. Luôn bọc trong dấu nháy. - Trang chậm bất thường nhưng CPU của node thấp: nghi ngờ limit CPU quá chặt, container bị throttle. Xem metric throttling trên Prometheus trước khi tối ưu code.
- Pod restart với lý do
OOMKilled: limit bộ nhớ thấp hơn mức dùng thật lúc cao điểm. Nâng limit hoặc giảm số worker PHP-FPM trong container.
8. Khuyến nghị và đánh đổi
- Request là lời hứa với scheduler, limit là hàng rào lúc chạy. Đặt requests và limits theo đúng vai trò của từng con số.
- "Request thấp, limit rộng" nhồi được nhiều pod, nhưng biến mọi pod thành Burstable và làm tổng limit vượt xa allocatable. Khi nhiều pod cùng bùng tải, node có thể thiếu RAM thật. Chấp nhận được ở giai đoạn thử, cần đo lại khi có traffic thật.
- Database cũng cần requests và limits sát thực tế. MySQL không có limit sẽ dùng RAM theo cấu hình buffer, có thể lấn sang các pod khác cùng node.
- Ghim theo thứ ổn định. Chọn label mô tả vai trò của node (pool), không chọn label mô tả một máy cụ thể.
- Đưa thay đổi manifest vào CI/CD. Mỗi lần sửa và apply bằng tay là một lần có thể sai.
Bài tiếp theo
CI/CD đầu tiên với Bitbucket Pipelines, Docker Hub và kubectl. Build image, đẩy lên registry và apply manifest tự động mỗi lần push code, thay cho những lần kubectl apply bằng tay.
