深夜的奶茶店,收银台前永远排着长队。老式奶茶店里,店员必须站在柜台后,等后厨把手里那杯奶茶做完、递给顾客,才能接待下一位——如果一杯奶茶要 3 分钟,这位店员一小时最多服务 20 位顾客。这就是“同步”的世界:每一步都被上一步卡住,谁都不能先走。 聪明的店长改了一条规矩:顾客来了先收银、发一个取餐号,后厨按号慢慢做,做好后取餐屏亮起号码,顾客凭号取餐。店员不再傻等一杯奶茶出炉,而是腾出手来不停地接待新顾客。同样的后厨、同样的人手,一小时却能服务上百位顾客。这就是“异步”的魔法。 大语言模型(LLM)应用正面临同样的困境。一次文本生成往往要花几秒甚至几十秒,如果用同步方式处理请求,模型一忙,整个服务就堵成一团。异步任务,就是给 LLM 应用装上“取号叫号”系统:先收下请求,把耗时操作丢到后台队列,生成完毕再通知用户。本文用奶茶店的完整故事,带你从零看懂异步任务背后的队列、状态机、回调、重试与死信,让你的 LLM 应用吞吐量成倍增长。
掌握异步任务设计,是 LLM 应用从小型 Demo 走向高并发生产系统的分水岭;它决定你是写“能跑的 Demo”的人,还是设计“扛得住流量”的系统的人。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
同步与异步
想象你是老式奶茶店的店员。一位顾客点了珍珠奶茶,你必须站在那里,眼巴巴等后厨做好、递到你手上、再递给顾客,然后才能喊“下一位”。如果一杯要 3 分钟,你一小时最多服务 20 位顾客。这个“等”字,就是计算机世界里的阻塞:当前线程被一个慢操作卡住,什么都干不了。
LLM 应用里的“慢操作”就是调用大模型。用户请求进来,应用调用模型接口生成一段文本,要花 5 秒甚至 30 秒。同步架构下,服务器处理这个请求的线程就被“钉”在这 5 秒里,无法服务其他用户。当并发请求一多,所有线程都被占住,新的请求只能排队,响应越来越慢——这就是所谓的“吞吐量塌陷”。吞吐量(可以理解为奶茶店每小时卖出的杯数)才是系统真实能力的度量,而异步化的核心目标就是拔高这个值。
异步的思路很简单:把“等结果”这件事和“受理请求”拆开。服务器收下用户请求、登记一个任务 ID、立刻返回“已受理”,然后把耗时的大模型调用丢给后台的 Worker 去执行。服务器线程瞬间释放,可以继续服务下一个请求。后台干完活,再把结果存好、通知用户来取。整个过程中,没有谁被“钉死”,系统的吞吐能力随之倍增。
描述这种排队系统,有一条著名的利特尔法则(Little's Law):
回到奶茶店:店长没有让奶茶做得更快,也没有增加人手,只是把“店员傻等”变为“后厨异步制作”,同样一小时内接待的顾客就翻了几倍。异步任务对 LLM 应用的增值正是如此——它不改变模型单次生成的速度,却通过消除阻塞、让资源持续运转,把系统的总吞吐拉高了一个量级。
- L(在途订单数)系统中同时存在的订单总量,包括正在排队和正在制作的。
- λ(到达速率)单位时间内新进入系统的任务数,相当于奶茶店每分钟来几单。
- W(逗留时间)一个订单从下单到取餐的平均总时长,包含排队等待和处理时间。
任务队列设计
店长给后厨添置了一个不锈钢订单架。所有新订单按顺序夹在架子上,后厨每做完一单,就从架子上取下最底下那一单接着做。这个订单架,就是任务队列。它的存在让“收银”和“制作”两个环节彻底解耦:前面接待得再快,订单架都能先接住;后面做得再慢,也不用堵住收银台。
LLM 应用的任务队列,可选方案很丰富。轻量场景常用 Redis List 或 BullMQ:Redis 的 LPUSH/BRPOP 天然支持先进先出,配上少量代码就能当队列用,适合中小规模任务。中大规模场景会选 RabbitMQ:它支持消息确认、路由和持久化,适合对可靠性要求高的业务。超大规模或需要长期保存数据流时选 Kafka:吞吐极高,但运维成本也高。选型的关键是问自己三件事:任务量有多大?消息丢了能不能接受?团队养得起多重的中间件?
队列不是简单地“先进先出”就完事。真实业务里,有的订单要插队(VIP 用户的分析任务优先执行),有的要延迟(“10 分钟后再开始生成”),有的要去重(同一用户 5 秒内不要提交两个一模一样的任务)。这些能力分别对应消息中间件里的优先级队列、延时队列、幂等键去重。设计队列时想清楚这些需求,比事后打补丁省力得多。
队列是否会被“撑爆”,可以用一个简单的公式衡量:
奶茶店的订单架选型也一样:小店用一块磁性白板就够了(Redis),大店要上电子叫号系统(RabbitMQ),连锁总部的中央调度才需要 Kafka 级别的能力。选队列不是选最贵的,而是选“任务量、可靠性、运维成本”三者平衡的那一个。记住:一个好的队列设计,要让 ρ 在你的业务高峰期内始终远离 1,留出安全余量。
- λ(任务到达率)单位时间新进入队列的任务数,相当于前台每秒接几单。
- μ(任务处理率)所有 Worker 单位时间能处理掉的任务总数,相当于后厨每秒出几杯。
- ρ(服务强度)到达率与处理率的比值,是队列稳定性的“温度计”。
任务状态机
奶茶店的每张订单,都在取餐屏上经历人生:已下单 → 制作中 → 已完成,偶尔也会变成已失败。取餐屏的每一格更新,就是一次状态转移。如果哪天店里的订单系统崩溃了,重启之后,后厨必须能根据每张订单当前停在哪个格子,决定是继续做、重新做,还是直接取消——这套规则,就是任务状态机。
LLM 异步任务的状态机通常包含这样几个状态:pending(已受理,排队中)、processing(正在调用模型)、succeeded(生成完成,结果已存储)、failed(执行出错,等待重试)、cancelled(被用户或系统取消)。更复杂的系统还会引入 dead(重试耗尽,等待人工处理)。每一个状态都不是凭空出现的,而是由某个事件触发转移:Worker 取到任务时 pending→processing,模型返回结果时 processing→succeeded,接口超时时 processing→failed。
状态不是只存在内存里就够了。生产环境里,服务器可能随时宕机、重启、扩容。如果任务状态只存在进程内存中,重启后一切归零,任务就丢了。所以状态要持久化到 Redis 或数据库里。重启恢复时,Worker 扫描所有 processing 状态的任务——它们可能是“做了一半”的——重新入队或标记 failed,确保没有一个任务被遗忘。这就是状态恢复。
用集合和转移函数可以精确地描述状态机:
奶茶店的取餐屏之所以让人安心,是因为顾客随时知道自己的单在哪个阶段。异步任务同理:状态机不只是程序员的内部约定,它也是对外的承诺——用户随时可以查“我的任务到哪一步了”。持久化+恢复机制则是这份承诺的保险丝:哪怕店铺停电重启,订单也不会凭空消失。
- S(状态全集)任务可能处于的所有阶段的集合,就像取餐屏上可能显示的所有文字。
- s_t(当前状态)任务此刻停留在哪个阶段。
- e_t(触发事件)导致状态变化的事情,如“模型返回成功”“接口超时”。
- δ(状态转移函数)输入当前状态和事件,输出下一状态,决定任务往哪里走。
回调通知
奶茶做好了,怎么让顾客知道?有两种极端做法。一种:顾客每隔 30 秒跑过来问一次“好了没”,这叫轮询。另一种:店里的取餐屏亮起号码并发出“叮”的一声,顾客听到后自己来取,这叫主动通知或回调。前者费顾客的腿,后者费店里的设备,但体验天差地别。
LLM 异步任务的“通知”同样是二选一。最简单的实现是轮询:客户端拿到 task_id 后,每隔几秒调用一次 GET /tasks/{id},直到状态变成 succeeded。好处是后端实现极简,缺点是一大堆请求其实都是“白跑一趟”。对于服务端到服务端的场景,更优雅的方案是 Webhook:任务完成时,后端主动向客户端预先注册的回调 URL 发送一个 HTTP POST,消息里带上 task_id 和结果地址。客户端一觉睡到被叫醒,零空转。
还有一类场景需要“实时感”给用户看——比如网页上显示“正在生成摘要(35%)”。这时可以用 SSE(Server-Sent Events) 或 WebSocket 建立长连接,后端把状态变化实时推送到浏览器。这相当于给顾客发了一个可以持续更新的“排队进度条”。三种方式各有定位:轮询简单但浪费,Webhook 适合系统间通知,长连接适合面向用户的实时体验。
轮询的资源浪费可以用一个公式直观感受:
奶茶店后来装了取餐屏,顾客就不用来回跑了;但屏幕坏了的时候,店员又会喊号“救场”。回调机制的选型也是如此:Webhook 和长连接带来了实时体验,就必须考虑消息丢失、回调失败等异常——所以真正的生产系统往往“通知为主、轮询兜底”,两者搭配使用,就像取餐屏和人工喊号双保险。
- t_proc(任务执行时长)从开始执行到完成的总耗时,比如大模型生成一篇摘要的 10 秒。
- Δt(轮询间隔)客户端每隔多久查一次状态,比如 2 秒。
- N_poll(总查询次数)任务期间客户端一共发起了多少次状态查询。
结果存储查询
奶茶做好了,不能一直放在后厨手里。它会被放到取餐台上,杯身贴着取餐号,顾客凭号领取。没有人取的奶茶放久了会坏,所以要定时清理。这个取餐台,就是异步任务里的结果存储:任务执行完成的产物,需要一个“台面”来存放,还需要一套“凭号取餐”的查询接口。
结果往哪儿放,取决于结果有多大。小结果——比如一段生成的摘要文本、一个分类标签——直接存 Redis 或数据库字段即可,value 就是 JSON 字符串。大结果——比如一份几十页的PDF分析报告、一批 500 条的生成文案——适合放对象存储(如 S3/OSS),数据库里只存一个下载链接。中间态的数据比如任务日志,则可以进 Elasticsearch 方便检索。存储选型和队列选型一样,按体积和访问频率分层。
查询接口的设计要“好找”。最朴素的设计是 GET /tasks/{task_id},返回 {status, result, error, created_at}。这里的关键是:把 task_id 建成主键索引,查询时直接命中目标记录。如果设计不当,每次查询都要把整张任务表扫一遍,任务一多就会越来越慢。此外,结果不能永久保留——每个结果都设一个 TTL(过期时间),比如 24 小时,到期自动清理,否则存储空间会像没清理的取餐台一样被旧杯子堆满。
查询效率取决于存储结构的设计:
奶茶店的取餐台管理得好,顾客体验就好;LLM 应用的结果存储管理得好,下游系统来“取餐”时就顺畅。记住三个关键词:分层存储(按体积分流)、索引优先(按 ID 直达)、TTL 兜底(防止存储膨胀)。这三点做好,异步任务的结果查询就是一杯“随到随取”的奶茶。
- O(1)(按号取餐)通过 task_id 主键直接定位结果,无论任务总量多大,查询时间基本恒定。
- O(N)(满店翻找)数据库没有索引时,每条查询都要扫描所有任务记录,耗时随任务数线性增长。
- 索引(取餐号码簿)为 task_id 建立索引,相当于给每一杯奶茶贴上唯一取餐号并排好架位。
失败重试
做奶茶也有翻车的时候:牛奶打翻了、封口机卡住了、珍珠煮糊了。店员会重做,但重试不能鲁莽。刚失败就立刻重来,机器可能还没恢复,大概率又失败;也不能无限重试,原料有限、顾客会疯。正确的做法是:等一等再试,而且越往后等得越久。这套章法,就是失败重试策略。
LLM 应用里的失败极其常见:模型 API 超时、触发 rate limit、网络抖动、下游服务 5xx。异步任务天然适合做重试——因为任务在后台跑,重试不阻塞任何用户。重试策略有三种常见形态:固定间隔(每次失败后都等 2 秒)简单直接,但无法应对持续故障;指数退避(间隔按 1 秒、2 秒、4 秒、8 秒递增)更适合生产;带抖动的指数退避则在此基础上再加一个随机数,避免大量任务在同一时刻齐刷刷重试造成的“惊群效应”。
重试还必须设上限,达到上限后任务不能再“挣扎”,要转入失败处理流程。另一个容易忽略的问题是幂等性:如果任务被重试了两次,会不会产生两份相同的账单、两条重复的短信?设计任务处理逻辑时必须保证重复执行的结果与执行一次一致——比如用业务幂等键去重,或在结果写入时做唯一约束。幂等是重试系统的“安全气囊”,少了它,重试越多、事故越大。
指数退避的等待时间公式:
奶茶店的重试哲学是“给机器一点喘息,给自己一个上限”。LLM 应用的重试策略也一样:用指数退避尊重下游服务的恢复节奏,用抖动避免群体踩踏,用上限防止死循环,用幂等保证“多试几次也不闯祸”。这一套组合拳,是异步任务系统稳定性的基石。
- t_base(初始等待)第一次失败后等待的基础时长,一般取几百毫秒到几秒。
- n(重试次数)当前已经重试到第几次,从 0 开始计数。
- 2^n(指数增长)等待时间随重试次数翻倍,给系统越来越长的恢复窗口。
- jitter(随机抖动)添加一个随机偏移,防止大量任务同时重试造成踩踏。
死信队列
总有一些奶茶,重做了三次还是失败——比如今天供应商没送来珍珠。这杯奶茶不能再硬做了,它被放到后厨角落一个贴着红色标签的篮子里,等店长下班后亲自来看:是原料问题?设备问题?还是配方本身有错?这个红色篮子,就是死信队列(DLQ,Dead Letter Queue)。
LLM 异步任务同样有“做到死也做不成”的任务。重试次数达到上限后,任务进入死信队列。死信队列不是一个“垃圾桶”,而是一个“待诊断病房”:正常业务队列继续跑,死信队列单独存放搞不定的任务,同时触发告警通知开发人员介入。进入死信的原因通常有三类:输入数据本身有问题(喂给模型的是乱码或超长文本)、模型接口持续报错(密钥过期、配额用尽)、业务规则不允许(生成内容违反审核策略)。
死信队列的价值在于“隔离故障、保留现场”。没有死信队列的系统,失败任务会在主队列头部反复重试,挡住后面的正常任务,形成队头阻塞。有了死信队列,坏任务被移走,好任务畅通无阻;同时死信本身成为宝贵的排查样本,开发人员可以从死信里提取输入参数、错误堆栈,定位根因。对死信的处理还应该提供“重放”能力——修好问题后,把死信重新投回业务队列再跑一次。
死信率是衡量异步系统健康度的核心指标:
奶茶店的红色篮子救了这家店:店长通过它发现供货商的问题,换了一家供应商,从此死信率归零。LLM 应用的死信队列同理——它不是失败的代表,而是系统“自我发现、自我修复”的起点。把死信处理流程设计好(告警、诊断、重放),你的异步系统就拥有了从故障中自动学习的能力。
- N_dead(进小黑屋的任务)重试耗尽仍失败、被移入死信队列的任务数量。
- N_total(全部任务)系统在一段时间内收到的任务总量。
- R_DLQ(死信率)死信任务占比,是异步任务健康度的重要监控指标。
异步化实践
周末的奶茶店迎来晚高峰,后厨忙得不可开交。店长发现:与其一杯一杯分别做,不如把相同配料的订单合并——十杯珍珠奶茶一起煮、一起封口,单位时间出杯量立刻上升。这个“合并同类项”的招数,就是异步任务实践里的批量处理。
异步化在 LLM 应用中最典型的实战场景有三个。第一是批量文本生成:运营要生成 500 条商品评论,如果前端一条条同步请求,用户体验是灾难;异步化后,用户提交一个批次任务,后台 Worker 逐个或分组调用模型,全部完成后统一通知下载结果。第二是长文档摘要:一份几十页的 PDF,需要切分、分段摘要、汇总——全程可能耗时几分钟;异步化让用户提交后就能关掉页面,完成后收到通知与结果链接。第三是大规模数据分类:数万条工单文本需要打标签,按批次灌入异步管道,弹性扩容 Worker,既稳又快。
要驾驭这些场景,监控指标不能少。异步系统的四大核心指标是:队列积压量(订单架上攒了多少单)、任务耗时 P95(95% 的任务多久完成)、失败率(重试前的失败占比)、死信率(最终失败占比)。这四个指标像奶茶店的经营仪表盘:积压量高了就加人手(扩容 Worker),P95 涨了就要排查是不是模型变慢,死信率报警就要启动人工介入流程。
批量合并带来的吞吐提升,可以用阿姆达尔定律估量:
奶茶店靠“合并同类项+弹性排班”,在晚高峰把出杯量提升了一倍;LLM 应用靠“批量处理+异步管道+监控仪表盘”,把批量生成和长文档分析从“卡死页面”变成“交单即走、完成通知”的顺畅体验。异步化的本质,就是把时间还给用户,把效率留给系统。
- p(可异步化占比)总工作量中可以被后台异步处理、并发执行的比例,比如模型调用占比 90%。
- N(并行度)同时运行的 Worker 数量,相当于后厨同时开火的灶台数。
- S(加速比)异步化后系统整体吞吐提升的倍数。
AI 应用场景
电商平台的批量商品描述生成
阶段:任务异步化与批量处理
解决的问题:运营需要为大促的 5000 件商品逐一生成卖点描述。传统做法是循环调用模型接口,前端页面一直转圈等待,单次生成 8 秒,5000 件要跑一整天,且任何一次失败都可能导致整个流程中断。异步化后,用户一次提交批量任务,后台队列分批调度、失败自动重试,全流程无人工值守。
具体案例:某电商中台设计了一个「批量文案生成任务」:运营上传 SKU 清单,系统返回一个 batch_id,后台按每批 100 个任务投入队列,Worker 池弹性扩到 20 个并发调用模型。完成后通过 Webhook 通知运营,附件存于 OSS 生成下载链接。整个任务 15 分钟完成,失败任务自动重试 3 次,重试仍失败的进入死信队列并告警。
企业知识库的长文档异步摘要
阶段:任务状态机与结果存储
解决的问题:用户上传一份 200 页的年报 PDF,需要切分成上百个片段并逐段生成摘要,再汇总成总摘要。同步方式下 HTTP 请求必然超时,用户只能干等。异步任务把整个过程拆成可追踪的状态:切分中、摘要中、汇总中、完成,用户随时查询进度。
具体案例:某知识库产品实现「文档异步处理管道」:上传后立即返回 document_id,后端按章节切分文档,将每个片段的摘要任务投入队列。进度条通过 SSE 实时推送到前端,显示“已处理 36/120 段”。全部完成后结果存入数据库,用户通过 GET /documents/{id}/summary 获取最终摘要。任务中途失败不会丢失已完成的片段摘要,重启后自动续跑。
Python 演示
import queueimport randomimport threadingimport timefrom dataclasses import dataclass @dataclassclass Task: task_id: int name: str status: str = "pending" # pending/processing/done/failed/dead retries: int = 0 result: str = "" class AsyncTaskQueue: """模拟 LLM 异步任务系统:队列 + 多 Worker + 重试 + 死信""" def __init__(self, workers: int = 3, max_retries: int = 3): self.task_queue = queue.Queue() self.results = {} self.workers = workers self.max_retries = max_retries self._lock = threading.Lock() self._stop = False def submit(self, name: str) -> int: task_id = len(self.results) + 1 task = Task(task_id=task_id, name=name) with self._lock: self.results[task_id] = task self.task_queue.put(task) return task_id def _call_llm(self, task: Task) -> str: """模拟一次可能失败的 LLM 调用""" time.sleep(random.uniform(0.05, 0.15)) if random.random() < 0.3: raise RuntimeError("上游模型接口超时") return f"生成结果-{task.name}" def _worker_loop(self): """Worker 不断从队列取任务并执行""" while not self._stop: try: task = self.task_queue.get(timeout=0.2) except queue.Empty: continue task.status = "processing" while task.retries < self.max_retries: try: task.result = self._call_llm(task) task.status = "done" break except RuntimeError: task.retries += 1 wait = 0.05 * (2 ** (task.retries - 1)) # 指数退避 time.sleep(wait) else: task.status = "dead" # 重试耗尽,进入死信 self.task_queue.task_done() def start(self): self.threads = [ threading.Thread(target=self._worker_loop, daemon=True) for _ in range(self.workers) ] for t in self.threads: t.start() def stop(self): self._stop = True for t in self.threads: t.join() def get_task(self, task_id: int): return self.results.get(task_id) if __name__ == "__main__": aq = AsyncTaskQueue(workers=3, max_retries=3) aq.start() ids = [aq.submit(f"订单-{i}") for i in range(8)] time.sleep(2) for tid in ids: task = aq.get_task(tid) print(f"#{task.task_id} {task.name}: {task.status}, " f"retries={task.retries}, {task.result}") aq.stop()
self.task_queue = queue.Queue()初始化任务队列,相当于奶茶店的订单架。task_id = len(self.results) + 1生成任务 ID,作为后续查询结果的“取餐号”。self.task_queue.put(task)提交任务入队,仅一步操作,主流程立刻返回,这就是“异步受理”。while not self._stopWorker 主循环,持续从队列取任务,模拟后厨不停工。self.task_queue.get(timeout=0.2)阻塞式获取任务,队列空了就短暂等待,避免忙轮询浪费 CPU。task.status = "processing"任务状态流转:pending → processing,对应取餐屏“制作中”。while task.retries < self.max_retries重试循环,达到上限即放弃,避免无休止重试。wait = 0.05 * (2 ** (task.retries - 1))指数退避:第 1 次重试等 0.05 秒,第 2 次 0.1 秒,第 3 次 0.2 秒。task.status = "dead"重试耗尽后标记死信,对应进入“小黑屋”等待人工排查。self.results.get(task_id)按 ID 查询任务状态与结果,模拟结果存储与查询接口。
常见误区
误区我以为把接口改成异步之后,用户拿到生成结果的速度会变快,因为“异步听起来就很快”。
正解异步并没有让模型生成速度变快,它只是让服务器不用傻等,从而能同时服务更多请求。单个任务的耗时(从提交到拿到结果)可能反而因为排队而略微增加,但系统整体的吞吐量大幅提升。就像奶茶店取号后,单一杯奶茶的制作时间没有缩短,但一小时内卖出的杯数变多了。
为何常见:因为“异步”这个词容易让人联想到“同时处理”“效率提升”,人们自然以为它对单个请求也有加速作用。实际上异步优化的是系统吞吐,不是单次延迟。
误区代码里加了一个 `queue.Queue`,我就以为自己的 LLM 应用已经是异步架构了。
正解队列只是异步化的第一步。真正的异步任务系统还需要完整的配套:持久化的任务状态机、结果存储与查询接口、完成后的回调通知、失败重试机制、死信队列兜底。如果任务被丢进队列后没有任何状态追踪、结果无处查询,那只是“把阻塞搬了个家”,而不是异步化。
为何常见:新手往往把“队列”这个最显眼的组件当作异步的全部,忽略了异步是一个贯穿任务全生命周期的架构模式。
误区任务失败就重试,直到成功为止,反正后台运行又不打扰用户,多试几次总行吧?
正解无限重试会导致死循环和资源浪费,还可能对下游模型服务造成持续压力,甚至把自己系统拖垮。正确做法是设置合理的最大重试次数,配合指数退避留出恢复时间,重试耗尽后转入死信队列让人工介入。如果下游模型服务已经故障,重试 100 次和重试 3 次结果一样,只是多消耗 97 次资源和时间。
为何常见:重试看起来是“多一次机会”,成本意识不强的人容易忽略每次重试都在消耗系统资源和下游资源。
误区反正任务是后台跑的,失败了重试一次就行,重复执行也就多调一次模型,应该没关系。
正解如果任务有副作用——比如发送短信、扣减积分、生成订单——重试就会导致重复执行、产生脏数据。必须通过幂等键或业务唯一约束保证“同一任务执行一次和执行多次效果相同”。这就像奶茶店重做一杯奶茶可以,但同一杯奶茶收了两遍钱就不行。
为何常见:Demo 阶段的任务都是“生成一段文本”这种无副作用操作,让开发者低估了生产环境中任务副作用的严重性。
面试题
Q1. 同步任务与异步任务最根本的区别是什么?
- A.异步任务运行速度更快
- B.请求无需等待任务即可返回← 正确答案
- C.异步任务完全不需要服务器
- D.异步任务只能用于批处理
答案:B。同步任务中,调用方必须等待任务完成才能继续;异步任务中,调用方提交任务后立即获得受理确认(task_id),无需等待任务真正完成。这就像奶茶店收银员接单后不用等奶茶做好就能接待下一位。A 是错觉,异步不改变单任务速度;C、D 明显错误。
Q2. 以下哪项不是任务队列应提供的常见能力?
- A.支持任务按顺序排队
- B.支持高优先级任务插队
- C.保证任务永远不会失败← 正确答案
- D.支持任务延迟到点执行
答案:C。任务队列通常提供 FIFO 排序、优先级、延时执行、去重等能力,但“保证任务永不失败”是不可能的——网络会抖、模型会挂、数据会出错。队列只能通过重试和死信机制来管理失败,不能消除失败。
Q3. 利特尔法则 L = λW 中,L 表示什么?
- A.队列里排队的任务总数
- B.系统中在途任务平均数量← 正确答案
- C.已经被丢弃的任务数量
- D.处理任务所需的服务器数
答案:B。利特尔法则中 L 是系统内平均在途任务数,既包括排队中的也包括正在处理的。它等于到达率 λ 乘以平均逗留时间 W。理解这个关系就能明白:要支撑更大的流量,要么缩短任务耗时,要么增加系统的在途容量。
Q4. 指数退避重试策略的核心思想是什么?
- A.重试间隔固定不变
- B.重试间隔随次数指数增长← 正确答案
- C.重试间隔完全随机抖动
- D.任务失败后不再重试
答案:B。指数退避让每次重试前的等待时间按 2 的指数次方增长(1 秒、2 秒、4 秒、8 秒),目的是给下游系统越来越长的恢复窗口。固定间隔在系统故障时无济于事,完全随机则不够稳健,不重试则浪费了恢复机会。
Q5. 以下哪些状态通常出现在异步任务的状态机中?
- A.待执行的 pending 状态← 正确答案
- B.执行中的 processing← 正确答案
- C.已成功的 succeeded 状态← 正确答案
- D.已编译的 compiled 状态
答案:ABC。异步任务状态机一般包含 pending(待执行)、processing(执行中)、succeeded(已成功)、failed(已失败)、dead(死信)等状态。compiled 是编译相关概念,与任务队列无关。这就像奶茶店取餐屏只显示“下单、制作中、已完成、已失败”,不会显示“已编译”。
Q6. Webhook 相比轮询方案的主要优势是什么?
- A.实现更加简单直接
- B.实时性更好无空转请求← 正确答案
- C.可以节省数据库存储
- D.不需要依赖网络连接
答案:B。Webhook 是任务完成时服务端主动推送通知,客户端不需要反复查询,因此实时性好且没有空转请求。轮询实现更简单但会不断产生无效 HTTP 请求;Webhook 同样依赖网络;存储与通知方式无关。
Q7. 任务进入死信队列后,合理的处理方式包括哪些?
- A.人工介入排查任务失败原因← 正确答案
- B.自动清空全部死信记录
- C.分析输入数据是否异常← 正确答案
- D.设置告警通知开发人员← 正确答案
答案:ACD。死信队列是“待诊断病房”而非垃圾桶。合理做法是告警、人工排查、分析输入数据与下游状态,修复后还可重放。直接清空死信会丢失排查线索,是错误操作。这就像店长要检查做坏的奶茶是原料问题还是机器问题,而不是直接把杯子扔掉完事。
Q8. 一个任务处理耗时 10 秒,客户端每 2 秒轮询一次,任务成功前一共发出多少次查询?
- A.约 5 次请求
- B.约 6 次请求← 正确答案
- C.约 10 次请求
- D.约 12 次请求
答案:B。根据公式 N_poll = t_proc / Δt + 1 = 10 / 2 + 1 = 6 次。任务执行期间每 2 秒查一次共 5 次,加上任务提交后立即确认的那一次查询,共 6 次。最后一个 +1 是提交任务后立刻发起的那次状态确认,这也是轮询计数的常见遗漏点。
Q9. 设计异步任务的结果存储时,以下做法合理的有哪些?
- A.按照 task_id 建立索引← 正确答案
- B.为结果设置合理的 TTL← 正确答案
- C.大结果使用对象存储← 正确答案
- D.所有结果永久保留保存
答案:ABC。按 task_id 建索引保证 O(1) 查询;TTL 防止存储无限膨胀;大结果放对象存储、数据库只存链接是标准做法。永久保存所有结果会导致存储成本无限增长,不适合大多数业务场景。
Q10. 某 LLM 应用在提交高峰时任务严重积压,以下哪项做法不能从根本上提升吞吐量?
- A.增加 Worker 进程数量
- B.将同步请求改为异步任务
- C.仅增大客户端轮询频率← 正确答案
- D.对相似任务做批量合并处理
答案:C。增加 Worker 提供更多并行处理能力,异步化消除阻塞,批量合并减少单位开销——三者都能从根本上提升吞吐。而增大客户端轮询频率只是让客户端更快“发现”任务完成,系统处理能力没有变化,反而会带来更多空转请求、加重服务器负担。