TaskQueuesAndBackgroundJobs
后台作业
1. 什么是后台作业?
定义: 在主请求-响应周期之外异步运行的任何逻辑。
示例
- 电子邮件通知: 在用户注册后发送“欢迎”电子邮件,这样他们就不必等待邮件服务器才能看到仪表板。
- 图像处理: 生成多个缩略图大小或在后台对上传的照片应用滤镜。
- 数据导出: 生成大量 CSV 或 PDF 报告,编译可能需要 30 秒以上。
- 第三方同步: 将数据推送到 CRM(如 Salesforce)或更新外部搜索索引(如 Algolia),而不阻塞 UI。
为什么使用它们?
主要目标是卸载耗时的非关键任务,以便主后端 API 保持响应并防止请求超时。
| 效益 | 描述 |
|---|---|
| 响应能力 | 当工作稍后进行时,用户会立即收到“成功”消息。 |
| 可扩展性 | 您可以独立于 Web 服务器扩展后台“工作人员”。 |
| 弹性 | 如果作业失败(例如,外部 API 关闭),可以自动重试,而不会导致用户会话崩溃。 |
它是如何工作的(概念流程)
- 生产者: 您的 API 接收请求并将任务“推送”到队列中。
- 代理: 存储层(如 Redis 或 RabbitMQ)保存任务。
- Worker: 一个单独的进程从队列中拉取任务并执行逻辑。
架构概述
用户触发器(生产者)
- 用户在应用程序上注册。
- 主应用程序进程创建一个包含注册数据的任务。
消息队列(缓冲区)
- 任务被序列化(从本机对象转换为 JSON 等格式)并存储在 队列(例如 Redis、RabbitMQ)中。
- 这可确保主进程不会因等待电子邮件发送而减慢速度。
消费者(后台进程)
- 一个单独的进程(消费者)轮询队列并拉出任务。
- 反序列化: 使用者将原始数据(JSON/字符串)转换回本机语言结构(Python
dict、Gostruct或 Node.jsobject)。
任务处理程序
- 消费者将数据传递给 Handler。
- Handler 的工作是构造有效负载:
- 为电子邮件生成 HTML 模板。
- 定义发送者和接收者地址。
- 管理邮件提供商的 API 密钥。
外部API调用
- 处理程序调用外部电子邮件服务提供商(例如,SendGrid、AWS SES、Mailgun)。
错误处理和重试
如果任务失败(例如,电子邮件 API 关闭),系统将采用可靠性策略:
指数退避
系统不会立即重试并使服务器不堪重负,而是在每次尝试之间等待越来越长的时间。
逻辑:
- 第一次失败: 2 秒后重试。
- 第二次失败: 4 秒后重试。
- 第三次失败: 8 秒后重试。
- 公式: $t = b^n$ (其中 $b$ 是基本延迟,$n$ 是尝试次数)。
🛠️汇总表
| 组件 | 责任 |
|---|---|
| 制片人 | 捕获用户数据并推送到队列。 |
| 队列 | 可靠地保存任务,直到消费者准备好为止。 |
| 消费者 | 在单独的进程中运行;处理“繁重的工作”。 |
| 反序列化 | 将存储的格式恢复为代码就绪对象。 |
| 处理程序 | 准备逻辑并调用外部 API。 |
| 指数退避 | 通过间隔重试确保系统在中断期间不会崩溃。 |
2. 核心架构
该系统遵循解耦的生产者-消费者模式,允许主应用程序保持高性能,同时将繁重的任务卸载给后台工作人员。
| 组件 | 角色 | 主要责任 |
|---|---|---|
| 制片人 | 应用逻辑 | 序列化任务数据(例如 JSON)并将其推送到队列中。 |
| 经纪人 | 消息队列 | 充当任务的临时保存区域(缓冲区)。 |
| 消费者 | 工人进程 | 监视队列、出队、反序列化和执行处理程序。 |
🛠️ 组件细分
📤 制片人
Producer 是应用程序代码中的入口点。当特定事件发生时(例如用户注册),生产者:
- 收集必要的数据。
- 将该数据序列化为标准化格式(通常是 JSON)。
- 将消息发送给 Broker。
🗄️ 经纪人/队列
Broker 确保在消费者繁忙或系统重启时任务不会丢失。它为您的数据提供了一个持久的“等候室”。
- 常用技术:
RabbitMQ、Redis和AWS 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) 攻击。