已合并
docs(api): config_refresh 明确代价与低峰期建议; 补齐 config-plane-api 三轮特性同步 #545
王明琦创建于 7 天前
docs(api): config_refresh 明确代价与低峰期建议; 补齐 config-plane-api 三轮特性同步 #545
已合并
共 1 个文件变更+41-34
| @@ -82,15 +82,16 @@ | |||
| 82 | 82 | ||
| 83 | Claw Manager **全量配置下发**(场景 M):一次请求同时携带 `containers` / `templates` / `scopes` 三个列表,**快照式全量替换**——upsert 本批全部条目 + 删除 DB 中已消失的条目(容器以本批为集 GC)。旧 `kind/op` 增量协议与无 `containers` 键的 legacy 内联载荷均 400 拒绝(三段式契约独占)。 | 83 | Claw Manager **全量配置下发**(场景 M):一次请求同时携带 `containers` / `templates` / `scopes` 三个列表,**快照式全量替换**——upsert 本批全部条目 + 删除 DB 中已消失的条目(容器以本批为集 GC)。旧 `kind/op` 增量协议与无 `containers` 键的 legacy 内联载荷均 400 拒绝(三段式契约独占)。 |
| 84 | 84 | ||
| 85 | -处理编排(全程持 `lock:config_sync` 串行化,忙 → 409):锁外校验(确定性 400、零副作用)→ 日落中间态检查 → **单事务写 DB**(全有或全无)→ 重建路由快照(原子 SET,`route` 匹配立即用新配置)→ 逐 scope 推 RM 池参数 + pod_spec(**eager 预热**:autoscale 下一拍即预热 `min_idle_pods`,从未被请求过的 scope 也生效)→ A 类软摘除老版本 Pod(不再接新会话,存量会话不受影响)→ 被删/禁用 scope 推 `min_idle=0` 自然排空。 | 85 | +处理编排(全程持 `lock:config_sync` 串行化,忙 → 409):锁外校验(确定性 400、零副作用)→ 日落中间态检查 → **单事务写 DB**(全有或全无)→ 重建路由快照(原子 SET,`route` 匹配立即用新配置)→ 逐 scope 推 RM 池参数 + pod_spec(**eager 预热**:autoscale 下一拍即预热 `min_idle_pods`,从未被请求过的 scope 也生效)→ A 类软摘除老版本 Pod(不再接新会话,存量会话在**排空窗口**内继续原 Pod,窗口 = 下发时刻 + `session_ttl`)→ 被删/过期 scope 推 `min_idle=0` 并日落排空。 |
| 86 | 86 | ||
| 87 | 变更分类(服务端逐字段 diff 自动判定,无需调用方声明): | 87 | 变更分类(服务端逐字段 diff 自动判定,无需调用方声明): |
| 88 | 88 | ||
| 89 | | 类别 | 触发 | 生效方式 | | 89 | | 类别 | 触发 | 生效方式 | |
| 90 | |---|---|---| | 90 | |---|---|---| |
| 91 | -| **A 类**(deploy 字段变更) | 镜像/端口/env/挂载/sidecar 等容器规格变化(`deploy_ver` 指纹不等) | 老 Pod 软摘除退出候选集,按新规格重建;存量会话自然跑完 | | 91 | +| **A 类**(deploy 字段变更) | 镜像/端口/env/挂载/sidecar 等容器规格变化(`deploy_ver` 指纹不等) | 老 Pod 软摘除退出候选集,按新规格重建;存量会话排空窗口(下发 + `session_ttl`)内继续原 Pod,过窗回收 | |
| 92 | | **B 类**(策略字段变更) | `scope_concurrency`/`pod_concurrency`/`session_ttl`/`pod_ttl`/`min_idle_pods` | 快照覆盖 + RM 池参数重推,**立即生效**,不动存量 Pod | | 92 | | **B 类**(策略字段变更) | `scope_concurrency`/`pod_concurrency`/`session_ttl`/`pod_ttl`/`min_idle_pods` | 快照覆盖 + RM 池参数重推,**立即生效**,不动存量 Pod | |
| 93 | -| **删除** | 本批载荷未携带的模板/scope/容器 | DB 删行;被删 scope 停预热自然排空,存量会话到期止 | | 93 | +| **路由性排除** | scope 的 `routing_rules` 变化 / `expires_at` 缩短到已过 | 同款日落排空:新会话立即走新规则,存量会话窗口内保持原池 | |
| 94 | +| **删除** | 本批载荷未携带的模板/scope/容器 | DB 删行;被删 scope 停保温 + 日落排空(**「禁用」= 删除**,两级 `enabled` 字段已删) | | ||
| 94 | 95 | ||
| 95 | 幂等性:同载荷重放收敛(`affected_scopes` 为空数组)。 | 96 | 幂等性:同载荷重放收敛(`affected_scopes` 为空数组)。 |
| 96 | 97 | ||
| @@ -147,7 +148,8 @@ Envelope 外层见 §0.2;`rawdata` 为三段式配置快照: | |||
| 147 | | `session_ttl` | int(秒) | 否 | 60 | 会话保活超时(下界 1) | | 148 | | `session_ttl` | int(秒) | 否 | 60 | 会话保活超时(下界 1) | |
| 148 | | `pod_ttl` | int(秒) | 否 | 300 | idle Pod 至 reclaim 的等待(下界 1) | | 149 | | `pod_ttl` | int(秒) | 否 | 300 | idle Pod 至 reclaim 的等待(下界 1) | |
| 149 | | `min_idle_pods` | int | 否 | 0 | 该 scope 最少热备 Pod 数(≥0) | | 150 | | `min_idle_pods` | int | 否 | 0 | 该 scope 最少热备 Pod 数(≥0) | |
| 150 | -| `enabled` | bool | 否 | true | 模板禁用则路由不解析、不预热 | | 151 | + |
| 152 | +> `enabled` 字段已删(2026-09):模板生命周期 = 存在性,「禁用模板」= 从载荷删除(引用它的 scope 必须同批删除/改引用,否则 400)。残留 `enabled:false` 键 → 该模板视为缺席(被引用 → 400);`enabled:true` 残留静默忽略。 | ||
| 151 | | `data` | object | 否 | `{}` | 透传扩展字段 | | 153 | | `data` | object | 否 | `{}` | 透传扩展字段 | |
| 152 | 154 | ||
| 153 | > 模板级 K8s 派生字段用 K8s 拼写(`nodeName`),snake 双形态并存 → 400(防静默二义)。 | 155 | > 模板级 K8s 派生字段用 K8s 拼写(`nodeName`),snake 双形态并存 → 400(防静默二义)。 |
| @@ -160,8 +162,9 @@ Envelope 外层见 §0.2;`rawdata` 为三段式配置快照: | |||
| 160 | | `index` | int | 是 | — | 匹配序号;请求按 `(index 升序, scope_id 升序)` **first-fit** 首个命中即止(bool 拒绝) | | 162 | | `index` | int | 是 | — | 匹配序号;请求按 `(index 升序, scope_id 升序)` **first-fit** 首个命中即止(bool 拒绝) | |
| 161 | | `template_id` | str | 是 | — | 引用模板,**必须在本批模板列表内**;多个 scope 可引用同一模板 | | 163 | | `template_id` | str | 是 | — | 引用模板,**必须在本批模板列表内**;多个 scope 可引用同一模板 | |
| 162 | | `routing_rules` | str | 否 | `""` | 布尔表达式字符串;**null/空串/纯空白 = 通配兜底 scope**(命中一切);语法见下 | | 164 | | `routing_rules` | str | 否 | `""` | 布尔表达式字符串;**null/空串/纯空白 = 通配兜底 scope**(命中一切);语法见下 | |
| 163 | -| `enabled` | bool | 否 | true | 禁用则不参与路由匹配与 eager 预热(仍落库/进快照) | | 165 | +| `expires_at` | str? | 否 | `null` | 可选过期时间(ISO-8601);到点后视为不生效;`null` = 永不过期。**生命周期唯一状态字段**(`enabled` 已删,「禁用」= 从载荷删除) | |
| 164 | -| `expires_at` | str? | 否 | `null` | 可选过期时间(ISO-8601);到点后视为不生效;`null` = 永不过期 | | 166 | + |
| 167 | +> 残留 `enabled:false` 的 scope → 视为缺席剔除 + WARNING(等价于删除该 scope,服务端日落排空);`enabled:true` 残留静默忽略。 | ||
| 165 | 168 | ||
| 166 | `routing_rules` 表达式语法: | 169 | `routing_rules` 表达式语法: |
| 167 | 170 | ||
| @@ -483,17 +486,35 @@ JSON | |||
| 483 | 486 | ||
| 484 | 每 scope 三步(顺序红线:bump 先于摘除,任何中途失败形态都收敛于"老 Pod 暂时继续接新流量",重试即收敛): | 487 | 每 scope 三步(顺序红线:bump 先于摘除,任何中途失败形态都收敛于"老 Pod 暂时继续接新流量",重试即收敛): |
| 485 | 488 | ||
| 486 | -1. **代次 +1**(`generation` HINCRBY 唯一写点)——老代 Pod 即刻退出候选集,**不接新会话**;存量会话亲和不受影响,自然跑完; | 489 | +1. **代次 +1**(`generation` HINCRBY 唯一写点)——老代 Pod 即刻退出候选集,**不接新会话**;存量会话在排空窗口内继续原 Pod 服务(亲和不受影响),见 §2.2; |
| 487 | -2. 重推池参数 + pod_spec(值未变,确保 RM 缓存/预热就绪); | 490 | +2. 重推池参数 + pod_spec(值未变,确保 RM 缓存/预热就绪;过期 scope 只日落不保温,min_idle=0); |
| 488 | -3. 候选集全量软摘除——reclaim 按 `pod_ttl` 回收老代空 Pod,autoscale 按缓存的 pod_spec(即存量配置)重建。 | 491 | +3. 候选集全量软摘除——老代 Pod 保留至**排空截止 = 刷新时刻 + `session_ttl`**(优雅排空窗口)后回收,autoscale 按缓存的 pod_spec(即存量配置)重建新代暖 Pod。 |
| 489 | 492 | ||
| 490 | 与 `config_sync` 共用串行化锁:上一次操作未完成 → 409 `CONFIG_SYNC_BUSY`。非幂等但收敛:每次调用 = 一轮全量日落重建,**成功后勿自动重试**。 | 493 | 与 `config_sync` 共用串行化锁:上一次操作未完成 → 409 `CONFIG_SYNC_BUSY`。非幂等但收敛:每次调用 = 一轮全量日落重建,**成功后勿自动重试**。 |
| 491 | 494 | ||
| 492 | -### 2.2 入参 | 495 | +### 2.2 代价与操作建议(务必低峰期执行) |
| 496 | + | ||
| 497 | +本端点是配置平面中**影响面最大的操作**——一次调用对**全部 scope 的全部 Pod**(含从未被请求的 `min_idle` 暖 Pod)做整体换代,代价如下,建议在业务低峰期执行: | ||
| 498 | + | ||
| 499 | +| 代价 | 说明 | | ||
| 500 | +|---|---| | ||
| 501 | +| **全量冷启动** | 每个 scope 的 Pod 池整体重建,重建完成前新会话承担全额冷启动等待(真镜像实测 p50≈12s,max≈16s/Pod) | | ||
| 502 | +| **瞬时容量上浮** | 排空纪元期间每个 scope 的容量上限临时 `max_pods+1`(surge 余量,给补位 Pod 让槽),瞬时物理 Pod 数可达 Σ(max_pods+1);排空收尾自动回落 | | ||
| 503 | +| **存量会话有界保留,过窗硬切** | 老代 Pod 保留至 刷新时刻 + `session_ttl`;窗口内存量会话**不打断**、继续原 Pod 原配置;**窗口截止仍未结束的活跃会话将被硬切重放置**——上下文不迁移、重放置再付一次全额冷启动 | | ||
| 504 | +| **配置面阻塞** | 排空窗口内再触发 `config_sync`/`config_refresh` 均可能 409 `CONFIG_SYNC_BUSY`,等待窗从"约一拍对账"拉长为 **≤ `session_ttl` + tick**——期间无法下发任何配置变更 | | ||
| 505 | + | ||
| 506 | +操作建议: | ||
| 507 | + | ||
| 508 | +1. **业务低峰期执行**,避开活跃会话高峰(排空窗口与 `session_ttl` 同量级,活跃会话越多、过窗硬切面越大); | ||
| 509 | +2. 配置变更**优先走 `config_sync`**(A/B 类按需日落,影响面只限受影响模板引用的 scope);`config_refresh` 仅用于漂移自愈、同 tag 镜像强拉、运行时异常兜底重建; | ||
| 510 | +3. **攒批一次执行、勿连续刷新**——多代日落会叠加占位,延长闸门阻塞窗; | ||
| 511 | +4. 执行后经 `GET /visualization/scopes` 确认各 scope 收敛(`phase=active`、`generation` 已更新、Pod 数回落 `max_pods`)再进行其他操作。 | ||
| 512 | + | ||
| 513 | +### 2.3 入参 | ||
| 493 | 514 | ||
| 494 | Envelope 外层见 §0.2;**`rawdata` 必须为空对象 `{}`**(非空 → 400 `VALIDATION`)。 | 515 | Envelope 外层见 §0.2;**`rawdata` 必须为空对象 `{}`**(非空 → 400 `VALIDATION`)。 |
| 495 | 516 | ||
| 496 | -### 2.3 返回值(`rawdata`) | 517 | +### 2.4 返回值(`rawdata`) |
| 497 | 518 | ||
| 498 | | 字段 | 类型 | 说明 | | 519 | | 字段 | 类型 | 说明 | |
| 499 | |---|---|---| | 520 | |---|---|---| |
| @@ -502,7 +523,7 @@ Envelope 外层见 §0.2;**`rawdata` 必须为空对象 `{}`**(非空 → 400 `V | |||
| 502 | | `pods_sunset` | int | 被软摘除的 Pod 总数(仅统计 SM 候选集内成员;从未被 route 过的 RM 暖 Pod 不计数,但同样被代次日落) | | 523 | | `pods_sunset` | int | 被软摘除的 Pod 总数(仅统计 SM 候选集内成员;从未被 route 过的 RM 暖 Pod 不计数,但同样被代次日落) | |
| 503 | | `generations` | object | `{scope_id: 新代次号}` 逐 scope 返回(bump 后的 `generation` 值,单调递增) | | 524 | | `generations` | object | `{scope_id: 新代次号}` 逐 scope 返回(bump 后的 `generation` 值,单调递增) | |
| 504 | 525 | ||
| 505 | -### 2.4 curl 示例 | 526 | +### 2.5 curl 示例 |
| 506 | 527 | ||
| 507 | ```bash | 528 | ```bash |
| 508 | curl -s -X POST "http://127.0.0.1:8091/api/session/config_refresh" \ | 529 | curl -s -X POST "http://127.0.0.1:8091/api/session/config_refresh" \ |
| @@ -519,7 +540,7 @@ curl -s -X POST "http://127.0.0.1:8091/api/session/config_refresh" \ | |||
| 519 | }' | 540 | }' |
| 520 | ``` | 541 | ``` |
| 521 | 542 | ||
| 522 | -### 2.5 返回值示例 | 543 | +### 2.6 返回值示例 |
| 523 | 544 | ||
| 524 | 成功(2 个 scope、1 个在候选集的 Pod 被日落,两 scope 代次均升到 1): | 545 | 成功(2 个 scope、1 个在候选集的 Pod 被日落,两 scope 代次均升到 1): |
| 525 | 546 | ||
| @@ -679,15 +700,13 @@ curl -s "http://127.0.0.1:8091/visualization/overview" | |||
| 679 | | 字段 | 类型 | 说明 | | 700 | | 字段 | 类型 | 说明 | |
| 680 | |---|---|---| | 701 | |---|---|---| |
| 681 | | `scope_id` | str | scope 标识 | | 702 | | `scope_id` | str | scope 标识 | |
| 682 | -| `phase` | str | 生效分类:`active`(正常)/ `disabled`(scope 禁用/过期或模板禁用/缺失)/ `missing_rm_cfg`(快照生效但 RM 无 config 键)/ `orphan_rm`(RM 有键但不在快照) | | 703 | +| `phase` | str | 生效分类:`active`(正常)/ `disabled`(scope 过期或引用模板悬挂)/ `missing_rm_cfg`(快照生效但 RM 无 config 键)/ `orphan_rm`(RM 有键但不在快照) | |
| 683 | | `template_id` | str? | 引用模板 | | 704 | | `template_id` | str? | 引用模板 | |
| 684 | -| `scope_enabled` | bool? | scope 自身 enabled | | ||
| 685 | | `expires_at` | str? | 过期时间(ISO-8601) | | 705 | | `expires_at` | str? | 过期时间(ISO-8601) | |
| 686 | | `pods` / `idle` / `deploying` | int | Pod 总数 / 空闲数 / 部署中数 | | 706 | | `pods` / `idle` / `deploying` | int | Pod 总数 / 空闲数 / 部署中数 | |
| 687 | -| `session_count` / `waiters` | int | 活跃会话数 / 等待队列长度 | | 707 | +| `session_count` | int | 活跃会话数 | |
| 688 | | `max_pods` / `min_idle_pods` | int | Pod 上限(派生 ⌈sc/pc⌉)/ 最小热备 | | 708 | | `max_pods` / `min_idle_pods` | int | Pod 上限(派生 ⌈sc/pc⌉)/ 最小热备 | |
| 689 | | `scope_concurrency` / `pod_concurrency` / `session_ttl` | int? | 模板策略字段(模板缺失时为 `null`) | | 709 | | `scope_concurrency` / `pod_concurrency` / `session_ttl` | int? | 模板策略字段(模板缺失时为 `null`) | |
| 690 | -| `max_waiters` | int? | 等待队列上限(2 × scope_concurrency) | | ||
| 691 | 710 | ||
| 692 | **curl 与返回示例**: | 711 | **curl 与返回示例**: |
| 693 | 712 | ||
| @@ -702,37 +721,31 @@ curl -s "http://127.0.0.1:8091/visualization/scopes?limit=100" | |||
| 702 | "scope_id": "scope-default", | 721 | "scope_id": "scope-default", |
| 703 | "phase": "active", | 722 | "phase": "active", |
| 704 | "template_id": "tpl-standard", | 723 | "template_id": "tpl-standard", |
| 705 | - "scope_enabled": true, | ||
| 706 | "expires_at": null, | 724 | "expires_at": null, |
| 707 | "pods": 1, | 725 | "pods": 1, |
| 708 | "idle": 0, | 726 | "idle": 0, |
| 709 | "deploying": 0, | 727 | "deploying": 0, |
| 710 | "session_count": 1, | 728 | "session_count": 1, |
| 711 | - "waiters": 0, | ||
| 712 | "max_pods": 3, | 729 | "max_pods": 3, |
| 713 | "min_idle_pods": 1, | 730 | "min_idle_pods": 1, |
| 714 | "scope_concurrency": 6, | 731 | "scope_concurrency": 6, |
| 715 | "pod_concurrency": 2, | 732 | "pod_concurrency": 2, |
| 716 | - "session_ttl": 120, | 733 | + "session_ttl": 120 |
| 717 | - "max_waiters": 12 | ||
| 718 | }, | 734 | }, |
| 719 | { | 735 | { |
| 720 | "scope_id": "scope-vip", | 736 | "scope_id": "scope-vip", |
| 721 | "phase": "active", | 737 | "phase": "active", |
| 722 | "template_id": "tpl-vip", | 738 | "template_id": "tpl-vip", |
| 723 | - "scope_enabled": true, | ||
| 724 | "expires_at": null, | 739 | "expires_at": null, |
| 725 | "pods": 0, | 740 | "pods": 0, |
| 726 | "idle": 0, | 741 | "idle": 0, |
| 727 | "deploying": 0, | 742 | "deploying": 0, |
| 728 | "session_count": 0, | 743 | "session_count": 0, |
| 729 | - "waiters": 0, | ||
| 730 | "max_pods": 2, | 744 | "max_pods": 2, |
| 731 | "min_idle_pods": 0, | 745 | "min_idle_pods": 0, |
| 732 | "scope_concurrency": 3, | 746 | "scope_concurrency": 3, |
| 733 | "pod_concurrency": 2, | 747 | "pod_concurrency": 2, |
| 734 | - "session_ttl": 300, | 748 | + "session_ttl": 300 |
| 735 | - "max_waiters": 6 | ||
| 736 | } | 749 | } |
| 737 | ], | 750 | ], |
| 738 | "total": 2, | 751 | "total": 2, |
| @@ -776,9 +789,9 @@ curl -s "http://127.0.0.1:8091/visualization/scopes?limit=100" | |||
| 776 | 789 | ||
| 777 | | 字段 | 类型 | 说明 | | 790 | | 字段 | 类型 | 说明 | |
| 778 | |---|---|---| | 791 | |---|---|---| |
| 779 | -| `sm.waiters` / `session_count` / `candidate_pods` | int/int/list | 等待队列 / 活跃会话 / 候选集 Pod id | | 792 | +| `sm.session_count` / `candidate_pods` | int/list | 活跃会话数 / 候选集 Pod id | |
| 780 | -| `sm.capacity` | object? | 生效容量闸门(快照无此 scope 的孤儿为 `null`):`template_id`、`template_enabled`、`scope_enabled`、`expires_at`、`scope_concurrency`、`pod_concurrency`、`session_ttl`、`pod_ttl`、`min_idle_pods`、`max_pods`、`max_waiters`(2×sc)、`session_utilization`/`waiter_utilization`(0–1)、`route_budget_sec`(= `scope_full_timeout + ready_timeout + 10`) | | 793 | +| `sm.capacity` | object? | 生效容量闸门(快照无此 scope 的孤儿为 `null`):`template_id`、`expires_at`、`scope_concurrency`、`pod_concurrency`、`session_ttl`、`pod_ttl`、`min_idle_pods`、`max_pods`(派生 ⌈sc/pc⌉)、`session_utilization`(0–1)、`route_budget_sec`(= `ready_timeout + 10`) | |
| 781 | -| `sm.routing` | object? | 快照内路由定义:`scope_id`、`index`、`template_id`、`routing_rules`、`enabled`、`expires_at`(不在快照为 `null`) | | 794 | +| `sm.routing` | object? | 快照内路由定义:`scope_id`、`index`、`template_id`、`routing_rules`、`expires_at`、`data`(不在快照为 `null`) | |
| 782 | 795 | ||
| 783 | **curl 与返回示例**: | 796 | **curl 与返回示例**: |
| 784 | 797 | ||
| @@ -825,13 +838,10 @@ curl -s "http://127.0.0.1:8091/visualization/scope?scope_id=scope-default&limit= | |||
| 825 | "truncated": false | 838 | "truncated": false |
| 826 | }, | 839 | }, |
| 827 | "sm": { | 840 | "sm": { |
| 828 | - "waiters": 0, | ||
| 829 | "session_count": 1, | 841 | "session_count": 1, |
| 830 | "candidate_pods": [], | 842 | "candidate_pods": [], |
| 831 | "capacity": { | 843 | "capacity": { |
| 832 | "template_id": "tpl-standard", | 844 | "template_id": "tpl-standard", |
| 833 | - "template_enabled": true, | ||
| 834 | - "scope_enabled": true, | ||
| 835 | "expires_at": null, | 845 | "expires_at": null, |
| 836 | "scope_concurrency": 6, | 846 | "scope_concurrency": 6, |
| 837 | "pod_concurrency": 2, | 847 | "pod_concurrency": 2, |
| @@ -839,9 +849,7 @@ curl -s "http://127.0.0.1:8091/visualization/scope?scope_id=scope-default&limit= | |||
| 839 | "pod_ttl": 600, | 849 | "pod_ttl": 600, |
| 840 | "min_idle_pods": 1, | 850 | "min_idle_pods": 1, |
| 841 | "max_pods": 3, | 851 | "max_pods": 3, |
| 842 | - "max_waiters": 12, | ||
| 843 | "session_utilization": 0.167, | 852 | "session_utilization": 0.167, |
| 844 | - "waiter_utilization": 0.0, | ||
| 845 | "route_budget_sec": 340.0 | 853 | "route_budget_sec": 340.0 |
| 846 | }, | 854 | }, |
| 847 | "routing": { | 855 | "routing": { |
| @@ -907,7 +915,6 @@ curl -s "http://127.0.0.1:8091/visualization/session?session_id=sess-7f3a2b" | |||
| 907 | "ttl_remaining_s": 119.9, | 915 | "ttl_remaining_s": 119.9, |
| 908 | "scope": { | 916 | "scope": { |
| 909 | "scope_id": "scope-default", | 917 | "scope_id": "scope-default", |
| 910 | - "waiters": 0, | ||
| 911 | "session_count": 1, | 918 | "session_count": 1, |
| 912 | "candidate_pods": [] | 919 | "candidate_pods": [] |
| 913 | }, | 920 | }, |