资讯动态

异步出图的状态同步:Webhook 回调、轮询兜底与乱序处理

发布时间:2026/11/6 5:26:30 来源:尧图企业网站定制
把商品图生成接进业务系统第一个绕不开的问题就是任务提交之后怎么知道它什么时候完成。最直觉的做法是同步等。请求发出去requests.post() 一直挂着等上游返回图片地址再往下走。这个方案在测试环境通常能跑通上线之后一定会出事出图耗时波动大同一批任务有的 8 秒有的 90 秒同步等会把连接池占满中间加一层网关网关的超时时间往往比出图时间短请求被切断任务却已经在上游跑起来了用户点一次生成要等半分钟页面体验很差还没有进度。正确的做法是异步提交拿任务号结果靠回调 兜底把状态补回来。一、五个环节一条完整的链路是这样走的示意图提交、生成中、回调、兜底轮询、对账五个环节本文绘制1.提交带幂等键落库返回任务号不等结果。2.生成中任务进入执行态记录发起时间和超时上限。3.回调到达上游推通知验签后更新状态。4.轮询兜底超过预期时长主动查一次补上丢掉的回调。5.对账每天比对本地任务表与上游记录找出不一致的。五步里第 3 步和第 4 步是很多人只做一半的地方——只做回调会丢单只做轮询会重复查两套并存才稳。二、回调先验签再条件更新回调接口第一件事不是解析业务字段是验签。不验签等于把状态更新权交给了任何人。# Flask 示例回调入口import hmac, hashlib, jsonfrom flask import Flask, request, jsonifyapp Flask(__name__)SIGN_SECRET byour-webhook-secretdef verify(raw_body: bytes, signature: str) - bool:expect hmac.new(SIGN_SECRET, raw_body, hashlib.sha256).hexdigest()return hmac.compare_digest(expect, signature or )app.post(/callback/image-task)def callback():raw request.get_data() # 必须用原始 body 验签if not verify(raw, request.headers.get(X-Signature, )):return jsonify(code401, msgbad signature), 401data json.loads(raw)task_id, status data[task_id], data[status]# 条件更新只允许单向流转旧状态不覆盖新状态updated db.execute(UPDATE image_taskSET status :status,image_url :url,finished_at NOW()WHERE task_id :tidAND status IN (queued, running), {status: status, url: data.get(image_url),tid: task_id})if updated 0:# 要么任务不存在要么已经是终态重复通知或乱序log.warning(callback skipped task_id%s status%s,task_id, status)return jsonify(code0)三个细节值得单独说验签用原始 body。先把 JSON 解析再序列化回去验签字段顺序和空格变了签名必然对不上。这个问题在本地测不出来上线才发现。用条件更新不要先查后写。WHERE status IN (...) 就是条件更新一句话解决并发覆盖。先 SELECT 再 UPDATE 是典型的竞态写法两个回调同时到达时会互相覆盖。updated 0 要记日志。这个分支包含了三种情况任务不存在、重复通知、乱序。不记日志出问题只能靠猜。三、兜底轮询按 P95 定阈值加随机抖动回调一定会有丢的网络抖动、服务重启、灰度发布都会导致丢。兜底不是可选项。# 每 2 分钟扫一次应该完成但还没完成的任务STALE_SQL SELECT task_id FROM image_taskWHERE status IN (queued, running)AND submitted_at NOW() - INTERVAL 3 minutesAND poll_count 5LIMIT 200def poll_pending():for row in db.query(STALE_SQL):detail api.get_task(row[task_id]) # 上游查询接口if detail[status] running:sql (UPDATE image_task SET poll_count poll_count 1 WHERE task_id :t)db.execute(sql, {t: row[task_id]})continueapply_status(row[task_id], detail[status],detail.get(image_url))阈值怎么定按上游耗时的 P95 定不要按平均值。平均值是 20 秒的时候P95 可能是 60 秒。按平均值定阈值会导致大量还在正常生成的任务被误判。轮询频率加随机抖动。固定间隔重试会把失败时间点对齐成新的一波尖峰加 random.uniform(0, 30) 秒的抖动就能打散。次数要有上限。poll_count 5 就是上限连续查不到结果就转人工不要无限重试——无限重试在夜里会一直占着任务第二天还没人知道。四、乱序三种处理同一个任务的多条通知可能前后颠倒。处理原则只有一条状态单向流转回退一律丢弃。除了条件更新还有两处要防通知体里必须带状态不能只带 ID。只有 ID 的通知无法判断这是开始生成还是生成完成收到之后还得反查一次白白多一次请求。落库时以任务号 状态作为去重依据。重复通知在入库这一层就被拦掉不用靠应用层判断。上线初期先只记录不处理。把收到的通知按时间顺序记下来观察几天真实乱序比例再决定要不要加版本号。大多数上游的乱序是低频事件先看清楚再设计。五、对账一天一次专治对不上回调和轮询都做了还是会有差异。差异来源很多回调延迟跨天、轮询漏了某批、上游自己的状态变更没通知。对账任务放在低峰期跑做三件事1. 拉昨天的全部任务与本地表按任务号比对2. 本地是生成中、上游是完成的补状态并补下载3. 本地是完成、上游查不到的标记异常人工确认。对账本身也要幂等。同一份数据跑两次结果应该一致。六、状态口径对外只暴露三种后端状态可以有很多queued、running、retrying、succeeded、failed、blocked。但前端只该看到三种处理中、可下载、失败。中间态一旦外透产品经理会问retrying 和 running 有什么区别客服会照着状态给用户解释最后所有人都被内部实现绑住。失败要分类因为处理方式不同类别典型原因处理方式参数错提示词为空、尺寸超限直接丢弃不重试内容拦截触发审核规则通知用户修改不重试上游故障5xx、限流退避重试上限三次环境问题网络、存储写入失败重试同时告警四类混在一起统计看上去合格率九成扣掉拦截和超时实际能用的可能只剩七成。分开记才知道该加并发还是该改提示词。七、接入方式的选择上面这套逻辑自己写大约两三百行不算复杂但要做好回调验签、条件更新、兜底、对账四件事需要一点耐心。如果不想维护这些也可以直接用平台的接口层。Flux Art 的开放接口在 https://open-api.flux-art.cn/openapi/v1走的是同一套提交-回调模式签名与重试的细节在文档里写清了。它是多模型 AI 视觉创作与生产平台接口后面挂着多个图像与视频模型换模型不用换接入方式。八、小结异步出图的状态同步核心就三句话提交拿号不等结果回调为主轮询兜底对账收尾状态单向流转旧的不覆盖新的。把这三点守住剩下的都是细节。细节里最容易翻车的是验签和条件更新两处都在代码里写完记得用并发压一遍。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑