基线自建、峰值上云:自建和云 API 混合部署怎么落地
有个场景我见过不止一次:团队咬牙买了卡、把推理服务跑起来了,结果一看监控,凌晨到上午的十几个小时里 GPU 利用率个位数,晚高峰又排队排到超时。财务那边问「这卡买得值不值」,工程这边只能说「峰值时候是够呛的」——两句话都对,但拼不出一个能拍板的结论。
自建和云 API 各有一个躲不掉的毛病。自建的毛病是波谷空转:卡是按月付钱的,不管你跑不跑它都在那儿,需求一低,固定成本就摊不薄,单位成本反而比云贵。云 API 的毛病是量大了单价扛不住:它把弹性卖给你了,代价是每一次调用都在付这份弹性的钱,你那部分完全可预测、七天二十四小时都存在的稳定负载,也在按弹性价买单。
混合部署想干的事就是把两边的长处拼起来:用自建承担稳定的基线,用云 API 承担峰值溢出。基线部分让自建的使用率尽量高,弹性部分不为闲置产能付钱。听起来很顺,难点全在细节里——尤其是「什么时候该溢出」这一个判断。
架构长什么样
结构其实很简单,一句话能说完:统一网关在前,后面挂两类上游——自建推理服务和云 API。
业务代码
│ (只认业务别名,比如 chat-main)
▼
统一网关(路由 / 回退 / 计量 / 鉴权)
├── 上游 A:自建推理服务(vLLM,OpenAI 兼容) ← 优先
└── 上游 B:云 API 供应商 ← 溢出目标
路由规则是「优先打自建,自建满了或不可用就溢出到云」。这里最关键的一条设计约束是:这套结构对业务代码必须是透明的。业务侧调用的是一个业务别名(比如 chat-main),它不知道也不需要知道这次请求最终落在自己机房还是云上。一旦业务代码里出现「如果自建忙就换个 client」这样的分支,你就把路由策略焊死在几十个调用点里了,之后想调阈值、想加第三个上游、想临时把自建摘掉做维护,全得改代码发版。
透明化还有个隐藏收益:计量口径统一。自建的成本是「卡时」,云的成本是「token 或秒」,两边单位不一样,但如果所有请求都过同一个网关,你至少能在网关上拿到统一的请求数、token 数和落到哪个上游的分布,之后再各自换算。这套多上游挂载和别名映射的做法,我在多供应商接入的网关架构里展开讲过,混合部署本质上就是它的一个特例——只不过其中一个”供应商”是你自己机房里的那台机器。
自建那一侧要先满足什么
自建要能被当成「一个普通上游」挂进网关,前提是它得长得像个上游。以 vLLM 为例,它提供的是 OpenAI 兼容服务,启动形式是 vllm serve <model>(官方示例是 vllm serve NousResearch/Meta-Llama-3-8B-Instruct),文档中提到存在 --served-model-name、--api-key、--host、--port 这几个参数。这些参数的默认值官方页面没有明确给出,所以别照着任何”常见默认”去填,起服务时全部显式指定,配置文件里写死,网关那边照抄,谁也不用猜。
真正让它能被”管起来”的是这三个基础设施端点(均出自 vLLM 官方 online serving 文档的端点清单):
| 端点 | 在混合部署里的用途 |
|---|---|
/health | 接进网关和负载均衡器的健康检查,决定「这个上游现在能不能收请求」 |
/metrics | Prometheus 格式指标出口,接监控与告警,是判断「自建现在忙不忙」的数据来源 |
/v1/models | 轻量探活,同时用来验证 --served-model-name 到底生效成什么名字 |
这三个端点的存在,是自建能被当作一个上游来管的前提,也是「自建就等于没有可观测性」这个偏见的反证。业务流量本身走 /v1/chat/completions 和 /v1/completions,跟云侧的调用形状一致,所以网关不需要为它写特殊的适配层。vLLM 起服务本身的参数取舍,我在用 vLLM 起一个 OpenAI 兼容服务里写得更细,这里只强调混合部署额外多出来的要求:/health 和 /metrics 不是可选项,没有它们你就只能靠”调用失败”来发现问题,那已经晚了。
顺带一提,/v1/models 这个探活我建议独立于健康检查再跑一次。原因很实际:服务进程活着不代表它加载的模型是你以为的那个。滚动更新时模型路径填错、--served-model-name 写错,/health 照样返回健康,网关也照样往里打请求,直到业务侧发现回复风格变了才有人去查。
溢出判据:这篇的核心
「自建满了就溢出」这句话里,“满了”是整套架构最难定义的一个词。我见过四种定义,难度和效果差得很远。
判据一:按健康状态
自建的 /health 不通、或者连续几次调用失败,就把流量切到云。
这是最容易实现的一种,几乎所有网关都原生支持。但它有个根本局限:它只解决故障,不解决拥塞。自建服务在过载的时候通常还是”健康”的——进程活着,端口通着,/health 返回正常,只是队列越排越长、首 token 延迟从几百毫秒涨到十几秒。在健康检查眼里这一切都没问题,流量继续往里灌,用户体验已经崩了。
所以这一档我的定位是:必须有,但不能只有它。它是兜底,不是峰值治理。
判据二:按队列深度或延迟
自建的排队长度、或者首 token 延迟超过阈值,就把新请求溢出到云。
这是最贴合「峰值」语义的一档——峰值的表现形式本来就是排队,而不是宕机。数据来源就是上面说的 /metrics,你需要在网关或者一个旁路的控制器里持续读它,把队列深度或延迟折算成一个「拥塞信号」。
实现难度比第一档高一个量级,误判风险也集中在这一档:
- 阈值定太低,正常的短暂抖动就触发溢出,云账单悄悄涨上去,你还以为混合方案在正常工作;
- 阈值定太高,等到指标超阈值的时候队列里已经积了一批请求,这批请求该等还是得等,溢出只救了后面的新请求;
- 没有回滞(hysteresis),流量在阈值附近来回抖,路由就在自建和云之间来回横跳,两边的缓存命中率一起垮掉。
第三条最阴,也最常被忽略。做法上给个经验:触发溢出的阈值和恢复回自建的阈值要分开设两个值,恢复阈值明显低于触发阈值,中间留一段死区,再叠一个最短驻留时间。这是控制系统里的老套路,放到路由上一样管用。
判据三:按并发上限
vLLM 有一个参数叫 max_num_seqs,官方说明是「一批里的并发请求数上限」(调小它会减少并发请求数,从而需要更少的 KV cache 空间)。既然它是你自己配的一个硬上限,那么在网关侧维护一个不超过这个值的并发配额,超出的请求直接走云,逻辑上就是自洽的。
这一档的好处是判据明确、可解释、不依赖指标采集链路——网关自己就知道现在有多少请求在自建上飞。坏处是它比较粗:并发数没满不代表不拥塞(长上下文请求吃的 KV cache 远多于短请求),并发数满了也不一定就该溢出(如果这批请求都很短,可能几百毫秒就排到了)。
我通常把它当保险丝用,跟判据二配合:日常调度看延迟信号,并发配额作为一条不能越过的红线,防止指标采集出问题时把自建打穿。
判据四:按请求特征预分流
在请求进入路由之前就按特征分:长上下文、需要大窗口模型的、批量离线的走云,短对话、高频、延迟敏感的走自建。
严格说这不算”溢出”,它是静态分流,但在混合部署里价值很高——它能把最容易压垮自建的那类请求从一开始就挡在外面。自建的显存是固定的,一个超长上下文请求吃掉的 KV cache 可能顶几十个短请求,让它进来会连累一整批人排队。
四种判据的对照:
| 判据 | 实现难度 | 解决什么 | 主要误判风险 |
|---|---|---|---|
| 健康状态 | 低 | 故障 | 过载时不触发,形同虚设 |
| 队列/延迟 | 高 | 拥塞(真正的峰值) | 阈值抖动、来回横跳 |
| 并发上限 | 中 | 硬性过载保护 | 粒度粗,长短请求一视同仁 |
| 请求特征 | 中 | 把重请求挡在自建外 | 分类规则跟不上业务变化 |
落地顺序我的建议是:判据一和判据四先上(成本低、收益立刻可见),跑一两个月攒够指标基线,再上判据二,判据三作为红线一直挂着。
回退配置怎么写,以及最容易出的那个事故
网关侧的具体配法,可以借 LiteLLM Proxy 的字段来说明。它的 reliability 文档里给出了一组 YAML 字段(下面这些字段名和示例值均逐字取自官方文档,核对于 2026-08-07;示例里的数字是官方示例值,不是默认值也不是推荐值,你得按自己的场景定):
| 字段 | 作用 | 官方示例 |
|---|---|---|
fallbacks | 失败时路由到备选模型 | fallbacks: [{"primary-model": ["fallback-model-1", "fallback-model-2"]}] |
context_window_fallbacks | 超上下文长度时回退到大窗口模型 | context_window_fallbacks: [{"small-model": ["large-model"]}] |
num_retries | 每个模型的重试次数 | num_retries: 3 |
request_timeout | 单次调用最长时间 | request_timeout: 10 |
allowed_fails | 触发冷却的失败次数阈值 | allowed_fails: 3 |
cooldown_time | 冷却时长,之后该模型重新启用 | cooldown_time: 30 |
放到混合部署里,fallbacks 就是「自建这个上游失败了,切到云上那个」的表达;allowed_fails 和 cooldown_time 合起来是熔断器语义——自建连续失败到阈值就进冷却,冷却期内流量全走云,冷却结束再放回来试。这个冷却时长值得单独想一下:自建服务重启加载模型是要时间的,冷却时间如果短于加载时间,网关会在模型还没就位的时候把流量放回去,然后再失败、再冷却,来回震荡。这些字段的完整语义(包括三种 fallback 为什么必须分开配)我在 LiteLLM 回退与故障转移 那篇里写透了,混合部署直接复用那套结论就行。
现在说混合部署最容易出的那个事故:模型 ID 忘了一起切。
自建这侧的模型名是你用 --served-model-name 指定的(或者不指定时由 vLLM 按模型路径决定,具体行为以官方文档为准),云那侧的模型名是供应商定的,两者通常不一样。而回退是按模型名做映射的:请求带着自建的模型名过来,回退触发了,网关把它发到云上游——如果发过去的还是自建那个模型名,云端返回的就是一个”模型不存在”的错误。
阴险的地方在于,这个错误只在回退真正触发的时候才会出现。平时自建好好的,回退路径根本不走,配置错了你完全看不出来。等到某天自建真挂了,回退按预期触发了,日志里也确实写着”fallback triggered”,然后所有请求 100% 失败——容灾机制在最需要它的那一刻,把故障从”一个上游挂了”放大成了”全线不可用”。
防这个坑只有两条:配置里两侧的模型名都必须显式声明并各自映射到正确的上游(网关的模型别名机制就是干这个的);以及回退路径必须演练。定期把自建上游手动摘掉几分钟,看请求是不是真的落到云上并且返回正常结果。没演练过的回退等于没配。
算一笔账:全云 / 全自建 / 混合
下面是一个按公开标价做的算术演示,所有业务参数都是假设值,你得换成自己的。目的不是给你一个数字,是让你看清混合方案的省钱到底从哪儿来。
假设参数(全部为演示假设):
- 一个月按 30 天计,即 30 × 24 × 3600 = 2,592,000 秒
- 业务负载有明显波峰波谷:70% 的时间平均需要 0.4 卡的并行算力,30% 的时间(晚高峰)需要 2.5 卡
- 自建单卡的月固定成本记作 C(含摊销/托管/电力/运维人力,各家差异极大,本文不给行情数字,你按自己的实际数填)
先把月度算力总需求折成”卡秒”:
- 波谷:0.7 × 2,592,000 = 1,814,400 秒 × 0.4 卡 = 725,760 卡秒
- 波峰:0.3 × 2,592,000 = 777,600 秒 × 2.5 卡 = 1,944,000 卡秒
- 合计 = 725,760 + 1,944,000 = 2,669,760 卡秒
云侧单价用一个真实的按秒计费公开样本:Replicate 定价页(核对 2026-08-07)列出 Nvidia L40S(标识 gpu-l40s)的价格为 $0.000975/sec。这里要说清楚两件事:其一,Replicate 不是 OpenAI 兼容端点,它用的是自己的 predictions API,真要把它当混合部署的云侧上游,接入范式跟前面讲的”挂个 OpenAI 兼容上游”不一样,得单独评估;其二,这里引用它只是因为它是一个公开可查的按秒计费样本,不构成选型建议。你实际用的云上游如果是按 token 计费,把下面的单价换成”每百万 token 单价 × 月 token 量”,结构完全一样。
方案 A:全云
2,669,760 × $0.000975 = $2,603.02 / 月
方案 B:全自建
卡数要按峰值配。峰值需要 2.5 卡并行,向上取整 = 3 张卡。
- 成本 = 3C
- 自建使用率 = 2,669,760 ÷ (3 × 2,592,000) = 2,669,760 ÷ 7,776,000 = 34.3%
三分之二的产能在空转,这就是开头说的”波谷空转”,也是纯自建方案挨骂的根本原因。
方案 C:混合(1 张自建吃基线,超出部分溢出到云)
卡数按基线配,只买 1 张卡。
- 波谷期:需求 0.4 卡 < 1 卡,自建全吃 → 725,760 卡秒走自建
- 波峰期:自建吃满 1 卡 → 1 × 777,600 = 777,600 卡秒走自建;溢出 2.5 − 1 = 1.5 卡 → 1.5 × 777,600 = 1,166,400 卡秒走云
- 自建承担合计 = 725,760 + 777,600 = 1,503,360 卡秒
- 自建使用率 = 1,503,360 ÷ 2,592,000 = 58.0%
- 云侧支出 = 1,166,400 × $0.000975 = $1,137.24
- 总成本 = C + $1,137.24
三方案摆一起:
| 方案 | 卡数 | 自建使用率 | 月成本 |
|---|---|---|---|
| A 全云 | 0 | — | $2,603.02 |
| B 全自建 | 3(按峰值配) | 34.3% | 3C |
| C 混合 | 1(按基线配) | 58.0% | C + $1,137.24 |
混合什么时候赢,解两个不等式就知道:
- 混合优于全自建:C + 1137.24 < 3C → 2C > 1137.24 → C > $568.62
- 混合优于全云:C + 1137.24 < 2603.02 → C < $1,465.78
也就是说,在这组假设下,只要单卡月固定成本落在 $568.62 ~ $1,465.78 这个区间里,混合方案同时打赢全云和全自建。区间挺宽,这也解释了为什么混合方案在实践中出现得这么频繁。
省钱的来源必须说清楚:不是云比自建便宜,也不是自建比云便宜,而是混合把自建的使用率从 34.3% 提到了 58.0%(约 1.69 倍)。 同一张卡,摊到每一份产出上的固定成本降了将近四成。混合方案真正卖的东西是使用率,不是任何一侧的单价。这个使用率变量为什么是自建经济性的命门,我在自建与 API 的盈亏平衡点怎么算里做过完整的参数化拆解,这篇的算例可以直接接到那套账本上。
反过来看,如果你的业务波动本来就小(波峰波谷差不多),全自建的使用率天然就高,混合能挤出来的空间非常有限——这直接引出下一节。
别忽略的运维成本,以及什么时候不该搞混合
上面那笔账里有一项没进公式:混合方案本身的运维开销。它不体现在账单上,但真实存在。
两套东西都要维护。 自建那侧的模型更新、显存调优、驱动和推理框架版本、机器本身的故障处理,一样都不少;云那侧的密钥轮换、额度监控、供应商侧变更跟进,也一样都不少。你没有”因为大部分流量在自建,云那边可以放着不管”这个选项——恰恰相反,云是你的容灾出口,它必须时刻可用,反而要盯得更紧。
故障域变复杂了。 单一方案出问题,排查范围是清楚的。混合方案出问题,第一个要回答的问题变成了”这个请求当时打在哪一侧”。如果网关日志里没有落上游标识,这个问题你回答不了,整个排查就卡在第一步。上游标识、请求 ID、耗时、是否经过回退,这四个字段必须在网关日志里,从第一天就在。
演练要覆盖溢出路径。 前面说过回退不演练等于没配,这里再补一句:演练要覆盖的不只是”自建挂了切云”,还有”自建拥塞时溢出”这条路径——两者触发条件不同,代码路径也常常不同。前者是异常路径,后者是正常路径,后者出问题往往更隐蔽。
那么什么时候不值得搞混合?我的判断是这三条,命中任意一条就先别上:
- 量不够大。 混合的收益是省下的固定成本,量小的时候这个绝对值可能还不够付你多花的工程时间。纯云 API 起步、把精力放在业务上,是更划算的选择。
- 没人运维。 混合方案要求你同时具备自建运维能力和网关运维能力。如果团队里没有一个人能在半夜爬起来看 GPU 显存和网关日志,这套架构的可用性会比纯云 API 更差——你为了省钱引入了一个自己接不住的故障源。
- 业务波动本来就小。 波峰波谷差不多的业务,全自建的使用率天然就高,混合能挤出的空间有限,却要背上双份运维。把上面那个算例里的波动参数改平坦,你会发现方案 B 和方案 C 的差距迅速收窄。
还有一条软性的:如果你的自建产能连基线都吃不满,那说明卡买多了,该做的是缩容而不是加个云出口。混合方案不能修复一个配错的容量决策,它只能在容量配对的前提下提高使用率。
落地自查清单
- 业务代码里是否只出现业务别名,没有任何”判断走自建还是走云”的分支?出现一处就得改掉。
- 自建服务的
--served-model-name、--host、--port、--api-key是否全部显式指定(不依赖任何未经核实的默认值),并与网关配置逐字一致? /health是否接进了网关健康检查、/metrics是否接进了监控、/v1/models是否有一条独立探活验证模型名?- 溢出判据写下来了吗?至少要有健康状态兜底 + 一条基于队列或延迟的拥塞判据,并且触发阈值与恢复阈值是两个不同的值(留死区防横跳)。
- 网关侧的并发配额是否与自建的
max_num_seqs对齐,作为不可越过的红线? - 回退配置里,自建模型名与云侧模型名是否都显式声明并各自映射到正确的上游?(这条单独列,因为它是最高频的事故点)
- 回退路径最近一次演练是什么时候?手动摘掉自建上游,请求是否真的落到云上并返回正常结果?
- 网关日志是否记录了上游标识、请求 ID、耗时、是否经过回退这四个字段?
- 按自己的真实参数重算一遍那三个方案——混合方案把自建使用率提高了多少个百分点?如果提升不明显,这套架构对你可能就不成立。
最后提醒一句:本文涉及的 vLLM 参数与端点、LiteLLM 字段与示例值、Replicate 单价,均以各自官方文档/定价页当次为准,版本和价格都会变,落地前请自行复核。