
- 为什么大模型接口必须做灰度路由
- Nacos 元数据驱动的灰度路由原理
- 网关侧权重路由实战
- 基于请求特征的大模型灰度分流
- 灰度发布流程与一键回滚
- 最佳实践与踩坑点总结
1. 为什么大模型接口必须做灰度路由

大模型接口与传统 RPC 接口有本质区别:模型版本迭代快、推理结果不可完全预知、单个失败请求的成本高(一次 32K token 的对话可能消耗数角钱,且用户体感极差)。如果直接全量上线新模型或新推理集群,一旦命中性能退化、幻觉率上升、超时激增等问题,会在分钟级内放大为线上事故。
灰度路由(Gray/CANARY Routing)的核心思想是:**让新版本只承接可控比例的流量或特定特征的流量**,在真实生产环境用小样本验证稳定性与质量,再逐步放量。结合 Spring Cloud Gateway 作为统一入口、Nacos 作为服务注册与配置中心,我们可以用「Nacos 实例元数据 + 网关自定义过滤器 + 权重负载均衡」三件套,零停机地完成大模型服务的灰度发布。
本章我们先界定几个关键概念,后续章节逐步落地代码。
- **灰度服务(canary)**:新版本实例,携带 `version=canary` 或 `gray=true` 元数据。
- **稳定服务(stable)**:旧版本实例,承载绝大多数流量。
- **权重(weight)**:canary 实例承接流量的百分比,例如 10% 表示每 10 个请求约 1 个打到 canary。
- **路由特征(route key)**:用于精确灰度的请求维度,如用户 ID、租户 ID、Header 中的 `X-Model-Version`。
> 注意:大模型接口通常走 HTTP/SSE 长连接,灰度路由要在「建立连接前」决策,因此必须在 Gateway 的 `RouteToRequestUrlFilter` 之前完成实例选择,这一点在第 3 节会重点说明。
2. Nacos 元数据驱动的灰度路由原理

2.1 元数据从哪来
Nacos 服务发现(Nacos Discovery)允许每个实例注册时携带任意 `metadata` 键值对。我们在大模型推理服务的 `application.yml` 中声明元数据:
spring:
application:
name: llm-inference-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: gateway-llm-prod
metadata:
version: canary # stable 实例写 stable
region: east
model: qwen2.5-72b
weight: "10" # 该实例期望承接的权重
Nacos 控制台与 OpenAPI 都能看到这些元数据。网关侧通过 `NacosDiscoveryClient` 或 `NacosServiceManager` 拿到 `ServiceInstance` 列表,每个实例的 `getMetadata()` 即返回上述键值。
2.2 灰度路由决策链
整体决策链如下,网关在路由到具体实例前完成三步:
- **解析灰度开关**:从 Nacos 配置(动态)读取 `gray.enabled` 与 `gray.weight`,支持实时调权。
- **解析请求特征**:读取 Header / 参数,判断请求是否属于「白名单灰度用户」。
- **实例选择**:若命中灰度规则,则从 canary 实例集合中选一个;否则按权重在 stable/canary 间分配。
下面用一张对比表说明两种灰度策略的适用场景:
|----------|-----------|-----------|----------|-----------|
| 灰度策略 | 决策依据 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 权重路由 | 全局百分比 | 实现简单、放量平滑 | 无法精确定位用户 | 模型版本平滑升级 |
| 特征路由 | 用户/租户标签 | 可定向验证、易回滚 | 需埋点传参 | 重点客户试用新模型 |
| 组合路由 | 特征优先+权重兜底 | 兼顾精准与放量 | 逻辑稍复杂 | 生产环境推荐方案 |
2.3 为什么用元数据而不是硬编码
把灰度信息写入 Nacos 元数据,可以实现**配置与代码解耦**:运维在 Nacos 控制台改一个 `version` 字段,无需重新打包镜像即可把某实例从 stable 切到 canary。配合第 12 篇讲的配置监听,网关还能实时感知变化。
3. 网关侧权重路由实战

3.1 自定义灰度负载均衡器
Spring Cloud LoadBalancer 默认是轮询。我们需要一个按 metadata 中 `version` + 权重选择实例的 `ReactorServiceInstanceLoadBalancer`。
public class GrayWeightLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final String serviceId;
private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;
// 由 Nacos 配置监听注入,key=version,value=权重百分比
private final AtomicReference<Map<String, Integer>> versionWeight =
new AtomicReference<>(Map.of("stable", 90, "canary", 10));
public GrayWeightLoadBalancer(String serviceId,
ObjectProvider<ServiceInstanceListSupplier> supplierProvider) {
this.serviceId = serviceId;
this.supplierProvider = supplierProvider;
}
public void refreshWeight(Map<String, Integer> newWeight) {
this.versionWeight.set(newWeight);
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = supplierProvider.getIfAvailable();
return supplier.get(request).next()
.map(instances -> getInstanceResponse(instances, request));
}
private Response<ServiceInstance> getInstanceResponse(
List<ServiceInstance> instances, Request request) {
if (instances.isEmpty()) return new EmptyResponse();
// 1. 特征路由优先:灰度用户直接走 canary
String uid = extractUid(request);
if (GrayUserCache.contain(uid)) {
return pickByVersion(instances, "canary");
}
// 2. 权重路由:按 versionWeight 概率分流
String targetVersion = WeightedRandom.next(versionWeight.get());
Response<ServiceInstance> resp = pickByVersion(instances, targetVersion);
return resp instanceof EmptyResponse ? pickByVersion(instances, "stable") : resp;
}
private Response<ServiceInstance> pickByVersion(
List<ServiceInstance> instances, String version) {
List<ServiceInstance> matched = instances.stream()
.filter(i -> version.equals(i.getMetadata().get("version")))
.collect(Collectors.toList());
if (matched.isEmpty()) return new EmptyResponse();
return new DefaultResponse(matched.get(ThreadLocalRandom.current().nextInt(matched.size())));
}
}
3.2 注册负载均衡器并声明路由
通过 `ReactiveLoadBalancerClientFactory` 的 `ServiceInstanceSupplier` 注册自定义实现,并在 Gateway 路由中指向 `lb://llm-inference-service`。
spring:
cloud:
gateway:
routes:
- id: llm-inference-route
uri: lb://llm-inference-service
predicates: - Path=/api/llm/**
filters: - name: GrayTagFilter # 自定义过滤器,注入灰度上下文
- StripPrefix=1
`GrayTagFilter` 在 `pre` 阶段从 `X-User-Id` 解析并判断是否在灰度名单,把结果写入 `exchange.getAttributes()`,供上面的 `extractUid` 使用。这样大模型请求在进入负载均衡前就带上了灰度特征。
4. 基于请求特征的大模型灰度分流
4.1 精细化分流维度
大模型网关的灰度往往不是「随机 10%」,而是「指定模型版本 + 指定租户」。一个典型诉求是:让 `tenant=A` 的客户试用 `qwen2.5-72b` 的 canary,而 `tenant=B` 继续使用 stable,互不干扰。
@Component
public class ModelVersionGrayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String tenant = exchange.getRequest().getHeaders().getFirst("X-Tenant-Id");
String wantVersion = exchange.getRequest().getQueryParams().getFirst("modelVersion");
if (wantVersion != null) {
// 用户显式指定模型版本,直接把 version 写入属性
exchange.getAttributes().put("targetVersion", wantVersion);
} else if (GrayTenantConfig.isCanaryTenant(tenant)) {
exchange.getAttributes().put("targetVersion", "canary");
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100; // 必须早于负载均衡过滤器执行
}
}
负载均衡器在 `getInstanceResponse` 中优先读取 `targetVersion` 属性,存在就直接按该 version 选实例,从而实现「租户级模型灰度」。
4.2 权重动态调整
权重存放在 Nacos 配置 `gray-weight.json`,网关通过 `@NacosConfigListener` 监听变更,调用 `loadBalancer.refreshWeight(...)` 热更新,无需重启:
{
"stable": 85,
"canary": 15
}
当观测到 canary 的 P99 延迟稳定、错误率低于阈值后,运维将 canary 调到 50、再调到 100,最后把旧实例下线,完成一次平滑升级。
5. 灰度发布流程与一键回滚
5.1 标准发布流程
- 新推理实例以 `version=canary` 注册到 Nacos,权重初始 0。
- 在 Nacos 配置中把 `gray-weight.canary` 设为 5,观测核心指标。
- 指标平稳则逐步放量至 100,旧实例缩容。
- 旧实例元数据改为 `version=offline` 或直接下线。
5.2 一键回滚
回滚 = 把 `gray-weight.canary` 调回 0(或把 canary 实例元数据 `version` 改为 `stable`)。由于权重与实例选择都是实时从 Nacos 读取,回滚是秒级的,不会丢请求。这正是「Nacos 配置治理 + 网关路由」组合的最大价值:**发布与回滚都是配置操作,而非发布操作**。
6. 最佳实践与踩坑点总结
- **踩坑 1:负载均衡过滤器顺序错误**。若自定义灰度过滤器 `order` 大于 `ReactiveLoadBalancerClientFilter`(默认 10100),则分流无效。务必让灰度过滤器 `order` 小于该值。
- **踩坑 2:权重和为 0 导致空实例**。当 `canary` 权重为 0 且白名单为空时,要确保兜底回退到 stable,否则返回 503。
- **踩坑 3:SSE 流式响应下的重试**。大模型常返回 SSE,灰度路由失败后**不能**简单重试(可能重复计费),应在过滤器中禁用 retry 或对 canary 失败做快速 failover 到 stable。
- **最佳实践**:灰度权重与名单统一收敛到 Nacos 配置,禁止散落在各服务代码里;每次放量都配合第 15 篇的链路追踪与 Prometheus 指标观测。
- **最佳实践**:canary 实例与 stable 实例共用同一 Nacos 服务名,仅用 metadata 区分,避免网关路由规则分裂。
通过以上设计,我们用纯配置 + 网关过滤器,实现了大模型接口的安全灰度,下一篇将深入 Nacos 配置本身的版本治理。