3. 资源模型与抽象:CPU、内存、IO、网络资源的建模方法,资源池与资源切片

调度系统要管好资源,第一步就是得把资源说清楚。你想想看,CPU、内存、IO、网络,这些东西物理上长得完全不一样,调度器怎么统一看待它们?

我个人习惯,先把资源抽象成三个层次:资源描述、资源模型、资源分配单元。描述是告诉系统「你有什么」,模型是告诉系统「你怎么用」,分配单元是告诉系统「我怎么切」。这三层搞明白了,后面的调度策略才有落脚点。

3.1 CPU 资源的建模

CPU 是最容易建模的资源,也是最容易出问题的。很多人以为 CPU 就是「几核几线程」,但调度器眼里,CPU 有三个关键维度:计算能力、时间片、拓扑结构。

核心思路:CPU 资源模型 = 核数 × 频率 × 权重。但实际调度时,我们更关心的是「这个任务能占用多少比例的 CPU 时间」。

我在项目中遇到过一个问题:两个任务各分配 0.5 核,但跑起来一个快一个慢。为什么?因为 CPU 的时间片分配是离散的,0.5 核意味着每 100ms 里只能跑 50ms。如果任务有频繁的上下文切换,实际吞吐会大打折扣。

所以,我建议用 CPU 份额(Shares) 而不是核数来做模型。比如 CFS 调度器里的权重值,1024 代表一个核,2048 代表两个核。这样调度器只需要按比例分配时间片,不用管底层是超线程还是物理核。

模型维度 描述 常见单位 避坑点
计算能力 单位时间能执行的指令数 MHz、vCPU 超线程的 vCPU 只有物理核 60%-80% 能力
时间片 任务每次获得 CPU 的连续运行时长 ms、μs 时间片太短 → 上下文切换开销大
拓扑 CPU 之间的缓存共享、NUMA 节点关系 socket、core、thread 跨 NUMA 访问内存延迟高 1.5-2 倍

我的经验:CPU 建模时,一定要把 NUMA 信息暴露给调度器。我曾经在一个 4 socket 的机器上,因为没考虑 NUMA,任务被调度到远端内存,性能直接掉了 40%。从那以后,我所有的 CPU 模型都带 topology 字段。

3.2 内存资源的建模

内存建模比 CPU 复杂,因为它有容量和带宽两个维度。容量不够会 OOM,带宽不够会拖慢计算。

我习惯把内存模型拆成三层:

  • 总量模型:物理内存总大小,减去系统预留,剩下的才是可分配量。
  • 分配模型:任务申请多少,实际使用多少,峰值多少。很多调度器只看申请量,不看实际使用量,结果资源利用率极低。
  • 带宽模型:内存带宽是共享资源,多个任务同时读写会互相争抢。这个在数据库、流处理场景特别明显。

举个例子,Kubernetes 里内存的 request 和 limit 就是两种模型。request 是预留量,limit 是上限。但说实话,这种模型太粗糙了——它没考虑内存带宽。我曾经在压测时发现,两个任务各自只用了 4GB 内存,但一起跑时吞吐下降 30%,查了半天才发现是内存带宽打满了。

注意:内存资源建模时,千万别只盯着容量。带宽、NUMA 亲和性、大页支持,这些都会影响实际性能。尤其是大页(HugePages),用了之后 TLB 缺失减少,内存访问能快 10%-20%。

3.3 IO 资源的建模

IO 是最难建模的资源,没有之一。为什么?因为 IO 的延迟方差太大了。同样是读磁盘,顺序读和随机读的吞吐能差 100 倍。

我一般把 IO 模型分成两类:

  • 块设备 IO:磁盘、SSD、NVMe。关键指标是 IOPS、吞吐量、延迟。
  • 文件系统 IO:在块设备之上,还有元数据操作、缓存命中率等。

调度系统里,IO 资源通常用 权重 或 上限 来控制。比如 cgroup 的 blkio 子系统,可以设置读写带宽上限,也可以设置权重比例。

但这里有个坑:IO 的隔离性很差。一个任务疯狂写盘,可能会污染整个块设备的缓存,导致其他任务的读请求变慢。我曾经遇到过,一个日志任务把 Page Cache 全刷掉了,结果数据库查询延迟从 2ms 飙到 200ms。

我的建议:IO 建模时,一定要区分「延迟敏感型」和「吞吐型」。延迟敏感的任务(如数据库)需要预留 IO 带宽,吞吐型的任务(如日志)可以共享剩余带宽。最好用 IO 优先级队列来做隔离。

3.4 网络资源的建模

网络资源建模,说白了就是管好带宽、连接数、延迟。但实际调度时,网络是最容易被忽略的。

我见过太多案例:CPU 和内存都够,但服务就是慢,最后发现是网卡队列打满了。或者多个任务共用同一个出口带宽,互相争抢导致丢包重传。

网络模型我推荐用 虚拟网络设备 加 流量整形 的方式:

  • 每个任务或租户分配一个虚拟网卡(veth、tap 等)
  • 用 TC(Traffic Control)或 HTB(Hierarchical Bucket)做带宽限制
  • 用 SR-IOV 或 DPDK 做高性能网络隔离

举个例子,在云计算平台里,每个虚拟机都有独立的带宽上限。但如果你不做网络资源建模,一个「吵闹的邻居」就能把整个物理机的网络打爆。

资源类型 关键指标 建模方法 隔离手段
网络带宽 bps、pps 令牌桶、漏桶 TC、HTB、QoS
连接数 并发连接、新建连接速率 连接池、连接跟踪 iptables、conntrack
延迟 RTT、P99 延迟 延迟分布模型 优先级队列、RFS

3.5 资源池与资源切片

有了资源模型,下一步就是怎么组织这些资源。我习惯用资源池来管理同类资源,用资源切片来做分配。

资源池就是把同类型的资源聚在一起。比如 CPU 池、内存池、GPU 池。池化之后,调度器可以按需分配,不用关心底层物理拓扑。

但纯池化有个问题:资源争抢。所以需要资源切片——把资源池切成更小的单元,每个单元有独立的配额和隔离。

资源切片有三种常见粒度:

  • 静态切片:提前划分好,比如把 32 核切成 4 个 8 核的切片。简单,但浪费。
  • 动态切片:按需分配,用完归还。灵活,但需要好的调度策略。
  • 弹性切片:在动态切片基础上,允许超卖。利用率高,但有风险。

我个人比较喜欢动态切片加轻度超卖。比如 CPU 池里,每个任务保证 1 核,但允许它用到 2 核(如果其他任务没用)。这样既保证了隔离,又提高了利用率。

避坑指南:我曾经在一个项目里用了纯静态切片,结果有的切片跑满了,有的切片空着,整体利用率不到 50%。后来改成动态切片,利用率直接翻倍。但要注意,动态切片一定要有资源回收机制,不然会出现「资源泄漏」。

3.6 资源模型的核心逻辑图

下面这张图展示了从物理资源到调度单元的整体抽象流程。你可以看到,CPU、内存、IO、网络各自建模后,汇聚到资源池,再通过切片机制变成调度器能直接使用的分配单元。

资源模型与抽象核心逻辑 CPU 物理核 内存 DIMM 磁盘 / SSD 网卡 CPU 模型 内存模型 IO 模型 网络模型 统一资源池(CPU + 内存 + IO + 网络) 切片 A(静态) 切片 B(动态) 切片 C(弹性) 调度器:按切片分配任务 物理资源 → 抽象模型 → 资源池 → 资源切片 → 调度分配,每一层都在做「抽象」和「隔离」

3.7 小结:建模的核心原则

讲了这么多,其实就三个原则:

  1. 抽象要够:把物理资源变成调度器能理解的数字模型。CPU 用份额,内存用容量+带宽,IO 用权重,网络用令牌桶。
  2. 隔离要硬:资源切片必须做到硬隔离,不能一个任务把整池资源拖垮。cgroup、namespace、TC 这些手段要用上。
  3. 弹性要活:静态切片浪费,纯动态切片有风险。我建议用动态为主、超卖为辅,再加一个资源回收的兜底机制。