118 lines
7.7 KiB
Markdown
118 lines
7.7 KiB
Markdown
# 任务优先级与公平派发
|
||
|
||
状态:P3 已实现。优先级使用持久化任务字段,节点公平派发控制领取机会;任务执行权、应用配额与两类并发许可仍由 MySQL 事务决定。平台不增加缓存或消息队列服务。
|
||
|
||
## 1. 任务优先级
|
||
|
||
在 [POST /v1/tasks](tasks.md) 或 [计划版本的 definition.task](schedules.md) 中指定:
|
||
|
||
| 字段 | 类型 / 默认 | 含义 |
|
||
| --- | --- | --- |
|
||
| priority | 可选 JSON 整数,默认 0,范围 0–9 | 受理时固定的任务优先级;每级最多取得 30 秒排序优势 |
|
||
|
||
负数、超过 9、浮点表示、字符串、bool 和 null 均返回 HTTP 400 / INVALID_ARGUMENT。Go SDK 对应 `Priority int16`。零值省略以保留旧请求摘要;任务详情、摘要和 Worker 派发在非零时返回 priority。查询到零或省略都表示普通优先级。
|
||
|
||
权限、请求头、提交结果、受理幂等、取消和截止规则沿用任务接口,没有单独的优先级管理权限或运行中改级接口。相同受理键改变 priority 返回 409 IDEMPOTENCY_CONFLICT。安全重试保持冻结优先级,下一次 available_at 改变时重新计算排序时间;计划触发继承对应不可变版本里的 priority。
|
||
|
||
```json
|
||
{
|
||
"queue_id":"11111111111111111111111111111111",
|
||
"handler":"object.prepare",
|
||
"handler_version":"v1",
|
||
"payload_version":"v1",
|
||
"payload":{"object_ref":"objects/example","operation_ref":"prepare-001"},
|
||
"priority":9
|
||
}
|
||
```
|
||
|
||
成功响应仍是任务接口的完整信封,首次 201、重复受理 200;任务对象包含 `"priority":9`。提交确认不代表已执行。
|
||
|
||
## 2. 排序与等待补偿
|
||
|
||
同一队列、与当前 Worker 版本和能力匹配且满足门控/租约/许可条件的到期任务,按下面三个键升序排列:
|
||
|
||
1. `dispatch_at = available_at - priority × 30 秒`。
|
||
2. available_at。
|
||
3. task_id(只用于确定性打破同时间相等,不表示业务顺序)。
|
||
|
||
dispatch_at 为数据库生成的只读字段,并有相应派发索引;它不对外提供执行授权。真正的最早执行时间始终是 available_at,派发仍检查截止、次数、门控、能力和资源键占用。
|
||
|
||
| 两条已就绪任务 | 先选择的任务 |
|
||
| --- | --- |
|
||
| A:0 级、5 秒前到期;B:9 级、刚到期 | B,获得 270 秒排序优势 |
|
||
| A:0 级、271 秒前到期;B:9 级、刚到期 | A,旧任务的等待已超过最大优势 |
|
||
| A:0 级、已到期;B:9 级、1 小时后才到期 | A,B 不具备领取资格 |
|
||
| 同一优先级、不同 available_at | 更早到期者 |
|
||
|
||
由此限制新高优先级工作插队的时间优势,避免纯粹按优先级排序造成持续饥饿。**270 秒不是最大排队时长**:旧积压、队列/应用额度、兼容 Worker、暂停、共享资源和每个任务执行时长仍影响等待。已被预占或正在运行的任务不被抢占;没有全局 FIFO 或跨队列优先级比较。
|
||
|
||
## 3. 节点内应用与队列轮转
|
||
|
||
Worker 连接不再各自直接并发发起一批数据库领取。节点采用有界领取池:
|
||
|
||
| 约束 | 当前开发基线 |
|
||
| --- | --- |
|
||
| 同时执行的领取 SQL 操作 | 每节点最多 4 个 |
|
||
| 一个应用同时在途的领取 | 每节点最多 1 个,其他应用可以继续领取 |
|
||
| 待处理和在途领取请求合计 | 每节点最多 128 个 |
|
||
| 每个连接未完成领取请求 | 最多 1 个 |
|
||
| 选择顺序 | 活跃应用轮转 → 该应用的队列轮转 → 队列内连接请求顺序 |
|
||
| 单个连接派发节奏 | 每个扫描周期最多 4 次领取,当前周期 1 秒 |
|
||
|
||
应用身份来自服务端验证过的命名空间归属,客户端不能通过增加命名空间或伪造应用字段分散公平组。一个应用增加队列或 Worker 数量,也不会在所有应用都持续有待处理请求时得到额外轮次。某个应用没有请求或已有在途领取时,其他应用继续使用空闲池容量。
|
||
|
||
同命名空间、队列、精确处理器能力集合及资源键支持声明的空领取结果,在本节点合并一个扫描周期,缓存最多 128 项。不同能力不共用空结果。缓存过期重新查询;任务新到达或容量释放可能多等待一个周期。这只抑制重复探测,不能据此授权任务或更改持久化状态。
|
||
|
||
领取在后台有界执行,WS 控制循环继续接收心跳、Start、续租、Finish 和排空。关闭节点时取消待领取操作并等待在途收尾;已提交但尚未发送的预占仍由数据库租约恢复。任务处理器运行在业务 Worker,完全不占这个 SQL 领取池。
|
||
|
||
这是**节点内领取机会的公平性**。各节点分别轮转,没有宣称跨节点完全相等吞吐、全局权重或严格最大延迟。跨节点执行并发和开始额度仍由已实现的 [应用配额](application-resources.md) 与 [队列控制](queues.md) 原子保证。连接接入总上限仍为每节点 128;该机制不提供每应用连接预留,也不代替请求层限流或生产容量规划。
|
||
|
||
## 4. SDK 完整场景
|
||
|
||
源码:[priority/main.go](../../examples/priority/main.go)。创建专用队列,先按 0、9、4 的顺序提交三条任务,再启动容量为 1 的自动 Worker;三条任务在近同时到期的条件下按 9、4、0 执行,每条只有一次尝试。
|
||
|
||
从仓库根目录准备专用开发数据库,设置 ORBIT_MYSQL_DSN 和 `ORBIT_HTTP_ADDR=127.0.0.1:8080` 后:
|
||
|
||
```sh
|
||
go run ./cmd/orbit-migrate up
|
||
go run ./cmd/orbit-admin bootstrap --application priority-example --namespace default \
|
||
--environment test --queue initial --worker-access --output /tmp/orbit-priority.credentials.json
|
||
go run ./cmd/orbit
|
||
```
|
||
|
||
凭据需要全部队列范围的 queues:manage、tasks:submit/read、workers:connect/consume。另一个终端执行:
|
||
|
||
```sh
|
||
cd examples
|
||
ORBIT_URL=http://127.0.0.1:8080 \
|
||
ORBIT_CREDENTIALS_FILE=/tmp/orbit-priority.credentials.json go run ./priority
|
||
```
|
||
|
||
预期输出:
|
||
|
||
```text
|
||
submitted_priorities=0,9,4 executed_priorities=9,4,0
|
||
completed=3 attempts_per_task=1 worker_drained=true
|
||
```
|
||
|
||
示例返回输入对象及精确大整数,不访问外部业务资源。总任务截止为 20 秒,整个场景 25 秒;每次创建独立队列/处理器与任务,正常结束排空,历史保留。异常非零退出并有界收尾。这个短场景演示优先级加速,等待补偿的 271 秒边界由真实数据库夹具验证,不让读者实际等待几分钟。
|
||
|
||
调用片段(上下文、客户端和队列已初始化):
|
||
|
||
```go
|
||
accepted, err := api.SubmitTask(ctx, "prepare-operation-001", orbit.SubmitTaskRequest{
|
||
QueueID: queueID, Handler: "object.prepare", HandlerVersion: "v1", PayloadVersion: "v1",
|
||
Payload: json.RawMessage(`{"object_ref":"objects/example"}`), Priority: 9,
|
||
})
|
||
```
|
||
|
||
在处理器中通过 `e.Task.Priority` 读取冻结值;调度顺序由平台确定,业务 Worker 不应绕过 Start 或自行重排已授权任务。
|
||
|
||
## 5. 升级与验收边界
|
||
|
||
停止旧写入节点,应用 000012,再启新版服务。历史任务优先级为 0,生成排序时间等于原 available_at;既有任务、索引和历史保留,新增索引单独建立。优先级限制附 CHECK,generated 字段与 Go 模型均有中文说明,数据库最终仍无外键。
|
||
|
||
新版 SDK 开始提交非零 priority 前,先升级服务端;旧服务严格拒绝未知输入字段。公开 Worker Schema 已扩展可选 priority,其他语言的严格验证器需同步升级;默认 0 的派发报文保持原形状。
|
||
|
||
单节点公平领取及真实 MySQL/WSS 故障隔离已纳入回归。多节点不均匀连接分布、混合负载下各应用延迟、SQL 锁等待、海量未来任务扫描及峰值容量在 P6 验收,当前参数不能作为百万租户容量承诺。完整记录见 [本阶段报告](../reports/phase-3-priority-fairness.md)。
|