Kubernetes 中的 Resource requests 和 limits
1. 概览
当在同一个集群上运行多个 Pod 时,你需要告诉 Kubernetes 每个容器需要多少 CPU 和内存。如果不声明,某个 service 可能会占用整个节点的资源,影响其他 service。如果声明不当,Pod 可能会频繁被 OOMKilled 或由于被 throttle 而运行缓慢。
本文解释了什么是 requests 和 limits,以及 Kubernetes 如何使用它们。本文还讨论了 QoS class,以及如何配置 Java/Spring Boot 应用以适配这些值。
2. Requests 和 limits 是什么
Pod 中的每个容器可以声明两种类型的值:
- requests:容器被保证的最少资源量。调度器使用这个值来选择放置 Pod 的节点。
- limits:容器被允许使用的最大值。Kubelet 和容器运行时在内核级别实施这个限制。
最常见的两种资源类型是 cpu 和 memory。还有 ephemeral-storage 和扩展资源如 GPU。
2.1. CPU 和内存的单位
CPU 按核心计算。1 表示一个 vCPU,500m(500 毫核)表示半个核心。内存按字节计算。Kubernetes 接受二进制后缀(Ki、Mi、Gi)和十进制后缀(K、M、G)。
新手容易混淆 512M 和 512Mi。为了清楚地看到差异,我们编写一个小类来转换这些值:
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);
}
}
cpuToMillicores 函数处理两种形式:带 m 后缀和如 1.5 的十进制数。memoryToBytes 函数在单字符后缀(M)之前检查两字符后缀(Mi),因为 "512Mi" 也以 "i" 结尾,不能混淆匹配。
用 JUnit 5 和 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);
}
}
这两个值相差约 24 MB。对于接近限制的容器,这足以决定 Pod 是活着还是被杀死。
注意: 不要写
memory: 512m。字母m通常表示“毫”,所以这个值是 0.512 字节。Kubernetes 仍然会接受它,但错误很难被发现。
3. 在 Deployment 中声明
以下示例使用用 Spring Boot 编写的 order-service:
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"
各值的含义:
requests.cpu: 500m:调度器仅将 Pod 放在至少还有 0.5 个核心未被请求的节点上。requests.memory: 512Mi:类似,但计算内存。limits.cpu: "1":容器最多可以使用 1 个核心。超过这个限制就会被 throttle。limits.memory: 768Mi:超过这个限制,容器会被内核杀死。
应用 manifest 并检查结果:
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. 调度器如何使用 requests
调度器 不 查看节点的实际使用情况。它只是将节点上已有 Pod 的 requests 相加,然后与 allocatable 进行比较。你可以查看这些数字:
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%)
结果表明两点:
- 总
requests永远不会超过 100% 的可分配资源。如果没有节点有足够的空间,Pod 会处于Pending状态,事件为Insufficient cpu或Insufficient memory。 - 总
limits可以超过 100%。这称为 overcommit。当 Pod 不会同时使用到限制时,overcommit 是安全的。
结果:如果你设置 requests 过高,节点看起来“满了”,尽管实际 CPU 还是闲置的。如果设置过低,许多 Pod 会被挤到一个节点上并争夺资源。
5. 当容器超过限制时
CPU 和内存处理方式完全不同。
5.1. CPU:被 throttle
CPU 是 可压缩的 资源。当容器用完配额时,内核(通过 cgroup CFS)会暂停容器直到下一个周期,通常长度为 100ms。容器不会死亡,只是运行变慢。症状是 latency p99 急剧上升,而 kubectl top 看起来仍然正常。
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 接近限制 1,所以几乎肯定被 throttle 了。
5.2. 内存:被杀死
内存是 不可压缩的 资源。当容器超过 limits.memory 时,内核 OOM killer 会立即终止进程:
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
退出代码 137 表示进程收到 SIGKILL (128 + 9)。应用没有机会运行 shutdown hook 或记录日志。
另一种情况:容器使用超过 requests.memory 但仍低于限制。当节点内存不足时,kubelet 会 evict 这些 Pod,优先于使用正确请求量的 Pod。
6. QoS class
根据你声明 requests 和 limits 的方式,Kubernetes 给每个 Pod 分配一个 QoS class。这个 class 决定当节点资源不足时被 evict 的顺序。
| QoS class | 条件 | 被 evict 的顺序 |
|---|---|---|
Guaranteed |
所有容器的 requests == limits,同时包括 CPU 和内存 |
最后 |
Burstable |
至少有一个 request 或 limit,但不满足 Guaranteed 条件 | 中间 |
BestEffort |
没有容器声明任何内容 | 最先 |
上面的 Pod order-service 属于 Burstable class。你可以用以下命令检查:
kubectl get pod -l app=order-service -o jsonpath='{.items[*].status.qosClass}'
Burstable Burstable
对于像 payment-service 这样重要的 service,你可以设置 requests 等于 limits 使 Pod 属于 Guaranteed class:
resources:
requests:
cpu: "1"
memory: "1Gi"
limits:
cpu: "1"
memory: "1Gi"
如果只声明 limits 而留下 requests 空白,Kubernetes 会自动将 requests 设置为 limits。Pod 将变成 Guaranteed。
7. 容器中的 Java 应用
从 Java 10 开始,JVM 能够识别 cgroup。Java 21 同时支持 cgroup v1 和 v2。JVM 读取 limits.memory 来计算堆大小,读取 limits.cpu 来计算处理器数量。问题在于默认值:最大堆只有 25% 的内存限制。你的应用会浪费大部分请求的内存。
在 Spring Boot 中添加以下组件来记录 JVM 实际看到的内容:
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);
}
}
使用 768Mi 限制且没有额外配置,日志输出为:
INFO ContainerResourceLogger : availableProcessors=1, maxHeap=192Mi
你应该使用 MaxRAMPercentage 调整堆的比例,而不是硬编码 -Xmx:
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
重新部署后:
INFO ContainerResourceLogger : availableProcessors=1, maxHeap=576Mi
剩余的 25% 用于 metaspace、thread stack、code cache、direct buffer 和 GC。不要将这个比例设置为 90% 或更高。堆还没满,但进程的总内存已经超过限制,容器会被 OOMKilled,但日志中没有 OutOfMemoryError。
注意:
availableProcessors从 CPU 限制 计算,然后向上舍入。500m的限制会产生 1 个处理器。此时 JVM 通常会选择SerialGC,而像ForkJoinPool.commonPool()这样的线程池只有 1 个 worker。如果应用需要大量并行线程,请重新检查这些值。
8. 为 namespace 设置默认值
为了避免 BestEffort Pod 进入集群,你可以使用 LimitRange 为每个 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"
任何未声明 resources 的容器在创建时将自动接收上述值。
结合 ResourceQuota 来限制整个 namespace 允许请求的总资源:
apiVersion: v1
kind: ResourceQuota
metadata:
name: shop-quota
namespace: shop
spec:
hard:
requests.cpu: "8"
requests.memory: "16Gi"
limits.memory: "24Gi"
当 namespace 对 CPU 或内存有配额时,任何缺少相应 requests/limits 的 Pod 都会被 API server 拒绝。这是应该与 LimitRange 配合的原因。
9. 常见错误和何时不应使用
- 什么都不声明。 Pod 变成
BestEffort,最先被 evict,调度器没有数据进行分配。 - 内存限制低于启动时的需求。 Spring Boot 通常在上下文初始化阶段使用最多的内存。通过实际负载周期用
kubectl top进行测量,然后再确定数字。 - 请求设置过高“以防万一”。 节点看起来满了虽然 CPU 还是闲置的,导致集群 autoscaler 添加不必要的节点,浪费金钱。
- JVM 应用的 CPU 限制过紧。 JIT 编译器和 GC 与应用并行运行,所以启动时容易被 throttle。结果是启动缓慢,readiness probe 失败。
- 将
-Xmx设置为内存限制。 容器肯定会被OOMKilled,因为 JVM 还会使用堆外内存。
注意: 许多团队选择为对延迟敏感的 service 不设置 CPU 限制,只设置 CPU 请求。容器可以使用节点的空闲 CPU 而不被 throttle,而请求仍然保证了最低值。这种方法不适合在多个团队共享的集群中使用,因为没有限制时一个故障的 service 可能会拖慢其他 service。相反,你 应该始终设置内存限制:没有内存限制,一个内存泄漏可能会导致整个节点内存不足。
10. 总结
在本文中,你了解了 requests 决定调度和保证的资源级别,而 limits 决定上限。超过 CPU 限制时容器被 throttle,超过内存限制时被 OOMKilled。你如何结合这两个值会产生 QoS class,而 QoS class 决定当节点资源不足时被 evict 的顺序。
对于 Java 应用,使用 MaxRAMPercentage 使堆按内存限制成比例,并为非堆部分保留足够的空间。最后,使用 LimitRange 和 ResourceQuota 确保整个 namespace 遵循相同的标准。最好的数字总是来自测量实际负载,而不是猜测。