外观
模型网关与灰度发布:路由、限流、金丝雀与 A/B 实验
一句话定义:模型网关是模型服务集群前的统一流量入口,负责把请求路由到正确的模型/版本,并在其上实施限流、熔断、灰度切流、缓存与审计——它是"模型版本变多之后"从直连模型服务升级出的必要一层。
为什么值得动手:模型一多、一更新,问题就来了:怎么让 5% 的流量先试试新模型而不影响全量?新版本效果不好怎么 10 秒内切回去?多个团队各挂一个模型服务,入口谁来统一管? 答案是网关。本文用 Nginx(权重路由 + 灰度)和 Envoy(高级路由)给出两种落地形态,再讲清金丝雀发布与 A/B 实验的完整流程。
一、模型网关的职责
| 职责 | 说明 |
|---|---|
| 路由 | 按路径/请求头把请求分到不同模型或版本 |
| 限流 | 保护下游模型服务不被突发流量打垮(令牌桶/QPS 上限) |
| 熔断 | 下游连续失败时快速失败,不再继续压垮它 |
| 灰度/金丝雀 | 按权重或用户把流量切到新版本 |
| 缓存 | 对同输入请求复用结果(如人脸检索、模板化查询) |
| 审计 | 记录调用方、模型版本、耗时、结果,供 A/B 与对账 |
| 鉴权 | 校验 API Key,控制谁能调哪个模型 |
先给结论:小型团队用 Nginx 起步(配置即代码、零维护),中大型团队用 Envoy(可编程、可观测、可灰度),需要"模型级"语义(版本、自动回滚、多框架)时用 KServe 这类专用推理平台。完整决策见框架与平台怎么选。
二、方案对比
| 方案 | 优势 | 劣势 | 适用 |
|---|---|---|---|
| Nginx 简单路由 | 零依赖、配置直观 | 无内置熔断/重试,灰度仅权重 | 2~3 个模型、快速上线 |
| Envoy 高级路由 | 头/权重/镜像流量路由、熔断重试内置、xDS 动态更新 | 学习曲线陡,配置 DSL 复杂 | 微服务化、多团队、K8s 原生 |
| 应用层网关(自研/KServe) | 懂模型语义:版本管理、自动扩缩、批量入口 | 自研成本高;KServe 绑定 K8s | 模型为产品的团队 |
三、实战一:两个模型版本并存的路由(Nginx)
场景:CTR 模型从 v1 升级到 v2,先让 10% 流量走 v2。
nginx
# /etc/nginx/conf.d/model-gateway.conf
upstream ctr_v1 {
server 10.0.1.10:8000; # 旧版本模型服务(如 FastAPI/Triton)
}
upstream ctr_v2 {
server 10.0.1.11:8000; # 新版本模型服务
}
server {
listen 8080;
# 按请求头灰度:带 x-canary: v2 的请求强制走新版本(内部测试/特定用户组)
location = /predict {
if ($http_x_canary = "v2") {
proxy_pass http://ctr_v2;
}
proxy_pass http://ctr_v1;
}
}权重灰度用 Nginx 的 split_clients,基于请求某个字段的哈希做稳定的百分比切分——同一用户总是落到同一版本,这对实验一致性很重要:
nginx
# 按用户 ID 哈希,10% 的 user_id 哈希值落在 v2
split_clients "${http_x_user_id}" $ctr_backend {
10% ctr_v2;
* ctr_v1;
}
location = /predict {
proxy_pass http://$ctr_backend;
}为什么按用户而不是按请求数轮询
灰度发布和 A/B 都要求同一用户看到的结果一致(不漂移)。按请求数轮询会让同一个用户前一条走 v1、后一条走 v2,业务侧会困惑"到底新模型什么效果"。所以灰度分流要么按用户哈希、要么按 cookie/请求头。
四、实战二:金丝雀发布流程(5% → 20% → 50% → 100%)
金丝雀(Canary)的目标是把新版本逐步放量,任何一步观测到恶化就停。标准流程:
- 先部署新版本但零流量:v2 服务就绪,走一遍冒烟测试(正确性 + 延迟);
- 5% 流量:跑 30~60 分钟,盯核心指标对比 v1/v2;
- 20% → 50%:每级观察 30 分钟以上,指标无恶化才继续;
- 100% 全量:全量后再观察一段时间,然后清理旧版本。
切流时的观察指标(口径必须 v1/v2 一致,否则没法比):
| 指标 | 说明 |
|---|---|
| P99 延迟 | 新版本不能比旧的慢超过设定阈值(如 +20%) |
| 错误率 | 5xx/超时比例,恶化即停 |
| 业务效果 | 如点击率、转化率——金丝雀的最终裁判 |
| 资源 | GPU 利用率、显存、CPU,确认新模型跑得动 |
权重切换在 Nginx 里改一行(10% → 20%),在 Envoy 里改路由配置(见下)。发布策略的完整方法论见发布策略:灰度与回滚。
五、实战三:Envoy 实现按流量灰度 + A/B
Envoy 的优势是请求头/权重/镜像流量(shadow traffic)路由内建。以下配置把 10% 流量路由到 v2,并额外支持 x-ab: b 请求头强制走 v2(用于 A/B 实验对照组):
yaml
# envoy.yaml(片段)
static_resources:
clusters:
- name: ctr_v1
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: ctr_v1
endpoints:
- lb_endpoints:
- endpoint: { address: { socket_address: { address: ctr-v1, port_value: 8000 } } }
- name: ctr_v2
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: ctr_v2
endpoints:
- lb_endpoints:
- endpoint: { address: { socket_address: { address: ctr-v2, port_value: 8000 } } }
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
# 路由规则(实际在 VirtualHost 中)
route_config:
virtual_hosts:
- name: model_api
domains: ["*"]
routes:
# A/B:携带 x-ab: b 的请求强制走 v2(对照组)
- match:
headers:
- name: x-ab
exact_match: "b"
prefix: /predict
route: { cluster: ctr_v2 }
# 金丝雀:其余请求按权重 90/10 切分
- match: { prefix: /predict }
route:
weighted_clusters:
clusters:
- name: ctr_v1
weight: 90
- name: ctr_v2
weight: 10Envoy 内置熔断与重试,一行配置:
yaml
route:
cluster: ctr_v1
retry_policy:
retry_on: "5xx"
num_retries: 2
timeout: 2s先给结论:生产环境的灰度推荐 Envoy 或 K8s 网关(如 Istio/Argo Rollouts)而非手写 Nginx if——配置可动态更新(xDS)、有熔断重试、有指标集成。Nginx 适合"5 分钟搞定"的临时灰度。
限流:网关的第二种保命技能
下游模型服务容量是固定的,网关的限流保护它们不被突发流量打垮。Nginx 令牌桶限流:
nginx
# 每个 IP 每秒最多 10 个请求,突发允许 20;超限返回 429
limit_req_zone $binary_remote_addr zone=model_api:10m rate=10r/s;
location = /predict {
limit_req zone=model_api burst=20 nodelay;
limit_req_status 429;
proxy_pass http://$ctr_backend;
}Envoy 的本地限流(envoy.filters.http.local_ratelimit)同理,且可以按 x-user-id 等请求头维度限流,配合下游能力设置"每模型每秒 N QPS"。限流参数怎么定,取决于压测与容量规划测出的单实例容量,再留 20% 余量。
熔断:保护自己,也保护下游
熔断的默认参数先给结论:连续失败阈值 5~10 次、熔断窗口 10~30 秒、熔断期间直接快速失败(返回 503)。Envoy 配置:
yaml
clusters:
- name: ctr_v1
circuit_breakers:
thresholds:
- priority: DEFAULT
max_connections: 1024
max_pending_requests: 100 # 排队上限
max_requests: 2000
max_retries: 3熔断 vs 限流的区别一句话:限流挡"进来太多",熔断挡"出去打不动"——下游已经故障时,熔断让你不再傻等,而是快速失败让上游降级。配合 推荐系统在线推理 里的级联降级,整条链路才有韧性。
trace_id:灰度与 A/B 对账的地基
灰度切流后要回答"这条请求走了哪个模型版本",网关必须生成并透传 trace_id,把 model_version 写进响应头:
nginx
# Nginx 响应头带上模型版本,业务侧事后可对账
add_header X-Model-Version "v1" always;
add_header X-Trace-Id $request_id always;trace_id 贯穿网关→模型服务→日志→埋点,A/B 分析时按它 join 业务结果。这套链路追踪的实现细节见可观测性落地。
六、A/B 实验设计:分流、埋点、显著性、时长
金丝雀是"效果防御"(新版本不能更差),A/B 是"效果求证"(新版本是否显著更好)。完整设计四步:
- 分流:实验组/对照组各 50%(或按流量预算),用用户 ID 哈希保证稳定(同用户同组),如第五节
x-ab方案或网关内置分流; - 埋点:网关在响应里打上
model_version(v1/v2)、trace_id,业务侧把模型版本与业务结果(点击/转化)join 起来。版本号必须随响应返回,否则事后对不上; - 显著性检验:用双样本比例检验/卡方检验(转化类)或 t 检验(数值类),观察
p < 0.05才下结论,并关注效应量(提升几个百分点); - 时长:至少覆盖一个完整业务周期(如一周,含周末)。样本量提前用功效分析估算——常见错误是实验跑 2 天就下结论,样本不足得不出显著差异。
A/B 的分流/埋点/结果回看,最终都要落到监控与可观测性的指标体系上,否则数据对不齐,实验结论不可信。
七、回滚机制:网关一键切回
金丝雀发现指标恶化,回滚动作只有一个:把权重从 10% 改回 0%,v2 服务可以留在原地排查。
nginx
# 回滚:v2 权重归零
split_clients "${http_x_user_id}" $ctr_backend {
0% ctr_v2;
* ctr_v1;
}要点:
- 回滚不要删服务,只切流量。v2 留着抓日志、看错误,比重启环境快得多;
- 网关侧做自动回滚门槛:新版本错误率 > 1% 或 P99 超阈值连续 N 分钟,自动切回(Envoy/Istio 或自研脚本都可实现);
- 全量后发现回归(覆盖了灰度没覆盖的流量形态),同样切回 + 复盘。完整回滚策略见发布策略:灰度与回滚。
常见坑与排查
| 坑 | 现象 | 排查 |
|---|---|---|
| 灰度口径不一致 | v1/v2 指标没法比 | 统一指标定义与埋点字段,见第六节 |
| 缓存污染 | 新旧版本结果被网关缓存混用 | 缓存 key 带上模型版本号;灰度期间对新版本禁用缓存或单独缓存 |
| 分流不"粘性" | 同一用户反复横跳两个版本 | 按用户 ID 哈希(split_clients),不要按请求轮询 |
| 权重改不生效 | Nginx 改了不重载 | nginx -s reload;Envoy 需 xDS 推送或重载配置 |
| 实验样本不足 | p 值不显著就宣布结论 | 先算样本量、跑满周期,再下结论 |
| 网关超时短于模型耗时 | 慢模型请求被网关 504 | 网关 timeout 要大于模型 P99 延迟 |
| 灰度只测延迟不测效果 | 延迟没问题但业务指标掉 | 灰度与 A/B 都要埋业务指标,网关只给你流量,不给你结论 |
延伸阅读
- 发布策略:灰度与回滚 —— 金丝雀/蓝绿/滚动发布的方法论与决策框架
- MLOps 部署流水线 —— 网关切流背后,模型版本从训练到上线的完整链路
- 服务化与推理 API —— 网关下游的模型服务该长什么样
- 部署架构模式 —— 网关在整体部署架构中的位置与演进
- 监控与可观测性 —— A/B 与灰度对账需要的指标、日志、追踪体系
- FastAPI + Docker 在线服务 —— 网关下游的典型模型服务实现
- vLLM 大模型推理服务 —— LLM 服务同样适用网关灰度,且常需要按模型路由
参考资料
- Nginx 官方文档:https://nginx.org/en/docs/
- Envoy 官方文档:https://www.envoyproxy.io/docs
- KServe 官方文档:https://kserve.github.io/website/
- Argo Rollouts(K8s 金丝雀发布):https://argoproj.github.io/rollouts/
- Nginx split_clients 模块:https://nginx.org/en/docs/http/ngx_http_split_clients_module.html