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、数据库、认证代理),持续出现 throttling 往往就值得重点排查。
如何检测#
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 可能偏低,或者这个容器的 CPU 行为已经和当前配额不匹配了。
通过 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。按我这套负载情况看,即使同时有多人访问,不同服务的实际 CPU 消耗通常也达不到各自 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 核心还存在空闲。
对延迟敏感服务来说,这种额外等待有时会放大尾延迟。
不设置 CPU limit 时,Pod 依然受内核 CFS Shares(由 requests 转换而来)的保护:
- 节点空闲时:Pod 可以贪婪地使用节点上所有的闲置 CPU,迅速处理完请求,降低请求延迟。
- 节点拥挤时:内核 CFS 会根据各个 Pod 的
requests比例分配 CPU 时间。由于 CPU 是可压缩资源(Compressible Resource),它只会让进程变慢,而不会像内存超卖那样触发 OOM Kill。
我更愿意按场景来看配置策略#
如果你也在维护生产环境,或者像我一样会长期折腾 Homelab,我会更倾向按下面几类场景来判断:
策略一:只设 Request,不设 Limit(常见默认选项)#
- 适用场景:绝大多数 Web 服务、API、微服务。
- 配置方式:CPU 仅设置
requests;内存必须同时设置requests和limits(内存是不可压缩资源,必须有 limit 防止把节点干挂)。 - 优势:可以减少因为 quota 造成的额外 throttling,也更容易把节点上的空闲 CPU 用起来。
策略二:同时设置 Request 和 Limit,显式控制上限#
- 适用场景:CPU 密集且可能失控的批处理任务(如视频转码、离线计算、无节制爬虫)、存在死循环或资源失控风险的遗留代码。
- 配置方式:按照本文前述的“档位设计”设定
limits。 - 优势:限制影响范围,避免单个组件把整机 CPU 长时间占满。
策略三:Guaranteed + CPU Manager 绑核隔离#
- 适用场景:对延迟极度敏感的网关(Envoy / Nginx / Cilium)、高并发数据库。
- 配置方式:
requests == limits并且严格设为整数核(如1,2)。在节点级别开启 Kubelet 的 CPU Managerstatic策略。 - 优势:Pod 能更稳定地占用物理 CPU 核心资源(Pinning),减少调度抖动带来的影响。
在我的 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 或小规模集群,我当前更倾向的做法是:多数 Web 服务优先评估只设 requests、不设 CPU limits;只给更容易失控的后台任务保留 CPU limit,并把内存保护配完整。 这不是唯一答案,但在我这套环境里更容易兼顾资源利用率和维护成本。