🩺 先用 30 秒判断故障在哪一层
“AI 请求超时”经常不是一个问题,而是浏览器、Serverless Function、应用队列、AI 提供商和回调链路中的某一层先到达了截止时间。先看任务有没有落库、有没有入队、Worker 是否领取、提供商是否返回任务 ID、Webhook 是否到达,再决定修哪一层。
页面一直转圈
前端等待时间过长,或者没有把长任务切成“提交任务 + 查询状态”。先查浏览器请求与 API 状态码。
函数到点被终止
平台函数时限小于提供商处理时间。应提前中止当前调用、落库并转交后台任务,而不是把时限调到最大。
队列反复消费
常见原因是 Worker 完成了外部调用,却没来得及确认消息;需要幂等键、租约和结果唯一约束。
Cron 偶发重复或漏跑
调度只保证触发,不等于业务恰好执行一次。任务生成必须幂等,并用补偿扫描找回卡住任务。
Webhook 收到多次
重试投递和网络不确定性会产生重复事件。必须验签,并以 delivery ID 或事件 ID 去重。
429 后流量更高
所有实例同时立即重试会形成重试风暴。需要限次、指数退避、随机抖动和全局并发上限。
🧱 同步、后台任务、队列和 Cron 怎么分工
一个稳定的 AI 项目通常不让一次 HTTP 请求承担整条长链路。先按用户是否必须立即拿到结果来分流:
-
**同步请求:**适合可在短时间稳定完成、失败后用户可以立即重试的轻任务。请求结束前必须留出序列化、日志和网络抖动余量。
-
**提供商后台任务:**适合模型处理时间不可预测的长任务。以 OpenAI Responses API 为例,可以用 background mode 创建后台响应,再轮询状态或接收完成 Webhook。
-
**应用队列:**负责控制并发、重试、租约、优先级和业务幂等。即使提供商支持后台任务,应用仍要保存本地 job 与 provider job ID 的映射。
-
**Cron:**只负责按时生成任务、扫描卡住任务和触发补偿,不在调度请求里直接执行长时间 AI 工作。
客户端**提交任务 / 查状态 → 任务记录 状态 / 幂等键 → 队列与 Worker 并发 / 重试 / 租约 → AI 提供商 同步或后台任务
Webhook 回来后也不要直接执行复杂业务:验签、去重、记录事件、入队,然后尽快返回成功。这样即使下游数据库或通知服务暂时变慢,也不会让提供商不断重投。
🔁 先把任务状态做成可恢复状态机
只有 success: true/false 无法回答“正在处理、等待重试、卡住、人工停止还是彻底失败”。最少保存以下状态和恢复字段:
type JobStatus =
| "pending"
| "processing"
| "succeeded"
| "failed"
| "dead_letter"
| "cancelled";
type Job = {
id: string;
status: JobStatus;
idempotencyKey: string;
attempts: number;
maxAttempts: number;
nextRunAt: string | null;
lockedUntil: string | null;
providerJobId: string | null;
lastErrorCode: string | null;
createdAt: string;
updatedAt: string;
};
pending:已接收但还没被 Worker 领取;可按nextRunAt延迟。processing:由带过期时间的租约保护,Worker 崩溃后其他实例可以接管。succeeded:结果已持久化;重复消息只返回已有结果,不再次扣费调用。failed:本轮失败但仍可能重试;必须保存脱敏后的错误分类。dead_letter:达到重试上限或确认不可恢复,等待人工判断。cancelled:用户或运维明确取消;Worker 领取后仍要再次检查。
⏱️ 函数超时要按整条链路分预算
不要只问“Vercel Function 最长能跑多久”。平台上限会随计划、运行模式和配置变化,部署前应查看当前官方限制。更重要的是把总时间拆成前端、网关、函数、队列租约、提供商请求和结果持久化预算。
用户等待预算
超过产品可接受等待时间,就返回 job ID 和状态页,不让浏览器一直占着连接。
函数执行预算
主动中止时间应早于平台硬终止,给异常处理、状态落库和响应留出余量。
外部调用预算
连接、首字节、整体响应分别设超时;长任务改用后台模式或队列,不无限等待。
租约时间也不能简单等于函数时限。租约过短会让两个 Worker 同时处理,过长会拖慢崩溃恢复。可以让 Worker 定期续租,并确保每次外部副作用都有幂等保护。
📈 重试不是再调用一次
网络中断、408、429 和部分 5xx 通常可以有限重试;400、401、403 等输入、认证或权限错误通常要先修正原因。失败请求也可能计入速率限制,因此立即循环重试只会让恢复更慢。
const RETRYABLE_STATUS = new Set([408, 409, 429, 500, 502, 503, 504]);
function shouldRetry(status?: number) {
return status === undefined || RETRYABLE_STATUS.has(status);
}
function retryDelayMs(attempt: number) {
const base = 1_000;
const cap = 60_000;
const exponential = Math.min(cap, base * 2 ** attempt);
return Math.floor(Math.random() * exponential); // full jitter
}
// 400 / 401 / 403 等配置或权限错误先修正原因,不做无边界重试。
// 每次重试都要受 maxAttempts、截止时间和幂等键约束。
生产策略还应同时限制单任务最大次数、全局并发、单用户并发和重试截止时间。若响应提供 Retry-After 或速率限制 Header,优先遵循服务端提示;否则使用带随机抖动的指数退避。重试前再次检查任务是否已成功或取消。
故障默认动作退出条件 网络中断 / 408退避后重试达到截止时间或最大次数 429降低并发并按 Header / 退避等待预算耗尽后转等待或降级 500 / 502 / 503 / 504有限重试,必要时熔断持续错误进入死信或降级 400修正输入,不自动重试新版本或人工修复后重新提交 401 / 403检查 Key、权限与项目配置凭据或权限确认恢复
🔒 幂等、锁和唯一约束缺一不可
队列和 Webhook 通常提供“至少一次”语义:同一任务可能被交付多次。业务层要接受重复输入,并保证只产生一次扣费、一次订单状态变更或一份最终结果。
- 客户端为一次业务意图生成稳定的
idempotencyKey,重复点击继续使用同一个键。 - 数据库对“用户 + 业务类型 + 幂等键”建立唯一约束,不能只依赖内存锁。
- Worker 用带过期时间的租约避免正常情况下并发处理;租约只是协调,不替代唯一约束。
- 外部调用成功后先持久化 provider job ID 或结果,再确认队列消息。
- 通知、扣费、积分等副作用分别建立自己的去重记录,不假设主任务只会执行一次。
async function processJob(job: Job) {
const lease = await jobs.tryAcquireLease(job.id, 60_000);
if (!lease) return;
try {
const current = await jobs.get(job.id);
if (!current || current.status === "succeeded") return;
const result = await callProvider({
inputRef: current.id,
idempotencyKey: current.idempotencyKey,
});
await jobs.complete(current.id, result);
} catch (error) {
const latest = await jobs.get(job.id);
const retryable = shouldRetry(getHttpStatus(error));
if (retryable && latest.attempts 🗓️ Cron 只负责触发和修复扫描
以 Vercel Cron 为例,官方文档明确提醒调度失败不会自动重试,长任务可能和下一次调度重叠,事件也可能重复投递。因此 Cron Handler 应尽快完成以下三件事:
- 验证调度请求,例如使用 `CRON_SECRET`;不要让公开 URL 任意触发高成本任务。
- 按“任务类型 + 计划时间窗口”生成唯一幂等键,只创建一次 job。
- 把 job 入队并快速返回;真正的 AI 调用、批处理和通知交给 Worker。
另设一个低频补偿扫描:寻找租约过期的 `processing`、长时间未推进的 `pending`、以及已经完成但通知未发送的任务。Cron 时间通常按 UTC 解释,写配置时同时记录业务时区,避免夏令时或人工换算造成偏移。
## 🪝 Webhook 要先验签、去重、快速返回
OpenAI 官方 Webhook 文档建议用官方 SDK 验证签名,并让处理器尽快返回 2xx;非必要工作交给后台 Worker。投递可能重试并出现重复,官方文档建议使用 `webhook-id` 作为幂等键。不要在验签前解析并重写原始请求体。
import OpenAI from “openai”;
const client = new OpenAI({ webhookSecret: process.env.OPENAI_WEBHOOK_SECRET });
export async function POST(request: Request) { const body = await request.text(); const headers = Object.fromEntries(request.headers);
// 官方 SDK 会校验签名;验签失败直接拒绝。 const event = await client.webhooks.unwrap(body, headers); const deliveryId = request.headers.get(“webhook-id”);
if (!deliveryId || await deliveries.exists(deliveryId)) { return new Response(“ok”, { status: 200 }); }
await deliveries.recordAndEnqueue({ deliveryId, event }); return new Response(“ok”, { status: 200 }); }
示例中的存储接口只是架构占位,实际实现必须让“记录 delivery ID + 入队”成为原子操作,或用可重放的 outbox 补偿。Webhook Secret 与 API Key 分开存储和轮换,日志只记录事件类型、delivery ID、关联 job ID 和处理结果。
## 📮 死信队列和人工恢复怎么设计
达到最大重试次数后,消息不能悄悄消失,也不能永久循环。Cloudflare Queues 等队列支持把失败消息送入 Dead Letter Queue;若没有配置死信队列,达到重试上限的消息可能被删除。无论平台是否内建 DLQ,都应在业务数据库保留最终状态。
- 死信记录只保存恢复所需的引用、错误分类、尝试次数和时间,不复制敏感 Prompt、Key 或完整用户数据。
- 提供“重放前预览”:确认代码版本、凭据、配额、输入和幂等键仍然有效。
- 重放生成新的执行记录,但继续关联原 job;避免覆盖历史失败证据。
- 不可恢复的 400/权限错误由人工关闭;可恢复的提供商故障在确认恢复后分批重放,避免瞬间回灌。
## 🛟 生产故障时怎样降级而不是雪崩
降级目标不是假装服务正常,而是保护已有结果、控制成本并给用户清楚预期。建议按影响从轻到重准备以下开关:
- 排队而非失败:**返回 job ID、当前状态和合理的再次查询时间,避免前端高频轮询。
- **暂停非关键任务:**先停摘要刷新、批量标签和低优先级离线任务,把配额留给用户直接请求。
- **最后成功结果:**对允许陈旧数据的场景展示最后一次成功结果,并明确更新时间,不能冒充实时结果。
- **受控替代路径:**只有在输出质量、隐私、地区可用性和成本都经过验证时才切换模型或提供商;不可静默改变关键业务语义。
- **熔断和慢启动:**持续失败时暂时停止新调用;恢复后逐步放量,而不是让所有积压任务同时冲回上游。
- **人工重试:**给运维和用户提供有权限、有次数限制、可审计的重新执行入口。
## 🔭 最少要记录哪些观测字段
只记录“AI 调用失败”无法排障。每一次执行至少要能串起客户端请求、业务 job、队列交付和提供商请求:
### 身份
job_id、idempotency_key 哈希、用户或租户的内部 ID
### 队列
入队时间、开始时间、queue_delay、delivery / attempt 次数
### 执行
worker 版本、处理时长、租约更新时间、最终状态
### 上游
提供商、模型配置标识、provider request/job ID、HTTP 状态
### 容量
队列深度、消费速率、并发、429 比例、死信新增量
### 安全
只记错误分类和脱敏摘要,不记录 Key、Authorization、Cookie 或完整敏感正文
告警优先看趋势而非单次失败:队列等待时间持续上升、成功率下降、429 突增、processing 租约过期、Webhook 延迟和死信新增量,往往比 CPU 使用率更接近真实用户影响。
## ✅ 上线前做五组故障演练
下面的命令名是建议的测试入口,不是可直接复制到所有项目的现成脚本。把同类场景接入你自己的测试夹具或预发布环境:
1. 模拟提供商限流或暂时故障
PROVIDER_FAULT=429 npm run test:worker
2. 同一个幂等键并发提交两次,只允许产生一个业务结果
npm run test:idempotency
3. Worker 在完成前退出,锁到期后任务应能被其他实例接管
npm run test:lease-recovery
4. 连续失败达到上限后进入死信,不得无限重试
npm run test:dead-letter
5. Webhook 重放同一 delivery ID,状态更新只能执行一次
npm run test:webhook-replay
- 限流场景不会立即重试,也不会超过单用户和全局并发上限。
- 两个 Worker 同时收到同一任务时,只产生一份最终结果和一次业务副作用。
- Worker 在外部调用后、确认消息前崩溃,恢复后能识别已有 provider job 或结果。
- 连续失败进入死信并触发告警;人工重放保留历史,且不会一次性回灌全部消息。
- Webhook 重放、乱序和延迟到达都不会把已成功任务改回旧状态。
## ❓ 常见问题
### 用了 OpenAI background mode,还需要自己的队列吗?
通常仍需要。background mode 解决提供商侧长任务的异步执行;你的应用仍要管理用户请求、并发、状态映射、幂等、通知、取消和跨提供商降级。
### 把函数最大执行时间调高就能解决超时吗?
只能缓解一部分场景。平台硬上限、用户等待、网络中断和提供商处理时间仍然存在。长任务更适合返回 job ID,再用队列、后台任务和状态查询完成。
### Cron 每分钟跑一次,能当队列 Worker 吗?
不建议。Cron 适合生成任务和补偿扫描,不能自然提供高效消费、并发控制、租约、重试和背压。低流量原型可以轮询任务表,但也要实现唯一约束与锁。
### 为什么消息处理成功后还会再来一次?
Worker 可能已经完成业务操作,但在确认消息前崩溃或网络中断。队列会认为没有成功消费并再次交付,所以业务必须幂等。
### 所有 5xx 都应该重试吗?
可以把部分 5xx 作为候选,但仍要限制次数、截止时间和并发,并结合请求是否安全重放。持续 5xx 应进入熔断、降级或死信,而不是无限循环。
### 轮询状态应该多频繁?
根据任务预计时长逐步放慢,并返回建议的下一次查询时间。页面失焦后降低频率,完成或失败后停止。高并发场景优先用事件通知配合低频兜底轮询。
### 死信消息可以自动全部重放吗?
不应默认全量重放。先确认根因已修复,再按批次、速率和影响范围逐步恢复;否则会重新触发限流、重复副作用或把旧输入带回生产。
## 📚 官方资料来源
- [OpenAI Background mode](https://developers.openai.com/api/docs/guides/background):后台响应创建、状态轮询与适用边界。
- [OpenAI Webhooks](https://developers.openai.com/api/docs/guides/webhooks):签名验证、快速返回、重试投递和重复事件处理。
- [OpenAI Rate limits](https://developers.openai.com/api/docs/guides/rate-limits):速率限制维度、响应 Header 与指数退避建议。
- [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs/manage-cron-jobs):重试、重叠执行、重复投递、时区和鉴权注意事项。
- [Vercel Function duration](https://vercel.com/docs/functions/configuring-functions/duration):函数执行时限配置与当前计划边界。
- [Cloudflare Queues Dead Letter Queues](https://developers.cloudflare.com/queues/configuration/dead-letter-queues/):最大重试次数与死信队列配置。
平台限制和产品能力可能更新。本文避免写死套餐时限;上线前请以对应平台的当前官方文档和账户控制台为准。
## 🚀 下一步行动
异步链路稳定后,再补齐部署环境和 API 错误码的分层排查:
[ 🧭
#### AI 项目部署环境排障
继续检查 CI/CD、Vercel、Docker、远程服务器中的变量、Secret 与网络差异。
开始学习 ](/guides/ai-cicd-deployment-network-env-troubleshooting-guide)[ 🧯
#### AI API 错误码排障
把网络超时、401、403、429 与 5xx 分层处理,避免无边界重试。
开始学习 ](/guides/ai-api-connectivity-error-troubleshooting-guide)[ ⭐
#### VPS 推荐榜单
查看 VPS 实测与服务商索引,先看结论再去官网。
查看推荐 ](/vps-recommendations)[ 📚
#### 浏览更多教程
继续探索服务器安全、网站搭建、性能优化和 AI 环境主题。
探索教程 ](/guides)
读完后建议
先验证,再选择 把判断落到具体选择
准备购买 VPS 时,先对照推荐榜单和真实测评确认线路、价格、用途与风险;只是继续学习,可以回到教程索引按主题往下看。
[ 查看推荐榜单 ](/vps-recommendations) [ 回到教程索引 ](/guides)