补充 BgFilter 单实例同步调度方案

新增同步内部 HTTP 原地等待版架构方案,明确并发、超时、熔断与降级边界

补充外部生成 Worker 化方案和共享决策日志中的受限资源调度约定

更新文档目录并关联 BgFilter 专题方案
This commit is contained in:
2026-07-21 08:09:48 +00:00
parent 55f71e13fc
commit a51b625101
4 changed files with 453 additions and 1 deletions
+1
View File
@@ -27,6 +27,7 @@
### 后端与公开数据
- [外部生成 Worker 化方案](./technical/【后端架构】外部生成Worker化方案-2026-06-03.md)
- [BgFilter 受限资源调度方案(同步内部 HTTP 原地等待版)](./technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md)
- [统一公开作品 Read Model 设计](./technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md)
- [外部 OpenAPI 与 API Key 接入方案](./【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md)
- [SpacetimeDB 连接池取消安全](./【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md)
@@ -16,6 +16,18 @@
---
## 2026-07-21 BgFilter 首版采用单实例同步内部 HTTP 与父流程原地等待
- 背景:角色动画在单个 `external_generation_job` 内通过 `buffer_unordered(frame_count)` 可并发发射最多 `48` 次 BgFilter 请求;限制父 worker 并发不能限制单个父 job 内的实际 BgFilter 并发。父 job checkpoint / continuation 和 SpacetimeDB 持久子任务都会扩大父状态机、attempt、计费、恢复和清理改动,而当前 BgFilter 成功结果本来就是 HTTP 图片二进制。
- 决策:父 future 保持原调用栈、lease 和 attempt,等待期间继续占用通用 worker 槽并由现有 heartbeat 续租;所有调用统一同步请求唯一 `bgfilter-worker` 的内部 loopback HTTP。输入只传 OSS object key、参数和剩余预算,成功直接返回经过校验的图片二进制。子 worker 使用有界 admission `Q` 和进程内 `Semaphore(N)`,负责最多两次顺序 provider attempt、flat 进程级熔断和失败审计;父流程继续负责 flat 降级、complex 失败、Alpha / 尺寸恢复、动画 finalizer、最终 OSS、业务写回、计费和父终态。
- 超时边界:父 job 总预算仍为普通 `900s` / 长任务 `1800s`,并保留现有 `60s` 终态写回窗口;内部 RPC 总预算包含等待 semaphore、两次 attempt、结果校验和二进制返回,子 worker 的单次 provider timeout 默认 `180s`。动画删除旧的 `2000ms × frame_count` 单次 timeout 增量。父侧不得在内部 timeout / 断连后重试整次 RPC。
- 故障边界:首版不新增 `bgfilter_task_group``bgfilter_request_task`、raw OSS、checkpoint、continuation、数据库 capacity slot、共享熔断或 QPS token bucket。父或子进程崩溃、RPC 丢失时不查询、不恢复结果;父 job 沿用现有 lease / `max_attempts=1` 失败退款语义。动画首版保持所有已提交帧 collect / drain,不增加跨帧取消组。
- 影响范围:后续实现涉及 `api-server` 内部 HTTP client、专用 `bgfilter-worker` listener / process role、并发与超时配置、部署和运维观测;不修改 SpacetimeDB schema、父 job schema、用户任务 DTO、任务列表或收费归属。
- 验证方式:`48` 帧并发进入父 future 时,健康唯一子 worker 进程持有的 BgFilter HTTP future 峰值不得超过生产显式配置的 `N`,且 `queued + running` 不超过 `Q`;覆盖 flat / complex 降级矩阵、内部 deadline、断连后已启动请求排空、动画全帧 drain、External v1 / inline 旁路扫描、二进制大小 / MIME / 尺寸门禁、单实例部署、DDD、编码和 diff 门禁。
- 关联文档:`docs/technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md``docs/technical/【后端架构】外部生成Worker化方案-2026-06-03.md``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
---
## 2026-07-20 角色动作抠图前禁止透明 padding
- 背景:图片画布角色动作此前在 BgFilter 前复用最终帧 finalizer,把 FFmpeg 抽帧先转成目标尺寸 RGBA 画布并用透明黑像素补边;透明区域进入 BgFilter、阿里云和本地键色共同读取的 OSS 源帧后,会干扰主体边缘判断并降低抠图质量。
@@ -0,0 +1,437 @@
# BgFilter 受限资源调度方案(同步内部 HTTP 原地等待版)
更新时间:`2026-07-21`
状态:`待实施`
> 本文替代此前讨论的“SpacetimeDB 持久子任务 + raw 结果 OSS”以及更早的“父 job checkpoint / continuation”方案。首版改为父流程在原调用栈内同步等待唯一 `bgfilter-worker` 的内部 HTTP 响应。当前代码仍由各调用方直接请求 BgFilter,本文描述目标实现。
## 1. 决策摘要
| 决策项 | 首版结论 |
| --- | --- |
| 调度单位 | 一次逻辑 BgFilter 调用;角色动画为单帧 |
| 父流程 | 保持原 future、调用栈、lease 和 `attempt`,同步等待内部 HTTP |
| 通用 worker 槽 | 等待期间继续占用;父 heartbeat 继续运行 |
| 输入 | 只传私有 OSS `objectKey`、BgFilter 参数、剩余预算和有界审计关联;不传源图字节或签名 URL |
| 成功输出 | 内部 HTTP body 直接返回图片二进制;不使用 Base64,不先写 raw OSS |
| BgFilter worker | 首版只运行一个内部 HTTP worker 实例 |
| 并发 | 进程内 `Semaphore(N)`,并增加有界 admission 上限 `Q` |
| 重试 | 子 worker 对一次逻辑调用最多做两次顺序 provider attempt;父侧不重试整次内部 RPC |
| 超时 | 父侧管理 job / request 总预算和内部 RPC 总预算;子 worker 管理排队及单次 provider `180s` 上限 |
| flat 熔断 | 迁到唯一子 worker 的进程内状态;连续失败达到阈值后暂时跳过 BgFilter,complex 完全不参与 |
| 业务语义 | 父流程继续负责 Alpha / 尺寸恢复、flat fallback、最终 OSS、画布写回、计费和父终态 |
| 动画失败 | 首版保持当前“所有已提交帧都等待并排空”语义,不新增跨帧取消组 |
| 崩溃恢复 | 不查询、不恢复 BgFilter 结果;父 job 沿用现有 lease、失败和退款语义 |
| 数据模型 | 不新增 SpacetimeDB 表,不修改 `external_generation_job` schema |
首版明确不实现:
- `bgfilter_task_group``bgfilter_request_task``bgfilter_capacity_slot`
- 持久队列、claim、lease、reaper、subscription 或 waiter registry
- raw 中间结果 OSS、raw TTL 或 raw 持久化预留;
- 父流程 checkpoint、`waiting_bgfilter`、continuation、requeue 或恢复不计 attempt 状态机;
- 多实例 BgFilter worker、共享持久熔断或 QPS token bucket
- 内部 RPC 失败后偷偷回退为父进程直连 BgFilter。
## 2. 背景与目标
### 2.1 当前问题
角色动画当前使用 `buffer_unordered(frame_count.max(1))`。单个父 job 会让 `32 / 40 / 48` 个帧 future 同时进入 BgFilter 调用,因此限制 `external-generation-worker` 的父 job 并发不能限制实际 BgFilter 请求并发。
编辑器 queue job 固定 `max_attempts = 1`。普通 requeue 会改变 attempt、失败和退款语义;checkpoint / continuation 又会扩大父状态机和计费恢复改动。父流程既然可以接受继续占用 worker 槽,首版无需为 BgFilter 建第二套持久任务系统。
当前 BgFilter 成功结果本来就是 HTTP 图片二进制,调用方读取后再由父流程做最终处理和 OSS 持久化。因此让专用 worker 通过内部 HTTP 直接返回二进制,最接近现有数据流。
### 2.2 目标
- 所有现役 flat / complex 调用统一经过唯一内部 `bgfilter-worker`
- `48` 帧可同时提交内部请求,但健康唯一 worker 持有的 BgFilter HTTP future 不超过 `N`
- 父流程不重建,不改变父 job schema、attempt、计费和用户可见任务。
- 保留现有 flat 两次尝试后“阿里云通用抠图 → 本地键色”、complex 两次失败直接失败的语义。
- External v1、inline 和 queue 路径复用同一个低层 client,不能绕过限流。
- 不把源图片、签名 URL、Token 或图片 Base64 写入数据库、JSON、审计 payload 或日志。
### 2.3 非目标
- 不释放等待中的通用 worker 槽。
- 不保证父进程、子 worker 或内部连接崩溃后的结果恢复。
- 不绝对限制客户端 timeout 后仍可能在 BgFilter 服务端继续的远端计算。
- 不做跨 job 公平调度;首版有界等待采用 FIFO。
- 不限制阿里云抠图、本地键色、OSS 或其它 provider 的并发。
- 不改变前端 DTO、External OpenAPI、任务列表或收费归属。
## 3. 总体架构
```mermaid
flowchart LR
P["父流程:queue job / inline / External v1"]
O["已持久化的私有 OSS 源对象"]
C["内部 HTTP client"]
W["唯一 bgfilter-worker\n有界 admission(Q) + Semaphore(N)"]
B["BgFilter provider\n最多两次顺序 attempt"]
F["父流程原有后处理\nfallback / finalizer / 最终 OSS / 业务写回"]
P --> O
P --> C
C -->|"objectKey + 参数 + requestBudgetMs"| W
W --> B
B --> W
W -->|"图片二进制或类型化 JSON 错误"| C
C --> P
P --> F
```
职责边界:
- 父流程负责源对象已持久化、owner 校验、请求预算、flat fallback、Alpha / 尺寸恢复、动画 finalizer、最终 OSS / `asset_object` / 画布写回、计费和父终态。
- `bgfilter-worker` 负责内部协议校验、OSS 签名、并发与排队上限、BgFilter 协议、两次顺序尝试、flat 熔断、provider 失败审计和结果图片校验。
- SpacetimeDB 不参与本次内部调度;不新增表、reducer、procedure、facade 或生成 bindings。
`bgfilter-worker` 从实现形态看是只监听内部地址的同步 worker service,不是队列 consumer。父 worker 调另一个 worker 在这里是允许的:父进程明确选择保留调用栈和槽位,因此同步内部 HTTP 正是首版的最小交接方式。
## 4. 内部 HTTP 契约
### 4.1 请求
首版新增内部接口:
```http
POST /internal/bgfilter/v1/remove-background
Content-Type: application/json
Authorization: Bearer <internal-token>
```
示例:
```json
{
"requestId": "bgfilter-call-uuid",
"sourceObjectKey": "generated-character-drafts/editor/source.png",
"backgroundMode": "flat",
"screenColor": "#00ff00",
"segModel": "birefnet",
"crossCheck": true,
"requestBudgetMs": 300000,
"auditContext": {
"userId": "bounded-internal-id",
"profileId": null,
"requestId": "parent-request-id"
}
}
```
约束:
- 当前部署只有一个配置内私有 OSS bucket,因此请求只传 `sourceObjectKey`,子 worker 从自身 OSS 配置取 bucket 并生成短期签名 URL。
- 如果未来确实支持多个 bucket,新增字段也必须由服务端 allowlist 校验;不能接受调用方提供任意下载 URL。
- `backgroundMode` 只允许 `flat / complex``segModel` 继续沿用当前 `birefnet / anime-seg` allowlistcomplex 固定使用当前参数组合。
- `screenColor` 只对 flat 必填;complex 不得误接 flat 参数或熔断。
- `requestBudgetMs` 是从子 worker 收到请求开始计算的相对预算,不是跨机器绝对时间。
- JSON body 设置很小的固定上限;源图字节不进入该 JSON。
签名 URL 必须在取得 provider permit 后、每次 attempt 前生成,避免排队期间过期。签名 URL 只存在于子 worker 内存和发往 BgFilter 的请求中。
### 4.2 成功响应
成功直接返回经过大小、MIME 和像素尺寸校验的图片字节:
```http
HTTP/1.1 200 OK
Content-Type: image/png
<raw image bytes>
```
`image/png` 是常见响应示例。为保持当前行为,首版可以返回实际受支持的 `image/png``image/webp``image/jpeg`,但必须保证响应头与实际解码类型一致;不使用 JSON、Data URL 或 Base64 包装,也不为传输先落 raw OSS。
子 worker 必须完整读取并校验 provider body 后才向父侧返回成功,这样 provider body 中途断开时仍可在预算内执行第二次 attempt。父侧收到二进制后继续执行一次独立校验,不能只信任内部响应头。
### 4.3 错误响应
失败返回有界 JSON,稳定错误码只保留:
- `provider_exhausted`:两次真实 provider attempt 都失败;
- `circuit_open`:flat 熔断已打开,未发送 provider 请求;
- `deadline_exceeded`:排队、provider 或响应阶段预算耗尽,使用 `phase = queue | provider | response`
- `overloaded``queued + running` 已达到 `Q`
- `cancelled`:调用方已取消且尚未开始新的 provider attempt
- `invalid_request`:内部契约不合法;
- `unauthorized`:内部 Token 缺失或不匹配;
- `invalid_result`provider 回图超限、MIME / 魔数不匹配或无法安全解码;
- `internal_error`:内部配置、OSS 签名或其它非 provider 故障。
示例:
```json
{
"error": {
"code": "deadline_exceeded",
"phase": "queue",
"attemptsStarted": 0,
"message": "BgFilter 内部请求预算已耗尽"
}
}
```
HTTP status 只作粗粒度传输分类,父侧以稳定 `error.code` 映射业务语义。错误中不得包含签名 URL、Token、图片字节、完整 provider body 或无界错误文本。
## 5. 超时所有权
### 5.1 三层预算
新版不是“父 worker 完全不再管理 BgFilter 超时”,而是三层分工:
1. 父 worker 继续管理父 job 总预算:普通 `900s`、长任务 `1800s`
2. 父侧为每次内部 RPC 计算 worker `requestBudgetMs` 和略长的 parent client timeout;前者覆盖等待 semaphore、最多两次 provider attempt、结果校验和响应构造,后者再覆盖 loopback 传输与父侧读取。
3. 子 worker 管理单次真实 BgFilter attempt,默认上限仍为 `180s`
queue job 的总预算从父 job 开始执行时起算,不从开始申请 BgFilter 时重新计时。现有父 worker 还会把 provider deadline 设在 job deadline 前 `60s`,为最终写回和终态保留时间。同步 RPC 实现必须显式读取父侧剩余 provider budget 并传入 `requestBudgetMs`,不能像当前 BgFilter helper 一样忽略 `RequestContext` deadline。
父总 deadline 到达时,现有 worker 会停止续租、释放 JoinSet 槽并把 work 交给 lease fencing 仲裁,不是立刻杀死所有内部工作。正常情况下内部 RPC 自身应在更早的 provider deadline 内结束,避免进入这条脱管路径。
### 5.2 子 worker attempt timeout
父侧与子 worker 的预算必须满足:
```text
requestBudgetMs + 2s parent transport window
<= parent client timeout
<= 父侧剩余绝对预算
```
parent client timeout 从父侧开始发请求时起算,`requestBudgetMs` 从子 worker 收到请求时起算,因此两者不能设成同一个值。额外 `2s` 用于 loopback 传输、调度抖动和父侧读取类型化错误,不增加 provider 可执行时间。
子 worker 收到请求后使用本机单调时钟计算 RPC deadline。每次 attempt 的 timeout 为:
```text
min(180s, RPC 剩余时间 - 1s worker 响应构造窗口)
```
worker 内部 `1s` 和 parent transport `2s` 都只是固定的小型进程 / 传输窗口,不是 raw 结果持久化预留。剩余时间不足时不得开始新的 attempt;第一次失败后只有预算仍足够才开始第二次。
flat 调用还要由父侧从可分配给 BgFilter 的预算中保留当前阿里云 request timeout(默认 `30s`)和少量本地处理余量,避免 BgFilter 排队吃完全部 provider budget 后名义上有 fallback、实际上已无时间执行。complex 没有 flat fallback,不使用该预留。
inline / External v1 没有 queue job deadline 时,内部 RPC 仍必须有界;默认总上限按“两次现有 BgFilter attempt 上限 + 内部响应窗口”计算,排队时间同样包含在内。
### 5.3 动画旧增量
当前动画把单次 BgFilter timeout 设为:
```text
180s + 2000ms × frame_count
```
该增量原本用于容纳 32 / 40 / 48 个请求直接进入 BgFilter 后的服务端排队。新版已把排队前移到内部 worker,必须删除该增量;所有真实 provider attempt 统一使用 `180s` 上限,动画尾部请求能等待多久由各自内部 RPC 剩余预算决定。
## 6. 并发、排队与熔断
### 6.1 `N` 与 `Q`
唯一子 worker 使用两个简单上限:
- `Q`:内部请求 admission 上限,满足 `queued + running + egress <= Q`;超出立即返回 `overloaded`
- `N`BgFilter provider 并发 permithandler 取得 permit 后才允许开始第一次 provider HTTP。
同一次逻辑调用的 `N` permit 从第一次 provider attempt 前一直持有到两次尝试结束、provider body 读完、完成结果校验,并随内部成功 response body 一直持有到发送完成或 body drop。第二次 attempt 不重新排队。保守延长 permit 生命周期可以防止父侧慢读时积累多份完整结果;loopback 传输很短,首版接受这点吞吐代价。
仅使用 provider semaphore 会形成无界 waiter,因此 `Q` 不是可选优化。内部 listener 必须在解析 JSON body 前使用连接 / request concurrency limit 与 load shedding 完成 admission,设置固定 listen backlog 和小型 body limit`Q` permit 同样跟随 response body 到发送完成或 drop。`Q` 不替代内核 listen backlog,也不宣称消除所有已 accept socket。首版接受 FIFO,不增加持久优先级队列或公平调度状态机。
当前父 worker 可能提交的最大帧请求数并不只有 `96`
| 场景 | 潜在同时提交的动画帧调用 |
| --- | ---: |
| 一个动画父 job | `48` |
| 一个默认 external-generation-worker,父并发 `2` | `96` |
| controller 最多 `8` 个父 worker、每个并发 `2` | `768` |
`Q` 是保护子 worker 的 overload 取舍,不要求覆盖理论最大 `768`。例如选择 `Q = 128` 可以容纳两个满帧动画并留少量余量,但更多父 worker 会收到 `overloaded`flat 将进入 fallbackcomplex 将失败。生产必须明确是否接受该行为。
理论最坏时间必须纳入容量评估:
```text
48 帧、每帧两次 180s:约 ceil(48 / N) × 360s
```
例如 `N = 4` 时理论最坏为 `4320s`,超过长 job 的 `1800s`。上线值不能只按理论最大超时推断,必须用真实 BgFilter P95、动画帧数和主机内存压测冻结;容量不足时尾部 flat 请求会在内部 deadline 后进入父侧 fallback。
### 6.2 并发承诺边界
首版只能承诺:
```text
部署中只有一个健康 bgfilter-worker 时,
该进程当前持有的 BgFilter HTTP future <= N。
```
它不能绝对保证 BgFilter 服务端实际计算始终 `<= N`:客户端 timeout、进程退出或网络断开后,远端可能继续计算,而本地 permit 已被释放。若必须严格限制服务端计算,只能把全局 semaphore 放到 BgFilter 服务本身,或让 BgFilter 支持 request id、取消和状态查询。
两个子 worker 实例会得到 `2N`,因此首版使用固定内部监听地址和非模板化 systemd unit;同机第二实例应因固定端口 bind 失败。发布必须 `stop old -> 等待排空/退出 -> start new`,不能让新旧进程重叠。生产 `N``Q` 缺失、为 `0` 时 fail-closed。
### 6.3 熔断
熔断是故障保护:flat 连续多次请求失败后,在 cooldown 内暂时不再请求 BgFilter,而是快速返回 `circuit_open`,由父流程进入“阿里云 → 本地”fallback,避免故障 provider 持续占满并发和超时。
保持当前语义:
- 只有 flat 读取和更新熔断;complex 完全不读写。
- flat 在取得 permit、即将发送第一次 provider HTTP 前重新检查熔断,避免 48 个排队请求在熔断打开前全部通过旧检查。
- 已经获准执行的逻辑调用,即使第一次失败使熔断打开,也仍允许在预算内完成自己的第二次顺序 attempt;后续请求快速返回 `circuit_open`
- 每个真实失败 attempt 计一次失败,保持当前计数口径;成功重置。
- 只有拿到完整配置 attempt 上限(默认 `180s`)后发生的 provider timeout,以及真实传输失败、非 2xx 和无效 / 超限图片计入。因 `requestBudgetMs` 剩余不足而被截短的 timeout 返回 `deadline_exceeded`,不更新熔断;排队满、排队超时、客户端取消、鉴权和本地配置错误同样不计入。
- 进程重启后熔断状态清零是首版接受行为。
首版不增加 QPS 限制。若 provider 以后要求 QPS,需要另加 token bucket;不能把并发 semaphore 当作 QPS。
## 7. 断连、崩溃与动画语义
### 7.1 内部 RPC 断连
- 父侧不得自动重试整次内部 HTTP。断连时结果未知,重试可能让一次逻辑调用从最多两次 provider attempt 扩大为四次,并可能突破瞬时并发预期。
- 尚在等待 permit 的请求到达自身 deadline 后必须取消,不再发送 provider 请求;运行时若能可靠观察客户端断连,也可提前取消,但正确性不能只依赖断连事件。
- 已经开始的 provider attempt 必须继续读取到完成或本次 attempt timeout,并持有 permit;可观察到的 handler / client drop 只丢弃最终结果,不能让已启动请求变成无人管理的本地 future。
- deadline 或显式 cancellation signal 已被子 worker 观察到后,不再开始第二次 attempt。单纯 TCP 断连只能 best-effort 触发 cancellationAxum / Hyper 不保证立刻通知 handler,因此不能承诺所有断连都阻止二试。
实现时,已启动 provider attempt 应由持有 permit 的独立 task 管理;request handler 可观察到的 Drop / cancellation signal 只影响“是否继续重试和是否返回结果”,不直接丢弃已经开始的 provider future。`requestBudgetMs` deadline 是最终可靠的停止条件。
### 7.2 进程崩溃
- 父进程崩溃:内部连接最终断开,父 job 按现有 heartbeat、lease、`max_attempts = 1`、失败和退款语义收口。
- 子 worker 崩溃或重启:当前内部 RPC 失败;父侧不查询、不恢复、不重发同一次 RPC。
- 子 worker 成功但响应在网络中丢失:结果视为未知;flat 进入原 fallbackcomplex 失败。
- 子 worker 不得反向 complete / fail 父 job,也不得写画布、业务资源或账单。
### 7.3 动画首版范围
动画继续让最多 `48` 个 frame future 提交内部 HTTP,并保持当前 collect / drain 行为:所有已提交的帧调用都等待自己的成功、失败或 deadline 后,父流程再按稳定帧序号选择根因并失败。首版不增加 `groupId`、跨请求 registry 或 cancel endpoint,也不宣称“某一帧失败后立即取消仍在 worker 排队的兄弟帧”。
这是为了把改动限制在低层 BgFilter 调用和调度服务。若真实观测证明排队兄弟帧浪费严重,可在第二阶段增加纯内存 `groupId + CancellationToken`:取消尚未取得 permit 的请求,已经开始的 provider attempt 仍排空。该升级仍不需要数据库表,但需要同时修改父侧动画收集逻辑,因此不并入首版。
## 8. 业务语义保持
| 场景 | 内部响应 | 父流程行为 |
| --- | --- | --- |
| flat 单图 / 动画帧 | 图片二进制 | 继续现有 Alpha / 尺寸恢复、finalizer 和最终持久化 |
| flat,父业务预算仍有效 | `provider_exhausted / circuit_open / deadline_exceeded / overloaded / invalid_result / internal_error` 或内部断连 | 记录对应故障后进入现有“阿里云通用抠图 → 本地键色”;这些内部错误本身不都计入熔断 |
| flat | `cancelled`,或父 job cancellation / 绝对 deadline 已生效 | 立即向上退出,不再启动阿里云或本地 fallback |
| flat | `invalid_request / unauthorized` | 作为内部契约或部署配置错误失败,不 fallback、不计入 BgFilter 熔断 |
| complex 手动去背景 | 图片二进制 | 父流程继续最终 OSS、资源和画布写回 |
| complex 手动去背景 | 任意非成功或断连 | 父流程直接失败;不得接 flat fallback,不得修改 flat 熔断 |
| 角色 / 图标 / UI 后处理最终失败 | BgFilter 与 fallback 都未得到可用结果 | 保留已持久化 provider 原图,以现有 `completed + warning` 收口 |
| 动画任一帧最终失败 | 该帧完整 fallback / finalizer / PUT 仍失败 | 排空其它已提交帧后,整项动画按现有语义失败退款 |
父侧移除现有 flat / complex BgFilter retry loop 和本地 flat 熔断,避免父侧两次 × 子 worker 两次变成四次。所有入口只替换共同的低层 BgFilter helper;这样 External v1 直接调用 `_for_owner` 的路径也会自然经过内部 worker。
BgFilter 成功二进制不是一份新的业务资产:
- 不创建 raw object key,也没有 raw 结果 TTL。
- 父侧按现有路径消费字节并写最终 OSS;手动 complex 只写最终结果一次。
- 角色、图标、UI 的 provider 原图和动画源帧继续沿用现有对象生命周期,不归子 worker 清理。
## 9. 安全、内存与可观测性
### 9.1 内部安全
- 首版限定父 worker 与子 worker 同机部署,内部 listener 只绑定 loopback 固定端口,不挂公共 Axum router、Nginx、BFF 或 OpenAPI。
- 使用独立内部 Token;缺失时生产 fail-closed。Token 不复用 BgFilter provider Token。
- 只接受配置 bucket 下的规范化 object key;禁止 `http://``https://``data:``blob:` 和路径逃逸。
- 不在日志、trace、metrics、错误 JSON 或 SpacetimeDB 审计中写签名 URL、Token、图片字节或 Base64。
### 9.2 图片与内存边界
父、子两侧都复用当前保护:
- 响应体最大 `32 MiB`,有 `Content-Length` 和 chunked 两种路径都累计限长;
- 解码宽高最大 `8192 × 8192`,限制解码分配;
- MIME、魔数和实际解码结果必须一致;
- 空 body、截断 body 和超限图片按 `invalid_result` 处理。
二进制跨进程传输期间,子 worker 和父 worker 可能同时持有同一张图片。前述 `N / Q` permit 必须由 response-body guard 持有到发送完成或 body drop,才能把子 worker 中的完整成功 body 控制在 `N` 级;在此前提下,极端内存至少按 `2 × N × 32 MiB` 再加解码缓冲评估。若实现没有该 guard,完整 body 可能积累到 `Q` 级,本文的内存模型即不成立,不能上线。生产 `N / Q` 必须结合主机内存压测,而不是只看 BgFilter 吞吐。
### 9.3 指标与日志
最小指标:
- `bgfilter_internal_waiting_requests`
- `bgfilter_internal_in_flight`
- `bgfilter_internal_request_seconds{mode,outcome}`
- `bgfilter_provider_http_seconds{mode,attempt,outcome}`
- `bgfilter_internal_request_total{mode,outcome}`
- `bgfilter_internal_response_bytes`
- `bgfilter_circuit_state`
日志只写 `requestId`、父 job / request correlation、mode、attempt、排队耗时、provider 耗时、结果码和安全 object key;不得记录请求/响应图片 body。
当前 flat 的每次 provider 失败审计必须迁到子 worker,保留“第一次失败、第二次成功”也可观察的事实;内部 admission、鉴权和本地配置错误不伪装成 BgFilter provider 失败。
## 10. 实施与部署计划
### 10.1 实施顺序
1. 在现有 Rust 后端增加 `bgfilter-worker` 进程角色和独立 loopback Axum listener;它不启动用户 HTTP router,也不 claim `external_generation_job`
2. 增加内部 request / binary response / typed error 契约、Token 校验、JSON body 上限、object key allowlist 和健康检查;listener 在 body 解析前接入连接 / request concurrency limit、固定 backlog 和 load shedding。
3. 增加 admission `Q``Semaphore(N)`、两次顺序 attempt、预算检查、结果限长 / 解码校验、response-body permit guard 和 flat 进程级熔断。
4. 增加父侧共享内部 HTTP client。该 client 不自动重试,把父剩余预算显式转换为较短的 `requestBudgetMs` 和略长的 client timeout,并保证两者都早于父绝对 deadline。
5. 用内部 client 替换两个集中调用边界:
- flat`remove_editor_generated_screen_background_with_bgfilter_with_request_timeout`
- complex`request_editor_background_removal_image_with_retry`
6. 从父侧移除 BgFilter provider retry 和 flat 熔断实现;保留 flat fallback、complex 失败、Alpha / 尺寸恢复和所有最终持久化。
7. 删除动画 `2000ms × frame_count` 单 attempt timeout 增量;保留现有所有帧 collect / drain。
8. 扫描 queue、inline、External v1 的角色、图标、UI、动画和手动 complex 路径,确认没有 direct BgFilter HTTP 旁路。
9. 增加单实例 systemd unit、内部地址 / Token、`N / Q` 配置、readiness 和指标;真实压测 `32 / 40 / 48` 帧后启用。
本计划不产生 SpacetimeDB schema、migration、bindings 或表目录改动。
### 10.2 部署与回滚
1. 先部署并启动唯一 `bgfilter-worker`,确认 loopback health、鉴权和 provider smoke。
2. 再发布使用内部 client 的 API / external-generation-worker。
3. 确认所有父进程只访问内部 endpointBgFilter provider 日志中不再出现父进程直连。
4. 发布子 worker 时执行 `stop old -> 等待排空/退出 -> start new`,不得滚动重叠。
内部 worker 不可用时禁止自动 direct fallback。flat 仍可走业务已有阿里云 / 本地 fallback;complex 明确失败。需要整体回滚时回滚父、子进程版本和配置,不在运行中混用两种 BgFilter 调度方式。
## 11. 验收门禁
必须覆盖:
- `48` 帧同时进入时,唯一子 worker 观测到的客户端 BgFilter HTTP future 峰值不超过 `N`
- listener 在 body 解析前执行 concurrency limit / load shedding`queued + running + egress` 达到 `Q` 后新请求立即返回 `overloaded`handler 数不超过 `Q`,内核 socket backlog 按独立固定值验证。
- 排队时间计入 `requestBudgetMs`;deadline 到达后不开始新的 provider attempt。
- `requestBudgetMs + parent transport window <= parent client timeout <= 父剩余绝对预算`,worker 有时间把类型化错误交回父侧。
- 第一次失败后预算不足时不开始第二次;父侧从不重试整次内部 RPC。
- 父业务预算仍有效时,flat 两次失败、熔断、overload、内部 RPC deadline 或断连仍走“阿里云 → 本地”;complex 任意失败直接失败且不读写熔断。
- flat 熔断按真实失败 attempt 计数;由剩余业务预算截短的 timeout 不计入。已获准调用可完成第二次,后续排队请求快速 `circuit_open`
- `cancelled`、父 cancellation / 绝对 deadline、`invalid_request``unauthorized` 不启动 flat fallback;其它 flat 错误只在父业务预算仍有效时进入 fallback。
- 客户端断连时,等待 permit 的请求最终由 deadline 收口;已开始 provider attempt 持有 permit 并排空。明确 cancellation 已被观察到后不再开始第二次,单纯 TCP 断连只作 best-effort 测试,不作为硬保证。
- 动画全部已提交帧继续 collect / drain,根因按现有稳定帧序号收口;首版不存在未实现的 group cancellation 承诺。
- 成功 body 为原始二进制而非 Base64;父、子两侧都拒绝空 body、MIME / 魔数不一致、chunked 超 `32 MiB` 和超过 `8192 × 8192` 的图片。
- `N / Q` response-body guard 在发送完成或 body drop 前不释放,慢读 / 断连时完整成功 body 不积累到 `Q` 级。
- worker 重启 / RPC 丢失不查询、不恢复结果;父 job 的 heartbeat、lease、失败退款和 fencing 保持现状。
- External v1 / inline 不再直连 BgFilter;公共 router、BFF、账单和任务列表中没有内部 endpoint 或内部调用记录。
- 生产不存在两个同时运行的 `bgfilter-worker`,配置缺失或 `N / Q = 0` 时 fail-closed。
实现后按范围运行:
```bash
cargo test -p api-server bgfilter --manifest-path server-rs/Cargo.toml
cargo test -p api-server character_animation --manifest-path server-rs/Cargo.toml
cargo check -p api-server --manifest-path server-rs/Cargo.toml
npm run check:server-rs-ddd
npm run check:encoding
git diff --check
```
不需要运行 `npm run spacetime:generate``npm run check:spacetime-schema`,因为本计划明确不修改 SpacetimeDB。
## 12. 实施前需冻结的参数
架构边界已固定:首版单实例、无 QPS、无数据库任务表、无 checkpoint / continuation、输出直接走内部二进制响应。
编码前只需冻结两个容量值:
- 生产 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY = N`
- 生产 `GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS = Q`。若要求一个满帧动画不因自身 admission 被拒绝,`Q` 至少为 `48`;候选 `128` 可容纳两个满帧动画并留少量余量,但明确不能覆盖 controller 理论最大 `768` 次同时提交,超出部分会按 mode fallback 或失败。最终值以可接受的 overload 行为和内存压测为准。
其余首版固定边界:provider 单 attempt 默认 `180s`,内部响应最大 `32 MiB / 8192 × 8192`,内部 listener 只绑定 loopback 且必须鉴权。若要多实例、严格服务端全局并发、QPS 或动画 peer cancellation,应先升级本文,不在编码中临时扩 scope。
@@ -2,7 +2,9 @@
> 2026-07-18 退役覆盖:旧创作模板 job 类型、玩法写回和玩法恢复链路均已退出现役 worker。当前 worker 只领取 `source_module = editor-canvas` 的任务;本文涉及拼图、跳一跳、拼消消、敲木鱼等玩法的内容仅作为历史设计记录,历史队列行不得被领取或改写。
更新时间:`2026-07-15`
> 2026-07-21 待实施专题:BgFilter 作为受限内部资源,将成为“单用户动作一个外部生成 job”的受控内部例外。用户可见层仍只有父 `external_generation_job`;父 future 保持原 lease 和 attempt,在当前调用栈内同步请求唯一 `bgfilter-worker` 的内部 HTTP,成功图片字节直接返回父流程。首版不新增 SpacetimeDB 子任务表、父 checkpoint / continuation 或 raw 中间结果 OSS。完整边界见 [`BgFilter 受限资源调度方案(同步内部 HTTP 原地等待版)`](./【后端架构】BgFilter受限资源调度方案-2026-07-21.md)。
更新时间:`2026-07-21`
## 背景