Uber 如何让 CPU 从“专用独占”走向“可靠共享”:Odin 的混合核心分配实践
2026/7/8 6:10:45 网站建设 项目流程

本文是对 Hybrid Core Allocation: From Overallocation to Reliable Sharing 的整理与翻译。

原文发布于 2026 年 4 月 21 日,来自 Uber Engineering,作者是 Alexandr Sudakov 和 Ivan Shibitov。文章讨论的是 Uber 内部有状态容器编排平台 Odin 在 CPU 资源分配上的一次演进:从过去更偏“专用核心”的模型,进一步走向“专用核心 + 共享核心”的混合核心分配模型。其目标不是单纯把 CPU 利用率拉高,而是在延迟、可靠性、资源利用率和成本之间找到一个更稳定的平衡点。(Uber)

内容结构概览

本文会按下面的脉络展开:

  1. 为什么 Uber 之前要在 Odin 中引入 cpusets
  2. 纯专用核心模型带来了什么新问题
  3. 混合核心分配模型是什么:Dedicated Cores + Shared Cores
  4. Linuxcpu.shares在共享核心池中如何发挥作用
  5. 为什么严重争抢时,cpu.shares并不总是完全公平
  6. NUMA 对 CPU 与内存亲和性的影响
  7. Odin 在主机层面如何分配专用核和共享核
  8. 垂直扩缩容如何决定给一个 workload 分多少核
  9. Kubernetes 在这个场景下缺了什么,Uber 如何补上
  10. 当前方案的局限性
  11. 对基础设施工程实践的启发

一、背景:从 CPU quota 到 cpusets

在容器环境中,CPU 资源隔离通常有几种方式。比较常见的一种是 CPU quota:给容器一个固定的 CPU 时间预算。例如在一个 100ms 的调度周期里,如果容器被分配了 2 个 core,那么它理论上可以得到 200ms 的 CPU 时间。问题在于,现代服务通常是多线程的,如果一个容器内部有很多线程并发运行,它可能很快把这个周期内的 CPU quota 消耗完,然后在剩余周期里被 throttling,也就是被限流。对于低延迟服务来说,这种限流会直接拉高尾延迟。Uber 早先关于 cpusets 的文章中提到,切换到 cpusets 后,P99 延迟变得更稳定,并通过减少为应对抖动而预留的过量资源,在全局节省了约 11% 的核心分配。(Uber)

cpusets 的思路不再是“你在每个周期最多能用多少 CPU 时间”,而是“你只能运行在哪些 CPU 核心上”。也就是说,一个容器被绑定到一组 CPU core 上,它可以在这些 core 上尽可能运行,但不能跑到其他没有分配给它的 core 上。这种方式牺牲了一部分临时 burst 的能力,但换来了更稳定的尾延迟。

这就是 Uber 在 Odin 中引入 cpusets 的背景。Odin 是 Uber 内部用于有状态服务的容器编排系统。所谓有状态服务,通常包括数据库、消息队列、缓存、存储系统等。这类服务对尾延迟、稳定性和数据迁移成本都很敏感。因此,Uber 希望通过更严格的 CPU 控制来减少 throttling,降低延迟波动。

但问题也随之而来:如果完全依赖专用 CPU 核心,就意味着每个 workload 都要拿到完整的、独占的 CPU core。这样做确实稳定,但成本会很高,尤其面对 CPU 使用呈现明显突发特征的 workload 时,很容易出现资源浪费。

二、旧的垂直扩缩容假设为什么不够用了

Uber 原来的垂直 CPU scaler,也就是负责给 workload 调整 CPU 配额的组件,长期依赖几个假设:

第一,CPU 使用量可以通过一分钟平均值比较准确地衡量。也就是说,只要看一段时间内的平均 CPU 消耗,就能判断 workload 需要多少 CPU。

第二,分配给节点的 core 是专用的,不会被多个 workload 共享。资源模型中更像是“一个 workload 拿到几个 core,这几个 core 就归它”。

第三,CPU 利用率目标可以随着 failover 负载的变化而调整。例如当故障转移发生时,部分 workload 可能短时间承担更多请求,系统可以通过调整利用率目标来适应。

这些假设在平稳负载下可以工作,但面对突发型 CPU 使用模式时就不够了。因为突发负载的特点不是长期平均值很高,而是某些时刻会突然冲上去。如果只按平均值分配,峰值时可能不够;如果按峰值长期预留,又会浪费大量 CPU。

这篇文章要解决的核心矛盾就是:怎样既保留 cpusets 带来的稳定性,又不必为偶发 burst 长期独占完整 CPU core?

三、从 Dedicated 到 Hybrid:混合核心分配模型

Uber 在 Odin 中引入的新模型叫 hybrid core allocation,可以理解为“混合核心分配”。它的基本思想是:一个 workload 不再只有专用核心,也可以额外获得一部分共享核心。

也就是说,CPU 分配被拆成两部分:

第一部分是 dedicated cores,也就是专用核心。它们为 workload 的稳定、基础负载提供保障。一个 workload 的常态 CPU 消耗,应主要由这部分承接。

第二部分是 shared cores,也就是共享核心。它们不归某一个 workload 独占,而是放在主机级别的共享池中。多个 workload 可以在需要 burst 的时候使用这些核心。

这样就形成了一个更灵活的模型:稳定负载靠 dedicated cores,突发负载靠 shared cores。

原文把新模型概括为几个要点:

第一,workload 可以同时获得专用核心和可选的共享核心。

第二,共享核心在每台 host 上形成一个 pool,并按照一定比例进行 over-allocation,也就是超额分配。这里的超额分配不是说真实 CPU core 变多了,而是说分配给各个 workload 的“共享 CPU 需求总和”可以大于这台机器上真实可用的共享核心数量。

第三,当多个 workload 同时争抢共享核心时,Linux 的cpu.shares会根据各自配置的权重动态分配 CPU 时间。

第四,垂直扩缩容器也需要升级:它不再只计算“这个 workload 需要多少 dedicated core”,而是要同时计算 dedicated cores 和 shared cores 的组合。(Uber)

这背后的思路其实很像“把资源分成确定性部分和弹性部分”。确定性部分保证服务的基本质量,弹性部分提升整体资源利用率。

四、为什么不是所有 CPU 都共享?

乍一看,既然共享可以提高利用率,那为什么不把所有 CPU core 都放进共享池?

原因在于,有状态服务往往不能只追求平均利用率,还必须控制尾延迟和抖动。如果所有 CPU 都共享,那么在高争抢时,关键 workload 可能被其他 workload 影响。对数据库、缓存、消息队列这类服务来说,这种不确定性很危险。

专用核心的价值在于提供确定性。只要 workload 的稳定负载被正确估算,它就能有一部分不会被别人抢走的 CPU。共享核心的价值在于提供弹性。只有当 workload 需要短时间多跑一点,或者主机上有空闲 CPU 时,共享核心才让系统从“死板分配”变成“可靠共享”。

所以 hybrid model 的重点不是简单共享,而是可靠共享。它不是把隔离取消掉,而是在隔离之上增加一层受控弹性。

五、cpu.shares:共享核心争抢时怎么分?

在 Linux cgroups 中,cpu.shares是一个相对权重。它不是硬限制,不代表一个容器最多只能用多少 CPU;它主要在 CPU 发生争抢时生效。谁的 shares 更高,谁在争抢时理论上能拿到更多 CPU 时间。

Uber 在实验中使用了cpuset.cpuscpu.shares,模拟不同共享核心配置在不同负载模式下的行为。实验观察到,在没有明显争抢,或者只有少量 workload 造成轻微争抢时,Linux scheduler 通常能在 CPU 之间比较好地平衡 workload,并满足预期的cpu.shares比例。(Uber)

但文章也提醒:当争抢非常严重时,实际 CPU 分配比例可能会明显偏离预期。也就是说,cpu.shares不是一个在任何极端情况下都严格精确的公平分配器。它更像是“相对优先级指导”,而不是强 SLA 保证。

Uber 最终使用的 shares 计算公式是:

cpu.shares = round((dedicated_CPUs + shared_CPUs_for_contention) × 100)

这里有几个细节值得注意。

dedicated_CPUs是整数,因为专用核心通常按完整 core 分配。

shared_CPUs_for_contention可以是小数,因为共享核心表达的是一种弹性需求或者争抢权重。比如一个 workload 不一定需要完整的 1 个共享 core,它可能只需要 0.5 个共享 core 的 burst 能力。

乘以 100 是为了把小数转换成整数,因为最终cpu.shares需要是整数。同时,100 这个因子也方便理解:可以把它看作“一个 CPU 的百分比单位”。

例如,一个容器有 2 个专用 CPU,另外希望在共享争抢中拥有 0.5 个 CPU 的权重,那么:

cpu.shares = round((2 + 0.5) × 100) = 250

这使得cpu.shares和“期望分配的 CPU 资源”之间保持直观对应。不是写一个难以理解的魔法数字,而是把 dedicated + shared 的总意图映射成 shares。

六、用一个例子理解专用核和共享核共存

可以想象一台机器上有多个 CPU core,两个容器 Foo 和 Bar 分别运行在不同的 cpuset 中。

Foo 可能有 CPU 0 和 CPU 1 作为专用核心,同时还能访问 CPU 2 和 CPU 3 组成的共享区域。Bar 可能有 CPU 4 和 CPU 5 作为专用核心,同时也能访问 CPU 2 和 CPU 3。这样一来,CPU 0、1 更像是 Foo 的基本盘,CPU 4、5 是 Bar 的基本盘,而 CPU 2、3 是它们都可以在需要时使用的共享池。

如果 Foo 突然有一波请求,它可以利用共享池获得额外 CPU。如果 Bar 此时也在 burst,二者就需要通过cpu.shares按权重争抢共享 CPU 时间。原文中的第一张图正是说明这种“多个 workload 容器同时拥有专用核和共享核”的 colocated 模型。(Uber)

这个模型适合什么场景?

适合那些大部分时间 CPU 使用相对平稳,但偶尔会 burst 的服务。比如某个存储服务平时只需要 4 个 core,但在 compaction、rebalance、流量峰值、故障转移时会短时间需要更多 CPU。过去为了峰值长期分配 6 个甚至 8 个 core,会浪费;现在可以分配 4 个专用 core,再给它一定共享 core 权重,让它在需要时从共享池借力。

七、NUMA:CPU 分配不能只看 core 数量

在多路服务器上,CPU 和内存不是简单的“全局均匀资源”。NUMA,也就是 Non-Uniform Memory Access,意思是不同 CPU 访问不同内存区域的成本并不相同。一个 CPU 访问本地 NUMA node 上的内存更快,访问远端 NUMA node 上的内存更慢。

因此,CPU 分配不能只看“给了几个 core”,还要看这些 core 在硬件拓扑上的位置。把一个 workload 的 core 随机分散到多个 socket 或多个 NUMA node 上,可能带来额外的缓存和内存访问成本。

Uber 在测试中观察到几个现象。

第一,Linux scheduler 通常会倾向于把进程调度到靠近其主要内存所在位置的 CPU 上。如果一个进程的大部分内存在某个 NUMA socket 上,那么在 cgroup 约束下,它通常也倾向于运行在同一 socket 的 CPU 上。不过,共享库等因素可能影响这个平衡。

第二,Linux 默认会从当前执行内存分配的 CPU 最近的 NUMA node 上分配内存。如果本地内存不足,再回退到其他 node。

第三,Linux 的 automatic NUMA balancing 可以把内存页迁移到更常访问它们的 NUMA node。Linux 内核文档也说明,numa_balancing可以启用基于 page fault 的自动 NUMA 内存平衡,把内存移动到经常访问它的节点上;但这种机制也有额外开销,并没有对所有 workload 都保证收益。(docs.kernel.org)

Uber 的结论不是“所有东西都要手动 NUMA pinning”,而是更克制:内核本身已经在优化本地内存访问,显式 NUMA 绑定不总是必要;内存迁移调优确实可做,但需要通过 benchmark 验证收益。最终策略是,专用核心尽量放在同一个 NUMA node 内,而共享核心池可以跨多个 NUMA node 分布。(Uber)

这个判断很工程化。很多时候,系统调优不是参数越多越好,而是要避免在没有证据的情况下过度干预内核。

八、Odin 在主机层面如何分配 CPU

在 Odin 的 hybrid model 中,CPU 分配是在 host 层面执行的。也就是说,每台机器上的 odin-agent 负责管理本机 CPU core 的分布。

对于共享核心,odin-agent 首先会尝试把它们均匀分布在所有 NUMA node 上。如果还需要更多共享核心,它会优先选择那些已经承载较多 dedicated CPU 负载的 node。这样做的目的,是避免把原本比较空闲的 node 也过度压上共享负载,从而尽量保持整台机器的资源分布平衡。

对于专用核心,odin-agent 会尽量把 dedicated cores 放在 workload 的 shared cores 所在的同一个 node 上。这样可以降低延迟,让计算更本地化。同时,它也会确保 dedicated cores 不会和 shared cores 重叠。如果首选 node 没有足够容量,就继续尝试其他 node,直到满足需求。

在真正执行分配之前,Odin 会先检查请求的 CPU 数量是否真的可以被满足。如果不能满足,会返回错误。不过系统中有一个 ensure loop,会持续重试,直到主机状态收敛到期望状态。(Uber)

这类设计在基础设施系统里很常见:不要假设一次操作一定成功,而是把资源状态建模成 desired state 和 actual state,然后不断 reconcile。

九、原地垂直扩缩容:为什么对有状态服务特别重要

文章中特别强调了一个能力:host-level in-place vertical scaling,也就是在主机层面进行原地垂直扩缩容。

所谓垂直扩缩容,是调整单个 workload 的 CPU 或内存资源;所谓原地,是不把 workload 迁移到其他机器上,而是在当前 host 上直接调整。

这对有状态服务非常重要。因为有状态 workload 往往依赖本地磁盘。如果要把它从一台机器迁移到另一台机器,就可能需要复制大量数据。这个过程不仅慢,而且会扰动服务,甚至影响可靠性。

如果可以在当前机器上直接调整 CPU allocation,就能减少整个 fleet 的 churn,也就是减少大规模迁移、重排、重启带来的扰动。虽然这种调整不是完全无感的,有些 workload 仍然需要重启才能识别新的 CPU limit,但即便如此,也比把它迁移到另一台机器要便宜得多。(Uber)

这也是这篇文章的一个重要工程背景:Uber 要解决的不是单个容器资源配置问题,而是在大规模有状态 fleet 中,如何用尽量小的扰动持续修正资源分配。

十、垂直扩缩容如何决定 dedicated 和 shared 的数量

新的 scaler 不再只看 workload 要多少 CPU,而是把 CPU 需求拆成两类:

平均 CPU 使用量,用来决定 dedicated cores。

围绕平均值的偏差,用来决定 shared cores。

这个逻辑非常关键。平均值代表稳定需求,应该由专用核心承接。偏差代表 burst 或波动,适合交给共享核心处理。

举个例子,一个 workload 长期平均使用 4 个 CPU,但经常会短时间冲到 5.5 个 CPU。如果按旧模型,可能要给它 6 个专用 core,才能覆盖峰值。但这样在大多数时间会浪费 2 个 core 左右。新模型可以给它 4 个 dedicated cores,再给它 1.5 个 shared cores 的争抢权重。这样在平稳时不浪费,在 burst 时又有机会获得额外 CPU。

当然,共享池本身还要做更复杂的计算。原文提到,每台 host 上所有 workload 被分配的 shared CPUs 总和,通常会大于 host 上真实可用的 shared CPUs。也就是说,共享部分是 over-allocated 的。

这听起来有风险,但关键在于:不是所有 workload 都会在同一时刻 spike。如果峰值之间相关性不强,超额分配就能显著提高资源利用率。如果多个 workload 恰好同时 spike,cpu.shares会按比例分配共享池中的 CPU 时间。虽然这种情况下某些 workload 可能拿不到全部期望的 burst CPU,但这类同时峰值足够少,系统可以承受。(Uber)

这其实就是基础设施资源池化的核心:用统计复用换利用率,但要通过隔离和权重控制风险。

十一、Kubernetes 缺了什么,Uber 如何补上

Uber 正在让 Odin 尽量向 cloud-native 模式和标准靠拢,但这里有一个关键缺口:Kubernetes 目前没有内建支持 hybrid core allocation。也就是说,Kubernetes 可以做一些 CPU 管理,也支持 cpuset 相关能力,但“一个 workload 同时拥有专用核心和共享核心,并在 host 级别进行共享池 over-allocation 与 shares 控制”并不是 Kubernetes 原生直接提供的能力。Uber 因此把自己的 cgroups 管理代码抽成了独立库,再封装成 Kubernetes CRI plugin,让 Odin 在拥抱 Kubernetes 的同时,不牺牲主机层面的资源效率。(Uber)

这里可以看到一个典型的大厂基础设施演进路径:不是简单地把内部系统完全替换成 Kubernetes,而是把内部已经验证过的关键能力拆出来,以插件、库或扩展的形式接入云原生生态。

这也解释了为什么很多大规模基础设施团队即便使用 Kubernetes,也仍然保留大量自研组件。Kubernetes 提供了通用抽象,但对特定业务、特定硬件、特定性能目标来说,仍然需要补齐更细粒度的调度和资源管理能力。

十二、当前方案的局限性

这套方案并不是终点。原文也明确提到了两个限制。

第一个限制是,当前所有 CPU allocation 决策都发生在 host 层面。host agent 只能看到本机资源状态,它不知道整个 fleet 上其他机器的全局情况。更理想的方式,是把一部分决策上移到 workload scheduler,让调度器具备跨 host 的全局视角。这样它不仅能决定“这台机器上怎么放 core”,还能决定“这个 workload 一开始应该放到哪台机器上”。

第二个限制是 CPU allocation fragmentation,也就是 CPU 分配碎片化。随着 workload 不断放置、迁移、扩缩容,主机上的可用 core 可能变得零散。某些 workload 需要连续或者拓扑更接近的 core,但碎片化后就不容易找到合适的组合。为了解决这个问题,odin-agent 未来可以周期性执行 defragmentation pass,对 CPU 分配进行整理。(Uber)

这两个限制都很现实。资源管理从来不是一次计算就结束的事情。大规模 fleet 会长期运行,workload 会不断变化,机器会故障,服务会扩缩容。资源布局如果不持续整理,迟早会产生碎片。

十三、这篇文章真正想解决的问题

如果只看表面,这篇文章讲的是 cpusets、cpu.shares、NUMA、vertical scaling、CRI plugin。但更深一层,它讲的是一个基础设施问题:

如何在“稳定性”和“利用率”之间做可靠折中。

纯 CPU quota 的模型灵活,但容易引入 throttling 和尾延迟问题。

纯 cpusets 专用核心模型稳定,但对 bursty workload 来说容易过度分配。

完全共享模型利用率高,但隔离性和可预测性不足。

Uber 的 hybrid model 试图把这三者重新组合起来:

用 dedicated cores 保证稳定负载。

用 shared cores 承接突发负载。

cpu.shares在争抢时提供相对公平。

用 NUMA-aware allocation 控制硬件拓扑带来的性能风险。

用 in-place vertical scaling 减少有状态 workload 迁移。

用 host-level agent 负责落地执行。

用后续 scheduler-level 改造弥补全局视角不足。

这不是一个单点优化,而是一套资源管理体系的演进。

十四、对工程实践的启发

第一,平均值不是容量规划的全部。

很多系统最容易犯的错误,就是只看平均 CPU 使用率。平均值可以描述长期趋势,但无法说明 burst 的形态。两个 workload 的平均 CPU 都是 4 core,其中一个可能非常平稳,另一个可能频繁在 2 到 8 之间波动。它们需要的资源策略显然不同。

第二,隔离和利用率不是非黑即白。

过去我们容易在两个极端之间摇摆:要么强隔离、低利用率;要么强共享、高不确定性。Uber 的方案提供了一个中间层:基础资源独占,弹性资源共享。这种模型在数据库、缓存、消息队列等有状态服务上尤其有意义。

第三,cpu.shares要理解成相对权重,而不是绝对保证。

在轻度争抢时,shares 可以比较好地表达预期比例。但在严重争抢时,实际效果可能偏离。因此设计共享池时不能只依赖 shares,还要控制 over-allocation ratio,理解 workload spike 的相关性,并观察真实生产表现。

第四,NUMA 不应该被忽略,但也不应该盲目手调。

NUMA-aware allocation 对低延迟服务很重要,但文章的态度不是“一切都手动绑定”,而是尊重内核已有优化,在需要的地方谨慎介入。特别是内存迁移和 NUMA balancing,必须结合 benchmark,而不是凭经验硬改参数。

第五,对有状态服务来说,迁移成本本身就是资源管理的一部分。

无状态服务可以更容易地杀掉重建、迁移、重新调度;有状态服务不行。数据复制、本地磁盘、缓存预热、leader 切换、复制延迟,这些都会让迁移变成昂贵操作。因此原地垂直扩缩容对有状态 fleet 很重要。

第六,Kubernetes 不是所有资源管理问题的终点。

Kubernetes 给了一个通用平台,但大规模生产系统中的 CPU 拓扑、NUMA、共享池、资源碎片、tail latency,并不会因为使用 Kubernetes 就自动消失。真正的工程能力往往在 Kubernetes 之下或旁边:CRI plugin、cgroups library、node agent、scheduler extension、custom controller,这些才是把通用平台变成生产级平台的关键。

十五、总结

Uber 这篇文章讲的不是一个简单的 CPU 参数调优,而是有状态容器平台资源管理的一次体系化升级。

最开始,Uber 为了避免 CPU quota 带来的 throttling,引入 cpusets,让 workload 绑定到专用 CPU core 上。这带来了更稳定的尾延迟,但也让资源分配变得更“硬”:专用 core 必须完整分配,burst 能力变弱,面对突发型 CPU workload 时容易过度预留。

Hybrid core allocation 的出现,就是为了补上这块短板。它把 CPU 分成专用部分和共享部分:专用核心保证稳定性能,共享核心承接突发需求。共享池通过 over-allocation 提高利用率,通过cpu.shares在争抢时分配相对权重,通过 NUMA-aware placement 降低硬件拓扑带来的延迟风险。

这套方案也还在演进中。当前决策主要发生在 host 层面,未来可以进一步上移到 scheduler,以获得跨主机的全局视角;同时,CPU 分配碎片化也需要通过周期性整理来缓解。

对基础设施工程来说,这篇文章的价值在于,它展示了资源管理从“静态分配”走向“动态、分层、可控共享”的过程。真正成熟的资源系统,不是简单追求 CPU 利用率最大化,而是在性能稳定性、成本效率、故障恢复和运维复杂度之间持续寻找平衡。

参考资料

  • Uber Engineering: Hybrid Core Allocation: From Overallocation to Reliable Sharing
  • Uber Engineering: Avoiding CPU Throttling in a Containerized Environment
  • Linux Kernel Documentation: Documentation for/proc/sys/kernel/

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

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

立即咨询