
做虚拟化平台时，CPU 超卖几乎绕不开。

原因很简单：用户申请的 vCPU 通常不会一直满负载运行。如果严格按照物理 CPU 核数去售卖和调度，资源利用率会很低。但如果只是粗暴地把更多 VM 塞到节点上，迟早会遇到另一个问题：某些 VM 长时间吃满 CPU，拖慢同节点上的其他 VM。

所以 CPU 超卖真正要解决的不是"怎么多放几台 VM"，而是两个问题：

- 调度时，允许放多少 VM？
- 运行时，某台 VM 最多能吃多少 CPU？

这两个问题不能混在一起。混在一起之后，很容易以为改了一个参数就实现了超卖，最后发现调度、限速、监控和用户体验全都对不上。

## CPU 超卖不是一个开关

在 KubeVirt 里，可以把 CPU 超卖控制拆成三层：

- 调度层：决定 VM 能不能继续调度到某个节点。
- 执行层：决定已经运行的 VM 最多能使用多少 CPU。
- 策略层：根据节点压力、VM 优先级、套餐和监控指标动态调整前两层。

这三层对应几类手段：

| 手段 | 定位 |
|---|---|
| KubeVirt `cpuAllocationRatio` / Kubernetes `requests` | VM 创建时的静态超卖模型 |
| Kubernetes `limits` | Pod 级运行上限 |
| 调度入口控制 | 动态控制后续 VM 是否还能调度进来 |
| libvirt `period/quota` / `global_period/global_quota` | 对已经运行的 VM 做运行时 CPU 限速 |

这些方式都可以参与 CPU 超卖，但它们不是同一层的东西。有些是 KubeVirt/Kubernetes 原生能力，有些是平台侧策略，有些需要二次开发。

## 用 requests/limits 做静态超卖

Kubernetes 里最常见的资源模型是 `requests` 和 `limits`。KubeVirt 在这套模型上又加了一层默认的 CPU request 计算逻辑。

`requests.cpu` 首先是调度承诺。Scheduler 主要根据 request 判断节点是否还有足够资源。同时，它也会影响运行时 CPU shares/weight：没有 CPU limit 时，CPU 空闲资源可以被抢占使用；节点发生竞争时，request 会影响不同 Pod 之间的相对权重。

`limits.cpu` 是运行上限。Kubelet 会通过 cgroup CFS quota 限制 Pod 最多能用多少 CPU。KubeVirt 默认不会给 VM 设置 CPU limit；如果启用了 auto CPU limits，或者手动设置了 limit，virt-launcher container 才会有对应的 CPU limit。

在 KubeVirt VM 场景里，最常见的静态超卖方式是让 VM 暴露的 vCPU 数量大于 virt-launcher pod 的 `requests.cpu`。

KubeVirt 原生支持这个模型。默认情况下，virt-launcher container 的 CPU request 会按 `vCPU 数量 * 1 / cpuAllocationRatio` 计算。`cpuAllocationRatio` 默认是 10，也就是每个 vCPU 默认 request 0.1 CPU。比如一个 4 vCPU VM，如果没有手动覆盖 request，默认 request 大约是 0.4 CPU。

也可以手动设置 VM/VMI 的 `resources.requests.cpu`。手动 request 会覆盖 KubeVirt 的默认计算逻辑。例如一个 VM 内部看到 4 个 vCPU，但 launcher pod 只 request 1 CPU。调度器会按 1 CPU 计算这个 VM 的资源承诺，这就形成了 CPU overcommit。

这种方式的优点是原生、清晰、和 Kubernetes 调度体系集成得好。它适合作为 VM 创建时的基础资源模型。

但它有一个明显缺点：不适合运行时动态调整。

Pod 创建后，resource request/limit 不是一个适合频繁动态修改的接口。对 KubeVirt 来说，改 VM/VMI 的 resource 配置通常还会牵涉 launcher pod 规格、重建、迁移或额外的 live update 逻辑。

所以 `cpuAllocationRatio` 和 requests/limits 更适合回答：

- 这个 VM 创建时承诺多少 CPU？
- 这个 VM 创建时最多允许用多少 CPU？

它不适合单独承担：节点现在压力高，马上把某台正在运行的 VM 限下来。

## 动态控制调度入口

另一个问题是运行过程中怎么控制后续 VM 还能不能继续调度到某个节点。

直觉上很容易想到动态调整 Node allocatable：低负载时把可调度容量调高，高负载时把 allocatable 降下来，阻止更多 VM 调度进来。

但这不应该是首选方案。

Kubernetes 里的 `Node.status.allocatable` 通常由 kubelet 根据节点 capacity、system reserved、kube reserved 等配置上报。它表达的是这台节点可供普通 Pod 使用的资源量，调度器不会在 request 维度超订阅 allocatable。业务 controller 直接 patch Node status，容易和 kubelet 上报、scheduler cache、权限模型产生冲突，也会让问题变得很难排查。

更稳的做法是把动态策略放在调度入口，而不是直接把 Node allocatable 当业务控制面：

- 用自定义 scheduler plugin 在 Filter/Score 阶段根据节点压力、VM 类型、超卖比做决策。
- 用独立 scheduler 或调度 extender 承接 VM 调度策略。
- 通过 node label、taint、cordon 等手段粗粒度控制节点是否继续接收 VM。
- 通过 KubeVirt `cpuAllocationRatio` 和 VM request 模型定义静态超卖基线，再在调度器侧做动态收敛。

这类能力解决的是调度层问题：后续还能不能继续往这个节点放 VM？

它的边界也很明确：

- 不会自动限制已经运行的 VM。
- 已经放进来的 VM 不会因为调度入口收紧就主动变慢。
- 它影响的是后续 placement，不是运行中 VM 的 CPU 消耗。
- 策略必须处理调度缓存、指标延迟和短时间不一致。

所以调度入口控制更像入口阀门。它可以控制后续 placement，但不是运行中 VM 的刹车。

如果节点上已经有用户长期吃满 CPU，单靠收紧调度入口不够。它最多阻止新的 VM 进来，不能解决已经发生的 noisy neighbor。

## 用 libvirt quota/period 限制已运行 VM

对已经运行的 VM，比较直接的方式是用 libvirt CPU tuning 参数限制 vCPU 线程可使用的 CPU 时间。

这里有两组容易混淆的参数：

- `period` / `quota`：限制每个 vCPU 线程的 CPU 时间。
- `global_period` / `global_quota`：限制整个 domain 的 CPU 时间。

`period` 和 `quota` 的单位通常是微秒。对单个 vCPU 来说，可以粗略理解为 `effective CPU per vCPU = quota / period`。例如 `period=100000, quota=50000` 表示每个 vCPU 最多使用 0.5 CPU。对于 4 vCPU VM，这不是整台 VM 只有 0.5 CPU，而是每个 vCPU 0.5 CPU，整台 VM 理论上最多约 2 CPU。

如果目标是限制整台 VM 的总 CPU 使用量，应该优先看 `global_period/global_quota`。例如 `global_period=100000, global_quota=200000` 才更接近“整个 domain 最多 2 CPU”的语义。

quota 为负数通常表示不限制；quota 为 0 通常表示没有设置值。具体取值范围要以 libvirt 和当前 hypervisor 支持为准。

这个机制的本质是 CPU throttling，不是物理 CPU 降频。vCPU 线程运行时还是按宿主机 CPU 当前频率执行，但一个周期内用完 quota 后会被暂停，等下一个周期再继续运行。

它的优势很明确：

- 可以作用于已经运行的 VM。
- 可以按单个 VM 精准限速。
- 适合处理长期高 CPU 用户。
- 可以作为 CPU credit、baseline、burst 这类策略的执行器。

它的限制也很明确：

- 不影响 Kubernetes scheduler 的容量判断。
- quota 太低会让 guest 内 steal time 升高。
- 业务可能出现延迟抖动、吞吐下降、定时器不准。
- 可能和 Pod cgroup limit 形成双重限流。
- `period/quota` 和 `global_period/global_quota` 语义不同，不能混用。
- 不适合随便用于 dedicated CPU、realtime 或低延迟业务。

所以 libvirt quota/period 更适合回答：这台已经运行的 VM，现在最多允许用多少 CPU？它不能单独回答：这个节点还能不能继续调度新的 VM？

## 几种方式怎么组合

几种机制的职责划分：

| 机制 | 层次 | 职责 |
|---|---|---|
| `cpuAllocationRatio` / `requests.cpu` | 静态资源模型 | 定义 VM 创建时的资源承诺，是静态超卖模型的基础 |
| `limits.cpu` | Pod 执行层 | 定义 virt-launcher pod 的 cgroup CPU 上限 |
| 调度入口控制 | 调度层 | 动态控制后续还能调度多少 VM，是入口阀门 |
| libvirt quota 参数 | VM 执行层 | 动态限制已运行 VM 的 CPU 使用量，是运行时限速器 |

一个比较完整的闭环：

1. 通过 `cpuAllocationRatio` 和 `requests.cpu` 定义 VM 的基础承诺。
2. 根据节点 CPU pressure、load、run queue 等指标动态收紧或放宽调度入口。
3. 节点压力低时，提高可调度容量，允许更高超卖比。
4. 节点压力高时，降低可调度容量，阻止更多 VM 调度进来。
5. 对已经运行且持续高 CPU 的低优先级 VM，下发合适的 libvirt quota 参数。
6. 压力恢复后，逐步放宽 quota，或者恢复到不限额。

分层之后职责清楚：

- `cpuAllocationRatio` 和 requests/limits 负责初始资源模型。
- 调度入口控制负责后续调度容量。
- libvirt quota 负责运行中限速。
- 策略 controller 负责判断什么时候调大、调小、恢复。

## 什么样的 VM 可以被限速

CPU 超卖不能只看节点指标，还要看 VM 类型。

| 类型 | 策略 |
|---|---|
| 独享型 | 不参与超卖，不下发 quota |
| 共享型 | 可以调度超卖，必要时按比例限速 |
| 突发型 | 有 baseline，允许 burst，积分耗尽后限到 baseline |

独享型 VM 通常对应 dedicated CPU、realtime、低延迟业务，不能随便限速。否则用户买的是独享资源，平台却在运行时压 quota，这会破坏产品语义。

共享型 VM 可以参与超卖，但需要设置合理的下限。比如一个 4 vCPU VM，不能轻易把整台 VM 的总上限压到 0.5 CPU，除非业务明确接受强限速。

突发型 VM 可以参考公有云常见做法：平时允许超过 baseline，持续超出后消耗积分，积分耗尽后回到 baseline。libvirt quota 参数是这里的最终执行动作。

## 运行时调参接口的定位

libvirt quota/period 本身是运行时执行层能力，但在 KubeVirt 里还需要一个调用入口。

这里要先划清边界：下面说的是自研扩展设计，不是 KubeVirt 已经提供的公开 annotation。

一种比较自然的扩展方式，是通过 VMI annotation 把策略层计算出的 CPU 限额下发给节点侧组件，由 virt-handler 或 virt-launcher 侧逻辑调用 libvirt API 生效。这类接口的设计要点是：

- annotation 只描述期望状态（quota 值），不描述执行过程。
- 节点侧组件负责将期望状态同步到 libvirt，并在 VMI status 里反映当前实际值。
- 策略 controller 只负责写 annotation，不直接调 libvirt。
- VM 重启、迁移、virt-launcher 重建后，限速状态必须能重新下发。
- 调参失败要能在 status 或 event 里暴露，不能静默失败。

这不是完整的 CPU 超卖策略，而是超卖控制的运行时执行层。完整策略还需要一个 controller，根据节点和 VM 的指标决定：

- 什么时候写入限速参数
- 写入多大的 quota
- 什么时候调小
- 什么时候恢复
- 哪些 VM 永远不能限速

## 必须配套观测

CPU 超卖如果没有观测，很难判断问题出在哪里。

最少应该看这些指标：

- VM 内 steal time
- VM 内 load average
- virt-launcher pod 的 cgroup throttling
- VM/VMI 的 request、limit、vCPU 数量
- libvirt quota 实际设置值
- libvirt/domain CPU time
- Node CPU pressure
- Node load 和 run queue
- VM CPU 使用率
- 业务 P95/P99 延迟
- 限速、恢复、迁移、重启、调参失败事件

几个典型问题：

**VM 看到 4 vCPU，但每个 vCPU 实际 quota 只有 0.5 CPU。**

guest 调度器以为自己有 4 个 CPU，实际运行时间却很少，结果通常是 steal time 高、load 高、业务慢。

**调度入口放得太宽。**

调度层放进来的 VM 太多，节点整体 CPU pressure 上升，最后只能靠限速、迁移或驱逐补救。

**cgroup limit 和 libvirt quota 同时生效。**

用户只看到性能下降，但排查时可能不知道到底是 Pod cgroup 在限，还是 libvirt 在限。

## 推荐落地路径

**第一阶段：手动治理**

遇到某台 VM 长时间吃满 CPU 时，手动下发 libvirt CPU quota 参数，观察节点 CPU pressure、VM steal time 和业务延迟。这个阶段要先确认自己限制的是单个 vCPU 还是整个 domain，再验证 libvirt scheduler 参数在当前运行模式下是否可用，以及限速效果是否符合预期。

**第二阶段：半自动策略**

给 VM 打标签，标记哪些允许被限速。controller 根据持续 CPU 使用率和节点压力生成建议 quota，但先不自动执行，人工确认后再写入。

**第三阶段：自动闭环**

节点压力升高时，先通过调度入口策略阻止更多 VM 进入高压节点；再对低优先级、高 CPU、允许限速的 VM 下发 quota 参数。压力恢复后，逐步放宽 quota。

**第四阶段：产品化**

定义共享型、突发型、独享型规格。共享型参与超卖，突发型引入 baseline、burst、credit，独享型不参与超卖。等语义稳定后，再考虑把 annotation 迁移成正式 spec/status API。

## 参考文档

- [KubeVirt Resources requests and limits](https://kubevirt.io/user-guide/compute/resources_requests_and_limits/)
- [KubeVirt Node overcommit](https://kubevirt.io/user-guide/compute/node_overcommit/)
- [Kubernetes Resource Management for Pods and Containers](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
- [Kubernetes Node Allocatable](https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/)
- [libvirt Domain XML CPU Tuning](https://libvirt.org/formatdomain.html#cpu-tuning)

## 总结

CPU 超卖不是单个参数，也不是单个开关。它至少需要调度层和执行层配合。

`cpuAllocationRatio` 和 requests/limits 适合做静态资源模型，调度入口策略适合做后续 placement 控制，libvirt quota 参数适合对运行中的 VM 做限速。

如果只做 request 超卖，运行时治理能力不够。如果只收紧调度入口，已经运行的 VM 不受影响。如果只做 libvirt quota，调度器又不知道节点还能不能继续承载新 VM。

比较稳的方案是：

- 用 `cpuAllocationRatio` 和 `requests.cpu` 定义基础承诺。
- 用调度入口策略控制后续调度容量。
- 用 libvirt quota 控制已运行 VM 的 CPU 使用量。
- 用策略 controller 把三者串起来。

这样才能把"多卖一点 CPU"和"节点不要被打爆"同时做起来。

