3. 任务模型与元数据设计

调度框架的核心不是“把任务跑起来”,而是“让框架准确理解一个任务”。任务模型就是框架与业务之间的契约:业务用统一结构描述“我要做什么、依赖谁、失败怎么办、最晚多久结束”,框架据此完成排队、触发、重试、超时和优先级裁决。如果每个任务都靠散落的字段和隐式约定表达,调度器就无法做统一决策,可观测性也无从谈起。本节围绕任务定义、参数、依赖、超时、重试、优先级六类元数据,设计一份统一任务描述规范。

任务定义:稳定标识与执行入口

任务定义回答“这是谁、由谁执行”。最小可用字段包括:taskId(全局唯一,建议由业务域+业务键+版本组成,如 order.settle.v2)、taskType(决定由哪个执行器处理)、handler(执行入口,如类名或函数名)、version(任务定义版本,用于灰度与回滚)、owner(责任人,排障时定位)。taskId 必须稳定,不能把运行时间戳拼进去,否则重试和幂等判断会失效。执行入口建议只声明“做什么”,不携带运行期数据,运行期数据全部走参数。

参数:区分静态配置与运行期输入

参数最容易设计混乱。建议拆成两层:staticParams 是任务定义期就确定的配置(如目标库、批大小),随任务定义版本化;runtimeParams 是每次触发时传入的输入(如业务日期、订单号区间),随任务实例存储。两者合并后传给执行器,但来源和生命周期不同。参数需声明类型与是否必填,例如 {"bizDate":"date","shardTotal":"int","shardIndex":"int"}。参数校验应在任务入队前完成,避免执行到一半才发现缺参。参数值建议可序列化为 JSON,便于日志与重放。

依赖:用 DAG 表达先后与数据就绪

依赖决定任务何时“可以跑”。常见两类:时间依赖(如每天 02:00 触发)和数据依赖(上游任务成功且产出就绪)。统一规范中,依赖应表达为有向无环图(DAG)的边:dependsOn: ["order.extract.v2", "user.profile.v2"],并声明依赖类型 depType: SUCCESS | DATA_READY。框架在调度前检查所有前置节点状态,只有全部满足才把任务置为 READY。要防止环依赖,入图时做拓扑校验。依赖失败时,下游应进入 BLOCKED 而非直接失败,便于上游修复后自动恢复。

超时与重试:失败处理的两种语义

超时是“最长允许多久”,重试是“失败后是否再试”。二者必须分开建模,否则会出现“重试把超时时间累加”的误判。建议字段:timeoutMs(单次执行上限)、retry.maxAttempts(含首次的总尝试次数)、retry.backoff(如 FIXED 或 EXPONENTIAL)、retry.intervalMs、retry.retryOn(可重试错误码白名单)。例如 maxAttempts=3, backoff=EXPONENTIAL, intervalMs=1000 表示第 1 次失败后等 1s,第 2 次失败后等 2s,第 3 次仍失败则终态 FAILED。超时触发时,框架应中断执行并标记 TIMEOUT,是否重试由 retryOn 决定。注意:非幂等任务必须显式声明 idempotent=false,框架据此禁止自动重试或要求业务提供去重键。

优先级:在资源有限时决定谁先跑

优先级不是“越大越先”,而是调度器在队列积压时的排序依据。建议用整数 priority,约定数值越小优先级越高(与 Linux nice 一致,避免歧义),范围 0~9,0 为最高。调度器按 (priority, enqueueTime) 排序,同优先级先入先出。优先级只影响排队顺序,不应改变依赖判定:未就绪的高优先级任务仍需等待上游。对抢占式场景,可增加 preemptible 标记,允许高优先级任务占用低优先级任务的执行槽。

统一任务描述规范:元数据分层与调度判定 任务定义 TaskDef taskId: order.settle.v2 taskType: BATCH_SETTLE handler: SettleHandler.run version: 2 owner: trade-team 稳定标识 · 执行入口 · 版本 参数 Params staticParams: {batchSize:500} runtimeParams: {bizDate:2024-06-01} schema: bizDate:date(必填) shardIndex:int shardTotal:int 定义期配置 vs 运行期输入 依赖 DAG dependsOn: [order.extract.v2, user.profile.v2] depType: SUCCESS | DATA_READY 入图时拓扑校验,禁止环依赖 上游未就绪 → BLOCKED 超时与重试 timeoutMs: 30000(单次上限) retry.maxAttempts: 3 retry.backoff: EXPONENTIAL retry.intervalMs: 1000 idempotent=false 时禁止自动重试 优先级 Priority priority: 0~9(0 最高) 排序键: (priority, enqueueTime) preemptible: true | false 不改变依赖判定,仅影响排队 同优先级 FIFO 调度判定顺序 ① 参数校验通过? ② 依赖全部就绪? ③ 资源槽可用? ④ 按优先级出队执行 任一不满足 → 保持等待/阻塞 任务实例状态流转 CREATED READY RUNNING RETRYING FAILED SUCCEEDED RETRYING 达到 maxAttempts 后转 FAILED;超时直接标记 TIMEOUT

统一任务描述规范:一份可落地的 YAML 示例

把上述字段收敛成一份声明式规范,业务只需填写 YAML,框架负责解析、校验、入图与调度。以下为教学示例,参数与取值均为合理假设,用于说明字段语义:

taskId: order.settle.v2
taskType: BATCH_SETTLE
handler: com.demo.settle.SettleHandler
version: 2
owner: trade-team

staticParams:
  batchSize: 500
  targetDb: settle_db

runtimeParams:
  bizDate: { type: date, required: true }
  shardIndex: { type: int, required: true }
  shardTotal: { type: int, required: true }

dependsOn:
  - taskId: order.extract.v2
    depType: SUCCESS
  - taskId: user.profile.v2
    depType: DATA_READY

timeoutMs: 30000
retry:
  maxAttempts: 3
  backoff: EXPONENTIAL
  intervalMs: 1000
  retryOn: [TIMEOUT, CONN_RESET]
idempotent: true
priority: 2
preemptible: false

这份规范的关键在于“字段各司其职”:taskId 与 version 保证可追溯,staticParams 与 runtimeParams 分离了配置与输入,dependsOn 把先后关系显式化,timeoutMs 与 retry 分别约束“多久”和“几次”,priority 只在排队时生效。框架解析后应做四类校验:必填字段、参数类型、依赖无环、重试与幂等一致性(idempotent=false 时若 maxAttempts>1 应告警或拒绝)。

设计建议:把 taskId 当作数据库主键来对待,一旦发布不再修改;需要变更语义时递增 version,旧版本保留一段时间以支持回滚。参数 schema 建议随任务定义一起版本化,避免“定义没变、参数含义变了”的隐性破坏。
常见误区:把超时时间写成“重试总时长”,导致每次重试都重新计时;或把优先级当成依赖的替代品,认为高优先级任务可以跳过上游。正确做法是:超时只约束单次执行,重试次数与退避单独声明;优先级只影响就绪任务的出队顺序,不改变依赖判定。

当任务模型稳定后,调度器、执行器、日志与告警都能围绕同一份元数据工作:调度器读依赖与优先级,执行器读参数与超时,重试组件读 retry 策略,监控按 taskId 与 version 聚合指标。统一规范的价值正在于此——它让“任务”从一段散落的代码,变成一个可被框架理解、可被系统治理的一等对象。