开头:转发那一站,藏着 AI 网关最"AI 味"的东西
上一篇结尾我留了个钩子:请求走完 rewrite、access 阶段后,要进入"转发"这一步------把请求真正发给上游模型。
在传统网关里,"转发"这一步基本就是"按负载均衡策略挑一台机器,发过去,等回来"。这件事做得很成熟,轮询、加权、最少连接,几行配置就搞定。
但在 AI 网关里,这一步突然变得一点都不简单 。因为你要调度的不是"一堆无差别的 Web 服务器",而是一堆昂贵、慢、状态不一、还会"看起来活着其实已经不行了"的模型实例。
这一篇,我就把"转发"这一站拆开讲:AI 网关怎么挑实例、怎么判断实例健康、实例挂了怎么办。难度 🟡,延续上一篇的机制拆解风格。
二、为什么传统负载均衡在这里失灵了
先讲清楚问题,才知道解法为什么长那样。传统负载均衡的假设,在 AI 场景下被三条现实击穿了。
差异一:连接是"流式长连接",不是"短请求快响应"
传统网关面对的是 REST API,请求进去、几十毫秒返回、连接关闭。负载均衡可以简单地"谁当前连接少就发给谁"。
而大模型几乎都是流式输出 (SSE),一个请求的连接会持续几秒到几十秒,token 一个一个往外吐。这时候"当前连接数"这个指标就失真了------一个慢实例可能连接数很少,但每个连接都拖着一堆长任务;一个快实例连接数很多,但吞吐其实更高。
用"连接数"去衡量 AI 实例的负载,基本是瞎指挥。
差异二:实例是"昂贵且不均"的
传统 Web 服务器,扩容是加一台"一样的机器",大家都差不多快。而模型实例不一样:
- 不同实例可能跑在不同算力上(有的 GPU 好、有的差),吞吐和延迟能差出几倍;
- 不同实例可能对应不同配额(有些是付费额度、有些是免费额度、有些有 RPM/TPM 上限);
- 你甚至可能把不同供应商的同一个模型混在一个池子里(作为降级备胎)。
这决定了:AI 网关的负载均衡,不能平均主义,必须支持"按实例的能力权重来分配"。
差异三:"健康"的定义变了
传统健康检查是"能不能 ping 通 / 返回 200"。但对模型实例来说,"能返回 200"离"能用"还差很远:
- 实例可能活着,但慢到不可接受(延迟从 2 秒飙到 20 秒);
- 实例可能活着,但错误率飙升(因为并发打满、或内部降级);
- 实例可能活着,但上下文窗口对不上(配置错了,长请求必然失败)。
所以 AI 网关的健康检查,不能只看"通不通",得看**"快不快、错不错、够不够"**。
三、多实例调度:从"平均分配"到"按能力分配"
讲完问题,讲解法。调度策略本身不算新,但 AI 场景下的用法变了。
#mermaid-svg-D5iI83p38cX8uQ0v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-D5iI83p38cX8uQ0v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-D5iI83p38cX8uQ0v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-D5iI83p38cX8uQ0v .error-icon{fill:#552222;}#mermaid-svg-D5iI83p38cX8uQ0v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-D5iI83p38cX8uQ0v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-D5iI83p38cX8uQ0v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-D5iI83p38cX8uQ0v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-D5iI83p38cX8uQ0v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-D5iI83p38cX8uQ0v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-D5iI83p38cX8uQ0v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-D5iI83p38cX8uQ0v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-D5iI83p38cX8uQ0v .marker.cross{stroke:#333333;}#mermaid-svg-D5iI83p38cX8uQ0v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-D5iI83p38cX8uQ0v p{margin:0;}#mermaid-svg-D5iI83p38cX8uQ0v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-D5iI83p38cX8uQ0v .cluster-label text{fill:#333;}#mermaid-svg-D5iI83p38cX8uQ0v .cluster-label span{color:#333;}#mermaid-svg-D5iI83p38cX8uQ0v .cluster-label span p{background-color:transparent;}#mermaid-svg-D5iI83p38cX8uQ0v .label text,#mermaid-svg-D5iI83p38cX8uQ0v span{fill:#333;color:#333;}#mermaid-svg-D5iI83p38cX8uQ0v .node rect,#mermaid-svg-D5iI83p38cX8uQ0v .node circle,#mermaid-svg-D5iI83p38cX8uQ0v .node ellipse,#mermaid-svg-D5iI83p38cX8uQ0v .node polygon,#mermaid-svg-D5iI83p38cX8uQ0v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-D5iI83p38cX8uQ0v .rough-node .label text,#mermaid-svg-D5iI83p38cX8uQ0v .node .label text,#mermaid-svg-D5iI83p38cX8uQ0v .image-shape .label,#mermaid-svg-D5iI83p38cX8uQ0v .icon-shape .label{text-anchor:middle;}#mermaid-svg-D5iI83p38cX8uQ0v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-D5iI83p38cX8uQ0v .rough-node .label,#mermaid-svg-D5iI83p38cX8uQ0v .node .label,#mermaid-svg-D5iI83p38cX8uQ0v .image-shape .label,#mermaid-svg-D5iI83p38cX8uQ0v .icon-shape .label{text-align:center;}#mermaid-svg-D5iI83p38cX8uQ0v .node.clickable{cursor:pointer;}#mermaid-svg-D5iI83p38cX8uQ0v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-D5iI83p38cX8uQ0v .arrowheadPath{fill:#333333;}#mermaid-svg-D5iI83p38cX8uQ0v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-D5iI83p38cX8uQ0v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-D5iI83p38cX8uQ0v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D5iI83p38cX8uQ0v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-D5iI83p38cX8uQ0v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D5iI83p38cX8uQ0v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-D5iI83p38cX8uQ0v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-D5iI83p38cX8uQ0v .cluster text{fill:#333;}#mermaid-svg-D5iI83p38cX8uQ0v .cluster span{color:#333;}#mermaid-svg-D5iI83p38cX8uQ0v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-D5iI83p38cX8uQ0v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-D5iI83p38cX8uQ0v rect.text{fill:none;stroke-width:0;}#mermaid-svg-D5iI83p38cX8uQ0v .icon-shape,#mermaid-svg-D5iI83p38cX8uQ0v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D5iI83p38cX8uQ0v .icon-shape p,#mermaid-svg-D5iI83p38cX8uQ0v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-D5iI83p38cX8uQ0v .icon-shape .label rect,#mermaid-svg-D5iI83p38cX8uQ0v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D5iI83p38cX8uQ0v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-D5iI83p38cX8uQ0v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-D5iI83p38cX8uQ0v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 健康
不健康/失败
成功
失败
请求进入转发阶段
实例池里有谁?
按权重挑一个实例
实例健康吗?
发送请求
切换下一个备选
结果如何?
流式透传响应
基础策略:轮询与加权轮询
最基础的是轮询 (round-robin)------大家轮流来。但这在差异二面前明显不够,于是有了加权轮询:给每个实例配一个权重,权重高的多分流量。
权重的设定,在 AI 场景里通常对应这些现实因素:
- 算力强弱------GPU 好的实例,权重调高;
- 配额高低------额度充足的实例,权重调高;
- 成本高低------贵的实例,权重调低(省钱导向时)。
一个真实做法:把权重做成"可运行时调整"的。我们在线下发现某个实例明显更快,就临时调高它的权重、把流量往它身上倾斜,观察一段时间再决定是否固化。权重不是拍脑袋写死,而是根据观测动态校准的。
进阶策略:最少连接 + 语义修正
"最少连接"在传统网关是主力,但在 AI 场景需要修正:不要只看"当前连接数",要看"当前在途的 token 数"或"当前平均延迟"。
因为一个流式请求占用的是"持续的算力",而不是"一个瞬时连接"。一个实例即便连接数少,只要每个连接都是大并发、长流式,它已经饱和了。把"在途 token 数"纳入负载判断,才接近真实负载。
四、健康检查:从"通不通"到"快不快、错不错"
这是 AI 网关最值得讲的一环。健康检查分两种,我们两种都用:
主动探测:定时"敲门"
网关定时向每个实例发一个极轻量的探测请求(比如一个最小 token 的推理请求),看它能不能在规定时间内正常返回。
这里的关键是探测请求本身要够轻------你不能为了健康检查,每次都让实例真的生成一大段文本,那既费钱又费算力。一个 token 输出就够判断"它还活着、还能算"了。
被动观察:靠"真实流量"积累判断
主动探测只能回答"活没活着",回答不了"现在好不好用"。所以更关键的是被动观察------让每个真实请求都"顺手"上报自己的表现:
- 这次请求的延迟是多少?
- 这次请求成功还是失败?
- 失败时错误码是什么?
把这些观测聚合起来,就能给每个实例"打健康分"。当某个实例的错误率超过阈值、或延迟持续高于阈值时,把它标记为"不健康",暂时从调度池里摘掉。
我们踩过的一个真实场景:某实例因为上游供应商内部降级,没有返回错误,而是返回了极慢的流式 (每 token 间隔拉长到秒级)。主动探测看它"活着",被动观察看它"慢得像乌龟"------最后是靠着延迟阈值的被动观察,才把它自动摘掉的。只做主动探测,这种"活着但废了"的实例永远发现不了。
摘除与恢复
不健康的实例被摘除后,不是永久封杀,而是进入观察期 :继续用少量探测流量试探它,如果表现恢复正常,再把它加回池子。"能摘也能回",避免一个抖动就让实例永久下线。
五、故障切换:主模型挂了,怎么兜住
健康检查的目的是"提前发现",但总有措手不及的时候------比如请求发到一半,实例直接崩了。这时候就要故障切换(fallback)。
故障切换的核心逻辑不复杂:主选实例失败 → 换下一个备选实例 → 再失败再换 → 全失败才报错。 但落到 AI 场景,有几个细节很要命。
细节一:切换要"带着语义"切
普通请求切换,重发一遍就行。但 AI 请求切换,你要保证切到新实例后,请求的语义还是一致的。
这意味着:请求体(提示词、历史、参数)要原样重发;但实例相关的部分(比如某个供应商特有的字段、模型名映射)要能对应替换。如果两个实例来自不同供应商、模型名不同,切换时还得做一层"模型名翻译",否则备胎实例压根不认这个请求。
细节二:切换要"赶在流式开始前"
最尴尬的故障是:响应已经开始流式输出,吐了半截,实例挂了。 这时候你怎么办?
- 如果直接断掉重发,用户已经看到的那半截就作废了,体验很差;
- 如果硬着头皮把半截传下去,又是不完整的结果。
我们的做法是:尽量把 fallback 的判定放在流式开始之前(连接建立、首 token 之前)。首 token 之后,除非彻底断连,否则不轻易切------宁可把"已经流出来的部分"交给上层应用去判断,也不在流中间粗暴重发。
细节三:全失败要有"明确的失败"
如果所有备选实例都试完了还是失败,网关必须返回一个明确的错误(而不是把请求静默吞掉,或者返回一个半截响应)。这个错误要能告诉调用方"发生了什么",否则调用方会一脸懵。
六、踩坑与边界
照例收尾上坑。
坑一:健康检查阈值拍脑袋,导致"误杀"或"漏杀"
延迟阈值、错误率阈值如果定得太激进,网络一抖动就把实例全摘了,反而放大故障;定得太保守,坏实例一直留在池里拖后腿。
教训:阈值要基于历史基线来定,而不是拍脑袋。 先观测一段时间每个实例的正常延迟分布,再定"几倍于基线算异常",比直接写死一个绝对数字靠谱。
坑二:fallback 的"语义丢失",是最隐蔽的坑
前面说了,切换要带着语义切。但实际最容易翻车的是参数不兼容:主实例支持某个参数,备胎实例不支持,切换过去后请求直接报 400。这种坑只在"切换发生时"才暴露,平时测不出来。
教训:凡是配置了 fallback 的,一定要做一次"强制切到备胎"的演练,不能等真出事了才第一次切。
坑三:多实例 + 流式 + 观测,监控成本翻倍
多实例调度让系统更强壮,但观测维度也跟着翻倍------每个实例的延迟、错误率、在途 token、健康分,都要有地方看。如果只做了调度、没做观测,出问题就是"两眼一抹黑"。
边界:单实例场景,别上这套
最后呼应一下第 4 篇说过的"过度设计"。如果你背后就一个模型实例、没备胎、也没多供应商,那这套多实例调度+健康检查+fallback 就是纯负担。先问自己"有没有多个可调度对象",再决定要不要上这套机制。
结语
这一篇,我把"转发"这一站讲透了:为什么传统负载均衡在 AI 场景失灵(长连接、昂贵不均、健康定义变)、多实例怎么调度(按能力加权)、健康检查怎么做(主动探测+被动观察)、故障怎么切换(带语义切、赶在流式前、明确失败)。
到这里,阶段二(架构与改造)就完整了:从选型(第 5 篇)→ 数据面/控制面分离(第 6 篇)→ 插件体系(第 7 篇)→ 多实例调度(第 8 篇),你已经对"一个开源网关要做成 AI 网关,架构层要动哪些地方"有了完整认知。
但这些都是"架构层"的故事。从下一篇开始,我们要下探到源码级(🔴) :那个数据面到底是跑在什么运行时上、请求进来后底层经历了什么、配置又是怎么做到秒级生效的。下一篇,就从最底层的运行时处理模型讲起。
