为什么需要 Gateway 网关,它有什么作用?
结论先行:网关是微服务的统一入口。它把「鉴权、限流、路由、灰度、跨域、日志、协议转换」这些横切逻辑从每个服务里抽出来,集中到入口处;同时屏蔽内部拓扑,让前端只面对一个域名。Spring Cloud Gateway 基于 WebFlux + Netty,用「路由(Route)= 断言(Predicate)+ 过滤器(Filter)」模型描述转发规则。
1. 没有网关时的问题
前端 ──┬──▶ order-service (每处都要做鉴权、跨域、限流)
├──▶ user-service
└──▶ stock-service (服务扩缩容,前端要改地址)
| 问题 | 无网关 | 有网关 |
|---|---|---|
| 鉴权 | 每个服务各写一套 | 入口统一校验,内部服务信任头 |
| 跨域 | 每个服务配 CORS | 网关统一处理 |
| 限流 | 分散、标准不一 | 按路由/用户统一限流 |
| 前端地址 | 直连多个服务 | 只连网关一个域名 |
| 灰度发布 | 改 Nginx 或服务内逻辑 | 网关按 header/权重路由 |
| 日志审计 | 各服务格式不同 | 入口统一记录 |
2. 三个核心概念
请求 ──▶ Route(id, uri, predicates, filters)
│
├─ Predicate:Path=/api/order/**、Method=GET、Header=X-Tenant
│ 断言全部为 true 才匹配这条路由
└─ Filter:StripPrefix=1、AddRequestHeader、RequestRateLimiter…
匹配后依次执行,再转发到 uri
| 概念 | 作用 | 示例 |
|---|---|---|
Route | 路由的最小单位:id + 目标 uri + 断言 + 过滤器 | order_route → lb://order-service |
Predicate | 匹配条件,多个之间是「与」 | Path、Method、Header、Query、Weight |
Filter | 请求/响应处理链,分 GatewayFilter 与 GlobalFilter | StripPrefix、Retry、CircuitBreaker |
uri 前缀 lb:// 表示走注册中心负载均衡,http:// 表示直连。
3. 配置示例
spring:
cloud:
gateway:
discovery:
locator:
enabled: false # 关闭自动按服务名生成路由,避免暴露内部服务
routes:
- id: order_route
uri: lb://order-service
predicates:
- Path=/api/order/**
- Method=GET,POST
filters:
- StripPrefix=1 # /api/order/1 → /order/1
- AddRequestHeader=X-Gateway, rui-lin
- id: user_route
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 20
redis-rate-limiter.burstCapacity: 40
4. 自定义全局过滤器:统一鉴权
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtVerifier jwtVerifier;
public AuthGlobalFilter(JwtVerifier jwtVerifier) {
this.jwtVerifier = jwtVerifier;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (path.startsWith("/api/auth/")) { // 白名单
return chain.filter(exchange);
}
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !jwtVerifier.verify(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 把用户信息透传给下游,下游不再重复解析
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", jwtVerifier.userId(token))
.build();
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public int getOrder() {
return -100; // 数字越小越先执行
}
}
注意:网关返回 Mono<Void>,全程非阻塞,不要在过滤器里写阻塞式 JDBC 调用,否则会占满 Netty 的事件循环线程。
5. 网关不该做的事
- 不做业务逻辑:网关只做与业务无关的横切关注点,否则每次业务变更都要发网关。
- 不做重试风暴:对非幂等接口(POST 下单)开启重试会造成重复下单。
- 不做大文件转发:会占用内存与连接,静态资源交给 Nginx/OSS。
- 不承载数据库事务:网关无状态,不能成为业务数据的中转站。
6. 常见追问
追问一:Gateway 和 Nginx 怎么分工?
Nginx 在接入层做 TLS 卸载、静态资源、四层转发;Gateway 在业务层做路由、鉴权、限流。常见链路是 客户端 → Nginx/LB → Gateway → 服务。
追问二:lb:// 是怎么工作的?
ReactiveLoadBalancerClientFilter 解析 lb://service-name,从 ReactiveLoadBalancer 取一个实例,把 uri 替换成真实 http://ip:port 再转发。
追问三:网关的限流为什么常用 Redis?
RequestRateLimiter 基于令牌桶,默认用 Redis + Lua 保证多网关实例共享计数。单机 KeyResolver 只能用本地内存,扩容后限流会失真。
追问四:网关如何做灰度?
用 Weight 断言按比例分流,或自定义 GlobalFilter 读 header(如 X-Gray: true)后改写 lb://service-name 为带版本标签的实例。
追问五:网关挂了怎么办? 网关必须多实例 + 前置 LB/Nginx 健康检查;它应保持无状态,不存会话,才能随时扩缩容。
小结
网关的存在理由是「收敛横切逻辑 + 屏蔽内部拓扑」。它把鉴权、限流、灰度从 N 个服务里搬到 1 个入口,代价是它成了关键路径,必须无状态、可水平扩展,并且绝不能塞业务代码。