一篇读懂推理服务架构:路由、负载均衡与模型热切换

从引擎到服务:推理的最后一公里

引擎解决单卡效率,架构解决集群效率

前缀缓存、PagedAttention、连续批处理这些引擎层技术解决的是「一张卡怎么跑得快」;但生产环境里真正决定用户延迟的,往往是引擎之外的那几层:请求进集群后落在哪张卡上、同一个会话的第二次请求能不能命中已有的 KV cache、升级模型版本时怎么不断流。这三件事属于服务架构层,它们的工作原理与引擎层的优化完全正交,但在很多团队的部署里恰恰是最粗放的部分——一个 nginx 轮询打到四个 vLLM 实例,就以为是负载均衡了。本文拆解这一层的三个核心问题:请求怎么路由、流量怎么均衡、模型怎么热切换。先把 KV cache 的成本量级算清楚,因为后面所有设计的经济动机都来自它。

以 Llama-2-7B(MHA,32 头,hidden 4096,fp16)为例:每个 token 的 KV cache 是 2(K与V)× 32头 × 4096维 × 2字节 = 512KB;一段 4K 的多轮会话历史就是 2GB 显存。这意味着:第一,命中缓存的请求跳过整段 prefill 计算,首 token 延迟(TTFT)从秒级降到毫秒级;第二,没命中的重复计算是纯浪费——大量真实负载(客服、Agent、代码补全)的请求前缀高度重叠,轮询式分发等于把集群的 prefill 算力反复烧在同一份历史上。架构层优化的全部价值,就是把这 2GB 的复用率最大化。

一张生产拓扑的全景

一个最小而完整的生产拓扑分四层,每层职责单一。网关层(nginx/Envoy/云 LB):TLS 终结、认证限流、按路径把 /v1/chat 与 /v1/embeddings 分流到不同的模型池。路由层(SGLang router 这类模型感知路由器,或网关的 least_conn 策略):决定请求落到哪个引擎实例,这是本文的重点。引擎层(vLLM/SGLang/TensorRT-LLM 实例池):每实例独占整卡或按 MPS/MIG 分区,负责批处理与调度。模型池的编组:按模型版本与流量等级划分实例组,组内同质、组间隔离。这套拓扑的设计哲学是:网关只做协议与安全,路由做负载与缓存感知,引擎只做计算——任何一层越界(比如网关做 token 级调度)都会在流量高峰变成瓶颈点。

flowchart TD
    C[客户端] --> G[网关层: TLS 认证 限流]
    G --> R[路由层: 会话亲和与前缀感知]
    R --> E1[引擎实例 1: 模型 v2]
    R --> E2[引擎实例 2: 模型 v2]
    R --> E3[引擎实例 3: 模型 v1 影子]
    E1 -.共享会话表.-> R
    E2 -.共享会话表.-> R
    R --> V[版本灰度: 流量按比例分流]
    V --> E3
    E1 --> M[监控: TTFT TPOT 队列深度 命中率]
    E2 --> M
    E3 --> M

负载均衡:为什么 round-robin 是错的

会话亲和:第二个请求要回到第一张卡

轮询的分发在 LLM 服务里有三个结构性缺陷,逐个看。缺陷一:破坏会话连续性。多轮对话的第二轮请求如果被轮询到另一台实例,第一轮辛苦构建的 KV cache 完全无法复用,prefill 从头算。解法是会话亲和(session affinity):按会话 ID(或用户 ID)做一致性哈希,同一会话固定路由到同一实例。实现时要注意粘性的有效期管理——实例下线、扩缩容时一致性哈希环的重排会把一部分会话「搬家」,KV cache 丢失不可避免,所以亲和哈希的虚拟节点数要配得足够大(百级),减少重排时搬家的会话比例。

Cache-Aware 路由:把命中率当第一指标

缺陷二:看不见缓存状态。哪怕没有会话概念,不同用户的请求也常共享前缀(同一个系统提示词、同一段 few-shot、同一个文档的 RAG 上下文)。轮询把共享前缀的请求打散到所有实例,每台都要算一遍同样的前缀。SGLang 的 cache-aware 路由器给出了标准解法:路由器维护每个实例的前缀树缓存画像(哪些前缀在哪台实例上有缓存、占用多少),新请求到来时计算它与各实例已有前缀的重合度,优先路由到命中率最高的实例;同时用「均衡 in-flight 请求数」做次级约束,防止流量全部涌向一台。两个目标天然冲突(全贴热门前缀 vs 均匀分布),SGLang 的实现是可调的权衡曲线。工程收益在共享前缀密集的负载里非常显著:官方报告这类路由相对 round-robin 可以把有效吞吐提升数倍(TTFT 大幅下降),而代价只是路由器多维护一份前缀画像。

缺陷三:不看队列深度。LLM 请求的「重量」差异极大:一个 100 token 的补全与一个 32K 上下文的 RAG 查询,占用引擎的时长差两个数量级。按请求数均衡(round-robin 的本质)会让某台实例攒着一堆长请求而其他实例闲置。正确信号是排队中的 token 数或 in-flight 请求数(least-loaded 路由),更好的是引擎自己暴露的 KV cache 水位。这三个缺陷修完,负载均衡就从「分发请求」变成了「分发计算资源」——这是架构层思维与 Web 服务思维最大的分野。

# cache-aware 路由的决策骨架(伪代码)
def route(request, instances):
    # 1. 会话亲和优先
    if request.session_id in session_table:
        return session_table[request.session_id]
    # 2. 前缀命中率打分: 与各实例缓存画像的前缀重合度
    scores = []
    for inst in instances:
        hit = prefix_tree.overlap(request.token_prefix, inst.cache_profile)
        load = inst.inflight_tokens          # 次级信号: 在途 token 水位
        scores.append(hit * W_HIT - load * W_LOAD)
    return instances[argmax(scores)]

模型热切换与 LoRA 热加载

版本灰度与影子流量

模型升级是推理服务里最危险的操作:权重换掉,行为全变,一次糟糕的升级就是全量故障。热切换的正确姿势不是重启,而是多版本并存加流量灰度。具体做法:新版本模型加载到独立的实例组(shadow 池),先以 1% 流量试水,对比新旧版本在真实流量上的指标(TTFT、退订率、人工抽检质量),逐步放大到 5%、25%、100%,全程旧版本实例保持在线,随时可回切。vLLM/SGLang 都支持同一集群内多模型池并存,网关按权重分流;影子模式还可以只复制流量不返回给用户(对比新旧输出的差异做离线评估),适合大版本升级前的静默验证。工程细节:切换期间两类版本的实例并存,显存按池隔离而不是同卡混部署——同卡跑两个权重对 KV cache 水位的干扰远大于它省下的那点卡。

LoRA 热加载:不改权重就能换行为

很多业务场景的「模型切换」其实不需要换基座:不同租户、不同场景的差异化需求用 LoRA 适配器就能覆盖。vLLM 的 --enable-lora 让一个基座实例同时挂多个 LoRA 适配器,每个请求带 lora 参数指定用哪个;适配器可以运行期动态注册与卸载(新 LoRA 文件落盘后调用注册接口即可,无需重启引擎),这对多租户 SaaS 是关键能力——新客户上线分钟级生效。收益账也很清楚:LoRA 权重通常只有基座的百分之一量级(7B 模型的 LoRA 在几十 MB 级),一张 80GB 的卡跑一个 70B 基座加 N 个 LoRA,等效于 N 个「专属模型」的容量,而成本只有一份基座。代价要心里有数:每个请求实际使用的 LoRA 不同,连续批处理需要按适配器分组(vLLM 内部处理),吞吐比纯基座略降;LoRA 积数过多时显存里的适配器管理也要设上限与淘汰策略。

熔断、超时与退避:流量治理的三件套

最后补齐流量治理的基本件,它们决定架构在故障时的行为。超时要按阶段拆:TTFT 超时(首 token 迟迟不出,多半是排队或 prefill 过长)与 TPOT 超时(生成中途变慢,多半是批处理拥挤)分开度量与告警,客户端超时上限必须大于 TTFT P99 加预计生成时长,否则长文本生成永远「失败」。熔断与降级:实例级健康探测失败(引擎 OOM、CUDA 崩溃)立即摘除路由池,流量倒向其余实例;全池过载时的降级策略要预先定义——排队等待、切换小模型池、还是直接 429 返回,业务不同答案不同,但「必须在配置里写死」是共同的。重试要带幂等性:流式响应的中断重试必须从已收 token 续传或整体重来,网关侧的重试次数上限(常见 1 到 2 次)加指数退避防止重试风暴。这三件套平时没有存在感,故障时决定系统是「优雅降级」还是「雪崩」。

自动扩缩容:按什么信号扩

集群层还有一个 Web 服务没有的问题:LLM 实例的扩容信号选什么。CPU/内存这类通用指标完全失灵(GPU 推理的瓶颈在显存与算力,CPU 永远闲着),按请求 QPS 扩也不对(请求重量差两个数量级)。有效的扩容信号按优先级排列:引擎暴露的队列等待时间(TTFT 里排队占的分量,直接对应用户体感)、KV cache 水位(占用率超过阈值意味着批处理即将饱和)、以及网关侧的排队长度。三者组合成 HPA(或自研扩缩器)的输入,经验阈值是 P95 队列延迟超过 500 毫秒或 cache 水位超过 85% 触发扩容。扩容本身还有一道 LLM 特有的坎:冷启动。新拉起一个 70B 模型实例,权重从对象存储加载加显存初始化要几分钟量级,远慢于 Web 服务的秒级扩容——所以扩缩器必须配预热池:常备几个已加载模型的备用实例(可用 --load-format dummy 先占住显存布局,收到流量前再真加载),或者接受「扩容后的 3 分钟内不可用」并在容量规划里按峰值冗余预留。缩容的方向同样要慢:缩容杀掉的是带着 KV cache 的实例,正在多轮会话中的用户会被打断,所以缩容按最长会话超时(常见 10 到 30 分钟)延迟执行,先摘流量排空再下线。

异构请求的分流:一个引擎吃不下所有形状

最后一个规模化之后才会暴露的问题:请求形状的异构性。同一个模型池里混着 20 token 的分类请求、2K token 的对话请求与 128K 的长文档分析,它们的批处理行为互相干扰——长请求的 prefill 会卡住整批短请求的首 token(continuous batching 已缓解但未消除),长上下文请求的 KV 占用会把短请求挤出显存。生产解法是按形状分池:短请求池与长请求池分开部署(同模型不同引擎配置,长池配更大的 max-len 与更保守的批大小),网关按预估输入长度分流;embedding 类非生成请求也单独成池(它们没有解码阶段,批处理策略完全不同)。这个设计的收益是双重的:短请求池能把 P99 TTFT 压到极低(批内方差小),长请求池的吞吐也能单独调优,代价是多一套实例组的运维成本——流量没到一定量级时,一个池加好批处理参数就够,别过度设计。

小结

推理服务架构解决的是集群效率问题,核心三问各有成熟答案。路由层的共识是让请求追着缓存走:会话亲和保住多轮对话的 KV cache,cache-aware 路由(SGLang router 的前缀画像方案)让共享前缀的请求集中命中,实测吞吐收益数倍;负载均衡的信号必须从「请求数」换成「token 水位」,因为 LLM 请求的重量差两个数量级。模型切换的正确姿势是多版本实例池加流量灰度(影子流量静默对比),行为级差异化优先用 LoRA 热加载——一份基座挂 N 个适配器,动态注册分钟级生效。流量治理的 TTFT/TPOT 分离超时、实例熔断与带幂等的受限重试,决定了系统在故障时优雅降级还是雪崩。这一层的所有设计共享同一个经济学直觉:KV cache 每千 token 半 MB 量级的成本,让「复用」成为比「算得快」更便宜的优化——把命中率做上去,比换更强的引擎更能改善用户体感延迟。

← 返回资讯列表

读者留言

COMMENTS 暂无
仅本站原创文章开放留言 · 请勿留下手机号、邮箱等个人信息

还没有留言,来说第一句?