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、网络各自建模后,汇聚到资源池,再通过切片机制变成调度器能直接使用的分配单元。
3.7 小结:建模的核心原则
讲了这么多,其实就三个原则:
- 抽象要够:把物理资源变成调度器能理解的数字模型。CPU 用份额,内存用容量+带宽,IO 用权重,网络用令牌桶。
- 隔离要硬:资源切片必须做到硬隔离,不能一个任务把整池资源拖垮。cgroup、namespace、TC 这些手段要用上。
- 弹性要活:静态切片浪费,纯动态切片有风险。我建议用动态为主、超卖为辅,再加一个资源回收的兜底机制。