日期:2026-08-09
客户是一家集团公司,AI 网关上挂着二十多个业务方,共用后端一批 GPU 资源。矛盾出在流量结构上:既有面向员工和客户的在线问答,要求秒级响应;也有大批量的文档解析、报表生成、数据清洗,这类任务一跑就是几千上万个请求,对延迟不敏感但很吃算力。
原来的网关是简单的先进先出。这就导致一个很直接的后果,某个部门半夜启动的批量任务如果没跑完,早上九点的在线问答就得排在那几千个批量请求后面。有一次财务部门做季度报表,一次性提交了一万两千个请求,客服侧的在线问答首字延迟直接飙到三十七秒,客服主管的电话打到了平台团队。
平台团队试过一些临时办法。给批量任务限速,效果有限,因为限速阈值定高了不管用,定低了批量任务永远跑不完。也试过按时间段隔离,批量任务只能在夜里跑,但业务方不接受,很多任务本身就是白天触发的。还有人提议给在线问答单独拉一套资源池,测算下来成本翻倍,被否了。
问题的本质是,共享资源池下不同性质的请求需要不同的调度待遇,而先进先出这种最公平的策略,在这个场景里恰恰是最不公平的。
我们在网关的接入层和路由层之间加了一个调度层,做了四件事。
请求分级把进来的请求分成四档。最高档是交互式在线请求,用户在屏幕前等着的那种;第二档是准实时请求,比如工单自动分类,允许几秒延迟;第三档是批量任务;最低档是重试和补偿类请求。分级的依据是业务方在接入时申报的类型,加上网关侧的一些校验规则。
优先级队列调度按档位分队列,高档队列优先出队。这是基础逻辑,但直接这么做会有明显问题,所以配了下面两块。
防饥饿与老化机制保证低优先级请求不会被无限期挤压。每个请求在队列里等待的时间会转化成动态优先级的提升,等得越久优先级越高,到一定程度可以插到高优队列前面。
排队可视与超时快速失败面向业务方和运维。业务方能在控制台看到自己的请求排在第几位、预计等待多久;超过承诺时限还没被调度的请求直接失败返回,不让业务方在那边干等。
优先级定义的滥用是最先暴露的问题,而且是人的问题不是技术问题。分级机制上线第一周,二十多个业务方里有十七个把自己的全部请求申报成了最高档。理由都很充分:"我们这个也是用户在等""我们业务更重要"。全都最高档等于没有分级。
我们跟客户的平台负责人商量出的办法是给优先级定价。最高档的配额按照实际算力成本的一点六倍计价,第二档一点二倍,批量档零点七倍,最低档零点四倍。配额从各部门的年度预算里扣。这个机制一上,第二周就有十一个业务方主动把大部分请求降级了。有个业务方的技术负责人跟我说了句实话,之前报最高档是因为反正不要钱,现在要花钱就得算算哪些是真的急。
技术上,我们还加了申报一致性校验。如果一个申报为交互式的请求,其上下文长度超过一定阈值,或者请求来源 IP 是批处理集群的网段,网关会自动降级并记录。这类自动降级每月发生两千多次,说明校验是必要的。
低优先级饥饿的处理花了不少心思。最简单的老化算法是等待时间线性加权,我们先这么做的,结果发现在极端流量下批量任务还是会饿死,因为高优流量持续不断,线性加权的增速跟不上。改成分段加速:等待时间在承诺时限的百分之五十以内时,优先级不变;超过百分之五十开始线性提升;超过百分之八十进入加速区间,权重按平方增长;达到百分之九十五时无条件插队到最前。这套下来,批量任务的完成率从压力测试中的百分之七十一提升到百分之九十八点三。
跨租户公平是另一个维度。同一优先级内部,如果 A 部门提交了五千个请求,B 部门提交了五个,先进先出会让 B 部门的五个请求排在五千个后面。我们在每个优先级队列内部又做了一层按租户的加权轮询,每个租户按其配额占比分配出队机会。这样 B 部门的五个请求很快就能被调度到。实现上用的是分层的令牌桶,每个租户一个桶,桶的容量按配额算。
队列长度和超时的设定上,我们有过一次判断失误。最初把队列设得很长,想着能排就排着,别丢请求。运行了一段时间发现这是错的。队列太长的时候,排在后面的请求即使最终被处理了,业务方那边早就超时放弃了,算力白白浪费。我们统计过,在队列深度超过某个值之后,有百分之十九的已处理请求其结果根本没被消费。后来把队列长度按照"当前吞吐乘以承诺时限"动态计算,超出的直接拒绝并返回明确错误码,让业务方自己决定是重试还是降级。算力浪费的比例降到百分之二以下。
还有个细节值得一提。快速失败的错误码要区分清楚,"队列满被拒"和"排队超时"是两回事,业务方的处理策略也不同。前者建议退避后重试,后者说明系统整体过载,重试大概率也没用,建议降级。我们把这两个码分开之后,业务方的无效重试量减少了将近一半。
案例片段(已脱敏):调度层的优先级与配额配置。
yaml scheduler: classes: - {name: INTERACTIVE, level: 0, sla_ms: 3000, price_factor: 1.6} - {name: NEAR_RT, level: 1, sla_ms: 15000, price_factor: 1.2} - {name: BATCH, level: 2, sla_ms: 1800000, price_factor: 0.7} - {name: RETRY, level: 3, sla_ms: 3600000, price_factor: 0.4} aging: mode: piecewise segments: - {wait_ratio: "0-0.50", boost: 0} - {wait_ratio: "0.50-0.80", boost: "linear", k: 1.0} - {wait_ratio: "0.80-0.95", boost: "quadratic", k: 2.2} - {wait_ratio: ">=0.95", boost: "preempt"} tenant_fairness: mode: weighted_round_robin weight_source: quota_share min_share: 0.02 # 保底份额,防小租户被压死 queue: depth_formula: "throughput_1m * sla_sec" depth_cap: 20000 on_full: REJECT_429_QUEUE_FULL on_timeout: REJECT_504_QUEUE_TIMEOUT downgrade_check: - {if: "class==INTERACTIVE and prompt_tokens>8000", action: DOWNGRADE_TO_NEAR_RT} - {if: "class==INTERACTIVE and src_cidr in BATCH_NET", action: DOWNGRADE_TO_BATCH}案例片段(已脱敏):一次批量任务冲击下的调度表现。
``` event: 财务部批量提交 12,400 请求 (class=BATCH) time in_queue interactive_p95 batch_completed aging_preempt 09:00 143 1.12s 0 0 09:05 11,872 1.31s 412 0 09:20 10,105 1.28s 2,338 17 10:00 6,442 1.35s 6,001 186 11:30 917 1.19s 11,483 742 12:10 0 1.08s 12,400 803
对照(旧FIFO同类事件): interactive_p95 峰值 37.4s,客服侧投诉 11 起 本次: interactive_p95 峰值 1.35s,无投诉;batch 全部在 3h10m 内完成 ```
调度层上线运行四个月,覆盖全部二十多个业务方,指标如下。
高优先级请求的 P95 延迟从原来受批量冲击时的三十七秒,稳定在一点三五秒以内。日常无冲击时段是零点九秒左右。客服侧因为响应慢产生的投诉从月均十四起降到零。
低优先级任务的完成率百分之九十八点三。剩下的百分之一点七是达到超时时限被主动失败的,业务方通过重试机制在后续时段完成。批量任务的平均完成时间比原来长了百分之十八,这是让路给高优的代价,业务方接受。
排队超时率百分之一点九,队列满拒绝率百分之零点三。两项合计低于百分之三,在客户设定的可接受范围内。
整体吞吐没有下降,反而涨了百分之四点一。原因是减少了无效算力消耗:那些排太久已经被业务方放弃的请求不再被处理,退出的算力被有效请求使用了。
跨租户公平性上,小租户的请求平均等待时间从原来的四十一秒降到三点二秒。这个改善来自租户级加权轮询和保底份额。
配额定价机制的间接效果比较有意思。全网的最高档请求占比从最初申报的百分之八十九降到百分之二十三,各业务方开始认真区分自己的请求性质。平台团队说这是他们没想到的收获,一个计费规则起到的作用比技术手段还大。
以上数据为脱敏后的复盘口径。
共享资源的调度问题,技术方案只是一半,另一半是激励机制。我们在优先级滥用这件事上试过纯技术手段(自动检测、人工审批、抽查处罚),效果都一般,最后靠定价解决了大部分问题。如果一个资源是免费的,所有人都会把自己的需求标成最紧急的,这跟技术水平无关。
防饥饿的老化算法要做成非线性的。线性加权在中等负载下够用,一到极端负载就失效。分段加速的思路很朴素,但确实解决问题。我们在压测中把这套算法调了六轮才定下现在的参数,参数是压出来的不是算出来的。
队列不是越长越好,这一点反直觉。长队列看起来是"尽量不丢请求",实际上制造了大量无效计算。判断标准应该是"这个请求处理完之后,业务方还要不要",超出这个时间窗口就不该排。我们那个动态队列深度的公式很简单,效果很明显。
错误码的区分度直接影响客户端行为。把队列满和排队超时分开这个小改动,减少了近一半无效重试。设计对外接口时,错误信息的粒度值得多花点心思,这是低成本高回报的地方。
租户级公平和优先级是两个正交的维度,不能混在一起做。我们最初想用一套权重同时表达"这个请求多紧急"和"这个租户占多大份额",怎么调都别扭,拆成两层之后逻辑一下就清楚了。
现在还有一块没做好的是预测性调度。理想情况下网关应该能预判接下来一段时间的流量结构,提前给高优留出余量。我们试过用历史数据做简单预测,准确率不理想,暂时搁置了。目前的做法还是被动响应,靠老化和抢占来纠偏。