背景#

在为我的 Homelab 双 K3s 集群整理资源配置时,我发现几乎所有自定义 Deployment 都缺少 cpu limit——只设了 memory limit。这意味着任何一个 Pod 都可以无限制地消耗 CPU,在节点高负载时导致其他服务响应变慢甚至无法调度。

这篇文章系统梳理了 Kubernetes CPU 配置的核心概念,包括:

  • requestslimits 的本质区别
  • Linux CFS(完全公平调度器)如何实现 CPU throttling
  • 三种 QoS 类别(Guaranteed / Burstable / BestEffort)及其驱逐优先级
  • 节点内存压力下的 Pod 驱逐机制
  • 实际 Homelab 的配置策略与决策依据

requests 与 limits:两个完全不同的概念#

很多人把 requestslimits 当成一对配套参数,其实它们作用于完全不同的阶段:

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)都必须同时满足:

  • requestslimits 都已设置
  • 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 分配规则细节#

几个容易忽略的细节:

  1. 只设 limit 不设 request:Kubernetes 会自动将 request 设置为与 limit 相同的值,Pod 变为 Guaranteed
  2. 多容器 Pod:Pod 的 QoS 取决于所有容器中最低的那个
  3. 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 转换而来)的保护:

  1. 节点空闲时:Pod 可以贪婪地使用节点上所有的闲置 CPU,迅速处理完请求,降低请求延迟。
  2. 节点拥挤时:内核 CFS 会根据各个 Pod 的 requests 比例分配 CPU 时间。由于 CPU 是可压缩资源(Compressible Resource),它只会让进程变慢,而不会像内存超卖那样触发 OOM Kill。

年推荐的配置策略分级#

如果你在维护生产环境或像我一样深度优化 Homelab,建议参考以下 2026 年最新的架构策略:

策略一:只设 Request 不设 Limit(现代推荐默认选项)#

  • 适用场景:绝大多数 Web 服务、API、微服务。
  • 配置方式:CPU 仅设置 requests;内存必须同时设置 requestslimits(内存是不可压缩资源,必须有 limit 防止把节点干挂)。
  • 优势:彻底告别无谓的 CPU Throttling,充分压榨节点算力。

策略二:同时设置 Request 和 Limit,严格控制上限#

  • 适用场景:恶劣的批处理任务(如视频转码、离线数据计算、无节制的爬虫)、有内存泄露或死循环风险的遗留代码。
  • 配置方式:按照本文前述的“档位设计”设定 limits
  • 优势:控制爆炸半径,防止烂代码直接把整个节点的 CPU 跑满影响其他关键服务。

策略三:Guaranteed + CPU Manager 绑核隔离#

  • 适用场景:对延迟极度敏感的网关(Envoy / Nginx / Cilium)、高并发数据库。
  • 配置方式requests == limits 并且严格设为整数核(如 1, 2)。在节点级别开启 Kubelet 的 CPU Manager static 策略。
  • 优势: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,搭配合理的内存保护 是最省资源、最易维护的策略。

参考资料#