问题引入
面试官放下简历,问道:"你们网关用的什么?"
"Spring Cloud Gateway。"
"那你说说,Gateway 为什么比 Zuul 快?Zuul 1.x 的瓶颈在哪?"
这是一个切入点,但真正的深水区在后面:
- "Gateway 的 Predicate 和 Filter 有什么区别?执行顺序怎么定的?"
- "你们怎么做网关限流?Gateway 自带的 RequestRateLimiter 和 Sentinel Gateway Adapter 有什么区别?"
- "动态路由怎么实现?加一个新服务需要重启网关吗?"
- "JWT 鉴权做在网关层还是服务层?网关鉴权有什么暗面?"
- "灰度发布怎么实现?基于权重的路由和基于 Header 的版本路由,各有什么场景?"
这些问题不是孤立的。它们围绕一个核心命题:在微服务架构的入口层,如何做到高性能路由、精细化限流、动态可扩展?
困境具象化:Zuul 1.x 的线程池噩梦
2017 年,某电商平台使用 Zuul 1.x 作为网关。大促当天,QPS 冲到 5000,网关开始大量返回 503。运维紧急扩容 Zuul 实例,但新实例上线后效果有限——瓶颈不在机器,在线程。
Zuul 1.x 基于 Servlet 容器(Tomcat/Jetty),每个请求独占一个线程。默认线程池 200 线程,当后端接口 RT 升高(如数据库慢查询导致 2 秒响应),线程被长时间占用,新请求进来只能排队。即使后端只有 1% 的接口变慢,整个网关的吞吐量也会断崖式下跌。
团队当时的应急方案是疯狂扩容 Zuul 节点,从 10 台扩到 50 台,但成本极高。根本问题是架构设计:阻塞 IO + 线程池模型,决定了网关的并发上限是线程数除以平均 RT。
Spring Cloud Gateway 的出现正是为了解决这个困境——基于 Spring WebFlux + Netty,全链路异步非阻塞,一个 EventLoop 线程可以处理数千个并发连接。
但 Gateway 不仅仅是"更快的 Zuul"。它是整个微服务体系的流量入口,承载着路由、限流、鉴权、日志、灰度等一整套流量治理职责。
核心概念
Gateway 的三大核心概念
Gateway 的所有功能都围绕三个核心概念构建:
- Route(路由):网关的基本映射单元。一个 Route = ID + 目标 URI + 一组 Predicate + 一组 Filter。
- Predicate(断言):匹配条件。请求到达网关后,依次评估所有 Route 的 Predicate,首个全部匹配的 Route 被选中。
- Filter(过滤器):处理逻辑。请求命中 Route 后,按顺序执行该 Route 绑定的 Filter 链。
读图导引:关注匹配逻辑。请求只匹配一个 Route(首个匹配的),然后执行该 Route 的 Filter 链。如果没有任何 Route 匹配,直接返回 404。Predicate 的评估顺序很重要——应该把更精确的匹配放前面。
Gateway vs Zuul 的架构差异
| 维度 | Spring Cloud Gateway | Zuul 1.x |
|---|---|---|
| 编程模型 | Spring WebFlux(响应式) | Servlet(阻塞式) |
| IO 模型 | Netty(NIO,异步非阻塞) | Tomcat/Jetty(BIO/NIO,阻塞) |
| 线程模型 | EventLoop 少量线程处理大量连接 | 每请求一个线程 |
| 吞吐量 | 高(10万+ QPS 单节点) | 低(3000-5000 QPS) |
| 长连接支持 | 原生支持 WebSocket | 需要额外配置 |
| 过滤器 | GlobalFilter + GatewayFilter | 仅 ZuulFilter |
| 开发活跃度 | 活跃(Spring 官方维护) | 停滞(Netflix 已停更) |
GatewayFilter vs GlobalFilter
Gateway 有两类 Filter:
- GatewayFilter:绑定到特定 Route,只对匹配该 Route 的请求生效。如
StripPrefix、AddRequestHeader。 - GlobalFilter:对所有 Route 生效。如
ReactiveLoadBalancerClientFilter(负载均衡)、NettyRoutingFilter(HTTP 转发)、ForwardRoutingFilter。
两类 Filter 在执行时会合并排序,按 Order 值从小到大执行。
Sentinel Gateway Adapter
Sentinel 为 Gateway 提供了专门的适配器模块 sentinel-spring-cloud-gateway-adapter,支持:
- 网关限流:按 Route ID、API 分组、请求参数(Header、Query、IP)限流。
- 自定义 API 分组:将多个 Route 归为一个 API 组,统一限流。
- 请求参数解析:从请求中提取参数作为限流维度。
与 Gateway 内置的 RequestRateLimiter 相比,Sentinel Gateway Adapter 的限流维度更细、策略更丰富。
原理分析
第一层:Gateway 的请求处理链路
1.1 请求从入口到转发的完整流程
当一个 HTTP 请求到达 Gateway:
读图导引:从左上到右下是请求的完整链路。关键节点在 D(Route 匹配)和 H(Filter 链排序)。Gateway 的请求处理完全基于 Spring WebFlux 的响应式链,没有 Servlet 容器的参与。注意 M 节点的"反向执行"——Filter 的前置逻辑按 Order 正序执行,后置逻辑按逆序执行,类似 Spring Interceptor。
1.2 Route 的加载与匹配
Gateway 的 Route 通过 RouteDefinitionLocator 加载,支持多种来源:
PropertiesRouteDefinitionLocator:从配置文件加载(spring.cloud.gateway.routes)。DiscoveryClientRouteDefinitionLocator:从服务发现(Nacos/Eureka)自动加载。CompositeRouteDefinitionLocator:组合多个来源。RedisRouteDefinitionRepository/NacosRouteDefinitionRepository:从外部存储动态加载(需自定义)。
Route 匹配的核心代码在 RoutePredicateHandlerMapping:
java
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
// 1. 检查是否已设置 GATEWAY_HANDLER_MAPPER_ATTR
if (this.managementPortType == DIFFERENT && this.managementPort != null
&& exchange.getRequest().getURI().getPort() == this.managementPort) {
return Mono.empty();
}
// 2. 获取所有 Route
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, "true");
return lookupRoute(exchange)
.flatMap(route -> {
// 3. 将匹配到的 Route 放入 Exchange 属性
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, route);
// 4. 返回 FilteringWebHandler
return Mono.just(webHandler);
}).switchIfEmpty(Mono.empty()
.then(Mono.fromRunnable(() -> {
// 5. 没有匹配到 Route,记录日志
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
})));
}
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes()
.concatMap(route -> Mono.just(route)
.filterWhen(r -> r.getPredicate().apply(exchange))
.doOnError(e -> {})
.onErrorResume(e -> Mono.empty())
)
.next() // 取第一个匹配的
.map(route -> {
// 验证 Route
validateRoute(route, exchange);
return route;
});
}
关键点:concatMap + next() 确保只取第一个匹配的 Route。Predicate 的评估顺序就是 Route 的注册顺序,所以应该将更精确的匹配规则放在前面。
yaml
spring:
cloud:
gateway:
routes:
# 精确匹配放前面
- id: order-detail
uri: lb://order-service
predicates:
- Path=/api/order/detail/**
# 通配匹配放后面
- id: order-all
uri: lb://order-service
predicates:
- Path=/api/order/**
1.3 Predicate 的底层实现
Gateway 内置了 13 种 Predicate,都是 RoutePredicateFactory 的实现:
| Predicate | 说明 | 示例 |
|---|---|---|
| Path | 路径匹配 | Path=/api/order/** |
| Method | HTTP 方法 | Method=GET,POST |
| Header | Header 匹配 | Header=X-Request-Id, \d+ |
| Query | Query 参数 | Query=foo, ba. |
| Cookie | Cookie 匹配 | Cookie=chocolate, ch.p |
| Before | 时间之前 | Before=2024-01-01T00:00:00+08:00 |
| After | 时间之后 | After=2024-01-01T00:00:00+08:00 |
| Between | 时间之间 | Between=2024-01-01T00:00:00+08:00, 2024-12-31T23:59:59+08:00 |
| RemoteAddr | IP 匹配 | RemoteAddr=192.168.1.0/24 |
| Weight | 权重分流 | Weight=group1, 8 |
| Host | Host 匹配 | Host=**.api.example.com |
| ReadBody | Body 内容 | ReadBody=.* |
| CloudFoundryRoute | CF 路由 | CloudFoundryRoute |
以 PathRoutePredicateFactory 为例:
java
public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<PathConfig> {
@Override
public Predicate<ServerWebExchange> apply(PathConfig config) {
final List<PathPattern> pathPatterns = pathPatternParser.parse(config.getPatterns());
return exchange -> {
PathContainer path = parsePath(exchange.getRequest().getURI().getRawPath());
// 匹配路径
Optional<PathPattern> optionalPathPattern = pathPatterns.stream()
.filter(pattern -> pattern.matches(path))
.findFirst();
if (optionalPathPattern.isPresent()) {
PathPattern pathPattern = optionalPathPattern.get();
// 将路径参数放入 Exchange 属性
PathPattern.PathMatchInfo pathMatchInfo = pathPattern.matchAndExtract(path);
putUriTemplateVariables(exchange, pathMatchInfo.getUriVariables());
return true;
}
return false;
};
}
}
1.4 Filter 链的执行机制
FilteringWebHandler 负责构建并执行 Filter 链:
java
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
// 1. 获取匹配的 Route
Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
// 2. 获取该 Route 的 GatewayFilter 列表
List<GatewayFilter> gatewayFilters = route.getFilters();
// 3. 与 GlobalFilter 合并
List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
combined.addAll(gatewayFilters);
// 4. 按 Order 排序
AnnotationAwareOrderComparator.sort(combined);
// 5. 构建响应式执行链
return new DefaultGatewayFilterChain(combined).filter(exchange);
}
DefaultGatewayFilterChain 是一个递归链:
java
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.defer(() -> {
if (this.index < filters.size()) {
GatewayFilter filter = filters.get(this.index);
DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1);
return filter.filter(exchange, chain);
} else {
return Mono.empty();
}
});
}
每个 Filter 可以选择:
- 继续链:调用
chain.filter(exchange),将请求传递给下一个 Filter。 - 终止链:直接返回响应(如限流 Filter 返回 429)。
- 修改请求/响应:在
chain.filter(exchange)前后添加Mono.doOnSuccess/Mono.doOnError。
第二层:内置 Filter 的实战解析
2.1 最常用的 GatewayFilter
StripPrefix:去掉路径前缀
yaml
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1 # 去掉 /api,下游收到 /order/**
AddRequestHeader:添加请求头
yaml
filters:
- AddRequestHeader=X-Request-From, Gateway
RewritePath:重写路径
yaml
filters:
- RewritePath=/api/(?<segment>.*), /$\{segment} # /api/order → /order
Retry:失败重试
yaml
filters:
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET,POST
backoff:
firstBackoff: 50ms
maxBackoff: 500ms
注意:Retry Filter 要谨慎使用。如果下游已经过载,重试会放大压力。建议只对幂等的 GET 请求开启,并配合指数退避。
2.2 GlobalFilter 的执行顺序
Gateway 内置的 GlobalFilter 及其 Order:
| Filter | Order | 职责 |
|---|---|---|
| RemoveCachedBodyFilter | Integer.MIN_VALUE | 清理缓存的 Body |
| AdaptCachedBodyGlobalFilter | -2147483638 | 缓存请求 Body |
| NettyWriteResponseFilter | -1 | 写响应 |
| RouteToRequestUrlFilter | 10000 | 将 Route URL 转换为请求 URL |
| ReactiveLoadBalancerClientFilter | 10150 | 负载均衡选择实例 |
| NettyRoutingFilter | Integer.MAX_VALUE | 发送 HTTP 请求 |
| ForwardPathFilter | 0 | Forward 路由处理 |
| WebsocketRoutingFilter | Integer.MAX_VALUE - 1 | WebSocket 路由 |
| ForwardRoutingFilter | Integer.MAX_VALUE | Forward 路由 |
典型的执行顺序:
RemoveCachedBodyFilter → AdaptCachedBodyGlobalFilter → ... →
RouteToRequestUrlFilter → ReactiveLoadBalancerClientFilter → ... →
NettyRoutingFilter → NettyWriteResponseFilter
2.3 ReactiveLoadBalancerClientFilter:负载均衡
ReactiveLoadBalancerClientFilter 负责将 lb://order-service 解析为具体的实例地址:
java
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
return chain.filter(exchange); // 不是 lb 协议,跳过
}
// 保留原始 URL
addOriginalRequestUrl(exchange, url);
// 负载均衡选择实例
return choose(exchange).doOnNext(response -> {
if (!response.hasServer()) {
throw NotFoundException.create(true);
}
ServiceInstance instance = response.getServer();
URI uri = exchange.getRequest().getURI();
// 拼接实际请求地址
String overrideScheme = instance.isSecure() ? "https" : "http";
URI requestUrl = LoadBalancerUriTools.reconstructURI(
new DelegatingServiceInstance(instance, overrideScheme), uri);
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
}).then(chain.filter(exchange));
}
protected Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String serviceId = uri.getHost();
// 使用 Spring Cloud LoadBalancer
return loadBalancer.choose(serviceId);
}
Spring Cloud 2020 版本后,Ribbon 被 Spring Cloud LoadBalancer 取代。默认使用 RoundRobinLoadBalancer(轮询),也支持 NacosLoadBalancer(Nacos 权重)。
2.4 NettyRoutingFilter:HTTP 转发
NettyRoutingFilter 是 Gateway 最核心的 Filter,它负责实际发送 HTTP 请求到下游:
java
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
// 获取或创建 Netty HttpClient
ServerHttpRequest request = exchange.getRequest();
// 构建 Netty 请求
final HttpMethod method = HttpMethod.valueOf(request.getMethodValue());
final String url = requestUrl.toASCIIString();
HttpClient httpClient = getHttpClient(route, exchange);
// 发送请求
Mono<HttpClientResponse> responseMono = httpClient.request(method)
.uri(url)
.send((req, nettyOutbound) -> {
// 写入请求体
return nettyOutbound.send(request.getBody()
.map(dataBuffer ->
((NettyDataBuffer) dataBuffer).getNativeBuffer()));
})
.responseConnection((res, connection) -> {
// 处理响应
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.valueOf(res.status().code()));
response.getHeaders().putAll(getHeaders(res));
// 将 Netty 响应体写入 ServerHttpResponse
return response.writeWith(connection.inbound().receive()
.retain()
.map(buf -> exchange.getResponse().bufferFactory().wrap(buf)));
});
return responseMono.then(chain.filter(exchange));
}
关键设计:Gateway 使用 Netty 的 HttpClient 发送请求,而不是传统的 HTTP 客户端(如 Apache HttpClient、OkHttp)。Netty 的 HttpClient 也是异步非阻塞的,与 Gateway 的响应式链路完美匹配。
第三层:网关限流的双方案对比
3.1 Gateway 内置 RequestRateLimiter
Gateway 内置基于 Redis 的令牌桶限流:
yaml
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1000 # 每秒填充 1000 个令牌
redis-rate-limiter.burstCapacity: 2000 # 桶容量 2000
redis-rate-limiter.requestedTokens: 1 # 每个请求消耗 1 个令牌
key-resolver: "#{@ipKeyResolver}" # 按 IP 限流
java
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
);
}
底层基于 Redis 的 Lua 脚本实现原子化的令牌扣减:
lua
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity / rate
local ttl = math.floor(fill_time * 2)
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
local last_updated = tonumber(redis.call("get", timestamp_key))
if last_updated == nil then
last_updated = 0
end
local delta = math.max(0, now - last_updated)
local filled_tokens = math.min(capacity, last_tokens + (delta * rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
new_tokens = filled_tokens - requested
end
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
return allowed and 1 or 0
3.2 Sentinel Gateway Adapter
Sentinel 为 Gateway 提供了更细粒度的限流能力:
java
@Configuration
public class GatewayConfig {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
@PostConstruct
public void init() {
BlockExceptionHandler blockExceptionHandler = new JsonBlockExceptionHandler();
GatewayCallbackManager.setBlockHandler(blockExceptionHandler);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
}
配置限流规则:
java
@PostConstruct
public void initRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
// 1. 按 Route ID 限流
rules.add(new GatewayFlowRule("order-service")
.setCount(1000)
.setIntervalSec(1));
// 2. 按 API 分组限流
rules.add(new GatewayFlowRule("order_api")
.setCount(500)
.setIntervalSec(1));
// 3. 按请求参数限流(如按 Header 中的 AppId)
rules.add(new GatewayFlowRule("order-service")
.setCount(100)
.setIntervalSec(1)
.setParamItem(new GatewayParamFlowItem()
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER)
.setFieldName("X-App-Id")));
GatewayRuleManager.loadRules(rules);
// 定义 API 分组
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api = new ApiDefinition("order_api")
.setPredicateItems(new HashSet<>() {{
add(new ApiPathPredicateItem().setPattern("/api/order/**"));
add(new ApiPathPredicateItem().setPattern("/api/pay/**"));
}});
definitions.add(api);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
3.3 两种限流方案的对比
| 维度 | RequestRateLimiter | Sentinel Gateway Adapter |
|---|---|---|
| 限流维度 | Route ID、IP、User(自定义 KeyResolver) | Route ID、API 分组、Header、Query、IP |
| 限流算法 | 令牌桶(Redis 分布式) | 滑动窗口、令牌桶、漏桶 |
| 降级策略 | 无 | RT/异常比例/异常数熔断 |
| 热点参数 | 不支持 | 支持 |
| 系统保护 | 不支持 | CPU/RT/线程数保护 |
| 依赖 | Redis | 无额外依赖 |
| 规则持久化 | 配置在 YAML 中 | 支持 Dashboard + Nacos 动态推送 |
生产建议:简单场景用 RequestRateLimiter,复杂场景用 Sentinel Gateway Adapter。两者可以共存——Sentinel 做粗粒度保护(全局限流),RequestRateLimiter 做细粒度 IP 限流。
第四层:动态路由的实现
4.1 静态路由的痛点
yaml
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
静态路由的问题:
- 新增服务需要改 YAML 并重启网关。
- 路由规则无法运行时调整。
- 多套环境(dev/test/prod)需要维护多份配置。
4.2 基于 Nacos Config 的动态路由
核心思路:将路由配置放到 Nacos Config,Gateway 监听配置变更,实时刷新路由表。
java
@Component
public class NacosRouteDefinitionRepository implements RouteDefinitionRepository {
@Autowired
private NacosConfigManager nacosConfigManager;
private static final String DATA_ID = "gateway-routes";
private static final String GROUP = "DEFAULT_GROUP";
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
try {
String config = nacosConfigManager.getConfigService()
.getConfig(DATA_ID, GROUP, 5000);
List<RouteDefinition> routes = parseRoutes(config);
return Flux.fromIterable(routes);
} catch (NacosException e) {
return Flux.empty();
}
}
@PostConstruct
public void initListener() throws NacosException {
nacosConfigManager.getConfigService().addListener(DATA_ID, GROUP, new Listener() {
@Override
public void receiveConfigInfo(String config) {
// 配置变更,触发路由刷新
List<RouteDefinition> routes = parseRoutes(config);
// 发布 RefreshRoutesEvent
applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
@Override
public Executor getExecutor() {
return null;
}
});
}
private List<RouteDefinition> parseRoutes(String config) {
return JSON.parseArray(config, RouteDefinition.class);
}
}
Nacos 上的配置:
json
[
{
"id": "order-service",
"predicates": [
{
"name": "Path",
"args": { "pattern": "/api/order/**" }
}
],
"filters": [
{
"name": "StripPrefix",
"args": { "parts": "1" }
}
],
"uri": "lb://order-service"
},
{
"id": "user-service",
"predicates": [
{
"name": "Path",
"args": { "pattern": "/api/user/**" }
}
],
"uri": "lb://user-service"
}
]
读图导引:这是动态路由的标准模式。Nacos Config 作为路由配置的存储,通过长轮询监听变更。收到变更后,自定义的 RouteDefinitionRepository 发布 RefreshRoutesEvent,Gateway 内部重新加载路由表。整个过程在 1 秒内完成,无需重启网关。
4.3 基于服务发现的自动路由
Gateway 支持自动从服务发现加载路由:
yaml
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 开启自动路由
lower-case-service-id: true # 服务名转小写
filters:
- RewritePath=/service/(?<segment>.*), /$\{segment}
开启后,Nacos 中注册的服务 order-service 会自动映射为路由 /service/order-service/**,无需手动配置。
注意:自动路由适合开发/测试环境,生产环境建议手动配置,避免服务暴露过多接口。
第五层:鉴权与灰度
5.1 JWT 鉴权 GlobalFilter
在网关层做 JWT 鉴权是最常见的做法:
java
@Component
@Order(-100) // 高优先级,最先执行
public class JwtAuthGlobalFilter implements GlobalFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 1. 白名单放行
if (isWhiteList(path)) {
return chain.filter(exchange);
}
// 2. 获取 Token
String token = extractToken(request);
if (StringUtils.isEmpty(token)) {
return unauthorized(exchange);
}
// 3. 验证 Token
try {
Claims claims = tokenProvider.parseToken(token);
// 4. 将用户信息注入请求头,传给下游
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Role", claims.get("role", String.class))
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (JwtException e) {
return unauthorized(exchange);
}
}
private String extractToken(ServerHttpRequest request) {
String bearerToken = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
private Mono<Void> unauthorized(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = "{\"code\":401,\"message\":\"Unauthorized\"}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes());
return response.writeWith(Mono.just(buffer));
}
}
5.2 网关鉴权的暗面
在网关做鉴权看似简单,但有三个隐藏问题:
问题 1:Token 解析的 CPU 开销
JWT 验证涉及 Base64 解码 + HMAC 签名验证,每个请求都要做。如果网关 QPS 很高(如 10万+),JWT 验证会成为 CPU 瓶颈。
优化方案:
- 使用对称密钥(HS256)而非非对称密钥(RS256),签名验证更快。
- 网关本地缓存解析结果(如 Guava Cache,TTL 设为 Token 有效期的一半)。
- 极端场景下,鉴权下沉到独立的 Auth 服务,网关只做 Token 透传。
问题 2:网关不该承载复杂鉴权
RBAC 权限校验("用户 A 能否访问资源 B")涉及查询数据库/缓存,逻辑复杂。如果在网关做,网关会从"轻量路由层"变成"重逻辑层"。
推荐架构:
读图导引:Gateway 只做 Token 解析(验证签名是否有效),将用户 ID 注入请求头。具体的 RBAC 权限校验由独立的 Auth Service 或各业务服务自己做。Gateway 的职责严格限定为"路由 + 限流 + Token 解析 + 日志"。
问题 3:内部服务调用的鉴权空白
网关鉴权只保护外部流量,服务间内部调用(如 OrderService 调用 UserService)绕过了网关,没有鉴权。这导致"内鬼服务"可以任意调用其他服务。
解决方案:
- 内部服务也启用 Dubbo/Sentinel 鉴权(如 Token 透传 + 服务白名单)。
- 使用 Service Mesh(Istio)做零信任网络,所有流量(外部+内部)统一鉴权。
5.3 灰度发布:基于权重的路由
Gateway 内置 Weight Predicate 实现权重分流:
yaml
spring:
cloud:
gateway:
routes:
# 90% 流量走 v1
- id: order-service-v1
uri: lb://order-service
predicates:
- Path=/api/order/**
- Weight=order, 9
metadata:
version: v1
# 10% 流量走 v2
- id: order-service-v2
uri: lb://order-service-v2
predicates:
- Path=/api/order/**
- Weight=order, 1
metadata:
version: v2
WeightRoutePredicateFactory 的实现:
java
public class WeightRoutePredicateFactory
extends AbstractRoutePredicateFactory<WeightConfig> {
@Override
public Predicate<ServerWebExchange> apply(WeightConfig config) {
return exchange -> {
// 获取权重计算结果
Map<String, String> weights = exchange.getAttribute(WEIGHT_ATTR);
String routeId = exchange.getAttribute(GATEWAY_PREDICATE_MATCHED_PATH_ATTR);
// 按权重随机选择
String chosenRoute = weights.get(config.getGroup());
return config.getRouteId().equals(chosenRoute);
};
}
}
5.4 灰度发布:基于 Header 的版本路由
更精细的灰度是基于请求特征(如用户 ID、设备类型、地域)路由:
yaml
spring:
cloud:
gateway:
routes:
# 带 X-Canary: true 的请求走 v2
- id: order-service-canary
uri: lb://order-service-v2
predicates:
- Path=/api/order/**
- Header=X-Canary, true
# 默认走 v1
- id: order-service-default
uri: lb://order-service
predicates:
- Path=/api/order/**
或者基于自定义 GlobalFilter 实现用户白名单灰度:
java
@Component
public class CanaryGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private CanaryService canaryService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
if (canaryService.isCanaryUser(userId)) {
// 将请求路由到 canary 版本
ServerHttpRequest request = exchange.getRequest().mutate()
.header(GATEWAY_CANARY_HEADER, "true")
.build();
return chain.filter(exchange.mutate().request(request).build());
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return ROUTE_TO_URL_FILTER_ORDER - 1; // 在负载均衡之前执行
}
}
第六层:Gateway 的性能优化与边界
6.1 响应式链路的性能优势
Gateway 基于 WebFlux + Netty 的响应式链路,核心优势在于线程复用:
读图导引:对比左右两个模型。Zuul 1.x 的线程在下游响应期间一直阻塞,200 个线程只能处理 200 个并发请求。Gateway 的 EventLoop 将请求发出后立即释放,等待下游响应期间可以处理其他请求。Netty 的 EventLoop 默认数量是 CPU 核数 × 2,却能支撑数万并发。
6.2 性能优化的四个方向
方向 1:Netty 参数调优
yaml
spring:
cloud:
gateway:
httpclient:
connect-timeout: 2000 # 连接超时 2s
response-timeout: 5s # 响应超时 5s
pool:
type: elastic # 弹性连接池
max-connections: 1000 # 最大连接数
max-idle-time: 10s # 空闲连接回收
wiretap: false # 关闭抓包(生产关)
方向 2:关闭不必要的日志
Gateway 的 HttpClientWiretap 和 HttpServerWiretap 会记录所有请求/响应体,生产环境务必关闭:
yaml
spring:
cloud:
gateway:
httpserver:
wiretap: false
httpclient:
wiretap: false
方向 3:启用 HTTPS 卸载
如果 Gateway 前端有负载均衡(如 Nginx、SLB),让负载均衡做 HTTPS 卸载,Gateway 只处理 HTTP,减少 TLS 握手开销。
方向 4:本地缓存响应
对于不经常变化的静态数据(如商品类目),可以在 Gateway 层加本地缓存:
java
@Component
public class CacheGatewayFilterFactory
extends AbstractGatewayFilterFactory<CacheGatewayFilterFactory.Config> {
private final Cache<String, CachedResponse> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(30))
.maximumSize(10000)
.build();
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String cacheKey = generateKey(exchange);
CachedResponse cached = cache.getIfPresent(cacheKey);
if (cached != null && !cached.isExpired()) {
return writeCachedResponse(exchange, cached);
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 缓存响应(实际实现需要拦截响应体)
cache.put(cacheKey, cacheResponse(exchange));
}));
};
}
}
6.3 Gateway 的设计边界
Gateway 不是万能的。以下职责不应放在网关层:
| 职责 | 是否在网关做 | 原因 |
|---|---|---|
| 路由、限流、鉴权 | 是 | Gateway 的核心职责 |
| 日志、监控 | 是 | 统一入口,方便采集 |
| 跨域处理 | 是 | 统一配置,避免各服务重复 |
| 数据聚合(BFF) | 否 | 用独立的 BFF 服务 |
| 复杂业务逻辑 | 否 | 违反单一职责 |
| 分布式事务 | 否 | Gateway 是无状态的 |
| 文件上传/大文件下载 | 谨慎 | 可能打爆内存,考虑直传 |
实战/源码
实战 1:完整的 Gateway + Sentinel 配置
yaml
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
- AddRequestHeader=X-Request-From, Gateway
- name: Retry
args:
retries: 2
statuses: SERVICE_UNAVAILABLE
methods: GET
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
# 全局跨域配置
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "https://example.com"
allowedMethods: "*"
allowedHeaders: "*"
allowCredentials: true
# 全局默认 Filter
default-filters:
- AddResponseHeader=X-Gateway-Version, 1.0.0
nacos:
discovery:
server-addr: nacos-server:8848
config:
server-addr: nacos-server:8848
file-extension: yaml
# Sentinel 限流规则(也可放 Nacos Config)
sentinel:
transport:
dashboard: localhost:8080
datasource:
gateway-flow:
nacos:
server-addr: nacos-server:8848
dataId: gateway-flow-rules
groupId: SENTINEL_GROUP
rule-type: gw-flow
实战 2:自定义 GatewayFilter 实现请求签名验证
java
@Component
public class SignGatewayFilterFactory
extends AbstractGatewayFilterFactory<SignGatewayFilterFactory.Config> {
public SignGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// 1. 获取签名参数
String sign = request.getHeaders().getFirst("X-Sign");
String timestamp = request.getHeaders().getFirst("X-Timestamp");
String nonce = request.getHeaders().getFirst("X-Nonce");
// 2. 验证时间戳(防重放,允许 5 分钟误差)
long ts = Long.parseLong(timestamp);
if (Math.abs(System.currentTimeMillis() - ts) > 5 * 60 * 1000) {
return reject(exchange, "Timestamp expired");
}
// 3. 验证签名
String serverSign = generateSign(request, timestamp, nonce, config.getSecret());
if (!serverSign.equals(sign)) {
return reject(exchange, "Invalid sign");
}
return chain.filter(exchange);
};
}
private String generateSign(ServerHttpRequest request, String timestamp,
String nonce, String secret) {
String data = timestamp + nonce + secret;
return DigestUtils.md5DigestAsHex(data.getBytes());
}
@Data
public static class Config {
private String secret;
}
}
使用:
yaml
filters:
- name: Sign
args:
secret: "your-secret-key"
实战 3:动态路由的完整实现
java
@Component
public class DynamicRouteService implements ApplicationEventPublisherAware {
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
@Autowired
private RouteDefinitionLocator routeDefinitionLocator;
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
// 添加路由
public void addRoute(RouteDefinition definition) {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
publisher.publishEvent(new RefreshRoutesEvent(this));
}
// 删除路由
public void deleteRoute(String routeId) {
routeDefinitionWriter.delete(Mono.just(routeId)).subscribe();
publisher.publishEvent(new RefreshRoutesEvent(this));
}
// 更新路由
public void updateRoute(RouteDefinition definition) {
routeDefinitionWriter.delete(Mono.just(definition.getId()))
.then(routeDefinitionWriter.save(Mono.just(definition)))
.subscribe();
publisher.publishEvent(new RefreshRoutesEvent(this));
}
// 获取所有路由
public Flux<RouteDefinition> getRoutes() {
return routeDefinitionLocator.getRouteDefinitions();
}
}
// REST API 管理路由
@RestController
@RequestMapping("/admin/routes")
public class RouteAdminController {
@Autowired
private DynamicRouteService dynamicRouteService;
@PostMapping
public Mono<Void> add(@RequestBody RouteDefinition definition) {
dynamicRouteService.addRoute(definition);
return Mono.empty();
}
@DeleteMapping("/{id}")
public Mono<Void> delete(@PathVariable String id) {
dynamicRouteService.deleteRoute(id);
return Mono.empty();
}
@GetMapping
public Flux<RouteDefinition> list() {
return dynamicRouteService.getRoutes();
}
}
实战 4:CircuitBreaker GatewayFilter(Resilience4j)
Spring Cloud Circuit Breaker 整合了 Resilience4j,可以在 Gateway 层做熔断:
yaml
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- name: CircuitBreaker
args:
name: orderCircuitBreaker
fallbackUri: forward:/fallback/order
java
@RestController
public class FallbackController {
@RequestMapping("/fallback/order")
public ResponseEntity<?> orderFallback(ServerWebExchange exchange) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body("{\"code\":503,\"message\":\"服务暂不可用,请稍后再试\"}");
}
}
Resilience4j 的熔断配置:
yaml
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 10 # 统计窗口 10 次请求
failureRateThreshold: 50 # 失败率 50% 熔断
waitDurationInOpenState: 10s # 熔断后等待 10 秒进入半开
permittedNumberOfCallsInHalfOpenState: 3 # 半开允许 3 个探测请求
instances:
orderCircuitBreaker:
baseConfig: default
常见问题
Q1:Gateway 和 Nginx 有什么区别?为什么不用 Nginx 做网关?
| 维度 | Spring Cloud Gateway | Nginx |
|---|---|---|
| 生态整合 | 与 Spring Cloud 深度整合(服务发现、负载均衡、配置中心) | 需要额外模块(如 lua-resty) |
| 动态性 | 支持运行时动态路由、限流规则调整 | 需要 reload 配置 |
| 限流维度 | 丰富(Header、Query、IP、用户) | 基本(IP、连接数) |
| 鉴权 | Java 代码灵活实现 | Lua 脚本或外部服务 |
| 性能 | 高(10万+ QPS) | 极高(百万级 QPS) |
| 资源占用 | JVM + 较多内存 | 极低 |
| 适用场景 | 微服务入口、业务网关 | 边缘网关、静态资源、L4 负载均衡 |
推荐架构:Nginx 做边缘网关(抗 DDoS、SSL 卸载、静态资源),Spring Cloud Gateway 做业务网关(路由、鉴权、限流、灰度)。两者可以共存:
用户 → CDN → Nginx(边缘)→ Spring Cloud Gateway(业务)→ 微服务
Q2:Gateway 的响应式编程有什么坑?
坑 1:阻塞操作会拖垮整个 EventLoop
java
// 错误:在 GatewayFilter 中阻塞
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String result = restTemplate.getForObject("http://auth/check", String.class); // 阻塞!
// ...
}
RestTemplate 是阻塞式的,在 WebFlux 线程中调用会阻塞 EventLoop,导致该线程上的所有请求都被卡住。
正确做法:使用 WebClient(响应式 HTTP 客户端):
java
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return webClient.get()
.uri("http://auth/check")
.retrieve()
.bodyToMono(String.class)
.flatMap(result -> {
// 异步处理结果
return chain.filter(exchange);
});
}
坑 2:请求体只能读取一次
java
// 错误:多次读取请求体
exchange.getRequest().getBody().subscribe(dataBuffer -> {
// 第一次读取
});
exchange.getRequest().getBody().subscribe(dataBuffer -> {
// 第二次读取为空!
});
ServerHttpRequest.getBody() 返回的是 Flux<DataBuffer>,数据流只能消费一次。如果需要多次读取,先用 AdaptCachedBodyGlobalFilter 缓存:
java
// 正确:通过缓存读取
Object cachedBody = exchange.getAttribute(CACHED_REQUEST_BODY_ATTR);
坑 3:ThreadLocal 失效
WebFlux 的响应式链可能在不同线程上执行,ThreadLocal 会丢失。
java
// 错误:WebFlux 中 ThreadLocal 不可靠
String userId = RequestContext.getCurrentUserId(); // 可能为 null
正确做法:使用 ServerWebExchange.getAttributes() 或 Reactor 的 Context:
java
// 写入
exchange.getAttributes().put("userId", userId);
// 或使用 Reactor Context
Mono.just(data)
.subscriberContext(Context.of("userId", userId));
Q3:Gateway 怎么实现文件上传?
Gateway 支持文件上传,但要注意内存限制。默认 DataBuffer 会缓存在内存中,大文件可能导致 OOM。
方案 1:小文件直接透传
yaml
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
方案 2:大文件使用直传
绕过 Gateway,客户端直接上传到对象存储(OSS/S3),然后将文件地址传给业务服务。
方案 3:流式处理
java
// Gateway 层不做缓存,直接流式转发
return chain.filter(exchange.mutate()
.request(exchange.getRequest().mutate()
.header("Transfer-Encoding", "chunked")
.build())
.build());
Q4:Gateway 如何支持 WebSocket?
Gateway 内置 WebsocketRoutingFilter 支持 WebSocket 代理:
yaml
spring:
cloud:
gateway:
routes:
- id: websocket-route
uri: ws://websocket-service:8080 # 注意是 ws:// 不是 lb://
predicates:
- Path=/ws/**
WebSocket 路由不走 HTTP 转发,而是直接建立 TCP 长连接。Gateway 的 Netty 会维护 WebSocket 连接,将客户端和服务端的帧互相转发。
Q5:Gateway 的多节点部署,路由表怎么同步?
如果 Gateway 多节点部署,每个节点独立加载路由配置:
- 静态路由:所有节点从相同的 YAML/Nacos Config 加载,天然一致。
- 动态路由(API 修改):需要通过共享存储(Nacos Config / Redis / 数据库) + 事件通知保证各节点同步。
读图导引:Gateway 多节点部署时,路由变更通过共享配置中心(如 Nacos)同步。每个节点独立监听配置变更,各自刷新本地路由表。不需要节点间的直接通信,因为配置中心本身就是一致性保证。
总结
本文从"Zuul 1.x 线程池耗尽导致 503"的真实困境出发,穿透 Spring Cloud Gateway + Sentinel 的流量治理实现:
-
Gateway 的核心链路:请求到达 Netty Server →
DispatcherHandler→RoutePredicateHandlerMapping匹配首个 Route →FilteringWebHandler合并并排序 GlobalFilter + GatewayFilter → 递归执行 Filter 链 →NettyRoutingFilter发送 HTTP 请求 → 下游响应后反向执行 Filter 后置逻辑。 -
Predicate 与 Filter:Predicate 决定请求走哪条路由,Filter 决定请求怎么被处理。内置 13 种 Predicate 和 20+ 种 Filter,支持自定义扩展。Filter 按 Order 排序,前置正序、后置逆序。
-
网关限流双方案:Gateway 内置
RequestRateLimiter基于 Redis 令牌桶,适合简单 IP/Route 限流;Sentinel Gateway Adapter 支持滑动窗口、热点参数、系统保护,适合复杂场景。生产环境建议 Sentinel 做粗粒度保护 + RequestRateLimiter 做 IP 细粒度限流。 -
动态路由:通过自定义
RouteDefinitionRepository+ Nacos Config 监听,实现路由的实时刷新,无需重启网关。这是蓝绿发布和灰度路由的基础设施。 -
鉴权与灰度:Gateway 做 JWT 解析 + 用户信息透传,具体 RBAC 校验下沉到 Auth Service 或业务服务。灰度发布支持 Weight Predicate(权重分流)和 Header Predicate(白名单/特征路由)。
-
Gateway 的边界:Gateway 的职责应严格限定为"路由 + 限流 + 鉴权 + 日志",数据聚合用 BFF 服务,复杂业务逻辑下沉到业务服务。大文件上传考虑直传 OSS。
面试一句话:Gateway 是微服务的流量入口,用 WebFlux + Netty 解决了 Zuul 的线程阻塞问题;Predicate 匹配路由,Filter 处理逻辑,Sentinel 做网关限流,Nacos Config 做动态路由——四者组合构成完整的流量治理方案。