2661 字
约 8 分钟
3
旁路 Worker 的本质

旁路 Worker 的本质

面向 VOZEB PRO。讲清楚 generation-worker.mjs 不是第二套后端,也不是 Celery / Sidekiq。它是一个会按点敲门的 HTTP 客户端:主进程里的业务原封不动,只是多了一个关页面之后仍会续跑的进程。


0. 什么时候必须懂这个

适合:

  • ✅ 看到 Compose 里同时有 appgeneration-worker,不知道谁才是「后端」
  • ✅ 要加一种关标签后仍须完成的活(生成续取、退款、过期订单)
  • ✅ 有人想在 Worker 脚本里直接连 Postgres 或 otokapi
  • ✅ 想理解「整体式全栈」为什么没有立刻被长视频任务打死

现在不必深挖:

  • ⚠️ 只改后台表格 / 登录文案 / antd 主题
  • ⚠️ 把 Worker 当成通用消息队列来学(它不是)

在 VOZEB PRO 中的定位: Worker 用同一镜像、同一套 Route。业务状态机、SQL、上游协议全在 web 进程;脚本只负责心跳、多 lane 认领、失败退避。


1. 什么时候用到旁路 Worker

能用到的时候只有一类:用户把页面关了,这件事还得继续往前做,而且不能再向下游重新下单。

先问自己一句:

关掉标签 / 刷新 / 换手机之后,这活还要不要有人接着干?

答案 用不用 Worker
不要。下次用户再点就行 不用。 普通 POST /api/... 做完返回就结束
要。而且已经跟上游说过一次话了 要。 先落任务行,再让 Worker 到期来催
要,但是只是扫库做清理(退款、过期订单) 也要。 同样走 maintenance,换一条 lane

不是「写成了 async」就等于要 Worker。async 只表示这次请求里可以等;Worker 表示这次请求结束以后,世界上还得有人记得这单

本仓库里已经在用的场合

这些都满足「关页后还要继续 + 不能二次 create」:

  • 生图 / 视频 / 音频还在上游排队或生成
  • Agent、短剧合成跑好几分钟
  • 生成失败后的退积分(退款 lane)
  • 订单过期、邀请结算这类定时扫库(同一套维护门,不一定是生成 lane)

接单当时 HTTP 很快返回「已接单」。真正出片、退款,是任务行 + 有人在线时页面顺手催一把 + 没人在线时 Worker 保底

自己加功能时这样选

用 Worker(先落行,再加敲门):

  • 上游要几十秒到几分钟(视频尤其是)
  • 重复执行会多扣积分 / 多付账单 / 多创建上游任务
  • 用户关页是正常操作,不是异常
  • 你需要「过一会儿再问一次进度」,而不是「这次请求里 await 到死」

不要用 Worker:

  • 登录、保存提示词、改后台开关、表格筛选
  • 读一张已有的图(那是读路径,不是催单)
  • 只想把代码写成 async 好看一点
  • 想在脚本里图省事直接查库、直接打 otokapi(那是拆成第二套后端,本仓禁止)

还不到 Worker,先把任务行做对:

  • 连库都没写入,Worker 敲门也是空的
  • 恢复时还会再 create 一次上游 → 先修幂等,再加 lane

和「页面自己轮询」的分工

有人盯着屏幕时,页面 SSE / 轮询 / after() 已经能推一把。
Worker 解决的是:没人盯的时候谁来推。

所以不是「有生成就必须 Worker 才能出图」——人在线时主进程也能续。
是「人走了图还得出」才必须有它。

落地时的最小形状

以后加新活也按这个,缺前三步加脚本没有意义:

  1. 先有一张别人能认领的行(nextPollAt、租约)
  2. 推进逻辑放进 lib/server 的 batch(页面和 Worker 共用)
  3. 再开一个只做鉴权的 /api/maintenance/...
  4. 最后才在 generation-worker.mjs 里加一条 fetch 循环

一句话: 当场能做完、做错了重点就行的,别用;必须跨过「关页面」且再做一次会花钱的,才用旁路 Worker。


2. 为什么叫「旁路」

整体式全栈把 HTTP、业务、SQL、出站 AI 放进同一个 web 进程。这对短请求很合适,对「上游要跑几分钟的视频」不合适:

  • 用户关标签 → 浏览器里的 await 没了
  • Next 热更新 / 换容器副本 → 进程内存里的任务没了
  • 若恢复逻辑是「再 POST 一次创建」→ 账单翻倍

旁路的意思是:不把主进程拆开,只把「谁来敲门续跑」这件事挪到另一个进程。

用户请求 ──► web 进程(鉴权 / 建任务 / 扣积分 / 出站 / SQL)
                 ▲
                 │  POST /api/maintenance/...
                 │  Bearer 维护令牌
generation-worker(没有业务,只有 fetch 循环)

主进程仍然是唯一的业务盒子。Worker 走侧门,不另开一扇业务门。


3. 核心公式

旁路 Worker = 持久任务行 + 租约认领 + 维护身份的 HTTP 客户端。

拆开三件事,缺一不可:

零件 在哪 干什么
任务行 generation_tasks(或 JSON 回退) 进程死了任务还在;带 next_poll_atlease_untilworker_id
恢复批处理 runGenerationTaskRecoveryBatch 认领到期行,按类型推进一步;已有 upstream.id 只 poll,禁止再 create
Worker 脚本 web/scripts/generation-worker.mjs 定时 POST 本站 maintenance;自己不解释 phase

页面 after() 和 Worker 共用同一份恢复函数。所以:关页面只是少了一个敲门的人,不是少了一套逻辑。


4. 脚本里到底有什么(以及没有什么)

generation-worker.mjs 启动后做四件事:

  1. 校验 VOZEB_PRO_MAINTENANCE_TOKEN(至少 32 字符)
  2. 解析 originVOZEB_PRO_WORKER_API_ORIGIN / 运行时辅助函数),决定敲哪台 app
  3. 定时心跳:POST /api/maintenance/generation-tasks/heartbeat
  4. 并行循环:
    • N 条 generation lane(默认 2,范围 1~8)→ POST .../generation-tasks/run,单批超时 40 分钟
    • 1 条 refund lane → POST .../billing-refunds/run

认领到活就短睡(生成 250ms / 退款 1s),空转按间隔睡(生成默认 2s / 退款 10s)。连续失败指数退避,生成封顶 60s。

脚本里没有:

  • SQL
  • 渠道密钥解密
  • 图/视频/音频/Agent runtime
  • 积分计算公式
  • 「要不要再 create 上游」的判断

那些全部在被敲门的 Route 后面,和用户请求走同一套 lib/server


5. 它解决的是哪类失败,不是哪类失败

能扛住的:

  • 用户关掉工作台
  • 某个 web 副本重启,租约过期后另一副本或另一 lane 续领
  • 上游还在跑,本地只丢了内存里的 poll 循环
  • 多 lane / 多 Worker 同时抢活:FOR UPDATE SKIP LOCKED,一行只属于当前租约

扛不住、也不该指望它扛的:

  • 任务从未落库(还在某个 Route 的局部变量里)
  • 恢复时再次 create 上游(这是实现 bug,不是 Worker 能补的)
  • web 进程本身已经卡死,maintenance Route 都打不进去——Worker 再勤也只是打到 5xx
  • 把 CPU 打满的本地转码仍挤在同一事件循环里(那是该不该把转码移出 Node 的问题,不是再加一条 lane 能解决的)

一句话:Worker 提高的是「已落库任务的送达率」,不是「任意重活的吞吐量」。


6. 和真正的队列框架差在哪

本仓库 Worker Celery / Sidekiq / Bull
任务真相 PostgreSQL 任务表 通常是 Redis / Broker 里的消息
执行端 仍是 Next Route + lib/server worker 进程里跑函数体
鉴权 维护令牌进本站 HTTP 进程内调业务代码
重复消费 租约 + SKIP LOCKED + 「只 poll 不 create」 消息 ack / 幂等键,模型不同
加一种活 加 maintenance Route + 一条 loop 加一个 task 函数注册到 broker

所以不要把 generation-worker.mjs 越写越厚。新活的正确形状是:

  1. 先有一张别人能认领的行(或复用现有任务 / 退款行)
  2. 再有一个只做鉴权 + 调 runXxxBatch 的 Route
  3. 最后才在脚本里加一条 runXxxLane(),内容仍然只有 fetch

7. 对照本仓库的观察点

读代码或看环境时,用这些信号确认你理解对了:

观察 含义
Compose 日志 Generation worker started: 独立进程已起来,不是 Next 内部 setInterval
Worker POST /api/maintenance/generation-tasks/run 带 Bearer;可选 x-vozeb-pro-worker-id
令牌太短 / 未配 脚本直接 throw;配错则 Route 401
schema 未就绪 { claimed: 0 },文案等待初始化
claimed > 0 有活,短睡再敲
心跳约 15s,租约约 90s 环境变量可调;过期才能被别人领
Route maxDuration 很长(生成批 2400s 量级) 单批可以在 app 进程里跑很久,Worker 只是调用方
页面 after(recovery) 仍在 有人在线时不必全靠旁路;旁路是保底

源码入口:

  • web/scripts/generation-worker.mjs
  • web/src/app/api/maintenance/generation-tasks/run/route.ts
  • web/src/app/api/maintenance/generation-tasks/heartbeat/route.ts
  • web/src/lib/server/generation-task-recovery-service.ts
  • web/src/lib/server/generation-task-scheduler.ts

8. 写代码时的三问

  1. 关页面后还要不要推进? 不要,就别碰 Worker。
  2. 推进逻辑能不能放进现有 recovery / 一个新的 runXxxBatch 不能写进 .mjs
  3. 失败重试会不会二次 create 上游或二次退款? 会,先补幂等和「只 poll」不变量,再加 lane。

9. 读完能指挥自己(或 AI)做什么

  • 「续取加在 generation-task-recovery-service / maintenance Route,不要在 generation-worker.mjs 里连库或连上游。」
  • 「新后台活:先设计可认领的行和租约,再加 Route,最后只加一条 fetch lane。」
  • 「自动恢复路径禁止再次创建上游任务。」

相关文档

旁路 Worker 的本质
http://clxhxhhr.top/posts/522/
作者
clxstart
发布于
2026-09-08
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。