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

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 灰度路由决策链

整体决策链如下,网关在路由到具体实例前完成三步:

  1. **解析灰度开关**:从 Nacos 配置(动态)读取 `gray.enabled` 与 `gray.weight`,支持实时调权。
  2. **解析请求特征**:读取 Header / 参数,判断请求是否属于「白名单灰度用户」。
  3. **实例选择**:若命中灰度规则,则从 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 标准发布流程

  1. 新推理实例以 `version=canary` 注册到 Nacos,权重初始 0。
  2. 在 Nacos 配置中把 `gray-weight.canary` 设为 5,观测核心指标。
  3. 指标平稳则逐步放量至 100,旧实例缩容。
  4. 旧实例元数据改为 `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 配置本身的版本治理。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐