☰
Kubernetes容器资源限制实战:requests与limits配置避坑指南
2026/10/21 21:15:30 网站建设 项目流程

接手过不少业务团队的Pod资源投诉,也帮人排查过半夜三更被OOMKilled的窘况。Kubernetes里的容器资源限制,看着就是加几个字段的事,真正用起来才会发现:requests和limits写少了,服务在流量高峰直接被打穿;写多了,整个集群利用率惨不忍睹,扩容又得烧钱。这篇笔记不打算罗列概念,重点把我这些年配置资源限制时踩过的坑、验证过的做法、以及一套可以直接抄作业的配置思路讲清楚。适合刚接触Kubernetes、准备给存量应用补资源约束的开发者,也适合被Pod驱逐问题折磨过的运维朋友。

1. 资源限制到底在限制什么

先说一个容易被忽略的前提:Kubernetes 的"容器资源",指的是 CPU 和内存这两类可量化的计算资源。磁盘 IO、网络带宽这些虽然也影响服务质量,但 kubelet 默认不对它们做强制隔离和调度约束。所以下面所有讨论都围绕 CPU 和内存展开。

1.1 requests 与 limits 的本质区别

很多人第一次看 YAML 会懵:既然要限制资源,为什么还要分 requests 和 limits 两套字段?直接把最大值写上不就好了?这俩在调度和运行时各管一摊,缺一个就会出问题。

  • requests:是给调度器看的"最低保障"声明。它告诉 kube-scheduler,"这个 Pod 正常跑起来至少需要这么多资源",调度器在选节点时会统计每个节点上所有 Pod 的 requests 总和,确保不超过节点的可分配容量。
  • limits:是给 kubelet 和内核看的"硬上限"。容器实际能占用的资源不能超过这个值,CPU 超过会被限流(throttle),内存超过会被内核 OOM killer 干掉。

用一句话概括:requests 决定 Pod 能不能调度到某台节点,limits 决定 Pod 在节点上"能疯到什么程度"。如果只写 requests,等于告诉系统"我至少需要这么多",但没设置上限,一个内存泄漏的 Pod 可能把整台节点拖死;如果只写 limits,调度器把 Pod 安排上之后发现节点实际资源不够用,Pod 会因为无法满足 requests 而一直处于 Pending。

实际配置的时候,不管是 Deployment、StatefulSet 还是 DaemonSet,最终落到 Pod 上的资源配置都是通过容器级别的resources字段生效的,命令格式如下:

resources: requests: cpu: 500m memory: 512Mi limits: cpu: "1" memory: 1Gi

这里的 500m 表示 0.5 个 CPU 核心,1Gi 表示 1Gibibyte 内存。单位细节下一节展开。

1.2 CPU 和内存的计量方式

CPU 和内存的限制逻辑完全不同,这也是新手最容易踩坑的地方。

CPU 是可压缩资源。Kubernetes 使用 Linux 内核的 CFS 配额机制来限制 CPU 使用时间。比如 limit 设为 1 个核心,意味着容器在调度周期内最多使用 1 核的 CPU 时间。超过这个额度,任务会被暂时挂起,等到下一个周期再继续跑——表现就是 CPU 使用率被"压平",服务变慢,但进程不会死。

内存是"不可压缩资源"。内核没法把已经分配给进程的物理内存平滑回收,一旦容器内存 usage 超过 limit,内核的 OOM killer 会直接触发,选择进程杀掉,最常见的结果就是 Pod 重启,kubectl get pod里看到OOMKilled状态。

单位方面,CPU 超卖的单位是m(millicore),1000m 等于 1 个 CPU 核心;也可以用不带引号的整数 1 表示整核。内存单位支持 E、P、T、G、M、K 的十进制后缀,以及 Ei、Pi、Ti、Gi、Mi、Ki 的二进制后缀。注意1G是 1000MB,1Gi才是 1024MiB。多数时候我们习惯用Mi、Gi,避免语义含糊。

这里给一个生活化类比:CPU limit 像公司给员工的单日任务配额,活太多了就排到明天再做,人不会消失;内存 limit 像员工工位上能摆的文件上限,超过之后新文件塞不进来,强行塞的后果就是桌子被掀翻——进程被 OOM kill。

2. 动手配置容器资源限制

光讲概念不过瘾,直接上手配一个实际案例。下面以一个模拟的 Java 微服务为例,演示从 Pod 级配置到命名空间级管控的完整链路。

2.1 从最简单的 Pod 配置开始

先看一个最基础的资源配置写法:

apiVersion: v1 kind: Pod metadata: name: demo-app namespace: default spec: containers: - name: app image: registry.example.com/demo-app:1.0.0 ports: - containerPort: 8080 resources: requests: cpu: 250m memory: 512Mi limits: cpu: 500m memory: 1Gi

这个配置的意思是:调度器找一台至少还剩 250m CPU 和 512Mi 内存的节点来运行;运行时 CPU 最多用 500m,内存最多用 1Gi。

对于 Java 应用,有一个必须单独提醒的点:JVM 默认的堆大小是根据物理机内存动态计算的。如果容器 limit 设了 1Gi,但 JVM 启动时看到宿主机有 64G 内存,可能直接把堆撑到几个 G,然后瞬间触发 OOMKilled。解决方法是显式设置 JVM 参数,让堆上限跟着容器内存走,比如:

java -XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0 -jar app.jar

MaxRAMPercentage 设成 75% 是因为 JVM 除了堆还要留给 Metaspace、线程栈、直接内存一些空间,留 25% 的余量比较稳妥。这算是我在实战中见过的最多事故现场之一:没配容器内存限制的 Java 服务,和配了限制但没调 JVM 的服务,各有各的挂法。

提示:给 Java 容器配置内存 limit 时,务必同步调整 JVM 堆参数。只配容器不配 JVM,容器限制形同虚设;只配 JVM 不配容器,调度器不知道你需要多少资源,节点容易超卖。

2.2 通过 Deployment 给工作负载统一配资源

生产环境很少直接管理 Pod,基本都是通过 Deployment、StatefulSet 这类工作负载对象创建副本。资源字段写在 PodTemplate 里即可:

apiVersion: apps/v1 kind: Deployment metadata: name: api-server namespace: production spec: replicas: 3 selector: matchLabels: app: api-server template: metadata: labels: app: api-server spec: containers: - name: main image: registry.example.com/api-server:2.3.1 resources: requests: cpu: 500m memory: 1Gi limits: cpu: "1" memory: 2Gi readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: httpGet: path: /livez port: 8080 initialDelaySeconds: 20 periodSeconds: 10

有一个容易被忽略的细节:resources是容器级别的字段,一个 Pod 里有多个容器时,每个容器都要单独设置。只给主容器配资源、忘掉 sidecar 容器,是常见的半吊子配置。initContainer 的资源限制同样要单独写,而且调度器计算节点容量时会一并计入。

配置完后,可以用kubectl describe node查看节点的资源分配情况。重点关注输出里的这部分:

Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 1120m (58%) 2100m (110%) memory 1536Mi (20%) 4096Mi (54%)

Requests 的总和超过 100% 意味着超卖。超卖在多数集群里是被允许的,前提是你理解超卖可能带来的风险。CPU 超卖一般还好,内核会用 CFS 公平调度;内存超卖需要格外小心,节点内存一紧张,最先被驱逐的就是 QoS 等级低的 Pod。

2.3 给命名空间加上配额和默认值

单靠每个开发者自觉写 resources 不现实。总有人图省事不写,或者写了但非常离谱。解决办法是给命名空间套上两把"锁":LimitRange 和 ResourceQuota。

LimitRange 用来给命名空间里的容器设置默认的 requests/limits,以及取值范围约束:

apiVersion: v1 kind: LimitRange metadata: name: default-resource-limits namespace: production spec: limits: - max: cpu: "4" memory: 8Gi min: cpu: 50m memory: 64Mi default: cpu: 500m memory: 512Mi defaultRequest: cpu: 100m memory: 128Mi type: Container

这个 LimitRange 的三个作用:

  • min/max:限制单个容器能申请的最小和最大资源。防止有人申请一个 32Gi 内存的容器,把整个命名空间的份额全部占掉。
  • default:如果 Pod 里容器只写了 requests 没写 limits,就用 default 作为 limits;反过来只写 limits 不写 requests,就用 defaultRequest 作为 requests。简单说,它把"忘写"的坑自动填上。
  • maxLimitRequestRatio(可选项):限制 limits 和 requests 的比值。比如设为 2,就不允许一个容器请求 1Gi 内存却设置 8Gi 的 limits,防止无意义的高上限。

ResourceQuota 管的是整个命名空间的资源总量:

apiVersion: v1 kind: ResourceQuota metadata: name: production-quota namespace: production spec: hard: requests.cpu: "20" requests.memory: 40Gi limits.cpu: "40" limits.memory: 80Gi pods: "100"

当命名空间里所有 Pod 的 requests.cpu 总和达到 20 核、或 Pod 总数达到 100 个时,再创建新的 Pod 会被拒绝,kubectl 会返回一个带有exceeded quota字样的错误。这套机制保证了生产命名空间不会被某个团队"吃干抹净",其他服务全部挤到没资源的地方干等。

注意:ResourceQuota 一定要和 LimitRange 配合使用。如果不设默认值,一个没写任何 resources 的 Pod 在计算配额时 requests 和 limits 都按 0 算,可能绕过资源总量限制。在严格的集群里,OpenShift 甚至可以直接配置"没有资源声明的 Pod 拒绝创建"。

3. QoS 等级与驱逐优先级:资源限制的另一面

资源字段写完之后,Kubernetes 会自动给每个 Pod 打上 QoS 等级。这个等级平时不显眼,但节点资源紧张时,它直接决定谁先被牺牲。

3.1 三种 QoS 等级的含义

Kubernetes 根据 Pod 内所有容器的 requests 和 limits 配置,把 Pod 归入三个等级:

QoS 等级判定条件典型配置
Guaranteed每个容器的 requests 和 limits 都相等,且每个容器都设置了requests.cpu == limits.cpu,requests.memory == limits.memory
Burstable至少有一个容器的 requests 小于 limits,或者只设置了 requests 未设置 limitsrequests.cpu=500m,limits.cpu=1
BestEffort所有容器都没有设置 requests 和 limits不写 resources 字段

Guaranteed 是最高优先级,BestEffort 最低。下面用一个真实场景来说明为什么不要写出 BestEffort 的 Pod。

某次在生产集群里,我发现某团队核心服务的 Pod 资源字段为空。平时流量正常,看起来没问题。等到 "双11" 这类促销高峰,同一个节点上跑了很多无资源限制的批处理任务,内存持续增长,节点触发内存压力驱逐,kubelet 按 QoS 等级从低到高开始杀 Pod。结果核心服务因为属于 BestEffort,第一批就被杀掉了。那晚的教训:不管你的服务多重要,只要没有资源声明,节点资源紧张时它就是那个"祭品"。

反过来看 Guaranteed:因为 CPU 和内存都有严格上限,调度器更愿意把它放在资源余量充足的节点上,而且节点驱逐时会留到最后。如果业务对稳定性要求很高,把它配成 Guaranteed 是明智的。

3.2 节点压力驱逐与 OOM 杀手的区别

kubelet 会持续监控节点的内存、磁盘、文件系统使用情况。当节点内存不足时,它会触发节点压力驱逐,按照 QoS 等级低到高、以及实际资源使用量超过 requests 的多少来决定驱逐顺序。这就是为什么同一节点上 BestEffort 的 Pod 总是最先消失。

还有一种情况是容器的内存使用量超过了自身 limits,这时候不归 kubelet 管,而是 Linux 内核直接触发 OOM killer。每个容器都被内核挂到一个 cgroup 上,cgroup 内存限制被突破时,OOM killer 会在容器进程里选择最"占便宜"的进程杀掉。

我整理了一下,常见的组合触发情况如下表:

场景触发机制表现
容器内存超过自身 limitscgroup OOM kill容器进程退出,Pod 状态为 OOMKilled,重启次数增加
节点内存紧张,Pod 使用量超过 requestskubelet 节点压力驱逐Pod 被标记为终止,节点上出现驱逐事件
节点内存紧张,Pod 使用量未超过 requestskubelet 节点压力驱逐(较晚)可能等服务进程自己降级,驱逐优先级低
CPU 超过 limitsCFS 限流进程不退出,但 CPU 使用率被压到 limit 附近,延迟增加

有一个细节值得展开:kubelet 做节点压力驱逐时,判断依据并不是"容器是否超过 limits",而是"节点整体可用资源是否低于阈值"。换句话说,即使你的 Pod 设置了几百 MB 的 requests,如果宿主机总内存耗尽,它照样可能被驱逐。requests 只是调度时的宣告,不是运行时的硬保险。真正的硬保险是 limits,因为那是内核层面的约束。

某云计算厂商的支持案例里,我见过最离谱的情况是:一个 Pod 内存 requests 设为 4Gi、limits 没设,然后它把节点上剩余的 20G 内存全部吃光,同一节点的其他 Pod 全部被驱逐。这就是典型的"只设 requests、不设 limits"埋下的雷。

4. 常见问题与排查技巧实录

资源限制相关的问题是集群排障里最高频的,我把遇到过的典型问题整理成速查表,每个都附上排查思路。

4.1 Pod 一直 Pending,调度不上去

现象:kubectl get pod显示 Pending,kubectl describe pod里 Events 出现0/3 nodes are available: 2 Insufficient cpu, 1 Insufficient memory。

原因很直接:集群里没有任何节点能满足 Pod 声明的 requests。注意,这里说的是 requests,不是 limits。调度器从来不把 limits 作为节点容量判断的参考,只比较 requests 和节点可分配量。

排查顺序:

  1. 用kubectl describe node查看各节点 Allocated resources,确认剩余量。
  2. 确认 Pod 的 requests 数值是否合理,有没有因为复制粘贴把内存值写错。
  3. 检查是否有 ResourceQuota 限制,kubectl get resourcequota -n <namespace>看当前使用量。
  4. 如果集群节点很多却没有合适的,考虑是否存在节点选择器、污点容忍等约束把节点都排除了。

一个实际操作中的经验:业务方经常在压测时把副本数调到 50,发现 Pod 只有 20 个 Running,剩下 30 个 Pending。查看之后发现命名空间配额只允许 20 个 Pod。这不是资源不够,而是配额封顶。这种问题从事件里一眼就能看出来,不要浪费时间排查节点资源。

4.2 容器反复重启,状态 OOMKilled

现象:kubectl get pod显示CrashLoopBackOff,kubectl describe pod里 Last State 是OOMKilled,Exit Code 为 137。

这种情况 90% 是内存 limits 设置过低,或者应用本身存在内存泄漏。我见过很多团队遇到 OOM 就直接调大 limits,调完之后确实不重启了,但这治标不治本。

正确的排查步骤:

  1. 先看应用实际内存占用:kubectl top pod <pod-name> --containers。
  2. 确认是否所有容器都占满了内存,还是只有某一个容器在涨。
  3. 用kubectl exec进去看进程情况,或者打开监控面板看内存曲线。
  4. 如果是 Java 应用,优先检查 JVM 堆参数,确认堆上限和容器 limits 之间的配比。
  5. 如果确认是内存泄漏,调大 limits 只是延后爆炸时间,必须找到泄漏点。

诊断命令模板:

# 查看 Pod 重启次数和最近事件 kubectl get pod <pod-name> -o wide kubectl describe pod <pod-name> # 查看容器实际资源使用 kubectl top pod <pod-name> --containers # 查看节点层面 OOM 事件 kubectl get events --field-selector reason=OOMKilled -n <namespace> kubectl get events --field-selector type=Warning -n <namespace> --sort-by=.lastTimestamp

节点层面还有一个容易被忽视的信息源:kubelet 日志里的 OOM 记录。如果 Pod 频繁被 OOMKilled,但kubectl top显示的用量不高,可能是节点内存整体紧张,其他 Pod 把内存耗尽,导致这个 Pod 也被连带杀掉。这时候要看的不是单个 Pod,而是整个节点的内存总量和分配情况。

4.3 服务变慢,CPU 使用率被压在 limit 附近

现象:监控里 CPU 使用率曲线是一条被削平的直线,正好卡在 limits 数值附近,业务接口延迟升高。

这是 CPU CFS 限流的表现。当容器 CPU 使用量持续超过 limits 时,内核会限制它对 CPU 时间片的获取,进程大量时间在等待调度。

应对策略取决于你的业务性质:

  1. 如果是延迟敏感型在线服务,CPU limits 与请求量之间必须留足余量,或者干脆考虑 requests=limits,采用 Guaranteed 等级,减少被抢调度的概率。
  2. 如果是离线任务、批量计算,CPU 限流影响不大,保持合理的 limits 即可。
  3. 如果经常出现限流,先在监控里看 CPU 使用率的 P99,再看 Pod 启动时的线程数、连接池配置,确认是不是应用本身起线程太多。

我自己常用的指标是container_cpu_cfs_throttled_periods_total和container_cpu_cfs_periods_total,用这两个指标算一下限流比例。如果限流比例超过 5%,说明 CPU 资源已经是个瓶颈,该考虑扩容或者调优应用了。

4.4 节点内存耗尽,所有 Pod 一起遭殃

现象:节点 NotReady,或者多个 Pod 同时被驱逐,SSH 连上去发现操作卡顿严重。

这种状况通常是因为节点总内存被某些进程吃满,kubelet 无法正常上报心跳。排查要分两层:

  1. 系统层:通过带外管理(比如云厂商的控制台)连上节点,用free -h、ps aux --sort=-%mem | head查看内存占用进程,检查是否存在非 Kubernetes 管理的进程(比如节点的 Agent、监控组件、容器运行时残留进程)。
  2. K8s 层:kubectl describe nodes看 Conditions 里的MemoryPressure,以及kubectl get events -A里有没有驱逐事件。

一个我踩过的坑:某次节点出现大量驱逐,查了老半天发现罪魁祸首是节点上另一个非容器化的日志采集进程在做日志索引,把内存占满了。Kubernetes 资源限制只对 Pod 内的进程生效,管不到节点上直接跑的系统进程。所以节点级别的内存监控和告警必须单独做,不能把心里负担全压在 kubelet 身上。

5. 资源限制配置的实战建议

前面把原理、配置、排障都过了一遍,最后分享几个我用下来的配置思路,也算是对前面内容的一个落地提炼。

5.1 不同业务形态的推荐配置

在线服务、离线任务、批处理脚本的资源画像完全不同,一个统一模板不可能适配所有情况。我一般这么分:

业务类型推荐配置理由
核心在线服务requests = limits,即 Guaranteed避免节点抖动的干扰,驱逐顺序靠后
普通 Web 服务requests 设为基线负载,limits 设为基线负载的 1.5~2 倍允许流量尖峰,同时对整体资源有约束
离线批处理只设 requests,limits 放开或设置为较大值对延迟不敏感,允许尽量用完空闲资源
开发测试环境全量设置,但数值可以激进一点防止测试任务把集群资源打爆

5.2 学会通过监控数据做资源画像

很多团队一开始不知道 requests 该填多少,拍脑袋写了一个值,后面业务量变了也不调整。正确做法是建立资源基线:

  1. 先在测试环境不加 limits,只加监控。
  2. 跑两到三周的基线流量,记录 CPU 和内存的 P95、P99 值。
  3. 根据这些数据设置 requests(覆盖 P95 左右),limits(覆盖 P99 且留出 20%~30% 余量)。
  4. 定期复查监控数据,每季度或者在业务量有明显变化时重新校准。

这套方法论在多个项目里验证过,能把集群的资源利用率稳定提升 20%~30%,同时 Pod 驱逐率明显下降。很多团队抱怨资源不够,实际上 60% 是资源被某一个"无限制"的应用吃掉了,30% 是申请的资源远远大于实际用量。

5.3 谨慎调整,先看影响面再动手

修改资源限制不是小事。我的操作习惯是这样的:

  • 先在灰度环境改一组 Pod,跑一轮压测,观察延迟和错误率。
  • 确认没问题再滚动更新生产环境的 Deployment。
  • 内存相关的调整一次只改一点点,从 1Gi 调到 1.5Gi,观察内存曲线稳定后再继续。一次跳跃太大,容易掩盖真正的资源瓶颈。
  • 改完记得更新团队的资源配置文档,避免下次接手的人对着旧配置一脸茫然。

还有一个容易忽略的点:HPA(水平自动扩缩)的阈值是配置在 requests 上的。如果你开了 HPA,资源请求的调整会影响扩缩容的判断依据。比如 HPA 基于 CPU 利用率扩缩容,CPU 利用率是指"当前 CPU 使用量和 requests 的比值"。把 requests 调大,CPU 利用率百分比就变小,触发扩容的时机就会延后。调整 requests 前一定要全局考虑,不能只看资源限制这一环。

5.4 小技巧:利用 LimitRange 强制默认值

如果团队协作中总是有人忘写 resources,与其反复提醒,不如直接在命名空间层面用 LimitRange 强制兜底。配合准入机制,Kubernetes 不管你在 YAML 里有没有写,最终生效的 Pod 配置都会带上默认的资源声明。这种做法省心,也能让整个集群的资源规划显得有条理。

我个人在实际操作中的体会是:资源限制的配置没有标准答案,只有基于业务特征不断校准的过程。别指望一次配好一劳永逸,把它当成一个需要定期维护的配置项来看待,配合监控数据持续优化,才是让集群稳定和高效并存的正确姿势。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询