K8s资源管理:Request/Limit、LimitRange、ResourceQuota的生产配置
K8s资源管理:Request/Limit、LimitRange、ResourceQuota的生产配置
适读人群:负责K8s集群管理和Java服务部署的工程师 | 阅读时长:约20分钟 | 适用版本:K8s 1.22+
开篇故事
2023年的一个周三下午,我们的K8s集群突然大面积告警,七八个服务同时变成了Pending状态,一个都调度不进去。我一看Node的资源使用情况:CPU使用率60%,内存使用率55%,明明还有资源,为什么调度不了?
排查了将近一个小时,才找到根因:有个新来的开发同学在namespace里部署了一个AI模型推理服务,没有设资源限制,这个服务的Pod直接声明resources.requests.memory: 120Gi,把整个集群可调度的内存都预留了。虽然实际内存使用只有8GB,但在K8s的调度视角里,这个Pod已经"占用"了120GB内存。
这件事推动我们彻底整改了资源管理策略:每个namespace强制配LimitRange和ResourceQuota,没有资源声明的Pod一律拒绝部署。今天把这套体系完整写出来。
一、核心问题分析
资源管理的三个层次
K8s的资源管理从下往上有三个层次:
Pod级别(Request/Limit):单个容器/Pod能使用多少资源的声明和限制,是最基础的单元。Request影响调度决策,Limit影响运行时约束。
Namespace级别(LimitRange):为namespace内的Pod设置默认值和边界。没有设置Resources的Pod,会被LimitRange自动注入默认值;超出边界的Pod会被拒绝创建。
Namespace级别(ResourceQuota):限制整个namespace能使用的资源总量,包括CPU、内存、Pod数量、Service数量等。防止某个namespace"吃独食"。
Request和Limit的本质区别
Request:调度器保证分配给容器的资源量,影响Pod调度到哪个Node,也影响QoS等级。如果Node上所有Pod的Request总和超过Node容量,新Pod就无法调度(即使实际使用率很低)。
Limit:容器运行时的硬上限。CPU超过Limit会被限速(throttling);内存超过Limit会被OOM Kill。
三种QoS等级(影响被驱逐的优先级):
- Guaranteed:Request == Limit,最高优先级,最后被驱逐
- Burstable:Request < Limit,中等优先级
- BestEffort:没有设置Request和Limit,最低优先级,第一个被驱逐
二、原理深度解析
CPU Throttling的原理
很多人不知道CPU Limit超出后发生什么。K8s底层用Linux的CFS(完全公平调度器)的cpu.cfs_period_us和cpu.cfs_quota_us实现CPU限制。
如果设置limits.cpu: 1,表示在100ms(默认CFS period)的时间窗口内,该容器最多使用100ms的CPU时间。如果容器在一个100ms窗口内用完了100ms的CPU,剩余时间就被限速(throttle),必须等到下一个100ms窗口才能继续。
Java应用特别容易遇到CPU Throttling的问题:GC需要短暂的高CPU使用,GC过程中可能超出Limit,被throttle后GC暂停时间延长,应用响应变慢。
监控CPU Throttling的命令:
# 查看容器的CPU throttling情况
kubectl exec -it <pod-name> -- cat /sys/fs/cgroup/cpu/cpu.stat
# 关注 throttled_time 字段,单位是纳秒
# 或者通过metrics查看
kubectl top pod <pod-name>内存OOMKill的触发机制
内存超过Limit,进程会被Linux OOM Killer干掉,K8s会记录OOMKilled状态。对于Java应用,有一个特别要注意的地方:JVM的堆内存只是内存占用的一部分,还有堆外内存(Direct Memory、Metaspace、线程栈等)。
如果只限制了JVM的-Xmx(最大堆),但没有考虑堆外内存,实际内存占用可能显著超过-Xmx,触发OOMKill。
经验公式:K8s的memory.limit = -Xmx + 堆外内存估算(通常额外加512MB~1GB)
三、完整配置实现
生产Namespace资源配置
# namespace-resource-management.yaml
---
# 第一步:创建Namespace
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
env: production
---
# 第二步:配置LimitRange(为每个容器设置默认值和边界)
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: production
spec:
limits:
# 容器级别的限制
- type: Container
# 默认值:如果容器没有设置,自动注入
default:
cpu: "500m"
memory: "512Mi"
# 默认Request值
defaultRequest:
cpu: "100m"
memory: "128Mi"
# 最大值:超过这个会被拒绝
max:
cpu: "8"
memory: "16Gi"
# 最小值:低于这个会被拒绝
min:
cpu: "50m"
memory: "64Mi"
# Limit/Request的最大比例(防止过度超卖)
maxLimitRequestRatio:
cpu: "10"
memory: "4"
# Pod级别的限制
- type: Pod
max:
cpu: "16"
memory: "32Gi"
# PVC请求大小限制
- type: PersistentVolumeClaim
max:
storage: "100Gi"
min:
storage: "1Gi"
---
# 第三步:配置ResourceQuota(限制整个Namespace的总量)
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: production
spec:
hard:
# 计算资源
requests.cpu: "50" # 所有Pod的CPU Request总和上限
requests.memory: "100Gi" # 所有Pod的Memory Request总和上限
limits.cpu: "200" # 所有Pod的CPU Limit总和上限
limits.memory: "200Gi" # 所有Pod的Memory Limit总和上限
# 对象数量
count/pods: "200" # Pod数量上限
count/services: "50" # Service数量上限
count/deployments.apps: "50" # Deployment数量上限
count/configmaps: "100" # ConfigMap数量上限
count/secrets: "100" # Secret数量上限
# 存储
requests.storage: "500Gi" # PVC总存储上限
persistentvolumeclaims: "50" # PVC数量上限Spring Boot服务的资源配置规范
# spring-boot-resource-template.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: registry.company.com/order-service:1.2.3
# 资源配置是生产部署的必填项
resources:
requests:
# CPU Request:服务正常负载下的实际使用量 + 20%余量
# 根据监控数据设定,不要拍脑袋
cpu: "500m"
# Memory Request:JVM堆 + 堆外内存的实际使用量
memory: "1Gi"
limits:
# CPU Limit:允许突发的最高值
# 建议是Request的2~4倍,允许短暂突发
cpu: "2000m"
# Memory Limit:Request的1.5~2倍
# 要考虑堆外内存:Xmx设1G,Limit设2G
memory: "2Gi"
env:
- name: JAVA_OPTS
value: >-
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=50.0
-XX:InitialRAMPercentage=25.0
-XX:+ExitOnOutOfMemoryError
-XX:+UseG1GC注意Java堆的设置:MaxRAMPercentage=50.0表示最大堆为容器内存Limit的50%。对于2Gi的Limit,JVM堆最大为1Gi,剩余1Gi留给Metaspace、线程栈、Direct Memory等堆外内存。
查看资源使用情况的常用命令
# 查看namespace的资源配额使用情况
kubectl get resourcequota -n production
kubectl describe resourcequota production-quota -n production
# 查看LimitRange配置
kubectl get limitrange -n production -o yaml
# 查看各Pod的资源使用
kubectl top pod -n production --sort-by=memory
# 查看Node资源
kubectl top node
# 查看Pod的资源声明vs实际使用
kubectl get pod -n production -o custom-columns=\
NAME:.metadata.name,\
CPU_REQ:.spec.containers[0].resources.requests.cpu,\
MEM_REQ:.spec.containers[0].resources.requests.memory,\
CPU_LIM:.spec.containers[0].resources.limits.cpu,\
MEM_LIM:.spec.containers[0].resources.limits.memory
# 检查某个Pod是否有资源被throttle(需要在Pod所在Node上执行)
docker stats <container-id>四、生产最佳实践
资源配额的分级管理策略
根据服务的业务重要性,我们把namespace分成三个级别:
核心业务namespace(production-core):交易、支付等核心服务,Request较高,Quota总量大,LimitRange上限高。
普通业务namespace(production-biz):普通业务服务,Request适中,Quota总量适中。
基础设施namespace(infra):日志收集、监控、CI/CD等,使用DaemonSet,资源配置相对固定。
基于实际监控数据优化Request
资源Request的设定不能靠直觉,要基于监控数据:
- 用Prometheus采集容器CPU和内存使用率
- 取P95或P99的使用值作为基准
- CPU Request设为P95使用值的120%
- Memory Request设为P99使用值的110%(内存不能压缩,要留更多余量)
每个季度复查一次资源配置,避免"历史遗留"配置越来越偏离实际。
五、踩坑实录
坑一:CPU Limit设太低导致Spring Boot启动超时
有个服务把CPU Limit设成了200m(0.2核),服务倒是能启动,但启动时间从30秒变成了将近5分钟。原因是Spring Boot启动时要做大量类扫描、依赖注入初始化,CPU密集型操作被严重throttle。
startupProbe超时,Pod被重启,再启动再超时,无限循环。
解决方案有两个:要么提高CPU Limit;要么针对启动阶段临时放开throttle。实践中,Spring Boot服务的CPU Limit不应该低于500m,否则启动时间会大幅增加。
坑二:内存Limit设太低触发OOMKill
新来的同事按照"JVM堆1G,所以Limit设1G"的逻辑配了资源,结果服务时不时被OOMKill。
Java进程的内存组成:堆内存(Xmx)+ Metaspace(默认无限制)+ 线程栈(每个线程约1MB)+ Direct Memory + JVM内部开销。一个有200个线程的Spring Boot服务,光线程栈就有200MB。
正确做法:先让服务运行一段时间,用kubectl top pod或Prometheus观察实际内存使用峰值,在峰值基础上加20%的余量作为Limit。
坑三:ResourceQuota导致部署失败但报错不明显
某次发布,Deployment的Pod一直处于Pending,describe Pod只看到"Insufficient memory"。反复检查Node资源,明明够用。
花了20分钟才想起来去查ResourceQuota:namespace的memory Limit总配额已经用满了。之前有个Job没有清理,占用了大量Limit配额,导致新的Deployment无法创建Pod。
养成习惯:发布前先检查namespace的quota使用情况;废弃的Job和CronJob记得及时清理;CI/CD流水线里加上quota检查步骤。
六、总结
K8s资源管理的正确姿势是:从外到内,逐层收紧。
先用ResourceQuota给每个namespace划定总预算;再用LimitRange设置合理的默认值和边界,确保所有容器都有资源声明;最后在每个Pod的spec里根据实际监控数据精细设置Request和Limit。
Java服务特别需要注意:JVM堆只是内存占用的一部分,Limit一定要比Xmx大出至少50%的余量;CPU Limit不要设太低,否则Spring Boot启动阶段会被严重throttle。
资源配置不是一次性的工作,需要根据实际监控数据持续迭代优化。
