K8s CPU 配置完全指南:QoS、Throttling 与驱逐策略
目录
背景#
在为我的 Homelab 双 K3s 集群整理资源配置时,我发现几乎所有自定义 Deployment 都缺少 cpu limit——只设了 memory limit。这意味着任何一个 Pod 都可以无限制地消耗 CPU,在节点高负载时导致其他服务响应变慢甚至无法调度。
这篇文章系统梳理了 Kubernetes CPU 配置的核心概念,包括:
requests和limits的本质区别- Linux CFS(完全公平调度器)如何实现 CPU throttling
- 三种 QoS 类别(Guaranteed / Burstable / BestEffort)及其驱逐优先级
- 节点内存压力下的 Pod 驱逐机制
- 实际 Homelab 的配置策略与决策依据
requests 与 limits:两个完全不同的概念#
很多人把 requests 和 limits 当成一对配套参数,其实它们作用于完全不同的阶段:
requests |
limits |
|
|---|---|---|
| 作用时机 | Pod 调度时 | Pod 运行时 |
| 作用对象 | Kubernetes 调度器 | Linux 内核(cgroups) |
| 含义 | “我至少需要这么多资源才能正常运行” | “我最多只能用这么多资源” |
| 超出会怎样 | 节点上没有满足 requests 的节点,Pod Pending | CPU throttle |
CPU requests:调度器的依据#
调度器在为 Pod 寻找合适节点时,会把节点上所有 Pod 的 cpu request 求和,只有剩余可分配量 ≥ 新 Pod 的 cpu request 时,才会将该节点纳入候选。
可分配 CPU = 节点总 CPU - 系统预留 - 所有已运行 Pod 的 cpu requests 之和
requests 不是 Pod 实际能使用的上限,只是调度层面的保障。Pod 实际可以用更多——直到碰到 limit 或节点 CPU 被其他 Pod 争走。
CPU limits:内核的执法者#
limits 由 Linux cgroups(控制组)在内核层面执行。Kubernetes 将 cpu limit 转换为 CFS 配额:
cpu.cfs_quota_us = cpu_limit × cpu.cfs_period_us
默认 cfs_period_us = 100000(100ms)。例如 limits.cpu: 500m,则:
cpu.cfs_quota_us = 0.5 × 100000 = 50000μs
含义:在每 100ms 的统计周期内,这个 Pod 最多只能使用 50ms 的 CPU 时间。一旦超出,剩余时间内该 Pod 的进程会被内核强制暂停,直到下一个周期开始。
CPU Throttling:不是报错,但会让你的服务变慢#
什么是 Throttling#
当容器在一个 CFS 周期内用尽了配额,内核会暂停其进程直到下个周期。这就是 CPU throttling。
和内存 OOMKill 不同,CPU throttling 不会终止 Pod,也不会产生任何告警。它只是让你的服务悄悄变慢。对于延迟敏感的服务(API、数据库、认证代理),哪怕只有 10-20% 的 throttling 都可能导致 P99 延迟显著上升。
如何检测#
Throttling 数据存储在节点的 cgroup 文件中:
# cgroup v1 路径
cat /sys/fs/cgroup/cpu,kubepods/burstable/<pod-cgroup-id>/cpu.stat
# cgroup v2 路径 (现代 K3s 默认)
cat /sys/fs/cgroup/kubepods.slice/<pod-cgroup-id>/cpu.stat
关键字段:
nr_periods 500 # 经历的 CFS 周期总数
nr_throttled 120 # 被 throttle 的周期数
throttled_time 2400000 # 被暂停的总纳秒数
Throttling 率 = nr_throttled / nr_periods = 24%,说明有严重的 limit 过低问题。
通过 Prometheus(需要 cAdvisor)也可以查询:
# CPU Throttling 率(按容器)
rate(container_cpu_cfs_throttled_seconds_total[5m])
/ rate(container_cpu_cfs_periods_total[5m])
QoS 类别:Kubernetes 的优先级机制#
Kubernetes 根据 Pod 的 requests/limits 配置自动分配三种 QoS(服务质量)类别,这决定了在资源紧张时 Pod 的驱逐优先级。
Guaranteed(最高优先级)#
条件:所有容器的每种资源(CPU + Memory)都必须同时满足:
requests和limits都已设置requests == limits(完全相等)
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "500m" # 必须 == requests
memory: "256Mi" # 必须 == requests
特点:
- 调度器为其预留精确资源,不允许其他 Pod 占用
- 内存压力下最后被驱逐
- 适合关键有状态服务(主数据库、主认证服务)
缺点:requests 被锁死等于 limits,在节点 CPU 空闲时,这部分 CPU 无法被其他 Pod 使用——浪费资源。
Burstable(中等优先级)#
条件:Pod 不满足 Guaranteed,但至少有一个容器设置了 requests 或 limits。
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m" # limit > request,允许突发
memory: "512Mi"
特点:
- 正常时使用 requests 量,空闲时可 burst 到 limits
- 内存压力下,超出 requests 使用量越多的 Pod 越先被驱逐
- 适合绝大多数 Web 服务、后台任务
BestEffort(最低优先级)#
条件:所有容器均未设置任何 requests 或 limits。
# 什么都不设置
resources: {}
特点:
- 调度时不占用任何资源配额,可以塞进任何节点
- 内存压力下第一个被驱逐
- 生产环境绝对不应使用
QoS 分配规则细节#
几个容易忽略的细节:
- 只设 limit 不设 request:Kubernetes 会自动将 request 设置为与 limit 相同的值,Pod 变为 Guaranteed
- 多容器 Pod:Pod 的 QoS 取决于所有容器中最低的那个
- QoS 不可更改:Pod 创建后,QoS 类别不能通过 resize 修改(control plane 会拒绝)
节点压力驱逐:谁先被干掉#
触发条件#
kubelet 持续监控节点资源信号,当达到驱逐阈值时开始终止 Pod:
| 信号 | 软驱逐默认阈值 | 硬驱逐默认阈值 |
|---|---|---|
memory.available |
< 100Mi(需配置 grace period) |
< 50Mi(立即执行) |
nodefs.available |
< 15% |
< 10% |
nodefs.inodesFree |
— | < 5% |
- 软驱逐:触发后等待 grace period(默认配置),期间 Pod 可以优雅关闭
- 硬驱逐:立即强制终止,grace period = 0
驱逐顺序#
在同一 QoS 类别内,kubelet 会优先驱逐超出 requests 最多的 Pod:
驱逐优先级(高→低):
1. BestEffort(无 requests/limits)
→ 任何超出系统阈值的内存消耗都会触发
2. Burstable(超出 requests 部分)
→ 实际使用 > requests 越多,越先被驱逐
3. Guaranteed(requests == limits)
→ 只在系统极度紧张时才考虑驱逐
重要:驱逐不遵守 PodDisruptionBudget(PDB)。PDB 只对 Drain、滚动更新等主动操作有效,对节点压力驱逐无效。
为什么 CPU 压力不触发驱逐#
注意:CPU 不足不会触发驱逐,只有内存、磁盘、inode 和 PID 不足才会。CPU 压力的后果是 throttling,而非驱逐。这也是为什么 CPU limits 设置过低会让服务变慢,却不会产生任何 Pod 重启事件——问题更难被发现。
实践:Homelab 的配置策略#
我的 Homelab 由两个 K3s 集群组成:
- homelab:Proxmox 虚拟机,运行 Calibre-Web、Gotify、Kopia 等
- oracle-k3s:Oracle A1.Flex 4 OCPU / 24GB,运行大部分 Web 服务
日常 1–2 人使用,偶发 ~10 人同时访问(家庭分享场景)。
策略选择:全部 Burstable#
不使用 Guaranteed 的原因:
- 单节点家用环境,没有多租户隔离需求
- Guaranteed 把 requests 锁死等于 limits,节点空闲时资源无法共享
- 4 OCPU 节点上跑 20+ 个服务,若全部 Guaranteed 会有大量 CPU 预留浪费
不使用 BestEffort 的原因:
- 内存压力下第一个被干掉
- 调度器无法合理分配节点资源
CPU Limit 档位设计#
根据服务特性,设计了 5 个档位:
| 档位 | CPU Limit | 服务类型 |
|---|---|---|
| 1000m | 1 core | 无头浏览器(Chrome/Browserless)、备份压缩(Kopia) |
| 500m | 0.5 core | 入口流量(cloudflared)、数据库(Postgres)、主要 Web 服务 |
| 200m | 0.2 core | 轻量后台(Gotify、oauth2-proxy、“Homepage”、Redis) |
| 50–100m | 微量 | 监控 exporter、sidecar(log-exporter) |
以 oracle-k3s 为例,所有服务的 CPU limit 之和超过 4000m(节点总量),但这完全正常:
CPU limit 不是 reservation,而是每个 CFS 周期内的消耗上限。只有在节点整体 CPU 紧张时,才会触发 throttle。10 人同时访问不同服务时,各服务的实际消耗远低于各自的 limit,不存在争抢。
实际配置示例(karakeep)#
containers:
- name: karakeep # 主应用
resources:
requests:
cpu: 100m # 正常浏览书签时的消耗
memory: 256Mi
limits:
cpu: 500m # 批量导入时允许 burst
memory: 512Mi
- name: chrome # 无头浏览器,抓取网页截图
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m # 渲染 JS 页面 CPU 密集,给足余量
memory: 1Gi
- name: meilisearch # 全文搜索引擎
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m # 建索引时较重
memory: 768Mi
年的最佳实践:要不要完全抛弃 CPU Limits?#
在这几年里,社区里关于 CPU 配置的最佳实践发生了一个重大的范式转移:对于大多数在线业务(Web 服务、API),建议只设置 CPU requests,不要设置 CPU limits。
为什么抛弃 CPU limits?#
正如前文所述,CPU limits 是通过 Linux CFS Quota 实现的。当你设置了 limits: cpu: 500m,一旦 Pod 在 100ms 周期的前 50ms 用完了配额,后续的 50ms 就会被无情地 throttle——即使此时节点上其他的 CPU 核心完全空闲!
这种“人造的饥饿”会导致接口的 P99 延迟无意义地飙升。
不设置 CPU limit 时,Pod 依然受内核 CFS Shares(由 requests 转换而来)的保护:
- 节点空闲时:Pod 可以贪婪地使用节点上所有的闲置 CPU,迅速处理完请求,降低请求延迟。
- 节点拥挤时:内核 CFS 会根据各个 Pod 的
requests比例分配 CPU 时间。由于 CPU 是可压缩资源(Compressible Resource),它只会让进程变慢,而不会像内存超卖那样触发 OOM Kill。
年推荐的配置策略分级#
如果你在维护生产环境或像我一样深度优化 Homelab,建议参考以下 2026 年最新的架构策略:
策略一:只设 Request 不设 Limit(现代推荐默认选项)#
- 适用场景:绝大多数 Web 服务、API、微服务。
- 配置方式:CPU 仅设置
requests;内存必须同时设置requests和limits(内存是不可压缩资源,必须有 limit 防止把节点干挂)。 - 优势:彻底告别无谓的 CPU Throttling,充分压榨节点算力。
策略二:同时设置 Request 和 Limit,严格控制上限#
- 适用场景:恶劣的批处理任务(如视频转码、离线数据计算、无节制的爬虫)、有内存泄露或死循环风险的遗留代码。
- 配置方式:按照本文前述的“档位设计”设定
limits。 - 优势:控制爆炸半径,防止烂代码直接把整个节点的 CPU 跑满影响其他关键服务。
策略三:Guaranteed + CPU Manager 绑核隔离#
- 适用场景:对延迟极度敏感的网关(Envoy / Nginx / Cilium)、高并发数据库。
- 配置方式:
requests == limits并且严格设为整数核(如1,2)。在节点级别开启 Kubelet 的 CPU Managerstatic策略。 - 优势:Pod 独占物理 CPU 核心资源(Pinning),完全消除 CFS 调度开销与上下文切换的损耗。
在我的 Homelab 实践中,针对普通 Web 服务我已经开始逐步移除 CPU limit,仅保留对诸如监控组件抓取、批处理等可能不受控组件的 CPU limit 限制。
检查你的集群现状#
查看所有 Pod 的 QoS 类别#
kubectl get pods -A -o custom-columns=\
'NAMESPACE:.metadata.namespace,NAME:.metadata.name,QOS:.status.qosClass'
找出没有 CPU limit 的容器#
kubectl get pods -A -o json | jq -r '
.items[] |
. as $pod |
.spec.containers[] |
select(.resources.limits.cpu == null) |
[$pod.metadata.namespace, $pod.metadata.name, .name] |
join(" / ")
'
验证单个 Pod 的 QoS#
kubectl get pod <pod-name> -n <namespace> \
-o jsonpath='{.status.qosClass}'
总结#
| 概念 | 关键点 |
|---|---|
requests |
调度依据,不是运行上限 |
limits |
内核 CFS 配额,超出被 throttle |
| Guaranteed | requests == limits,浪费资源,适合关键有状态服务 |
| Burstable | requests < limits,兼顾节省与突发,适合大多数场景 |
| BestEffort | 无任何限制,内存压力下第一个被驱逐,生产不可用 |
| CPU throttle | 不报错,但让服务变慢,需主动监控 |
| 节点驱逐 | 只由内存/磁盘触发,按 QoS 顺序驱逐,不遵守 PDB |
对于 Homelab 或小规模生产集群,2026 年的最佳实践是:多数 Web 服务使用只设 requests 的 Burstable(不设 CPU limits),仅对后台恶劣任务设置 CPU limit,搭配合理的内存保护 是最省资源、最易维护的策略。