Java 程序员第 45 阶段07:网关统一路由大模型接口,配合 Nacos 配置治理,服务发现联动:Nacos 注册中心 + Gateway 路由自动同步微服务实例

第 06 篇我们解决了"路由规则能热更新"。但还有一个更底层的问题没解决:路由里的 `uri` 写的是 `lb://llm-openai-proxy`,这个 `llm-openai-proxy` 到底对应哪几台机器、`lb://` 是怎么把请求打到具体实例的?如果某个大模型代理服务扩容了一台、或者某台宕机下线了,网关怎么知道?
答案就是**服务发现(Service Discovery)**。Nacos 同时是配置中心和注册中心——本篇我们把后一个身份用起来:让网关通过 Nacos 注册中心自动感知每个大模型微服务的所有健康实例,并实时同步到负载均衡路由里。这样"加机器不用改配置、宕机自动摘流量"就成了天然能力。
- 为什么需要服务发现联动
- Nacos 注册中心与 DiscoveryClient 原理
- Gateway 如何把 lb:// 解析为实例列表
- 实战:开启服务发现路由自动同步
- 大模型微服务实例的动态上下线
- 实战:自定义负载均衡与实例过滤
- 最佳实践与踩坑点
- 本篇小结
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` 处理:
- 从 uri 抽出服务名 `llm-openai-proxy`;
- 通过 `ReactiveLoadBalancerFactory` 拿到该服务的 `ReactiveLoadBalancer`;
- 负载均衡器内部调用 `ServiceInstanceListSupplier`——它的默认实现会去问 `DiscoveryClient` 要实时实例列表;
- 按负载均衡策略(默认 `RoundRobin`,可换 `Random`/`Nacos` 权重)选一个实例;
- 把 `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 配置治理"推进到第二环——**服务发现联动**:
- Nacos 既是配置中心也是注册中心;微服务实例注册、心跳、下线,Nacos 维护实时注册表;
- 网关通过 `DiscoveryClient` 读取注册表,`lb://服务名` 在运行时被解析为"当前健康实例列表"并做负载均衡;
- 实例上下线经 Nacos 推送 → `DiscoveryClient` 缓存更新 → 路由定义刷新 → `Route` 缓存重建,全程自动;
- 用权重负载均衡 + 元数据过滤,可让大模型流量按机型算力、供应商版本精确调度。
至此,网关已经能做到"路由规则动态、后端实例动态"的双动态。下一篇(第 08 篇)我们给这套动态网关穿上"安全服"——**网关层统一鉴权:JWT/OAuth2 Token 校验与下游转发实战**,确保只有合法调用方能访问大模型接口。
更多推荐




所有评论(0)