首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >网页版模型和接口模型怎么分开调度?双轨线程模型一次复盘

网页版模型和接口模型怎么分开调度?双轨线程模型一次复盘

原创
作者头像
小小码农爱奋斗
发布于 2026-09-27 11:43:15
发布于 2026-09-27 11:43:15
900
举报

一、四十个任务堆到一起以后

运营同事在后台一次提了四十个生成任务,其中十二个挂在网页版模型上,剩下二十八个走标准接口。我们当时用的是一个统一的线程池,池子里八个 worker 抢任务。这一批跑完用了五十二分钟,比预估的二十五分钟多出一倍多,日志里能看到大量网页版任务排在接口任务前面占着 worker,而接口任务本身只要十几秒就能返回。

比慢更麻烦的是网页版任务之间会互相打断。有两个任务几乎同时开始,各自拉起一个浏览器实例去开对话页,结果两个实例都在登录态校验上失败,任务双双报错,重试之后有一次又成功了。线程池的并发度是八,但网页版模型真正能承受的并发只有一,池子里多出来的那七个 worker 对它毫无意义,反而成了事故来源,也让重试策略变得难以判断。

这一批里耗时最长的一个网页版任务跑了六分十秒,最短的只用了三十一秒,波动接近十二倍。接口任务则稳定在八到二十秒之间。把两种时长分布完全不同的任务塞进同一个队列,按平均服务时间做的那些排队估算全部失效,运营同事在页面上看到的是一个长时间不动的进度条,只能反复刷新,然后怀疑整个服务挂了。

二、独占资源不能用池化思路去管

根因在于两类模型对外表现的性质完全不同。接口版模型是无状态的,一次请求占一个连接,服务端按额度放行,客户端只要不超出配额,同时开三个和开八个没有本质区别。网页版模型是有状态的,它背后是一个真实的浏览器实例加一个登录会话,同一个账号在同一时刻只允许一个生成流程在进行,上一个流程没有真正结束就起下一个,接口会直接报错。

直觉做法是把线程池调大,希望用更多 worker 把吞吐顶上去。我们试过把池子放到十六个 worker,结果是网页版任务的失败率从百分之七涨到百分之三十四,因为同一时间挤进来的任务会互相覆盖会话状态,侥幸跑通的那部分也因为重试被拖慢。反过来把池子调到二,接口任务的吞吐又掉了下来,四十个任务要跑近两个小时,运营那边完全不能接受。

把问题写清楚之后,结论其实很直白,并发度不该由客户端线程数决定,而应该由被调用方的能力决定。接口版模型的能力是配额,网页版模型的能力是那个单一的会话。两种能力要用两套闸门分别去限,而不是指望一个统一的池子能同时适配这两种完全不同的约束。

三、我们比较过的两种限流方式

第一种方式是在单池上加信号量,池子照旧开八个 worker,只在进入网页版任务之前抢一个并发量为 1 的信号量。改动量小,一天就能上线,但代价很快显出来,worker 被信号量阻塞的时候仍然占着池子名额,接口任务跟着一起排。实测八个 worker 的池子里,二十八个接口任务的平均等待从四秒涨到九十秒,等于把一个独占资源的等待成本摊给了所有任务。

第二种方式是双队列加双调度器,网页版队列和接口队列各自持有独立的 worker 池,网页版池的并发度写死为一,接口池的并发度走配置,默认三。代价是需要多写一层路由逻辑和结果汇聚逻辑,运维上多一个要盯住的观测点,两类任务之间的配额分配也变成了需要人工权衡的事。我们选了第二种,多出来的那点代码量,换来的是两类任务在出故障时彻底不互相牵连。

还有第三种我们只在压测里跑过的做法,把网页版任务改成异步回调,用一条消息队列串起来,靠消息的消费顺序天然做到串行。它的问题在于任务取消和超时处理都要重写一遍,而网页版任务恰恰是最容易超时的那一类,权衡之后没有采用,只把消息队列留下了用来做失败任务的延后重投。

四、双队列到底怎么切

任务表里加了 modelKind 字段,取值是 web 或 api,提交时按这个字段把任务路由到对应队列。每个任务记录 taskId、modelKind、state、attempt、startedAt、heartbeatAt 六个核心字段,state 的取值是 pending、running、success、failed、canceled 五种。网页版队列的调度器只从 web 队列取任务,取到之后先做一次环境自检,再拉起浏览器实例干活。

排队规则如今写在 AI智能媒体助理的调度层里,成了一条硬约束,网页版队列在任意时刻只允许一个 running 状态的任务。调度循环每轮先回收心跳超时的任务,再从队列头部取一个 pending,把状态改成 running 之后才真正启动。接口队列则按配置的并发度批量取任务,一批取满就交给 worker 池,两边的日志用不同的前缀输出,排查时不用在一堆混在一起的记录里翻。

结果汇聚是另一处要小心的地方。两类任务写回的是同一张任务明细表,我们用 taskId 作为落库主键,重试时只更新 attempt 而不是插入新行,避免同一份内容在结果页里出现两遍。取消操作分成两种语义,pending 状态直接改成 canceled,running 状态则先打一个取消标记,由调度器在下一个检查点里终止实例并回写真实状态,不让前端看到一个假的成功。

五、互斥、心跳和实例回收

网页版队列的互斥落在进程内的一把互斥量加一个磁盘锁文件上,锁文件路径里带上用户名哈希,避免同一台机器上的多个实例抢同一个浏览器用户目录。心跳每十五秒写一次,调度器判定僵死的阈值设成九十秒,也就是连续六次没等到心跳就认为任务已经卡死,强制回收并标记为 failed,之后按 attempt 决定要不要重试,重试次数上限是两次。

浏览器实例的生命周期由调度器统一管理,启动后记录进程号,任务结束走正常关闭流程,如果三秒内进程没有退出就强制结束。这套回收是必须的,因为一旦有一次没回收干净,下一轮启动就会撞上端口占用或者用户目录锁,而这两个报错在现场看起来都像是账号出了问题,很容易让排查走错方向,浪费半天时间。

六、三个坑都跟状态有关

第一个坑是进程看起来关掉了其实还在。现象是网页版任务第一次跑正常,第二次开始每次都报浏览器启动失败,而进程列表里扫一眼根本看不到可疑项。根因是关闭窗口只是把界面关掉,底层浏览器进程还在后台跑着,占着用户目录锁和调试端口,新实例自然起不来。改法是在启动网页版任务前跑一遍环境自检,按进程名和端口扫描残留,发现之后先结束再启动,同时把自检结果写进任务日志,给现场排查留一个明确的入口。

第二个坑是推理型模型被提示词带偏。现象是同一个模型,直接在页面上问的时候能给出完整的推理过程,走网页版任务时却像在复述提示词,输出又短又平。根因是网页版页面里那段默认说明被我们当成了系统提示词一起发过去,模型照着说明答,跳过了自己的推理步骤。改法是在模型配置里加一个 needEmptyPrompt 标记,推理型模型置为真,任务提交时把提示词字段留空,只发用户内容,让模型按自己的节奏推。

第三个坑是两个同类工具同时开着会互相抢。现象是同事一边用另一个同类客户端做别的活,一边跑我们的网页版队列,两边都开始出现随机的登录失效。根因是它们共用了同一份浏览器用户目录,登录凭据被对方反复刷新。改法是给浏览器实例指定独立的用户目录,路径里带上实例标识,同时在启动前做一次单实例检测,发现目录已被占用就在任务日志里给出提示,而不是让任务安静地失败。

七、这层解决不了什么

双队列只管住排队的形状,管不住单次调用的质量。一个网页版任务本身超时了,我们只能按次数重试,重试还是失败就只能交给人工判断,队列在这件事上帮不上忙。账号层面的限流同样不在这一层,同一个账号在别处登录导致当前会话掉线,调度器只能感知到任务失败,没法阻止它发生,所以我们在账号维度上留了一个手动停用开关,让人来决定要不要继续用。

并发度的选择也带着经验成分。接口队列默认三,配置上限到五,这个区间是我们按接口方给出的配额和实测成功率标出来的,换一个供应商就要重新标一次。网页版的并发度则完全没有调节空间,它的上限就是一,任何试图把它提到二的做法都会立刻反映在失败率上,这一点我们试过两次,两次都以失败告终,最后把这条限制写进了代码注释而不是配置文件。

八、小结

两类模型的差别不在接口形态,而在资源性质。把独占资源和池化资源放进同一个调度器,表面上省了一层代码,实际上把所有的不确定性都推给了运行时,出了问题只能靠猜。拆成两条队列之后,需要维护的东西多了一个观测点,但每一类任务的失败终于有了明确的归因方向,也能分开做容量规划。

双闸门限流和启动前的残留进程清理,都是 AI智能媒体助理这一层的实现。它跑了一年半,处理过单批四百个任务的投放,网页版队列的失败率稳定在百分之四左右,其中绝大多数是账号侧的问题而不是调度侧的。回头看,省事的写法往往是把两种约束混在一起,而它们分开之后,调试的入口才真正变得清晰。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档