资源抽象与建模:从混乱到有序的第一步

说实话,我见过太多团队在资源管控上栽跟头。不是技术不行,而是从一开始就没把「资源到底是什么」说清楚。你想想看,一个叫 CPU 的资源,在 A 系统里是核数,在 B 系统里是时间片,到了 C 系统又变成了容器配额——这怎么统一管?

所以这一章,我们就来聊聊怎么给资源「建模」。说白了,就是给资源一个身份证,让它走到哪里都能被认出来。

资源类型定义:先分清楚你家有几口人

我习惯把资源分成两大类:基础资源和组合资源。基础资源就是那些不能再拆的原子单位,组合资源则是多个基础资源拼起来的逻辑单元。

资源大类 典型例子 特点
计算资源 CPU 核、GPU 卡、NPU 芯片 可抢占或独占,有量化单位
存储资源 内存、磁盘、SSD、对象存储桶 容量 + IOPS 双维度描述
网络资源 带宽、连接数、IP 地址 共享型资源,容易成为瓶颈
组合资源 容器组、虚拟机、任务实例 由多种基础资源按模板组合
我的经验:别一开始就把类型分得太细。我在一个项目中曾经定义了 30 多种资源类型,结果维护成本比管控收益还高。建议先覆盖 80% 的场景,留 20% 的扩展接口。

资源属性建模:给资源画一张完整的像

定义好类型之后,下一步就是描述这个资源长什么样。我一般把属性分成三组:标识属性、容量属性和状态属性。

  • 标识属性:资源 ID、名称、类型、所属集群。这些是查户口用的。
  • 容量属性:总量、已用量、可用量、单位。这些是算账用的。
  • 状态属性:健康状态、分配状态、锁定状态。这些是看脸色用的。

举个例子,一个 GPU 资源的属性模型大概长这样:

{
  "resourceId": "gpu-01a2b3c4",
  "type": "GPU",
  "model": "NVIDIA A100",
  "capacity": {
    "total": 80,       // GB 显存
    "used": 32,
    "available": 48
  },
  "status": {
    "health": "healthy",
    "allocated": false,
    "locked": false
  },
  "tags": {
    "zone": "az-1",
    "owner": "ml-team"
  }
}
避坑指南:我曾经遇到过团队把「可用量」直接等于「总量减已用量」,结果忽略了预留资源。后来加了 reserved 字段才把账算清楚。记住,可用量 = 总量 - 已用量 - 预留量。

资源生命周期模型:从生到死都要管

资源不是一成不变的。它从创建到销毁,中间会经历好几个阶段。我习惯用状态机来描述这个过程。

资源生命周期状态机 空闲 已分配 使用中 已释放 故障 维护中 状态说明: 空闲 → 已分配:调度决策 已分配 → 使用中:实际挂载 使用中 → 已释放:回收完成 故障/维护 → 空闲:恢复后

每个状态转换都对应一个操作:

  • 空闲 → 已分配:调度器决定把资源给谁用
  • 已分配 → 使用中:任务真正开始跑起来了
  • 使用中 → 已释放:任务结束,资源归还
  • 使用中 → 故障:硬件坏了或者任务异常
  • 故障 → 维护中:运维介入修复
  • 维护中 → 空闲:修复完成,重新可用

核心原则:资源在任何时刻只能处于一个状态。状态转换必须通过原子操作完成,不能出现「半分配」或「半释放」的中间态。这是保证资源不泄露的关键。

统一资源描述语言(URDL)设计

好了,现在我们有类型、有属性、有生命周期。但问题是——不同系统之间怎么交换这些信息?总不能每个系统都写一套自己的 JSON 格式吧?

所以我们需要一种统一资源描述语言,简称 URDL。说白了,就是给资源定一个通用的「说话方式」。

URDL 的核心设计原则有三条:

  1. 自描述:看到一段 URDL,不用查文档就能知道它描述的是什么资源
  2. 可扩展:允许自定义属性,但不能破坏基础结构
  3. 版本化:每个资源描述都带版本号,方便兼容

一个典型的 URDL 描述长这样:

urdl:1.0
resource gpu-01a2b3c4 {
  type: "compute.gpu"
  provider: "nvidia"
  model: "A100-80GB"

  capacity {
    cores: 6912
    memory: 81920  // MB
    bandwidth: 1555 // GB/s
  }

  status: "idle"
  location: "dc-east-1/rack-12/slot-4"

  constraints {
    exclusive: true
    max_allocations: 1
  }

  metadata {
    created: "2024-06-15T08:00:00Z"
    firmware: "520.61.05"
  }
}
我个人的习惯:URDL 的语法不要搞得太复杂。能用键值对解决的问题,就别引入嵌套表达式。我在一个项目中看到有人用 URDL 写了一个图灵完备的表达式引擎——结果没人敢用。

URDL 的序列化与反序列化

光有语法还不够,还得能把它变成机器能理解的数据结构。我一般会做两层处理:

层级 作用 实现方式
词法分析 把 URDL 文本拆成 流 正则 + 状态机
语法分析 把 流转成抽象语法树 递归下降解析器
语义校验 检查字段合法性、类型匹配 Schema 校验引擎
对象映射 AST 转成内存中的资源对象 反射 / 代码生成

解析器核心代码(伪代码)示意:

class UrdlParser:
    def parse(self, text: str) -> Resource:
        tokens = self.tokenize(text)
        ast = self.build_ast(tokens)
        self.validate(ast)
        return self.to_resource(ast)

    def tokenize(self, text):
        # 按 URDL 语法规则切分
        # 返回 [_TYPE, value] 列表
        pass

    def build_ast(self, tokens):
        # 递归下降构建语法树
        pass
我曾经踩过的坑:一开始我把 URDL 解析器写得特别「智能」,支持自动类型推断和隐式转换。结果一个字段写错了类型,解析器默默把它转成了另一个类型,导致资源调度出了大问题。后来我强制要求所有字段必须显式声明类型,宁可多写几行,也不让机器猜。

小结

资源抽象与建模,是整个统一管控体系的基石。没有这一步,后面所有的调度、编排、监控都是空中楼阁。这一章我们讲了:

  • 怎么给资源分类型——基础资源和组合资源
  • 怎么给资源建属性——标识、容量、状态三件套
  • 怎么管资源的生命周期——状态机驱动
  • 怎么用 URDL 统一描述——自描述、可扩展、版本化