第 06 篇我们解决了"路由规则能热更新"。但还有一个更底层的问题没解决:路由里的 `uri` 写的是 `lb://llm-openai-proxy`,这个 `llm-openai-proxy` 到底对应哪几台机器、`lb://` 是怎么把请求打到具体实例的?如果某个大模型代理服务扩容了一台、或者某台宕机下线了,网关怎么知道?

答案就是**服务发现(Service Discovery)**。Nacos 同时是配置中心和注册中心——本篇我们把后一个身份用起来:让网关通过 Nacos 注册中心自动感知每个大模型微服务的所有健康实例,并实时同步到负载均衡路由里。这样"加机器不用改配置、宕机自动摘流量"就成了天然能力。

  1. 为什么需要服务发现联动
  2. Nacos 注册中心与 DiscoveryClient 原理
  3. Gateway 如何把 lb:// 解析为实例列表
  4. 实战:开启服务发现路由自动同步
  5. 大模型微服务实例的动态上下线
  6. 实战:自定义负载均衡与实例过滤
  7. 最佳实践与踩坑点
  8. 本篇小结

1. 为什么需要服务发现联动

回到一个朴素的问题:没有服务发现时,路由 `uri` 只能写死 IP。

# 写死 IP 的痛:扩缩容、故障都要人肉改
uri: http://10.20.30.11:8080

 

写死 IP 有三个致命伤,对大模型服务尤其明显:

  • **弹性伸缩失效**:大模型推理服务常根据 GPU 利用率扩缩容(如 vLLM 节点从 2 台扩到 6 台),写死 IP 意味着新节点永远接不到流量。
  • **故障要人工摘除**:某台推理机显存爆了、进程假死,写死 IP 的网关仍会把请求转过去,得到一堆 500。
  • **多环境难以复用**:测试环境一套 IP、生产环境另一套 IP,网关配置无法统一。

服务发现的本质是:**把"服务名 -> 实例列表"的映射交给一个中心(Nacos)维护,网关只认服务名,实例的增删由注册中心自动广播**。结合第 06 篇的动态路由,`uri: lb://llm-openai-proxy` 就能在运行时被解析成"当前在 Nacos 上健康的全部 `llm-openai-proxy` 实例",并且**实例变化自动同步,无需任何人工介入**。

2. Nacos 注册中心与 DiscoveryClient 原理

Nacos 注册中心维护一张"服务名 -> 实例"的注册表。每个微服务实例启动时向 Nacos 注册自己(IP、端口、健康状态、元数据、权重、集群名),并定时发心跳;Nacos 据此剔除长时间失心跳的实例。

Spring Cloud 用统一抽象 `DiscoveryClient` 屏蔽不同注册中心差异:

public interface DiscoveryClient {
    String description();
    List<ServiceInstance> getInstances(String serviceId);  // 取某服务的所有实例
    List<String> getServices();                            // 取所有服务名
}

 

Nacos 的实现是 `NacosDiscoveryClient`。Gateway 的 `DiscoveryClientRouteDefinitionLocator` 正是依赖它,**为注册表中的每一个服务自动生成一条路由**(前提是你开启了 `spring.cloud.gateway.discovery.locator.enabled=true`)。

角色

职责

---

---

微服务实例

启动时注册、定时心跳、下线时注销

Nacos Server

维护注册表、健康检查、变更推送

`DiscoveryClient`

网关侧读取注册表的统一接口

`DiscoveryClientRouteDefinitionLocator`

把每个服务变成一条 Gateway 路由

关键点:**注册表的变更(实例上下线)会触发 `DiscoveryClient` 侧缓存更新,进而让 `DiscoveryClientRouteDefinitionLocator` 重新产出 RouteDefinition**。但这仍要走第 06 篇讲的 `RefreshRoutesEvent` 才能最终反映到 `Route` 缓存。Spring Cloud Alibaba 已帮你接好这条链路,但理解它对排障至关重要。

3. Gateway 如何把 lb:// 解析为实例列表

`lb://` 是 "load balance" 的协议前缀。当 Gateway 遇到 `uri: lb://llm-openai-proxy`,它会交给 `ReactiveLoadBalancerClientFilter` 处理:

  1. 从 uri 抽出服务名 `llm-openai-proxy`;
  2. 通过 `ReactiveLoadBalancerFactory` 拿到该服务的 `ReactiveLoadBalancer`;
  3. 负载均衡器内部调用 `ServiceInstanceListSupplier`——它的默认实现会去问 `DiscoveryClient` 要实时实例列表;
  4. 按负载均衡策略(默认 `RoundRobin`,可换 `Random`/`Nacos` 权重)选一个实例;
  5. 把 `lb://llm-openai-proxy` 重写成 `http://选定的IP:端口`,继续后续过滤器链。

// lb:// 解析核心示意(框架内部,仅帮助理解)
String serviceId = "llm-openai-proxy";
ServiceInstance instance = loadBalancer.choose(serviceId);   // 从 DiscoveryClient 拿到的实例里挑一个
URI realUri = rebuildUri(uri, instance);                      // http://10.20.30.11:8080

 

对大模型服务来说,这步还隐含一个价值:**请求被均衡到多台推理机,单台显存/算力瓶颈被分摊**。当某台推理机挂了,`DiscoveryClient` 拿到的实例列表里就没有它,负载均衡器自然不会选它——这就是"自动摘流"。

4. 实战:开启服务发现路由自动同步

要在网关侧启用"服务发现自动生成路由",配置非常轻量。

**pom.xml 依赖**(网关侧):

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

 

**application.yml 关键配置**:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: llm-prod
        group: LLM_GROUP
    gateway:
      discovery:
        locator:
          enabled: true                       # 开启服务发现路由自动生成
          lower-case-service-id: true         # 服务名转小写,避免路径大小写问题
      routes:
        # 你也可以保留自定义路由,与服务发现路由共存
        - id: llm-openai
          uri: lb://llm-openai-proxy
          predicates:
            - Path=/v1/llm/openai/**
          filters:
            - StripPrefix=2

 

开启 `discovery.locator.enabled=true` 后,Nacos 里只要有一个服务叫 `llm-openai-proxy`,网关就自动多出一条 `/llm-openai-proxy/**` 的路由。但这条"自动路由"路径前缀是服务名,不够优雅,**生产上我们更推荐第 06 篇的自定义 Nacos 路由 + 本篇的 `lb://` 解析组合**:路由路径自己定义,实例解析交给服务发现。

微服务侧的注册(大模型代理服务):

spring:
  application:
    name: llm-openai-proxy
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: llm-prod
        metadata:
          supplier: openai
          version: v1

 

5. 大模型微服务实例的动态上下线

这是本篇最精彩的地方——**让大模型推理节点像云资源一样弹性**。假设我们用 `llm-vllm` 跑本地大模型,节点数随 GPU 负载在 2~6 之间波动。

当新节点启动,它执行:

// 注册逻辑由 spring-cloud-starter-alibaba-nacos-discovery 自动完成
// 你只需保证应用名与 metadata 正确,无需手写注册代码
@SpringBootApplication
@EnableDiscoveryClient   // 显式声明(新版本可省略,自动装配)
public class VllmProxyApplication {
    public static void main(String[] args) {
        SpringApplication.run(VllmProxyApplication.class, args);
    }
}

 

注册成功后,Nacos 推送变更,网关 `DiscoveryClient` 缓存更新,`DiscoveryClientRouteDefinitionLocator` 重新产出定义,链路末尾的 `RefreshRoutesEvent` 让 `Route` 缓存刷新。此后新请求即可被负载均衡到这台新节点。

当节点优雅下线(收到 `SIGTERM`),Spring 的 `DisposableBean`/`@PreDestroy` 会触发反注册,实例从 Nacos 移除,流量自动不再路由过去——**典型场景下用户零感知**。

扩缩容时序(以扩容为例):
T0  新 vLLM 节点启动,向 Nacos 注册 10.20.30.16:8000
T1  Nacos 推送服务列表变更给网关
T2  网关 DiscoveryClient 缓存更新为 3 个实例
T3  RefreshRoutesEvent 重建 Route
T4  新请求按负载均衡落到 10.20.30.16(开始分担流量)

 

场景

写死 IP

服务发现联动

---

---

---

扩容一台

改配置 + 重启网关

自动感知,秒级接流

宕机一台

人工摘除,否则持续 500

心跳超时自动剔除

灰度一台

改路由 + 重启

打 metadata 版本,按元数据结构路由

6. 实战:自定义负载均衡与实例过滤

默认轮询(RoundRobin)对大模型推理并不友好——不同 GPU 机型算力不同,应该按**权重**或服务**元数据**选实例。Nacos 原生支持权重,我们接上 `NacosLoadBalancer`。

@Configuration
public class LlmLoadBalancerConfig {
    // 使用 Nacos 权重负载均衡(依赖 nacos 权重字段)
    @Bean
    public ReactorLoadBalancer<ServiceInstance> nacosLoadBalancer(
            Environment environment,
            LoadBalancerClientFactory factory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new NacosLoadBalancer(
                factory.getLazyProvider(name, ServiceInstanceListSupplier.class),
                name);
    }
}

 

更进一步:只把请求转发到"健康的、且版本匹配"的实例。自定义 `ServiceInstanceListSupplier`:

public class TaggedInstanceSupplier implements ServiceInstanceListSupplier {
    private final ServiceInstanceListSupplier delegate;
    public TaggedInstanceSupplier(ServiceInstanceListSupplier delegate) {
        this.delegate = delegate;
    }
    @Override
    public String getServiceId() { return delegate.getServiceId(); }
    @Override
    public Flux<List<ServiceInstance>> get() {
        return delegate.get().map(instances ->
            instances.stream()
                .filter(i -> "openai".equals(i.getMetadata().get("supplier")))
                .filter(i -> "v1".equals(i.getMetadata().get("version")))
                .collect(Collectors.toList()));
    }
}

 

这样网关层就能基于实例元数据做**同版本路由、按供应商隔离**,对多供应商大模型场景极为实用:一套 `llm-openai-proxy` 服务下,可以混部不同版本的代理,网关只挑符合 metadata 的实例。

7. 最佳实践与踩坑点

服务发现联动看似"开个开关就行",但大模型场景有几个专属坑:

踩坑点

现象

正确做法

---

---

---

心跳间隔太长

宕机后几十秒仍有流量打过去

适当缩短 `spring.cloud.nacos.discovery.heart-beat-interval`(如 1~3s),配合存活探针

只开 locator 不自定义路由

路径暴露服务名 `/llm-openai-proxy/**`,不雅且易泄露内部结构

关掉自动 locator,用第06篇自定义 Nacos 路由 + `lb://` 解析

权重未配置

强弱机型平分流量,弱机成瓶颈

Nacos 控制台配置实例权重,用 `NacosLoadBalancer`

元数据缺失

无法按供应商/版本隔离

注册时强制填 `supplier`、`version` 等 metadata

实例列表缓存未刷新

扩容后新节点迟迟不接流

确认 `DiscoveryClient` 缓存 TTL 合理,必要时手动 `RefreshRoutesEvent`

跨 namespace 不通

网关找不到服务

网关与微服务必须同 `namespace` + 同 `group`

额外建议:

  • **给推理节点配就绪探针(readiness)**:只有模型真正加载完、能出 token 了才允许注册,避免"进程在但模型没好"时被打挂。
  • **保护注册中心**:网关对 Nacos 是只读订阅,不要为图省事给网关写权限;注册中心的可用区(AZ)要和高可用部署对齐。
  • **结合第 06 篇做治理**:用 Nacos 配置管理"是否启用某服务的自动路由",实现"服务发现路由"的开关化治理。

8. 本篇小结

本篇我们把"网关统一路由大模型接口 + Nacos 配置治理"推进到第二环——**服务发现联动**:

  1. Nacos 既是配置中心也是注册中心;微服务实例注册、心跳、下线,Nacos 维护实时注册表;
  2. 网关通过 `DiscoveryClient` 读取注册表,`lb://服务名` 在运行时被解析为"当前健康实例列表"并做负载均衡;
  3. 实例上下线经 Nacos 推送 → `DiscoveryClient` 缓存更新 → 路由定义刷新 → `Route` 缓存重建,全程自动;
  4. 用权重负载均衡 + 元数据过滤,可让大模型流量按机型算力、供应商版本精确调度。

至此,网关已经能做到"路由规则动态、后端实例动态"的双动态。下一篇(第 08 篇)我们给这套动态网关穿上"安全服"——**网关层统一鉴权:JWT/OAuth2 Token 校验与下游转发实战**,确保只有合法调用方能访问大模型接口。

Logo

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

更多推荐