一句话

合成流水线是一条**”用户提交 → 后台抢占 → 调合成微服务渲染 → 服务自行入库 → 状态回写”的异步管道。pf-service 内嵌的 Job 只负责调度**,实际的 Puppeteer 离屏渲染与 MinIO 入库由独立的 pf-compose-service 完成——这是近期一次关键解耦(5414dbe / 2809bf4)。

入口与时序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
t=0          用户在 pf-app 点"提交合成"
▼
t=0+ε POST /api/app/order-templates/submission
│ 入库 order_template_submissions(status=pending)
│ + N 条 order_template_submission_items(pending)
│ 订单 uploadStatus=submitted, composeStatus=pending
│
t=Δ(≤10s) orderTemplateCompositionJob 轮询
│ FOR UPDATE SKIP LOCKED 抢占 ≤3 条明细 → processing
▼
t=Δ+T 并发 3 处理明细:
├─ buildPreviewSceneJson(快照 + uploads + texts + 订单变量)
├─ POST http://pf-compose-service/api/compose
│ → Worker 渲染 Leafer → 导出图 → 上传 MinIO
│ ← { fileKey, publicUrl, renderTimeMs, uploadTimeMs, totalTimeMs }
└─ 写回 item.composedImageUrls, status=completed
│
t=Δ+T+ε 聚合: 全 completed → submission/order completed
有永久失败 → failed;否则 processing
▼
t=... pf-manage 审核 / pf-download 下载

状态机

明细、提交主表、订单 composeStatus 共用 taskStatus 四态:

1
2
3
4
5
pending ──► processing ──► completed
│
└────► failed (item.failReason 写原因; attemptCount<3 可重试回 processing)

processing 超 10 分钟无进展 → 下一轮被判为"僵尸"重新抢占

任何一条明细失败不影响其它明细——父状态由所有明细聚合得出(syncParentSubmissionStatus)。

核心源码

文件 作用
apps/pf-service/src/jobs/orderTemplateCompositionJob.ts 轮询、SKIP LOCKED 抢占、并发 3、退避重试、状态聚合
apps/pf-service/src/utils/jobProcessor.ts processComposition:构 fileName、调合成服务、返回 publicUrl
apps/pf-service/src/utils/compositionScene.ts buildPreviewSceneJson:合并快照/填值/订单变量
apps/pf-service/src/services/composeService.ts HTTP 客户端:requestComposition / checkComposeHealth
apps/pf-compose-service/* 真正的 Puppeteer 渲染 + MinIO 入库(见专页)

Job 调度细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 简化伪代码
export function startOrderTemplateCompositionJob(intervalMs = 10_000) {
timer = setInterval(async () => {
if (isRunning || isStopped) return; // 防重入
isRunning = true;
try { await runOnce(); } finally { isRunning = false; }
}, intervalMs);
}

async function runOnce() {
// 1. claimNextPendingItems: FOR UPDATE SKIP LOCKED 抢占 ≤ MAX_COMPOSITION_CONCURRENCY(3) 条
// 条件: pending / (failed 且 attempt<3 且 nextRetryAt 到点) / (processing 且 stale>10min)
// 2. 置 processing
// 3. asyncPool(3, processSingleItem)
// 4. syncParentSubmissionStatus 聚合
}
  • DB 级抢占:FOR UPDATE SKIP LOCKED 天生支持多进程/多实例并行,不再依赖内存锁(旧文档里的”扩展点”已落地)
  • 并发:明细内实例也走 asyncPool(3);AGENTS.md 的并发上限 3 在 pf-service 侧(MAX_COMPOSITION_CONCURRENCY)和合成服务侧(3-Worker 池)双重生效
  • 轮询间隔:config.server.jobs.composition.intervalMs(默认 10s);enabled = false 可完全停掉

合成主流程(jobProcessor → 合成服务)

1
2
3
4
5
6
7
8
9
10
11
12
processSingleItem(item)
├─ 读 item.sceneJson 快照(优先)
├─ 对每个实例 buildPreviewSceneJson(scene, uploads, texts, orderVars):
│ ├─ 按 elementKey 替换 Image.url / Text.text
│ ├─ 注入订单变量(buyerName / goodsName / remark / orderDate...)
│ └─ faceSafe 节点:前端已算好裁剪,后端合成时按快照消费
├─ processComposition({ orderNo, userSceneJson, compositionKey }):
│ └─ requestComposition → POST {compose}/api/compose
│ body: { sceneJson, storage:{purpose:"composition", fileName}, options:{format:"png", pixelRatio:1} }
│ ← { fileKey, publicUrl, renderTimeMs, uploadTimeMs, totalTimeMs }
├─ 增量写回 item.composedImageUrls
└─ 失败 → status=failed, failReason, attemptCount++, nextRetryAt=now+2^n s

buildPreviewSceneJson 是”sceneJson + 用户填值合并”的单一实现,编辑器预览、用户端预览、后端合成共用同一套语义,保证三端视觉一致。渲染与截图/上传的实现细节全部在 合成微服务。

为什么把合成拆出去

维度 内嵌 Puppeteer(旧) 独立合成服务(现在)
主服务体积 每个 pf-service 进程都背 Chromium 主服务无 Puppeteer 依赖,轻量水平扩展
资源隔离 渲染 OOM 抖动直接影响 HTTP 延迟 渲染独立进程池,互不挤占
伸缩 API 与渲染耦合 合成服务可单独扩副本/配 Worker 数
入库 主服务上传 合成服务渲染完自行 PutObject 入库,只回传 URL

重试与容错

  • 自动退避重试:失败明细在 nextRetryAt(now + 2^attempt s)到点后被下一轮重新抢占,最多 3 次,之后永久失败
  • 无本地兜底:合成服务不可达/超时 → requestComposition 抛错 → 明细标 failed 走退避;不会退回本地 Puppeteer(主服务已无该能力)
  • 僵尸恢复:processing 超 10 分钟自动回收
  • 手动重试:pf-manage 的重合成接口把 composeStatus 回退 pending

常见 failReason:素材 URL 不可访问(MinIO 权限/网络)、字体加载超时、合成服务超时/不可达、渲染超时(模板复杂或大图未压缩)。

性能与扩展

维度 默认 升级路径
调度并发 pf-service 抢占 3(SKIP LOCKED 已支持多实例) 多 pf-service 实例并行拉取
渲染并发 合成服务 3 Worker(可配 puppeteer.workers) 加大 Worker 数 / 扩合成服务副本 / GPU 节点
渲染时间 单张约 0.5–3s(视模板复杂度) 素材缓存 / 复用 Page
输出格式 png(options.format 支持 jpg/webp) 后处理转 TIFF / 高 DPI(调 pixelRatio)

监控建议

  • pending 明细队列长度(堆积)
  • 合成服务 /health 的 workers(idle/active/waiting)
  • 单条合成 renderTimeMs / uploadTimeMs P95
  • failReason 分组统计(不打开数据库就知道是素材死链还是服务挂了)

下一步

站内搜索

没有找到内容!