分布式 Workers 的解耦设计
为什么需要拆分
AI 任务处理有个本质问题:时间不可预测。
一个图像生成请求可能 2 秒完成,也可能需要 30 秒。如果用同步接口,客户端要么超时,要么假死。用户体验会很糟糕。
解决方案不是让 AI 更快,而是承认异步的必然性。接受它,然后围绕它设计系统。这就是 Purikura Workers 架构的起点。
队列:解耦的核心
队列不只是消息传递工具,它是时间维度上的解耦。
💡 Click the maximize icon to view in fullscreen
这个流程的关键在于:客户端不等待。请求发出后立即返回,后续通过 WebSocket 接收状态更新。
这种设计让系统能够处理并发。一个用户等待 AI 生成时,系统可以同时处理其他用户的请求。队列确保任务不丢失,处理器按能力消费。
Cloudflare Queues 的优势不在功能丰富,而在简单。
它提供了可靠的消息传递,自动重试,死信队列处理。这些是异步系统的基础设施。
更重要的是,它与 Workers 深度集成。队列消费者直接是一个 Worker,不需要额外的消息代理或中间件。这降低了运维复杂度。
关于队列可靠性的更多思考,参见 queue-reliability-boundaries。
微服务的粒度
Purikura 将后端拆分成多个独立的 Workers:
API Gateway - 所有请求的入口点,处理路由、认证、CORS AI 处理器 - 消费 AI 任务队列,调用多厂商 API 业务处理器 - 处理支付和订阅逻辑 邮件服务 - 异步发送邮件通知 定时任务 - 僵尸任务处理、退款处理、DLQ 管理
每个 Worker 有明确的职责边界。它们通过队列通信,而不是直接调用。
这种拆分不是为了追求微服务的完美,而是为了隔离故障。当 AI 处理器因为厂商 API 超时而变慢时,不会影响支付处理。当邮件服务出问题时,不会阻塞任务生成。
微服务增加了系统复杂度。
你需要管理多个部署单元,需要监控更多的故障点,需要处理分布式系统的一致性问题。
但对于 AI 应用,这个代价是值得的。因为 AI 厂商 API 本身就是不可靠的外部依赖。把它隔离在独立的服务中,是降低整体系统脆弱性的必要手段。
监控:全链路追踪
分布式系统的调试是噩梦。一个请求可能经过 5 个不同的 Worker,每个 Worker 都可能失败。
解决方案是全链路追踪。每个请求生成一个 traceId,它随着消息在系统中传播。
// API Gateway 创建 trace
const traceManager = new TraceManager();
const { traceId } = traceManager.createTrace('ai_generation');
// 入队时传递 traceId
await env.AI_QUEUE.send({
jobId,
payload,
traceId // 追踪上下文
});
// AI 处理器继承 trace
const { traceId: incomingTraceId } = message.body;
const span = traceManager.createSpan(incomingTraceId, 'fal_api_call');
这让你可以在日志中搜索 traceId,重建整个请求链路。当用户报告问题时,你能快速定位是哪个环节出错。
结构化日志进一步增强了可观测性:
aiLogger.taskStatusChange(jobId, 'queued', 'processing', {
trace_id: traceId,
model: payload.model,
worker: 'ai-processor'
});
每个日志都包含上下文信息:时间戳、服务名、环境、请求 ID。这些信息在生产问题排查时至关重要。
分布式追踪不是记录所有信息,而是记录关键路径。
每个 Span 标记一个操作的开始和结束。通过 Span 的嵌套关系,你可以看到调用树。通过 Span 的时间戳,你可以看到性能瓶颈。
这是在复杂系统中保持可见性的唯一方式。参见 call-chain-tracing 了解更多追踪技术。
错误恢复:多层防护
异步系统的错误处理比同步系统复杂得多。
重试机制 - 指数退避重试,避免压垮下游服务
const maxRetries = 5;
const retryCount = job.retry_count || 0;
const backoffDelay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s, 8s, 16s
死信队列 - 多次失败后进入 DLQ,等待人工处理
if (retryCount >= maxRetries) {
await env.DLQ.send({ jobId, error, originalMessage });
return;
}
僵尸任务处理 - 定时任务检测卡在处理状态的任务
// 每 15 分钟运行
const zombieJobs = await supabase.rpc('get_zombie_jobs', {
p_timeout_minutes: 30
});
for (const job of zombieJobs) {
// 向厂商查询真实状态
const status = await vendorAdapter.getJobStatus(job.vendor_task_id);
await syncJobStatus(job.id, status);
}
退款处理 - 失败任务自动触发退款,保护用户利益
这些机制不是一次性设计的,而是在生产问题中逐步完善的。每个异常场景都对应一个防护措施。
目前的错误处理还有盲区:
- 网络分区:当 Worker 与数据库失联时,如何保证任务状态一致性?
- 厂商 API 降级:当主要 AI 厂商不可用时,如何自动切换到备用厂商?
- 成本控制:如何防止恶意用户通过大量失败重试消耗信用额度?
这些问题目前通过监控和人工干预处理。自动化解决需要更复杂的编排逻辑,这是下一步优化方向。
安全:多重验证
外部 Webhook 是潜在的攻击面。AI 厂商通过 Webhook 回调通知任务完成,但如何确保回调来自厂商而非恶意请求?
签名验证 - 验证 Webhook 签名,确保消息完整性
async function verifyFalSignature(body, signature, secret) {
const expectedSignature = await computeHmacSha256(body, secret);
return safeCompare(signature, expectedSignature);
}
时间戳验证 - 防止重放攻击
const timestamp = headers['X-Timestamp'];
const now = Date.now();
if (Math.abs(now - timestamp) > 300000) { // 5 分钟
return { error: 'Request expired' };
}
幂等性检查 - 重复的 Webhook 调用不会重复扣费或更新状态
const existing = await supabase
.from('ai_generations')
.select('status')
.eq('id', jobId)
.single();
if (existing.status === 'completed') {
return { message: 'Already processed' };
}
这些验证机制相互补充。签名防止伪造,时间戳防止重放,幂等性防止重复处理。
关于幂等性设计的更多细节,参见 idempotency-check。
配置管理:单一真相源
早期系统使用多个独立的 .toml 配置文件。这带来了维护问题:
- 配置分散,难以理解全局依赖
- 重复定义相同的环境变量
- 不同 Worker 使用不同版本的依赖
解决方案是统一配置:
# 主 Worker 配置
name = "api-gateway"
main = "src/api-gateway.js"
# 其他 Workers 通过 [[workers]] 节点定义
[[workers]]
name = "ai-processor"
main = "src/ai-processor.js"
# 全局 KV 和 Queue 绑定
[[kv_namespaces]]
binding = "MODEL_CACHE_KV"
[[queues.producers]]
queue = "ai-task-queue"
binding = "AI_QUEUE"
这种结构让你可以在一个文件中看到所有 Workers 和它们的依赖关系。修改环境变量时,你知道会影响哪些服务。
好的配置文件本身就是文档。
通过阅读 wrangler.toml,你可以理解系统的整体架构:有哪些 Workers,它们的入口点在哪,使用了哪些外部资源。
这比单独的架构文档更可靠,因为配置是可执行的。如果配置错误,系统会立即失败。
性能优化的权衡
并发处理是性能的核心。
队列批量消费 - 一次处理多个消息,减少网络往返
export default {
async queue(batch, env) {
const promises = batch.messages.map(msg =>
this.handleMessage(msg, env)
);
await Promise.all(promises);
}
}
连接复用 - 数据库连接池,避免频繁建立连接
KV 缓存 - 模型配置缓存在 KV 中,减少数据库查询
但并发也带来了新问题。当多个 Worker 同时更新同一条数据库记录时,如何避免竞态条件?
目前通过数据库的乐观锁和事务处理。这不是完美的解决方案,但在当前规模下足够。
架构的演化
这个架构不是一次性设计的。它从单体 Worker 开始,随着问题逐步演化。
第一阶段:所有逻辑在一个 Worker 中,简单但脆弱 第二阶段:拆分出 AI 处理器,解决超时问题 第三阶段:引入队列,实现异步处理 第四阶段:添加监控和错误恢复,提高可靠性
每个阶段都是对新问题的响应。架构不是预先规划的,而是在约束条件下持续调整的结果。
关于从意图到架构的思考过程,参见 from-intent-to-architecture。
分布式系统的复杂度不可避免。你能做的是选择在哪里承担这个复杂度。
Purikura 选择在基础设施层承担复杂度(队列、追踪、错误恢复),让业务逻辑保持简单。这是一种权衡,但对于快速迭代的产品,这个选择是合理的。
最后更新:2025-11-06
Related Posts
Articles you might also find interesting
多厂商 AI 调度:统一混乱的供应商生态
当你依赖第三方 AI 服务时,单点故障是最大的风险。多厂商调度不只是技术架构,更是对不确定性的应对策略。
Studio 系统架构:从状态机到端到端流程
深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑
统一积分系统的设计实践
从多套积分到单一积分池的架构演进,以及背后的原子性、一致性设计
定价界面优化的三层方法
数据诚实、决策引导、视觉精调——三层递进的优化方法。从移除虚假功能到帮助用户选择,再到像素级修复,每一步都在解决真实问题
适配器模式:对现实的妥协
当 PayPro 要求 IP 白名单而 Stripe 不需要,当一个按秒计费另一个按请求计费,架构设计不是消除约束——而是管理约束。适配器模式不是优雅设计,而是对现实混乱的务实投降。
离屏渲染:照片捕获为什么需要独立的 canvas
实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。
集中式配置:让 Reddit 组件脱离重复泥潭
当同一份数据散落在多个文件中,维护成本呈指数级增长。集中式配置不是技术选择,而是对抗熵增的必然手段。
约束驱动设计:为何选择内存追踪
在 Cloudflare Workers 环境中实现追踪系统,持久化和内存存储之间的权衡不是技术偏好,而是约束驱动的必然选择。
让文档跟着代码走
文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。
双重导出管道的架构选择
在用户生成内容场景中,速度与质量的权衡决定了导出架构。理解两种不同管道的设计逻辑,能够更准确地把握产品体验的边界。
Purikura的页面系统
通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。
重复数据的迁移实践:从 N 个文件到 1 个真相源
当同一份 Reddit posts 配置散落在多个文件中,维护成本以文件数量指数增长。迁移到集中式配置不是技术选择,而是对复杂度的清算。
在运行的系统上生长新功能
扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。
分层修复
生产问题没有银弹。P0 止血,P1 加固,P2 优化。优先级不是排序,而是在不确定性下的决策框架。
Studio 前端架构:从画布到组件的设计思考
深入 Purikura Studio 前端架构设计,探讨 DOM-based 画布、状态管理和组件化的实践经验
Context 驱动的认证状态管理
认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。
第三方回调的状态映射完整性
KIE.AI 视频生成的三个修复揭示了一个本质问题:回调不只是接收结果,是建立状态映射。没有 vendor_task_id,系统就失去了追溯能力。