ResourceQuota & LimitRange — Quản Trị Tài Nguyên Namespace
Vì sao đây là nền của multi-tenancy
Trong một cluster GKE chia sẻ giữa nhiều team/tenant, câu hỏi sống còn là: làm sao một team không nuốt hết tài nguyên của cả cluster? Không có cơ chế nào, một Deployment cấu hình sai replicas: 1000 hoặc một Job lỗi tạo Pod vô hạn sẽ chiếm sạch CPU/memory, đẩy mọi Pod khác vào Pending, và làm sập dịch vụ của team khác. Đây là bài toán "noisy neighbor" ở tầng tài nguyên.
Kubernetes giải bài toán này bằng hai admission plugin built-in (file 1) hoạt động phối hợp:
- ResourceQuota — đặt trần tổng tài nguyên một namespace được dùng (CPU, memory, số Pod, storage...). Plugin validating: từ chối request làm vượt trần.
- LimitRange — đặt default và ràng buộc per-object trong namespace (mỗi container mặc định bao nhiêu, tối đa/tối thiểu bao nhiêu). Plugin mutating + validating.
Hai cơ chế này phải đi cùng nhau — đây là luận điểm trung tâm của file. ResourceQuota một mình tạo ra một cái bẫy khó chịu cho developer; LimitRange là mảnh ghép giải cứu. Hiểu vì sao đòi hỏi nắm thứ tự chúng chạy trong pipeline.
ResourceQuota — trần tổng theo namespace
ResourceQuota object đặt giới hạn tổng cho một namespace (Resource Quotas). Plugin theo dõi usage hiện tại và từ chối (HTTP 403) mọi request làm tổng vượt hard.
Ba nhóm tài nguyên có thể giới hạn
1. Compute (CPU/memory):
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-a-compute
namespace: team-a
spec:
hard:
requests.cpu: "20" # tổng requests.cpu ≤ 20 core
requests.memory: 40Gi
limits.cpu: "40" # tổng limits.cpu ≤ 40 core
limits.memory: 80Girequests.cpu/requests.memory giới hạn tổng request (ảnh hưởng scheduling — xem Chương 8); limits.cpu/limits.memory giới hạn tổng limit (trần burst). cpu/memory là alias cho requests.*.
2. Storage (theo StorageClass):
spec:
hard:
requests.storage: 500Gi
persistentvolumeclaims: "20"
premium-rwo.storageclass.storage.k8s.io/requests.storage: 100GiQuota storage có thể chia theo từng StorageClass — quan trọng trên GKE nơi premium-rwo (SSD) đắt hơn standard-rwo nhiều (xem Chương 11 về storage). Giới hạn riêng SSD ngăn tenant vô tình dùng hết quota disk đắt tiền.
3. Object count:
spec:
hard:
pods: "100"
services: "20"
services.loadbalancers: "5" # mỗi LB là một IP công khai tốn tiền
count/deployments.apps: "50"
count/cronjobs.batch: "10"Object count quota chống các kiểu cạn kiệt không phải compute: quá nhiều LoadBalancer Service (mỗi cái một IP công khai + chi phí), quá nhiều PVC, quá nhiều Secret. count/<resource>.<group> là cú pháp tổng quát giới hạn bất kỳ loại tài nguyên nào.
Quy tắc bắt buộc: có quota compute → mọi Pod phải khai báo
Đây là hành vi gây bối rối nhất và là lý do LimitRange tồn tại. Theo tài liệu: khi namespace có quota cho cpu hoặc memory (request hoặc limit), thì mọi Pod tạo trong namespace đó buộc phải khai báo giá trị tương ứng — Pod thiếu sẽ bị từ chối thẳng (quota & requests).
Lý do hợp lý: plugin không thể cộng dồn usage nếu Pod không khai báo nó dùng bao nhiêu. Nhưng hệ quả với developer là khó chịu: họ apply một Deployment đơn giản không đặt resources, và bị từ chối với lỗi must specify limits.cpu, requests.cpu.... Trên cluster không có quota, Deployment đó chạy bình thường (thành BestEffort QoS). Thêm quota = phá vỡ mọi manifest không khai báo resource. Đây chính là chỗ LimitRange vào cuộc.
LimitRange — default và ràng buộc per-object
LimitRange đặt chính sách per-container/per-Pod trong namespace (Limit Ranges). Nó làm bốn việc:
apiVersion: v1
kind: LimitRange
metadata:
name: team-a-limits
namespace: team-a
spec:
limits:
- type: Container
default: # limit mặc định nếu container không khai
cpu: 500m
memory: 512Mi
defaultRequest: # request mặc định nếu container không khai
cpu: 100m
memory: 128Mi
min: # request/limit tối thiểu cho phép
cpu: 50m
memory: 64Mi
max: # request/limit tối đa cho phép
cpu: "4"
memory: 8Gi
maxLimitRequestRatio: # tỷ lệ limit/request tối đa
cpu: "4"
memory: "2"defaultRequest/default(mutating): nếu container không khairequests/limits, LimitRanger tự điền giá trị này vào Pod spec. Đây là mảnh ghép giải cứu ResourceQuota — Pod thiếu khai báo được tự điền, nên thỏa quy tắc bắt buộc của quota.min/max(validating): từ chối Pod có request/limit ngoài khoảng. Chống cả hai cực: container đòi 64 core (max), và container đặt request quá nhỏ rồi OOM/throttle (min).maxLimitRequestRatio(validating): giới hạn tỷ lệlimit/request. Ví dụ ratio CPU = 4 nghĩa là limit không được quá 4× request. Đây là control tinh tế về độ overcommit — ratio cao nghĩa Pod request ít nhưng burst nhiều, gây overcommit và node áp lực. Ratio thấp ép Pod khai request sát thực tế, giúp bin-packing và scheduling đoán định hơn.
Thứ tự phối hợp trong pipeline — vì sao cần cả hai
Đây là điểm mấu chốt kết nối file này với file 1. Nhớ lại pipeline: mutating phase chạy trước validating phase. Hai plugin này nằm ở các vị trí khác nhau:
CREATE Pod (không khai resources)
│
▼ [MUTATING] LimitRanger điền defaultRequest/default
▼ → Pod giờ có requests=100m/128Mi, limits=500m/512Mi
│
▼ [VALIDATING] LimitRanger kiểm min/max/ratio → OK
▼ [VALIDATING] ResourceQuota cộng dồn: tổng namespace + Pod mới ≤ hard? → OK/Từ chối
│
▼ Ghi etcdTrình tự này giải thích vì sao ResourceQuota mà không có LimitRange là cấu hình sai:
- Có quota compute, không có LimitRange → Pod thiếu khai báo không được điền default → vi phạm quy tắc bắt buộc → bị từ chối. Developer bối rối.
- Có cả hai → LimitRanger điền default trước → Pod hợp lệ → ResourceQuota cộng dồn giá trị đã điền. Mọi thứ trơn tru.
Quy tắc production sắt đá: mỗi namespace có ResourceQuota compute thì phải có LimitRange với defaultRequest/default. Đây là cặp bài trùng, không bao giờ tách.
Scoped quota — quota theo PriorityClass
Quota cơ bản áp cho mọi Pod trong namespace. Nhưng thường ta cần phân biệt: "team A được dùng 20 core cho workload production, và thêm 10 core cho workload batch ưu tiên thấp". scopeSelector cho phép quota chỉ đếm Pod thỏa điều kiện (quota scopes):
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-a-prod
namespace: team-a
spec:
hard:
requests.cpu: "20"
scopeSelector:
matchExpressions:
- operator: In
scopeName: PriorityClass
values: ["production"] # chỉ đếm Pod có priorityClassName=productionCác scope khả dụng:
PriorityClass(mạnh nhất) — đếm Pod theo PriorityClass cụ thể. Cho phép tách quota theo tầng ưu tiên: production có trần riêng, batch có trần riêng. Đây là nền cho mô hình "production không bao giờ bị batch lấn quota".Terminating/NotTerminating— Pod có/không cóactiveDeadlineSeconds(thường là Job vs service thường).BestEffort/NotBestEffort— Pod không có/có request-limit (theo QoS class).NotBestEffortthường dùng để đặt quota compute chỉ cho Pod thực sự reserve tài nguyên.
Scoped quota theo PriorityClass là một trong những công cụ multi-tenancy mạnh nhất: kết hợp với Priority & Preemption (Chương 8), nó đảm bảo Pod production luôn có quota dành riêng, còn batch chỉ dùng phần dư — và khi tài nguyên khan, batch bị preempt trước.
Production architecture patterns
Khuôn namespace chuẩn cho multi-tenant
Mỗi namespace tenant nên được tạo từ một khuôn gồm bốn thứ luôn đi cùng: (1) ResourceQuota compute, (2) ResourceQuota object-count (đặc biệt services.loadbalancers), (3) LimitRange với default + min/max + ratio, (4) PSA label (file 5). Bốn thứ này nên được áp tự động khi tạo namespace — qua Config Sync (file 6) hoặc một policy bắt buộc, không phải tùy team nhớ.
Quota theo PriorityClass cho tách production/batch
Đặt hai ResourceQuota scoped: một cho PriorityClass production (trần lớn, dành riêng), một cho batch (trần nhỏ hơn, phần dư). Workload production không bao giờ bị batch chiếm quota; batch dùng phần thừa và bị preempt khi cần. Đây là mô hình chuẩn cho cluster chạy lẫn workload latency-sensitive và batch.
Object-count quota chống chi phí ẩn
services.loadbalancers và quota PVC theo StorageClass đắt tiền là các control chi phí, không chỉ tài nguyên. Mỗi LoadBalancer là một IP công khai và một forwarding rule tính phí; giới hạn số lượng ngăn một team vô tình tạo hàng chục LB. Đây là điểm giao giữa governance tài nguyên và FinOps.
Common mistakes / anti-patterns
ResourceQuota không kèm LimitRange
Sai lầm phổ biến nhất của file này. Hậu quả: developer apply manifest không khai resource bị từ chối với lỗi khó hiểu, năng suất giảm, support ticket tăng. Luôn cặp ResourceQuota compute với LimitRange default.
Default request quá cao trong LimitRange
Nếu defaultRequest đặt cao (ví dụ 500m CPU), mọi Pod không khai báo sẽ reserve 500m — kể cả Pod thực tế chỉ dùng 10m. Hệ quả: quota cạn nhanh giả tạo, bin-packing kém, node nhiều mà utilization thấp (xem tương tác với autoscaler ở Chương 9). Đặt default sát với usage điển hình thực tế, không "phòng xa".
Quên limits.cpu/limits.memory trong quota
Đặt quota chỉ trên requests.* mà quên limits.* cho phép Pod đặt limit cực cao và burst không kiểm soát, gây node áp lực dù tổng request trong trần. Cân nhắc giới hạn cả hai, hoặc dùng maxLimitRequestRatio để chặn overcommit quá mức.
Đổi quota và tưởng ảnh hưởng Pod đang chạy
Thay đổi ResourceQuota không ảnh hưởng Pod đã tạo — nó chỉ áp cho request mới. Giảm quota dưới mức usage hiện tại không giết Pod đang chạy, nhưng chặn tạo Pod mới cho tới khi usage xuống dưới trần. Hiểu rõ để không bất ngờ khi siết quota.
GCP-native implementation guidance
Xem quota và usage hiện tại của một namespace:
kubectl get resourcequota -n team-a
kubectl describe resourcequota team-a-compute -n team-a
# Used vs Hard hiện ra ở đây — công cụ chính để biết còn bao nhiêuKhi một Pod bị từ chối vì quota, lỗi nói rõ resource nào:
Error from server (Forbidden): error when creating "pod.yaml":
pods "x" is forbidden: exceeded quota: team-a-compute,
requested: requests.cpu=2, used: requests.cpu=19, limited: requests.cpu=20Trên GKE, lưu ý phân biệt hai loại quota khác nhau: ResourceQuota cấp Kubernetes (file này, theo namespace) và quota cấp GCP project (số CPU/IP/disk toàn project, Chương 1). Pod Pending có thể do hết ResourceQuota namespace hoặc hết quota GCP project (Cluster Autoscaler không tạo được node) — hai nguyên nhân hoàn toàn khác, debug ở hai chỗ khác nhau.