Uber 如何让 CPU 从“专用独占”走向“可靠共享”:Odin 的混合核心分配实践

发布时间:2026/7/8 6:10:47

Uber 如何让 CPU 从“专用独占”走向“可靠共享”:Odin 的混合核心分配实践 本文是对 Hybrid Core Allocation: From Overallocation to Reliable Sharing 的整理与翻译。原文发布于 2026 年 4 月 21 日来自 Uber Engineering作者是 Alexandr Sudakov 和 Ivan Shibitov。文章讨论的是 Uber 内部有状态容器编排平台 Odin 在 CPU 资源分配上的一次演进从过去更偏“专用核心”的模型进一步走向“专用核心 共享核心”的混合核心分配模型。其目标不是单纯把 CPU 利用率拉高而是在延迟、可靠性、资源利用率和成本之间找到一个更稳定的平衡点。(Uber)内容结构概览本文会按下面的脉络展开为什么 Uber 之前要在 Odin 中引入 cpusets纯专用核心模型带来了什么新问题混合核心分配模型是什么Dedicated Cores Shared CoresLinuxcpu.shares在共享核心池中如何发挥作用为什么严重争抢时cpu.shares并不总是完全公平NUMA 对 CPU 与内存亲和性的影响Odin 在主机层面如何分配专用核和共享核垂直扩缩容如何决定给一个 workload 分多少核Kubernetes 在这个场景下缺了什么Uber 如何补上当前方案的局限性对基础设施工程实践的启发一、背景从 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.cpus和cpu.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 权重让它在需要时从共享池借力。七、NUMACPU 分配不能只看 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 恰好同时 spikecpu.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 SharingUber Engineering: Avoiding CPU Throttling in a Containerized EnvironmentLinux Kernel Documentation: Documentation for/proc/sys/kernel/

相关新闻