后台作业

1. 什么是后台作业?

定义: 在主请求-响应周期之外异步运行的任何逻辑。

示例

  • 电子邮件通知: 在用户注册后发送“欢迎”电子邮件,这样他们就不必等待邮件服务器才能看到仪表板。
  • 图像处理: 生成多个缩略图大小或在后台对上传的照片应用滤镜。
  • 数据导出: 生成大量 CSV 或 PDF 报告,编译可能需要 30 秒以上。
  • 第三方同步: 将数据推送到 CRM(如 Salesforce)或更新外部搜索索引(如 Algolia),而不阻塞 UI。

为什么使用它们?

主要目标是卸载耗时的非关键任务,以便主后端 API 保持响应并防止请求超时。

效益 描述
响应能力 当工作稍后进行时,用户会立即收到“成功”消息。
可扩展性 您可以独立于 Web 服务器扩展后台“工作人员”。
弹性 如果作业失败(例如,外部 API 关闭),可以自动重试,而不会导致用户会话崩溃。

它是如何工作的(概念流程)

  1. 生产者: 您的 API 接收请求并将任务“推送”到队列中。
  2. 代理: 存储层(如 Redis 或 RabbitMQ)保存任务。
  3. Worker: 一个单独的进程从队列中拉取任务并执行逻辑。

架构概述

  1. 用户触发器(生产者)

    • 用户在应用程序上注册。
    • 主应用程序进程创建一个包含注册数据的任务
  2. 消息队列(缓冲区)

    • 任务被序列化(从本机对象转换为 JSON 等格式)并存储在 队列(例如 Redis、RabbitMQ)中。
    • 这可确保主进程不会因等待电子邮件发送而减慢速度。
  3. 消费者(后台进程)

    • 一个单独的进程(消费者)轮询队列并拉出任务。
    • 反序列化: 使用者将原始数据(JSON/字符串)转换回本机语言结构(Python dict、Go struct 或 Node.js object)。
  4. 任务处理程序

    • 消费者将数据传递给 Handler
    • Handler 的工作是构造有效负载:
      • 为电子邮件生成 HTML 模板。
      • 定义发送者和接收者地址。
      • 管理邮件提供商的 API 密钥。
  5. 外部API调用

    • 处理程序调用外部电子邮件服务提供商(例如,SendGrid、AWS SES、Mailgun)。

错误处理和重试

如果任务失败(例如,电子邮件 API 关闭),系统将采用可靠性策略:

指数退避

系统不会立即重试并使服务器不堪重负,而是在每次尝试之间等待越来越长的时间。

逻辑:

  • 第一次失败: 2 秒后重试。
  • 第二次失败: 4 秒后重试。
  • 第三次失败: 8 秒后重试。
  • 公式: $t = b^n$ (其中 $b$ 是基本延迟,$n$ 是尝试次数)。

🛠️汇总表

组件 责任
制片人 捕获用户数据并推送到队列。
队列 可靠地保存任务,直到消费者准备好为止。
消费者 在单独的进程中运行;处理“繁重的工作”。
反序列化 将存储的格式恢复为代码就绪对象。
处理程序 准备逻辑并调用外部 API。
指数退避 通过间隔重试确保系统在中断期间不会崩溃。

2. 核心架构

该系统遵循解耦的生产者-消费者模式,允许主应用程序保持高性能,同时将繁重的任务卸载给后台工作人员。

组件 角色 主要责任
制片人 应用逻辑 序列化任务数据(例如 JSON)并将其推送到队列中。
经纪人 消息队列 充当任务的临时保存区域(缓冲区)。
消费者 工人进程 监视队列、出队、反序列化和执行处理程序。

🛠️ 组件细分

📤 制片人

Producer 是应用程序代码中的入口点。当特定事件发生时(例如用户注册),生产者:

  • 收集必要的数据。
  • 将该数据序列化为标准化格式(通常是 JSON)。
  • 将消息发送给 Broker。

🗄️ 经纪人/队列

Broker 确保在消费者繁忙或系统重启时任务不会丢失。它为您的数据提供了一个持久的“等候室”。

  • 常用技术: RabbitMQRedisAWS SQS
  • 目的: 将生产者的速度与消费者的速度分离。

⚙️ 消费者/工人

消费者是一个独立的进程——通常运行在不同的线程或服务器上——处理“繁重的工作”。

  • 轮询: 它不断监视队列中的新任务。
  • 反序列化: 它将 JSON/Binary 转换回本机语言对象(例如,Python Dict 或 Go Struct)。
  • **执行:**它将数据传递给 处理程序 以执行最终作业(例如发送电子邮件或处理图像)。

3. 可靠性和性能

可见性超时

可见性超时是消息队列(例如 AWS SQS、RabbitMQ)中使用的一种关键安全机制,用于确保容错。

  • 机制: 当消费者检索消息时,消息不会被删除。相反,队列会在预定义的持续时间内向其他消费者隐藏它。
  • 故障保护: 如果工作人员在超时到期之前 崩溃或失败 确认任务,则该消息将自动再次变得可见。
  • 结果: 这确保了“至少一次”交付,允许另一个工作人员接手任务,这样任务就不会因短暂的系统故障而丢失。

[!重要]
关键概念: 只有在工作线程成功处理任务并发送明确的 删除/确认 (ACK) 命令后,任务才会从队列中永久删除。


重试和指数退避

为了优雅地处理间歇性故障,系统采用了增加尝试之间等待时间的策略。

  • 重试: 盟友重新参与当由于临时问题(例如网络抖动或外部服务停机)而失败时自动清空任务。
  • 指数退避: 系统不会立即重试(这可能会压垮陷入困境的服务),而是会在每次尝试之间逐渐等待更长的时间(例如,1 秒、2 秒、4 秒、8 秒…)。
  • 服务恢复: 这个“呼吸空间”可以防止 重试风暴,为下游服务提供时间恢复并最终成功处理请求。

4. 任务类型

(1) 一次性任务

执行一次的单一、独立的工作单元。这些通常由特定的用户操作或系统事件触发。

  • 示例: 用户注册后发送欢迎电子邮件。
  • 特点: 高优先级,立即执行,不重复。

(2) 重复任务

根据特定计划重复运行的任务(通常通过 Cron 表达式管理)。

  • 示例: 每周一上午 9:00 生成每周账单报告。
  • 特征: 可预测、自动化且依赖时间。

(3) 链式任务

一系列任务,其中一个任务的完成会触发下一个任务。这创建了一个功能管道。

  • 示例: 步骤 1: 上传图像 → 步骤 2: 调整图像大小 → 步骤 3: 存储在 S3 中。
  • 特点: 顺序依赖;如果某一步骤失败,后续步骤通常会停止。

(4) 批量任务

执行大量相似的任务,这些任务组合在一起以便立即处理,通常是为了优化资源使用。

  • 示例: 在工作日结束时处理 10,000 笔信用卡交易。
  • 特点: 高吞吐量,通常是非交互式的,并在低流量期间进行处理。

5. 任务队列/后台作业的设计注意事项

(1) 幂等性

幂等性确保多次执行某个操作与执行一次具有相同的效果。在分布式系统中,任务可能会被多次交付(至少一次交付)。

  • 实现: 使用唯一的任务 ID 或“幂等键”来检查任务在再次执行之前是否已被处理。
  • 为什么重要: 防止重复操作,例如对同一订单向客户收取两次费用。

(2) 错误处理

系统必须妥善处理故障,以防止数据丢失或无限循环。

  • 策略: * 重试: 自动重新运行失败的任务。
    • 死信队列 (DLQ): 如果任务重复失败,请将其移动到单独的队列以进行手动检查。
    • 正常关闭: 确保工作人员在部署期间停止之前完成当前任务。

(3) 监控

队列运行状况的可见性对于维护系统可靠性至关重要。

  • 关键指标: * 队列深度: 待处理任务的数量(表明您是否落后)。
    • 消费者延迟: 添加任务和处理任务之间的时间差。
    • 失败率: 达到 DLQ 的任务百分比。

(4) 缩放

随着任务量的增加,系统必须能够处理增加的负载。

  • 水平扩展: 添加更多工作节点/进程以并行消耗队列中的任务。
  • 自动扩展: 根据 队列深度 指标自动启动工作线程。

(5) 订购

在某些情况下,任务必须按照接收的确切顺序进行处理(FIFO - 先进先出)。

  • 挑战: 标准队列通常优先考虑吞吐量而不是严格的排序。
  • 解决方案: 使用专门的 FIFO 队列或“消息分组”(例如 SQS FIFO)来确保特定数据集的顺序处理。

(6) 速率限制

速率限制控制任务处理的速度,以避免压垮下游资源(例如第三方 API 或数据库)。

  • 实现: 使用“令牌桶”或“漏桶”算法来限制工作人员。
  • 为什么重要: 防止您的后台作业意外地对您自己的基础设施执行拒绝服务 (DoS) 攻击。