分布式 Workers 的解耦设计

3 min read
Zekari
Purikura 项目系统架构Cloudflare 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 调度:统一混乱的供应商生态

3 min read

当你依赖第三方 AI 服务时,单点故障是最大的风险。多厂商调度不只是技术架构,更是对不确定性的应对策略。

Purikura 项目系统架构
Read More

Studio 系统架构:从状态机到端到端流程

3 min read

深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑

Purikura 项目系统架构
Read More

统一积分系统的设计实践

2 min read

从多套积分到单一积分池的架构演进,以及背后的原子性、一致性设计

系统架构数据库设计
Read More
Featured

定价界面优化的三层方法

4 min read

数据诚实、决策引导、视觉精调——三层递进的优化方法。从移除虚假功能到帮助用户选择,再到像素级修复,每一步都在解决真实问题

UI/UX定价策略
Read More

适配器模式:对现实的妥协

4 min read

当 PayPro 要求 IP 白名单而 Stripe 不需要,当一个按秒计费另一个按请求计费,架构设计不是消除约束——而是管理约束。适配器模式不是优雅设计,而是对现实混乱的务实投降。

系统架构支付集成
Read More

离屏渲染:照片捕获为什么需要独立的 canvas

2 min read

实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。

架构设计前端开发
Read More

集中式配置:让 Reddit 组件脱离重复泥潭

2 min read

当同一份数据散落在多个文件中,维护成本呈指数级增长。集中式配置不是技术选择,而是对抗熵增的必然手段。

架构设计React组件
Read More

约束驱动设计:为何选择内存追踪

2 min read

在 Cloudflare Workers 环境中实现追踪系统,持久化和内存存储之间的权衡不是技术偏好,而是约束驱动的必然选择。

架构设计Cloudflare Workers
Read More

让文档跟着代码走

2 min read

文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。

文档软件工程
Read More

双重导出管道的架构选择

2 min read

在用户生成内容场景中,速度与质量的权衡决定了导出架构。理解两种不同管道的设计逻辑,能够更准确地把握产品体验的边界。

架构设计图像导出
Read More

Purikura的页面系统

3 min read

通过五层分层继承复用架构,实现零代码修改的页面生成系统。从类型定义到页面渲染,每一层专注单一职责,实现真正的数据驱动开发。

架构设计React
Read More

重复数据的迁移实践:从 N 个文件到 1 个真相源

3 min read

当同一份 Reddit posts 配置散落在多个文件中,维护成本以文件数量指数增长。迁移到集中式配置不是技术选择,而是对复杂度的清算。

架构设计配置管理
Read More

在运行的系统上生长新功能

3 min read

扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。

系统设计架构
Read More

分层修复

3 min read

生产问题没有银弹。P0 止血,P1 加固,P2 优化。优先级不是排序,而是在不确定性下的决策框架。

工程实践问题修复
Read More

Studio 前端架构:从画布到组件的设计思考

3 min read

深入 Purikura Studio 前端架构设计,探讨 DOM-based 画布、状态管理和组件化的实践经验

Purikura 项目前端架构
Read More

Context 驱动的认证状态管理

3 min read

认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。

软件设计认证系统
Read More

第三方回调的状态映射完整性

5 min read

KIE.AI 视频生成的三个修复揭示了一个本质问题:回调不只是接收结果,是建立状态映射。没有 vendor_task_id,系统就失去了追溯能力。

Purikura 项目系统设计
Read More