一、为什么选择 Gateway + Nacos
在微服务体系中选择网关,我们面临多种方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Zuul 1.x | 成熟稳定 | 阻塞式,性能差,已停止维护 |
| Zuul 2.x | 非阻塞 | Netflix 社区活跃度下降 |
| Spring Cloud Gateway | 响应式、与 Spring 生态无缝集成 | 仅支持 Spring Boot 2.x+ |
| Kong / APISIX | 高性能、插件丰富 | 运维成本高,与 Java 体系割裂 |
Nacos 则是阿里开源的服务发现与配置中心,相比 Eureka + Config,它有两大优势:
- 服务发现 + 配置中心合一,减少组件数量
- 支持 CP / AP 模式切换,适应不同场景
二、整体架构
客户端请求
│
▼
┌─────────────────────┐
│ Spring Cloud │
│ Gateway (LB) │ ← 路由、鉴权、限流、日志
└──────┬──────────────┘
│ 服务发现 (Nacos)
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ user-service│ │ order-svc │ │ product-svc │
│ (2 nodes) │ │ (3 nodes) │ │ (2 nodes) │
└─────────────┘ └─────────────┘ └─────────────┘
三、Nacos 服务端部署
# docker-compose.yml
version: '3.8'
services:
nacos:
image: nacos/nacos-server:v2.3.0
container_name: nacos
environment:
- MODE=standalone
- PREFER_HOST_MODE=hostname
- SPRING_DATASOURCE_PLATFORM=mysql
- MYSQL_SERVICE_HOST=mysql
- MYSQL_SERVICE_DB_NAME=nacos_config
- MYSQL_SERVICE_USER=root
- MYSQL_SERVICE_PASSWORD=root123
ports:
- "8848:8848"
- "9848:9848" # gRPC(服务端-客户端通信)
- "9849:9849" # gRPC(客户端-服务端通信)
restart: always
Nacos 2.x 使用 gRPC 通信,务必开放 9848/9849 端口,否则服务注册会失败!
四、Gateway 核心配置
依赖
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
路由配置
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
namespace: production # 环境隔离
group: DEFAULT_GROUP
gateway:
discovery:
locator:
enabled: true # 自动根据服务名创建路由
lower-case-service-id: true
routes:
# 用户服务
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100 # 每秒令牌数
redis-rate-limiter.burstCapacity: 200 # 突发容量
# 订单服务 —— 灰度路由
- id: order-service-gray
uri: lb://order-service
predicates:
- Path=/api/orders/**
- Header=X-Version, v2 # Header 匹配走灰度
filters:
- StripPrefix=1
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
五、全局过滤器
认证鉴权
@Component
@Order(-1) // 优先级最高
public class AuthGlobalFilter implements GlobalFilter {
// 白名单路径
private static final Set<String> WHITELIST = Set.of(
"/api/auth/login", "/api/auth/register", "/api/public/**"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 白名单放行
if (isWhitelist(path)) {
return chain.filter(exchange);
}
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isBlank(token)) {
return unauthorized(exchange, "缺少认证令牌");
}
return verifyToken(token)
.flatMap(userInfo -> {
// 将用户信息写入请求头,下游服务可直接使用
ServerHttpRequest newRequest = exchange.getRequest().mutate()
.header("X-User-Id", userInfo.getUserId())
.header("X-User-Name", userInfo.getUsername())
.header("X-User-Role", userInfo.getRole())
.build();
return chain.filter(exchange.mutate().request(newRequest).build());
})
.onErrorResume(e -> unauthorized(exchange, "令牌无效或已过期"));
}
}
全局异常处理
@Configuration
public class GatewayExceptionConfig {
@Bean
@Order(-2)
public WebExceptionHandler globalExceptionHandler() {
return (exchange, ex) -> {
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
String message = "服务器内部错误";
if (ex instanceof ResponseStatusException rse) {
status = HttpStatus.valueOf(rse.getStatusCode().value());
message = rse.getReason();
}
exchange.getResponse().setStatusCode(status);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] body = String.format(
"{"code":%d,"message":"%s"}", status.value(), message
).getBytes(StandardCharsets.UTF_8);
return exchange.getResponse()
.writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(body)));
};
}
}
六、Sentinel 熔断与降级
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # Sentinel 控制台
datasource:
ds1:
nacos:
server-addr: localhost:8848
data-id: gateway-flow-rules
group-id: DEFAULT_GROUP
rule-type: gw-flow # 网关流控规则
Sentinel 控制台配置网关规则:
| 规则项 | 值 |
|---|---|
| 资源名 | user-service |
| QPS 阈值 | 1000 |
| 流控方式 | 快速失败 |
| 降级策略 | 慢调用比例 > 50% 触发降级 |
七、灰度发布
真实场景:新版本先放 10% 流量,观察无异常再全量发布。
@Component
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
// 从请求头读取灰度标记
String gray = ((DefaultRequestContext) request.getContext())
.getClientRequest().getHeaders().getFirst("X-Gray");
return serviceInstanceListSupplier.get().next().map(instances -> {
List<ServiceInstance> candidates = instances.stream()
.filter(i -> {
String version = i.getMetadata().getOrDefault("version", "stable");
return "gray".equals(gray)
? "gray".equals(version)
: "stable".equals(version);
})
.collect(Collectors.toList());
if (candidates.isEmpty()) {
return new EmptyResponse();
}
// 随机选一个实例
int idx = ThreadLocalRandom.current().nextInt(candidates.size());
return new DefaultResponse(candidates.get(idx));
});
}
}
八、可观测性
日志 + 链路追踪
# Micrometer + Brave (Zipkin)
management:
endpoints:
web:
exposure:
include: health,info,gateway,metrics
tracing:
sampling:
probability: 1.0
zipkin:
tracing:
endpoint: http://zipkin:9411/api/v2/spans
Gateway 自动为每个请求生成 TraceId,在响应头中返回 X-Trace-Id,排查问题时只需搜索一个 ID 就能还原完整调用链。
九、生产检查清单
| 检查项 | 说明 |
|---|---|
| ✅ Nacos 高可用 | 至少 3 节点集群 + MySQL 持久化 |
| ✅ Gateway 多副本 | 至少 2 个 Gateway 实例 + Nginx 前置 |
| ✅ 限流规则持久化 | Sentinel 规则存入 Nacos,避免重启丢失 |
| ✅ 健康检查 | 暴露 /actuator/health 给 K8s 探针 |
| ✅ 日志采集 | Gateway 日志接入 ELK / SLF |
| ✅ 优雅关闭 | spring.lifecycle.timeout-per-shutdown-phase=30s |
十、总结
Spring Cloud Gateway + Nacos 的组合已经成为国内微服务网关的主流方案:
- 低运维成本:Nacos 替代 Eureka + Config + 部分 Sentinel 职责
- 高性能:响应式架构,单节点轻松支撑 10w+ QPS
- 灵活扩展:Filter 机制允许任意定制,灰度、限流、鉴权开箱即用
网关是微服务的"守门人"——守好这道门,后面的 99% 问题都能挡在外面。
