跳到正文

部署、网关与多模型路由 ​

一句话:推理服务上线不是“把引擎跑起来”,而是把副本、探针、负载均衡、网关、路由、降级和计量拼成一条可运维的链路;引擎负责单卡效率,网关负责全局可用性与成本。

为什么需要 ​

一个 vLLM 进程能跑通 demo,离生产还差很多:Pod 启动要几十秒到几分钟,滚动发布时新 Pod 没就绪就接流量,直接 5xx;K8s Service 默认按连接轮询,长连接会把请求堆到同一个 Pod;模型超时、限流、降级、配额散落在每个客户端,换模型要改业务代码;多模型、多版本、多租户没有统一入口,成本算不清,灰度也做不了。

这些问题不属于推理引擎,而属于部署与网关层。把它们混进业务代码,最后每个应用都重复实现一遍,且口径不一致。

核心机制 ​

分层部署 ​

网关是统一入口,负责“谁能调、调哪个模型、用多少额度、失败怎么办”;Service 负责把请求分发到健康副本;Pod 内的引擎负责单卡调度。三层的职责不能互相替代:网关不知道 KV 够不够,Service 不知道模型版本,引擎不知道租户配额。

副本与探针 ​

探针检查什么起点值失败后果
startupProbe模型是否加载完成periodSeconds 10、failureThreshold 30(300 秒窗口)未完成前不接流量,避免误杀
readinessProbe是否能处理请求(显存、队列)periodSeconds 5、failureThreshold 3(15 秒)从 Service 摘除,停止分发
livenessProbe进程是否卡死periodSeconds 30、failureThreshold 3(90 秒)重启容器

大模型加载慢,必须用 startupProbe 给足时间;readiness 要检查“能接请求”,而不只是端口通。一个常见事故是端口已监听但模型未加载完,readiness 过早通过,流量打进来全部超时。liveness 不能和 readiness 用同一套阈值:加载慢是正常的,卡死才是异常,把 liveness 调激进只会在长 prefill 时误杀。

另外几组常被忽略的参数:PDB minAvailable: 2,保证滚动发布和节点故障时至少留两个副本;/dev/shm 给到 4 GB(YAML 写 4Gi),小于默认的 64 MB 时多进程加载和通信会直接报错;引擎侧把 gpu_memory_utilization 从 0.91 收到 0.86、max_model_len 从 32768 收到 8192,前者给 CUDA context 和激活峰值留波动空间,后者压低单请求能占的块上限,让同一个 KV 池容纳更多并发。这些都是起点值,按模型加载时间与误杀代价调整。

负载均衡的盲区 ​

K8s Service 默认按连接分发,对长连接和长请求不友好:一个处理长上下文的 Pod 连接保持久,新连接可能继续落到它身上;各 Pod 的队列深度不同,按连接轮询不等于按负载分发;滚动发布时旧 Pod 还在处理长请求,直接摘除会中断。改进顺序是先用 IPVS 的 least_conn 按连接数分流,再在网关层按队列深度、KV 使用率或延迟做健康感知路由,最后才考虑服务网格——不要一上来上网格,运维复杂度会先于收益到来。

网关的七项职责 ​

职责关键设计
鉴权代表真实用户或租户,不用共享长期密钥
配额按租户、模型、用户限 QPS 与 token
路由按模型、版本、租户、内容长度选择后端
降级主模型超时切备用,备用不可用返回可读错误
计量记录输入 / 输出 token、耗时、成功与否
审计记录谁在何时调了什么模型、用了哪个版本
协议翻译统一入口协议与后端原生协议互转:流式事件、工具调用、错误码、token 计数归一

网关不应该做 prompt 拼接和业务判断,那是 Agent 层的事;网关只做流量、策略与协议适配。协议翻译不等于业务改写:它让同一个客户端协议能打到不同厂商的模型端点,但不改变请求语义。边界不清会让网关变成第二个业务系统,升级一次影响所有场景。报文级的两轮工具调用、MCP 与 A2A 差异见 14 协议栈与互操作。

多模型路由策略 ​

策略适用风险
按任务类型对话、代码、推理各用擅长模型分类错误会选错模型
按长度长上下文走大窗口模型长度阈值要按真实分布定
按成本简单请求走小模型质量下降需评测兜底
按租户不同租户不同模型配置漂移,难统一治理
按可用性主备切换备用模型行为差异

路由要有可解释的决策记录:为什么选这个模型、依据是什么、失败后切了哪个。没有记录,质量下降时无法归因。

降级链 ​

降级不是“随便换个模型继续”。备用模型的输出分布、上下文长度、工具调用格式可能不同,切换后要做格式适配和质量标注。对用户可见的降级要说明,避免把低质量结果当正常结果。

灰度与回滚 ​

  1. 新版本先起少量副本,接内部流量或影子流量。
  2. 按租户或比例放量,比较成功率、P99、成本和人工接管率。
  3. 指标达标后逐步扩量;任一项显著退化立即回滚。
  4. 回滚要能切回旧版本镜像和旧模型权重,不能只回滚配置。

模型版本、引擎版本、量化格式和网关路由要一起版本化。只回滚其中一个,容易出现“配置回了、权重没回”的不一致。

自动扩缩 ​

指标适用注意
队列深度请求积压比 CPU 更贴近真实负载
GPU 利用率计算饱和高利用率不等于能接更多
KV 使用率显存饱和接近上限时应扩容或拒绝
P99 延迟体验退化反应慢,适合兜底
每副本 QPS简单场景请求长度差异大时不准

扩容有冷启动,必须预热或保留最小副本。缩容要看长请求,不能因为短时间空闲就杀掉正在生成的长任务。

实战场景 ​

场景:K8s 滚动发布 readiness 只探端口,4 分钟打出 6.8% 的 5xx ​

现象:14B 模型发布 v13 镜像,6 副本、maxSurge=1、maxUnavailable=0、PDB minAvailable: 2。新 Pod 第 8 秒端口监听,权重到第 210 秒才加载完(经验值,需校准);readiness 只做 TCP 端口探活,第 8 秒就 Ready 并进 Endpoints。4 分钟发布窗口内 5xx 从 0.2% 升到 6.8%,12,000 次请求里约 800 次失败,前端自动重试又把故障请求放大到 1.6 倍。

排查路径:

排除对象判定证据结论
引擎崩溃或 OOM新 Pod restartCount=0,liveness 未失败,日志无 CUDA error排除
网关超时阈值过短失败请求 0.3–0.9 s 就返回 502,远未到 60 s 超时排除:连接被拒,不是超时
readiness 语义kubectl describe pod 显示 Ready=True 于 t+8s,日志 model loaded 在 t+210s,同时刻 /health 已返回 200确认:探针只认端口
startup 窗口不足startupProbe 300 s 窗口未被触发,加载本身没有超时排除:不是加载慢

对症做法:三个探针各管一件事,readiness 必须探到“能推理”为止。

yaml
# 窗口 300 s:加载慢是正常,只给窗口不判死
startupProbe:   {httpGet: {path: /live,  port: 8000}, periodSeconds: 10, failureThreshold: 30}
# 窗口 15 s:探到“能推理”才进 Endpoints
readinessProbe: {httpGet: {path: /ready, port: 8000}, periodSeconds: 5,  failureThreshold: 3}
# 窗口 90 s:卡死才重启,不与 readiness 共用阈值
livenessProbe:  {httpGet: {path: /live,  port: 8000}, periodSeconds: 30, failureThreshold: 3}

/ready 在权重加载完成、KV 池分配成功、且一次 1 token 冒烟推理返回后才给 200,第 210 秒才进 Endpoints,落在 300 s 启动窗口内还有 90 s 余量;readiness 的 15 s 窗口则保证 Pod 真坏时最多 15 s 内摘除。把“进程起来”当成“能服务”,等于让 210 s 的加载期完全无保护地接流量。

复盘要点:判据是“readiness 的检查项必须包含一次真实推理,端口只是必要条件”,红线是 readiness 与 startup 指向同一个 handler,那说明两者语义被合并了。更早的发现点不在聚合 5xx,而在按 Pod 拆开的错误率:新 Pod 100%、旧 Pod 0.1%,聚合后的 6.8% 会被误读成网关问题。

场景:14B 六副本服务 L4 按连接轮询,40 条长连接把 62% 的流量钉在一个 Pod ​

现象:6 副本 14B 服务,客户端是 40 个常驻 Agent 进程,HTTP/2 keep-alive 复用 30 分钟。QPS 90 时 6 个 Pod 的入流量是 62% / 23% / 7% / 5% / 2% / 1%,pod-0 的 Waiting 队列到 180、KV 使用率先破 98% 并触发抢占,pod-5 的 P99 TTFT 只有 0.4 s,pod-0 已经 6.2 s(经验值,需校准)。

排查路径:

排除对象判定证据结论
副本配置不一致6 个 Pod 镜像 digest、启动参数、max_num_seqs 完全相同排除
模型版本不同/metrics 的 model_name 与 revision 标签一致排除
节点资源差异6 个节点 GPU 型号、SM clock 一致,无降频记录排除
连接分布ss -tnp 统计各 Pod 的 ESTABLISHED 连接数 24/9/3/2/1/1,与流量占比同序,单连接 QPS 基本恒定确认:按连接分发,长连接钉死流量

对症做法:把调度单位从连接换成请求,并按队列深度与 KV 使用率降权。

yaml
load_balancer:
  policy: least_request        # L7,一次请求一次选择,不是一条连接一次
  choice_count: 2
  health_score: [queue_depth, kv_cache_usage]   # kv > 0.90 降权
upstream:
  max_concurrent_streams: 32   # 单连接并发上限,防止一个 HTTP/2 连接吸干一个 Pod

Service 的调度单位是连接,推理的负载单位是请求和 KV 块:一条 keep-alive 连接上跑 100 个请求,在 L4 看来只占 1 个计数,24 条连接换不来 24 份负载。改成 least_request 后 6 个 Pod 的占比收敛到 18/17/17/16/16/16,P99 TTFT 从 6.2 s 回到 0.9 s;queue_depth 与 kv_cache_usage 是唯一能在 P99 变差之前反映“这个 Pod 快满了”的信号。

复盘要点:判据是“Pod 间流量占比极差超过 20 个百分点,先查连接数分布,再查队列深度”,红线是出网流量均衡不等于入站请求均衡,监控必须按 Pod 看 Waiting 与 KV 使用率。更早的发现点是 pod-0 的 KV 使用率过 85% 时就该告警,而不是等 P99 从 0.8 s 涨到 6.2 s。

场景:网关降级链静默切小模型,成功率 99.9% 但 27% 的答案是降级的 ​

现象:网关配了 fallback: [model-a-small],主模型 429、超时或命中语义缓存时自动切换,响应里没有降级标记。大促当天主模型配额打满,降级率从 3.1% 升到 27%,网关 2xx 成功率仍是 99.9%,所有看板绿色;用户侧工单量涨 3 倍,人工接管率 8% → 19%,平均回答长度 420 → 260 token(经验值,需校准),运营在周会上才知道“回答变短了”。

排查路径:

排除对象判定证据结论
主模型故障主模型 5xx 为 0,429 占 31%,自身 P99 正常排除:不是故障,是配额
网关转发错误抽样 200 条响应,模型字段都写 model-a(上游覆盖),实际由 model-a-small 生成确认:换了模型但没留痕
缓存答案未计数响应中 cached=true 占 6.4%,缓存命中不进降级计数确认:两类低质结果都在口径外
质量指标人工接管率、回答长度、追问率同时恶化,均未进告警规则确认:HTTP 200 掩盖了退化

对症做法:每一个“换了模型或换了答案来源”的响应都打标记,并聚合成独立 SLI。

python
@app.middleware("http")
async def mark_degrade(request, call_next):
    resp = await call_next(request)
    if request.state.served_by != request.state.requested_model:
        resp.headers["x-degraded"] = "model"      # 模型 / cache 两值必填
        resp.headers["x-served-by"] = request.state.served_by
        resp.headers["x-degrade-reason"] = request.state.reason   # quota / timeout / cache
        DEGRADED.labels(reason=request.state.reason).inc()
    return resp

降级链的每一跳在本章都要求“备用模型(标注降级)”,不标注等于把服务质量压缩成一个状态码:27% 的请求走了低质量路径,2xx 成功率的分母却把它们算成成功。标记同时给前端与 Agent 用——命中降级的请求可以选择重试、转人工或直接告知用户,而不是把 260 token 的答案当正常结果返回。告警规则用 fallback_ratio > 5% 持续 3 分钟触发,本例在配额打满前 40 分钟 429 斜率就已经抬头。

复盘要点:判据是“凡是换了模型或答案来源的响应都必须带可枚举标记,且标记能聚合成指标”,红线是没有降级率 SLI 就不许开自动降级。更早的发现点是降级率 3.1% 已经贴近 5% 的告警线,而 429 斜率比降级率更早出现——配额类故障永远先在 429 上显形,再在质量指标上显形。

最小上手 ​

先做“网关 + 三个副本 + 探针 + 计量”的最小生产形态:

yaml
service:
  replicas: 3
  startupProbe: { periodSeconds: 10, failureThreshold: 30 }
  readinessProbe: { path: /health, periodSeconds: 5, failureThreshold: 3 }
  livenessProbe: { periodSeconds: 30, failureThreshold: 3 }
pdb: { minAvailable: 2 }
pod:
  dshm: { medium: Memory, sizeLimit: 4Gi }
engine: { gpu_memory_utilization: 0.86, max_model_len: 8192 }
gateway:
  auth: on_behalf_of_user
  quota: { per_tenant_qps: 20, per_task_tokens: 60000 }
  route: { default: model-a, long_context: model-b }
  fallback: [model-a-small]

验收看五件事:新 Pod 未加载完不接流量;一个副本挂掉请求自动转移;主模型超时能切备用并标注;每个请求能查到 token 与版本;滚动发布期间没有 5xx。

生产动作 ​

  • 启动与就绪分离:大模型用 startupProbe 给足加载时间,readiness 检查真实可用性;长连接用 least_conn 或健康感知路由,避免请求堆叠。
  • 配额前置:网关先限流再转发,别让过载打到 GPU。
  • 版本固定:请求记录模型、引擎、量化、路由版本,回滚按整组切。
  • 容量预留与演练:保留最小副本和预热池;模型超时、副本全挂、网关限流、计量写入失败都要演练。

边界 ​

  • 部署层不是银弹:网关不解决引擎内的排队和 KV 不足,只能限流或拒绝;小规模用 Service + 网关足够,模型加载慢时扩缩只能应对趋势。
  • 多模型与降级有治理成本:每个模型都要评测、配额和版本管理;备用模型输出格式不同,必须适配并标注。
  • 起点值不是最优值:探针阈值和显存参数都依赖模型大小、加载介质和硬件,照抄后必须用灰度流量校准。

讲清楚 ​

推理上线要把引擎之外的链路补齐:网关统一鉴权、配额、路由、降级、计量和协议翻译;Service 负责副本发现与分发;探针区分启动、就绪和存活,startup 给加载留 300 秒、readiness 连续 3 次失败才摘除、liveness 90 秒才判死,阈值都按误杀代价调;PDB 至少留两个副本,/dev/shm 和显存参数要给出余量。多模型路由按任务、长度、成本和可用性选择,并记录决策;降级链要适配备用模型并标注;灰度回滚按模型、引擎、量化和路由整组切换。引擎决定单卡效率,网关决定全局可用性和成本,两层都做好,推理服务才算可运维。

适用:2026 年 LLM 推理服务部署实践;K8s 与网关能力以实际版本为准 | 初稿 2026-10

把 AI 工程讲到既懂也会用