Resource requests và limits trong Kubernetes
1. Tổng quan
Khi chạy nhiều Pod trên cùng một cluster, bạn cần cho Kubernetes biết mỗi container cần bao nhiêu CPU và memory. Nếu không khai báo, một service có thể chiếm hết tài nguyên của node và làm ảnh hưởng các service khác. Nếu khai báo sai, Pod có thể bị OOMKilled liên tục hoặc chạy chậm vì bị throttle.
Bài này giải thích requests và limits là gì và Kubernetes dùng chúng như thế nào. Bài cũng nói về QoS class và cách cấu hình ứng dụng Java/Spring Boot cho phù hợp với các giá trị này.
2. Requests và limits là gì
Mỗi container trong Pod có thể khai báo hai loại giá trị:
- requests: lượng tài nguyên tối thiểu mà container được đảm bảo. Scheduler dùng giá trị này để chọn node đặt Pod.
- limits: mức tối đa mà container được phép dùng. Kubelet và container runtime thực thi giới hạn này ở mức kernel.
Hai loại tài nguyên phổ biến nhất là cpu và memory. Ngoài ra còn có ephemeral-storage và các extended resource như GPU.
2.1. Đơn vị CPU và memory
CPU tính theo core. 1 nghĩa là một vCPU, còn 500m (500 millicore) nghĩa là nửa core. Memory tính theo byte. Kubernetes nhận cả hậu tố hệ 2 (Ki, Mi, Gi) và hệ 10 (K, M, G).
Người mới hay nhầm 512M với 512Mi. Để thấy rõ khác biệt, ta viết một lớp nhỏ chuyển đổi các giá trị này:
import java.util.Map;
public final class ResourceQuantity {
private static final Map<String, Long> SUFFIXES = Map.of(
"Ki", 1024L,
"Mi", 1024L * 1024,
"Gi", 1024L * 1024 * 1024,
"K", 1_000L,
"M", 1_000_000L,
"G", 1_000_000_000L
);
private ResourceQuantity() {
}
public static long cpuToMillicores(String value) {
if (value.endsWith("m")) {
return Long.parseLong(value.substring(0, value.length() - 1));
}
return Math.round(Double.parseDouble(value) * 1000);
}
public static long memoryToBytes(String value) {
for (String suffix : new String[]{"Ki", "Mi", "Gi", "K", "M", "G"}) {
if (value.endsWith(suffix)) {
String number = value.substring(0, value.length() - suffix.length());
return Long.parseLong(number) * SUFFIXES.get(suffix);
}
}
return Long.parseLong(value);
}
}
Hàm cpuToMillicores xử lý hai dạng: có hậu tố m và số thập phân như 1.5. Hàm memoryToBytes kiểm tra hậu tố hai ký tự (Mi) trước hậu tố một ký tự (M), vì "512Mi" cũng kết thúc bằng "i" và không được khớp nhầm.
Test bằng JUnit 5 và AssertJ:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class ResourceQuantityTest {
@Test
void givenMillicoreOrDecimal_whenConvert_thenReturnsMillicores() {
assertThat(ResourceQuantity.cpuToMillicores("500m")).isEqualTo(500);
assertThat(ResourceQuantity.cpuToMillicores("1.5")).isEqualTo(1500);
}
@Test
void givenBinaryAndDecimalSuffix_whenConvert_thenValuesDiffer() {
long mebibytes = ResourceQuantity.memoryToBytes("512Mi");
long megabytes = ResourceQuantity.memoryToBytes("512M");
assertThat(mebibytes).isEqualTo(536_870_912L);
assertThat(megabytes).isEqualTo(512_000_000L);
assertThat(mebibytes - megabytes).isEqualTo(24_870_912L);
}
}
Hai giá trị lệch nhau khoảng 24 MB. Với một container sát limit, chừng đó đủ để quyết định Pod sống hay bị kill.
Lưu ý: Đừng viết
memory: 512m. Chữmthường nghĩa là “milli”, nên giá trị này là 0,512 byte. Kubernetes vẫn chấp nhận nó, và lỗi rất khó phát hiện.
3. Khai báo trong Deployment
Ví dụ sau dùng một order-service viết bằng Spring Boot:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 2
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: app
image: nonla/order-service:1.4.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "768Mi"
Ý nghĩa của từng giá trị:
requests.cpu: 500m: scheduler chỉ đặt Pod lên node còn ít nhất 0,5 core chưa được request.requests.memory: 512Mi: tương tự, nhưng tính cho memory.limits.cpu: "1": container được dùng tối đa 1 core. Vượt mức này thì bị throttle.limits.memory: 768Mi: vượt mức này thì container bị kernel kill.
Áp dụng manifest và kiểm tra kết quả:
kubectl apply -f order-service.yaml
kubectl get pod -l app=order-service -o jsonpath='{.items[0].spec.containers[0].resources}'
{"limits":{"cpu":"1","memory":"768Mi"},"requests":{"cpu":"500m","memory":"512Mi"}}
4. Scheduler dùng requests như thế nào
Scheduler không nhìn mức sử dụng thực tế của node. Nó chỉ cộng tổng requests của các Pod đã có trên node rồi so với allocatable. Bạn có thể xem con số này:
kubectl describe node worker-1 | grep -A 8 "Allocated resources"
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 3200m (80%) 6 (150%)
memory 5Gi (66%) 9Gi (118%)
Kết quả cho thấy hai điều:
- Tổng
requestskhông bao giờ vượt 100% allocatable. Nếu không node nào còn đủ chỗ, Pod nằm ở trạng tháiPendingvới eventInsufficient cpuhoặcInsufficient memory. - Tổng
limitsđược phép vượt 100%. Đây gọi là overcommit. Overcommit an toàn khi các Pod không cùng lúc dùng tới limit.
Hệ quả: nếu bạn đặt requests quá cao, node trông “đầy” dù CPU thực tế đang rảnh. Nếu đặt quá thấp, nhiều Pod bị dồn lên một node và tranh nhau tài nguyên.
5. Khi container vượt limit
CPU và memory bị xử lý theo hai cách rất khác nhau.
5.1. CPU: bị throttle
CPU là tài nguyên compressible. Khi container dùng hết quota, kernel (qua cgroup CFS) tạm dừng container cho tới chu kỳ tiếp theo, thường dài 100ms. Container không chết, chỉ chạy chậm lại. Triệu chứng là latency p99 tăng vọt trong khi kubectl top trông vẫn bình thường.
kubectl top pod -l app=order-service
NAME CPU(cores) MEMORY(bytes)
order-service-7d9f8c6b5-k2xqp 998m 610Mi
order-service-7d9f8c6b5-tm4wz 412m 590Mi
Pod đầu tiên đang chạm sát limit 1, nên gần như chắc chắn đang bị throttle.
5.2. Memory: bị kill
Memory là tài nguyên incompressible. Khi container vượt limits.memory, kernel OOM killer kết thúc process ngay lập tức:
kubectl describe pod order-service-7d9f8c6b5-k2xqp | grep -A 4 "Last State"
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Tue, 13 Oct 2026 09:12:03 +0700
Exit code 137 nghĩa là process nhận SIGKILL (128 + 9). Ứng dụng không có cơ hội chạy shutdown hook hay ghi log.
Một trường hợp khác: container dùng nhiều hơn requests.memory nhưng vẫn dưới limit. Khi node thiếu memory, kubelet sẽ evict các Pod này trước những Pod dùng đúng mức đã request.
6. QoS class
Dựa trên cách bạn khai báo requests và limits, Kubernetes gán cho mỗi Pod một QoS class. Class này quyết định thứ tự bị evict khi node thiếu tài nguyên.
| QoS class | Điều kiện | Thứ tự bị evict |
|---|---|---|
Guaranteed |
Mọi container có requests == limits cho cả CPU và memory |
Sau cùng |
Burstable |
Có ít nhất một request hoặc limit, nhưng không thỏa Guaranteed | Ở giữa |
BestEffort |
Không container nào khai báo gì | Đầu tiên |
Pod order-service ở trên thuộc class Burstable. Bạn kiểm tra bằng lệnh:
kubectl get pod -l app=order-service -o jsonpath='{.items[*].status.qosClass}'
Burstable Burstable
Với service quan trọng như payment-service, bạn có thể đặt requests bằng limits để Pod thuộc class Guaranteed:
resources:
requests:
cpu: "1"
memory: "1Gi"
limits:
cpu: "1"
memory: "1Gi"
Nếu chỉ khai báo limits mà bỏ trống requests, Kubernetes tự gán requests bằng limits. Pod khi đó cũng thành Guaranteed.
7. Ứng dụng Java trong container
Từ Java 10 trở đi, JVM nhận biết cgroup. Java 21 hỗ trợ cả cgroup v1 và v2. JVM đọc limits.memory để tính kích thước heap, và đọc limits.cpu để tính số processor. Vấn đề nằm ở giá trị mặc định: heap tối đa chỉ bằng 25% memory limit. Ứng dụng của bạn lãng phí phần lớn memory đã xin.
Thêm component sau vào Spring Boot để log những gì JVM thực sự thấy:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class ContainerResourceLogger {
private static final Logger log = LoggerFactory.getLogger(ContainerResourceLogger.class);
@EventListener(ApplicationReadyEvent.class)
public void logResources() {
Runtime runtime = Runtime.getRuntime();
long maxHeapMi = runtime.maxMemory() / (1024 * 1024);
log.info("availableProcessors={}, maxHeap={}Mi",
runtime.availableProcessors(), maxHeapMi);
}
}
Với limit 768Mi và không cấu hình gì thêm, log in ra:
INFO ContainerResourceLogger : availableProcessors=1, maxHeap=192Mi
Bạn nên điều chỉnh tỉ lệ heap bằng MaxRAMPercentage thay vì hard-code -Xmx:
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
Sau khi redeploy:
INFO ContainerResourceLogger : availableProcessors=1, maxHeap=576Mi
Phần 25% còn lại dành cho metaspace, thread stack, code cache, direct buffer và GC. Đừng đặt tỉ lệ này lên 90% trở lên. Heap chưa đầy nhưng tổng memory của process đã vượt limit, và container bị OOMKilled mà không có OutOfMemoryError nào trong log.
Lưu ý:
availableProcessorsđược tính từ CPU limit, sau đó làm tròn lên. Limit500mcho ra 1 processor. Khi đó JVM thường chọnSerialGC, và các thread pool nhưForkJoinPool.commonPool()chỉ có 1 worker. Nếu ứng dụng cần nhiều luồng song song, hãy kiểm tra lại các giá trị này.
8. Đặt mặc định cho namespace
Để tránh Pod BestEffort lọt vào cluster, bạn có thể dùng LimitRange để gán giá trị mặc định theo namespace:
apiVersion: v1
kind: LimitRange
metadata:
name: default-resources
namespace: shop
spec:
limits:
- type: Container
defaultRequest:
cpu: "200m"
memory: "256Mi"
default:
cpu: "500m"
memory: "512Mi"
Container nào không khai báo resources sẽ tự nhận các giá trị trên khi được tạo.
Kết hợp thêm ResourceQuota để giới hạn tổng tài nguyên cả namespace được phép xin:
apiVersion: v1
kind: ResourceQuota
metadata:
name: shop-quota
namespace: shop
spec:
hard:
requests.cpu: "8"
requests.memory: "16Gi"
limits.memory: "24Gi"
Khi namespace có quota cho CPU hoặc memory, Pod nào thiếu requests/limits tương ứng sẽ bị API server từ chối. Đây là lý do nên đi kèm LimitRange.
9. Lỗi hay gặp và khi nào không nên dùng
- Không khai báo gì cả. Pod thành
BestEffort, bị evict đầu tiên, và scheduler không có dữ liệu để phân bổ. - Memory limit thấp hơn nhu cầu lúc khởi động. Spring Boot thường dùng nhiều memory nhất trong giai đoạn khởi tạo context. Hãy đo bằng
kubectl topqua một chu kỳ tải thực tế rồi mới chốt con số. - Requests quá cao “cho chắc”. Node trông đầy dù CPU rảnh, khiến cluster autoscaler thêm node không cần thiết và tốn tiền.
- CPU limit quá chặt cho ứng dụng JVM. JIT compiler và GC chạy song song với ứng dụng, nên dễ bị throttle khi khởi động. Kết quả là startup chậm và readiness probe fail.
- Đặt
-Xmxbằng đúng memory limit. Container chắc chắn bịOOMKilled, vì JVM còn dùng memory ngoài heap.
Lưu ý: Nhiều team chọn không đặt CPU limit cho service nhạy cảm về latency, chỉ đặt CPU request. Container khi đó được dùng CPU rảnh của node mà không bị throttle, còn request vẫn đảm bảo phần tối thiểu. Cách này không phù hợp với cluster dùng chung giữa nhiều team, vì không có limit thì một service lỗi có thể làm chậm cả các service khác. Ngược lại, bạn nên luôn đặt memory limit: thiếu memory limit, một memory leak có thể kéo cả node vào tình trạng thiếu memory.
10. Kết luận
Trong bài này, bạn đã thấy requests quyết định việc scheduling và mức tài nguyên được đảm bảo, còn limits quyết định mức trần. Vượt CPU limit thì container bị throttle, còn vượt memory limit thì bị OOMKilled. Cách bạn kết hợp hai giá trị này tạo ra QoS class, và QoS class quyết định thứ tự bị evict khi node thiếu tài nguyên.
Với ứng dụng Java, hãy dùng MaxRAMPercentage để heap tỉ lệ theo memory limit, và chừa đủ chỗ cho phần non-heap. Cuối cùng, dùng LimitRange và ResourceQuota để cả namespace tuân theo cùng một chuẩn. Con số tốt nhất luôn đến từ việc đo tải thực tế, không phải phỏng đoán.