> ## Content Index
> Fetch the complete content index at: https://www.hubwiz.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# 基于Celery的AI生成队列实战故事
- URL: https://www.hubwiz.com/blog/a-real-world-story-of-building-an-ai-generation-queue-with-celery/
- Published: 2026-04-27T18:57:06.000Z
- Updated: 2026-04-27T18:57:06.000Z
- Description: 我运营着 Apatero，这是一个基于 Django、Celery 和 Redis 构建的 AI 图像和视频生成平台。我们每周通过多个 GPU 提供商处理数以千计的 AI 生成任务，每一个任务都要经过我们的任务队列。
- Author: admin
- Tags: APPLICATION, CELERY, #Import 2026-10-14 14:41

上周四晚上 11 点，我看到我们的 Celery 队列深度达到了 247 个待处理任务。四个 Worker 全部卡在处理 Veo 3.1 视频生成上，每个任务都要耗费四分多钟。

那些本应在 30 秒内完成的新图片生成请求只能排在后面等待。用户盯着不停旋转的加载动画。有些用户在刷新页面，有些在反复点击生成按钮，让情况变得更糟。

> 这只是一个普通的周四晚上。

我运营着 [Apatero](http://app.apatero.com/?ref=hubwiz.com)，这是一个基于 Django、Celery 和 Redis 构建的 AI 图像和视频生成平台。我们每周通过多个 GPU 提供商处理数以千计的 AI 生成任务，涵盖图片、视频、3D 模型和音频。每一个任务都要经过我们的任务队列。

我在这里要告诉你的是，将 Celery 用于 AI 工作负载，完全不像教程博客文章中描述的那样简单。

这不是一篇配置指南——那篇我已经写过了。这是我收集的生产环境实战故事，我希望在我通过凌晨盯着仪表盘变红来吸取这些教训之前，有人能跟我分享过这些经验。

## 1、为什么 AI 工作负载打破常规

正常的 Celery 工作负载是可预测的。发送邮件，耗时 2 秒。调整图片大小，耗时 5 秒。处理 Webhook，耗时 200 毫秒。你可以据此规划容量。你大致知道每种任务类型需要多长时间，并可以相应地设置超时。

AI 生成任务不是这样的。

在 Fal.ai 上进行一次 FLUX 图像生成大约需要 15 到 45 秒，具体取决于模型变体、分辨率、其基础设施的当前负载，以及——似乎还有月相。Kling 视频生成需要 60 到 180 秒。Veo 3.1 生成可能需要 2 到 5 分钟，有时甚至更久。

再考虑到这些任务并不是在你自己的硬件上运行的。你调用的是外部 API——Fal.ai、RunPod、Replicate，或任何你使用的提供商。每一个都有自己的队列、自己的容量限制、自己的故障模式。你的任务不是"处理这些数据"，而是"调用这个外部 API，等待一段不可预测的时间，处理返回的任何结果，如果什么都没返回还要妥善处理"。

> 这改变了一切关于你如何设计队列系统的方式。

第一个改变的是超时设置。对于普通任务，你设置 30 秒的软超时、60 秒的硬超时就完事了。对于 AI 生成，你不能这样做。如果你的超时终止了一个已经进行了 3 分钟的 4 分钟视频生成任务，你就浪费了那些计算资源。用户什么都没得到，你承担了成本，而且用户很生气。

第二个改变的是重试逻辑。当普通 API 调用失败时，你使用指数退避重试，很简单。当 AI 生成调用失败时，你需要问一个更难的问题：生成失败是因为 API 暂时宕机了？还是因为 prompt 不好？还是因为模型本身在处理某些内容时出了问题？重试一个错误的 prompt 只是把钱烧两次。

第三个问题是成本。一封发送失败的邮件不花你一分钱。一次消耗了 GPU 时间的失败视频生成则花你真金白银。每一个重试决定都是一个财务决定。

## 2、技术栈及其有效的原因

我们的配置很直接。**Django** 处理 Web 层，**Celery** 处理异步任务执行，Redis 同时作为消息代理和结果后端。

我看到有人问为什么不用 **RabbitMQ**，或者为什么不用 **Django Channels**，或者为什么不直接用简单的数据库队列。

我的回答是：Redis 加 Celery 能毫无波澜地处理我们的工作负载。Redis 很快，我们已经用它做缓存了，而且少维护一个服务的运维开销对于小团队来说很重要。RabbitMQ 在复杂的路由模式和消息保证方面很棒，但对于我们的用例，Redis 已经足够可靠了。在一年多的生产使用中，我们没有因为 Redis 故障丢失过一个任务。

对于 AI 工作负载来说，真正重要的关键配置决策：

Worker 预取乘数设为 1。这非常关键。默认的预取设置会导致 Worker 一次抓取多个任务。这对于快速任务没问题。但对于需要几分钟的 AI 生成任务，你绝对不希望一个 Worker 占着 4 个排队任务而其他 Worker 却闲着。每次只处理一个任务，永远如此。

**可见性超时设置得很高**。当一个 Worker 从 Redis 取走一个任务时，Redis 需要知道等多久才认为 Worker 已死并重新投递任务。如果你的视频生成需要 5 分钟而可见性超时是 3 分钟，Redis 会在第一个 Worker 还在处理时就把任务重新投递给另一个 Worker。这样你就会有重复生成、双倍成本和困惑的用户。我们把超时设为 30 分钟。虽然激进，但很安全。

任务确认设为延迟确认。这意味着 Worker 只在实际完成后才告诉 Redis"我完成了这个任务"，而不是在开始时就确认。如果 Worker 在生成过程中崩溃，任务会自动回到队列中。这在早期 Worker 随机被 OOM Kill 的日子里帮了我们无数次。

## 3、2026 年 2 月的 Veo 大拥堵

![None](https://www.hubwiz.com/blog/content/images/2026/04/1-6BeCZldDgEEkYVoGXWZE7w.png)

这是我最喜欢的实战故事，因为它教会我最多。

我们在 1 月份集成了 Veo 3.1，Google 的视频模型。价格昂贵，质量令人印象深刻，生成时间却让我们现有的队列架构显得不堪一击。

Veo 生成平均大约 3.5 分钟，有些要 5 分钟。当时我们有四个 Celery Worker，全部从同一个默认队列拉取任务。

一个周二晚上，几个用户几乎同时发现了 Veo。在大约 20 分钟内，所有四个 Worker 都被 Veo 生成任务占满了。每个 Worker 都在那里轮询 Fal.ai API，等待 Google 的 GPU 完成视频渲染。

与此同时，图片生成请求开始堆积。FLUX 图片只需 20 秒就能生成。用户提交 prompt，得不到响应，再次点击生成，往队列里添加更多任务。队列深度在一小时内从 10 攀升到 50、100，直到超过 200。

付费用户和免费用户受到的影响一样。所有人都在同一条队列里。

> 解决方案是优先级队列，我应该从第一天就设置好。

我们现在运行三个独立的 Celery 队列：high、default 和 low。付费用户进入 high，免费用户进入 default，后台维护任务进入 low。我们为每个队列运行专属 Worker，并为高优先级分配更多 Worker。

但大多数文章都忽略了一点：你不能完全饿死低优先级队列。如果免费用户永远得不到他们的生成结果处理，他们就永远不会转化为付费用户。整个商业模式依赖于免费用户有足够好的体验从而想要更多。

所以我们按比例分配。每 3 个高优先级 Worker，我们保留 1 个 Worker 处理默认队列。免费用户等得更久，但不会永远等下去。队列始终在流动。

我们还按任务时长进行了拆分。短任务（图片、音频）和长任务（视频、3D）进入不同的队列，不论优先级如何。这样一批视频生成任务就不会阻塞图片请求。一个 20 秒的 FLUX 生成不应该卡在 5 分钟的 Veo 渲染后面。

## 4、不烧钱的重试逻辑

Fal.ai 是我们大多数模型的主要提供商。他们的 API 总体可靠，但每个外部 API 都有故障模式：服务器错误、超时错误、速率限制错误、偶尔的 500 错误，通常在 30 秒内自行恢复。

最简单的方法是对所有错误使用指数退避重试。第一次重试等 10 秒，然后 30 秒，然后 90 秒，然后 270 秒。

> 我们一开始就是这么做的，结果花了不少冤枉钱。

问题在于，并非所有失败都是暂时的。如果生成失败是因为模型拒绝了 prompt，重试毫无意义。如果失败是因为我们的账号触发了速率限制，立即重试只会让情况更糟。如果失败是因为模型确实宕机了，10 分钟内重试 4 次只是把钱烧 4 次。

以下是我们现在的重试逻辑实际工作方式。

对于 HTTP 429（速率限制）：我们严格按照 retry-after 头部信息执行。不猜测。如果 API 说等 60 秒，我们就等 60 秒。我们使用 Celery 的 countdown 参数在准确的时间安排重试。

对于 HTTP 500 或 502（服务器错误）：使用指数退避最多重试 3 次，起始间隔 15 秒。这些通常是暂时性的。

对于 HTTP 400（错误请求）：不重试。输入有误。立即退还用户的积分并发送通知解释失败原因。用同样的错误输入再试一次只是浪费所有人的时间。

对于超时（无响应）：这是最棘手的。生成是否实际发生了，只是我们丢失了响应？还是完全失败了？我们在重试前检查提供商的 API 获取任务状态。如果任务在他们那边仍在运行，我们就等待它而不是创建重复任务。如果确实在他们那边超时了，我们重试一次。

对于其他任何情况：退还积分，记录完整错误，通知团队。未知的故障模式需要人工关注，而不是自动重试。

这个系统与简单的"重试一切"方法相比，将我们浪费的 API 支出减少了大约 40%。

## 5、冷启动税

RunPod 是我们用于自定义模型推理的提供商。我们在其无服务器 GPU 基础设施上运行一些微调模型。优点是你只需为使用量付费，缺点是冷启动。

当一段时间没有请求进来时，RunPod 会将你的端点缩减到零个 Worker。从成本角度看这是合理的。但当下一个请求到达时，GPU Worker 需要启动、将模型加载到 VRAM 中，然后才能准备处理。这需要 30 到 90 秒，取决于模型大小。

用户不知道这些。他们点击生成，期望 20 秒内得到结果。实际上他们要等 90 秒只为了让 Worker 预热，之外还有实际的生成时间。

我们尝试了几种方法。

保活心跳。每 5 分钟发送一个虚拟请求以保持至少一个 Worker 是热的。这有效但 24/7 都要花钱。对于流量稳定的模型，这笔账算得过来。对于每天只被使用几次的模型，你最终为空闲 GPU 时间支付的费用会超过冷启动的代价。

我们最终采用的是诚实的方式。我们根据最近的端点活动估算冷启动时间，然后告诉用户。"这个模型最近没有被使用过。预计等待时间：90 秒。"没有假的进度条，没有假装生成已经开始了而实际上只是在等待 GPU。用户对诚实的赞赏超出你的想象。

我们还会在看到流量模式形成时主动预热 Worker。周一早上总有流量高峰。所以我们在每周一早上 8 点发送预热请求。小事一桩，但在感知响应速度上有巨大差异。

## 6、限制生成按钮的速率

有些事没人提醒过你。有些用户会连续点击生成按钮 15 次。不是因为恶意，而是因为不耐烦，或者没有意识到第一次点击已经生效了，或者他们想生成 15 个变体并认为点得越快就全部越快完成。

没有速率限制的话，每次点击都会创建一个新任务。15 次点击，15 个任务进入队列，15 次 API 调用到你的 GPU 提供商。你的队列被填满，成本飙升，用户得到 15 个几乎相同的结果。

**我们在两个层面处理这个问题。**

前端速率限制：点击生成后，按钮禁用 5 秒。简单，对偶然的双击很有效。这不是安全问题，这是用户体验。

后端速率限制：每个用户最多 3 个并发生成任务。如果他们试图在三个任务还在处理中时提交第四个，我们会拒绝并给出明确消息。"您有 3 个生成任务正在进行中，请等待其中一个完成。"

我们还有每分钟 10 次提交的速率限制。这能捕获自动化滥用和脚本。合法用户永远不会触发这个，但我们已经抓到了少数试图通过前端使用我们的 API 进行大规模生成而不支付相应积分的人。

速率限制器当然运行在 Redis 中。我们使用以用户 ID 为键的滑动窗口计数器。检查速度快，原子操作，而且与所有其他功能共享同一个 Redis 实例。

## 7、监控：我如何知道什么时候出了问题

我以前是通过用户投诉才发现队列问题的。那不叫监控策略，那叫祈祷策略。

现在我们使用 Prometheus 进行指标收集，使用 Grafana 进行仪表板和告警。我们监控的具体内容：

按队列名称的队列深度。如果任何队列超过 50 个待处理任务，我会收到告警。如果超过 100 个，告警升级。这是最有用的单一指标。不断增长的队列意味着要么 Worker 太慢，要么 Worker 卡住了，要么流量激增。

按任务类型的任务持续时间。我们追踪每个生成模型的 p50 和 p95 执行时间。如果 FLUX 图像生成的 p95 突然从 30 秒变成 90 秒，说明提供商那边有什么变化了。我们希望在用户开始投诉之前就知道。

Worker 利用率。有多少 Worker 在忙，多少空闲。如果所有 Worker 100% 的时间都在忙，我们需要更多 Worker。如果 Worker 80% 的时间空闲，我们在浪费钱。

按任务类型的失败率。如果某个特定模型的失败率飙升到 5% 以上，我们会收到告警。这有时比提供商自己的状态页面更新还要早地捕获到了提供商宕机。

**Redis 内存使用量。** Redis 在内存中存储我们的队列数据。如果内存使用量攀升到可用量的 80% 以上，我们需要调查。通常意味着任务积累速度快于处理速度，或者结果数据没有被清理。

Grafana 仪表板与所有其他服务一起运行在我们的 K3s 集群上。整个监控栈——Prometheus、Grafana、用于日志的 Loki、用于路由告警的 Alertmanager——都运行在与我们的应用相同的 OVH 服务器上。不起眼，但极其实用。

最救命的告警是"任务卡住"告警。如果任何任务处于"活动"状态超过 10 分钟仍未完成，就说明出问题了。要么 Worker 静默崩溃了，要么外部 API 无限期挂起了，要么某个地方出现了死锁。这个告警大概每个月触发两次，每次都能捕获到真正的问题。

## 8、当生成失败时：退款之舞

一个用户提交了一个视频生成任务，花费 126 积分。任务进入队列，一个 Worker 取走它，API 调用发送到 Fal.ai。三分钟过去了，Fal.ai 返回了 500 错误，生成失败了。

接下来发生的事情比大多数创始人意识到的更重要。

我们的故障处理流程：

任务捕获异常并记录带有上下文的完整错误信息：用户 ID、prompt（为保护隐私进行了哈希）、模型、提供商响应、时间戳。

我们使用上述逻辑检查这是否是可重试的错误。如果是，我们用适当的退避策略重试。用户在通知栏看到"正在重试您的生成…"。

如果所有重试用尽，或者错误从一开始就不可重试，我们立即退还积分。不是"24 小时内"，不是"联系客服"。积分在失败确认后几秒内就回到他们的账户。

我们向用户发送实时通知。"您的视频生成失败了。126 积分已退回到您的账户。错误：模型在处理您的请求时遇到了问题。"不是含糊的"出了点问题"。在可能的情况下给出实际解释。

失败信息被记录到我们的分析系统中。如果某个特定模型开始异常频繁地失败，我们可以及早发现模式。

即时退款是一个深思熟虑的商业决策。有些平台让你开客服工单才能在生成失败后拿回积分。我们试了大约一周。对于一个小组队来说，客服量是不可持续的，用户的不满在我们的留存数据中清晰可见。确认失败后的自动退款不花我们什么钱（反正我们会退），却节省了数小时的客服时间。

花了几个月才搞好的一个边缘情况是：部分失败。API 返回 200 状态码，但生成的内容是垃圾——一帧黑屏的视频，或一张完全不连贯的图片。从 API 的角度看生成"成功"了，但结果无法使用。

我们添加了基本的质量检查。对于图片，我们验证输出尺寸和文件大小。一个以 0 字节返回或尺寸为 1x1 的图片就是失败的生成，不管 API 状态码说了什么。对于视频，我们检查时长和文件大小。一个 5 秒的视频只有 2KB，那不是真正的视频。

这并不能捕获所有质量问题。如果模型生成了技术上有效但审美上糟糕的图片，那就是 AI 有时的工作方式。我们不能自动退款糟糕的艺术。但我们可以捕获那些永远不应该到达用户的明显失败。

## 9、扩展：更多 Worker 还是更聪明的 Worker

当队列开始持续积压时，你有两个选择。添加更多 Worker，或者让现有 Worker 更快。

我的直觉一直是添加更多 Worker。这似乎是显而易见的解决方案。队列慢了，加容量。但我用惨痛的代价学到了：更多 Worker 并不总是有帮助，有时还会适得其反。

更多 Worker 意味着对你的提供商更多并发 API 调用。如果 Fal.ai 已经因为负载高而响应缓慢，用 8 个并发请求而不是 4 个轰炸他们并不会让每个请求更快，反而可能更慢。一些提供商在你激增并发请求时会更严厉地限制速率。

更多 Worker 还意味着更多内存。每个 Celery Worker 是一个独立的进程（或线程，取决于你的池配置）。每个都把你的 Django 应用加载到内存中。在内存有限的服务器上，从 4 个增加到 8 个 Worker 可能把你推入交换空间区域，让一切都变慢。

实际上比添加 Worker 更有帮助的做法：

按任务时长拆分队列，我已经提到过了。仅此一项可能对感知性能产生了最大影响。

对外部 API 的连接池化。不是每个任务都创建一个新的到 Fal.ai 的 HTTP 连接，我们重用连接。节省了每个请求的 TCP 和 TLS 握手开销。单个请求节省很少，但乘以每天数千个任务，积少成多。

更智能的任务路由。不是所有 Worker 从所有队列拉取任务，而是为特定任务类型配置专属 Worker。处理图片生成的 Worker 与处理视频的 Worker 有不同的资源特征。图片 Worker 可以更轻量，视频 Worker 需要更多耐心（更长的超时、更大的结果存储）。

缓存模型预热调用。对于 RunPod 端点，我们缓存热/冷状态并据此路由任务。如果我们知道某个模型的端点是冷的，我们可以用适当的延迟来排队任务，而不是立即发送并让用户等待一个本可以预先告知的冷启动。

实际的扩展数学：我们在当前服务器上总共运行 6 个 Worker。3 个用于高优先级（付费用户），2 个用于默认（免费用户），1 个用于长时间运行的任务（视频、3D）。这能处理我们当前的负载，队列深度很少超过 15-20 个任务。当我们超越这个规模时，下一步是使用 Celery 内置的多节点支持进行跨多台服务器的水平扩展。但我们还没到那一步，过早扩展只是过早花钱。

## 10、我用惨痛代价换来的教训

最后总结我希望第一天就知道的事情。

将 Worker 预取设为 1。我已经说过了但我再说一遍，因为默认值在长时间运行任务下会毁了你的一天。这一个设置可能比我们遇到的任何其他配置问题都造成了更多的困惑和调试时间。

永远不要只相信 API 状态码。一个带有垃圾数据的 200 响应比 500 更糟糕，因为你的系统认为一切正常。在将每次生成标记为成功之前，验证实际输出。

记录一切。每个任务提交、每次重试、每次失败、每次退款。当凌晨 2 点出了问题时，日志是你和瞎猜之间唯一的东西。我们在每条日志行上使用结构化日志记录，包含任务 ID、用户 ID、模型名称、提供商和时间数据。

测试失败路径比测试正常路径更重要。正常路径没问题，每个人都测试正常路径。测试当 API 在第 3 次重试时返回 500 会发生什么。测试当 Redis 宕机 30 秒会发生什么。测试当 Worker 在任务中途被杀掉会发生什么。这些是让你半夜惊醒的场景。

监控队列深度，而不仅仅是任务成功率。99% 的成功率在队列深度 500 且还在增长时毫无意义。成功率告诉你任务正确完成了，队列深度告诉你是否跟得上需求。

不要重试一切。有些失败是永久性的。重试一个错误的 prompt 或无效的模型配置只是浪费钱。对你的错误进行分类，每种类型不同处理。我们花了几周才搞对，但立即省下了真金白银。

与用户沟通等待时间。当我们开始根据当前队列深度和平均任务时长显示预计等待时间后，关于"生成太慢"的客服工单减少了一半以上。用户实际上并不介意等 2 分钟。他们介意的是不知道 2 分钟是正常的还是出了什么问题。

在你需要之前就建好退款系统。我们是在被关于失败生成的客服请求淹没后才被动建设的。如果我们主动建设，就能省下一个痛苦的手动退款周——通过 Django 管理面板一个个处理。

> 我们的现状

Apatero 每周处理数千个生成任务，涵盖图片、视频、音频和 3D 模型。我们的队列系统处理从 15 秒到 5 分钟不等的可变处理时间，跨多个外部 GPU 提供商，具备自动重试、失败即时退款、付费用户优先队列和实时监控。

它并不完美。我们偶尔仍会遇到让我们措手不及的边缘情况。上周一个提供商在没有通知的情况下更改了错误响应格式，我们的重试逻辑没有识别新的错误码。类似这样的事情让你保持谦逊。

但系统是稳定的。它能处理流量激增而不崩溃。大多数时候能在没有人工干预的情况下从故障中恢复。所有这些都运行在一台运行 K3s 的 OVH 服务器上，成本只是托管队列服务费用的一小部分。

如果你正在构建类似的东西，特别是从异步任务队列调用外部 AI API，我希望这些实战故事能帮你省去我们经历的一些痛苦。教程能帮你入门，生产环境会教你剩下的。

先把你的预取乘数设为 1。拜托。先做这个。

---

原文链接: [How We Handle AI Generation Queues at Scale: Celery + Redis War Stories](https://kgabeci.medium.com/how-we-handle-ai-generation-queues-at-scale-celery-redis-war-stories-ad5ce8f96073?ref=hubwiz.com)

汇智网翻译整理，转载请标明出处