问题引入

面试官放下简历,问道:"你们网关用的什么?"

"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 链。
graph LR A[HTTP 请求] --> B{Predicate 匹配} B -->|Path=/api/**| C[Route: order-service] B -->|Path=/user/**| D[Route: user-service] B -->|不匹配| E[返回 404] C --> F[Filter 链执行] F --> G[StripPrefix=1] G --> H[AddRequestHeader] H --> I[转发到下游]

读图导引:关注匹配逻辑。请求只匹配一个 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 的请求生效。如 StripPrefixAddRequestHeader
  • 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:

graph TD A[Netty Server<br/>接收请求] --> B[HttpWebHandlerAdapter] B --> C[DispatcherHandler] C --> D[RoutePredicateHandlerMapping] D --> E{遍历所有 Route} E -->|匹配第一个| F[获取 Route 的 Handler] F --> G[FilteringWebHandler] G --> H[合并并排序 Filter 链] H --> I[依次执行 Filter] I --> J[NettyRoutingFilter] J --> K[发送 HTTP 请求到下游] K --> L[下游服务响应] L --> M[反向执行 Filter 后置逻辑] M --> N[返回响应给客户端]

读图导引:从左上到右下是请求的完整链路。关键节点在 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 版本后,RibbonSpring 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"
  }
]
graph LR A[Nacos Config] -->|配置变更| B[NacosRouteDefinitionRepository] B -->|发布事件| C[RefreshRoutesEvent] C --> D[Gateway 刷新路由表] D --> E[新路由生效 无需重启]

读图导引:这是动态路由的标准模式。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")涉及查询数据库/缓存,逻辑复杂。如果在网关做,网关会从"轻量路由层"变成"重逻辑层"。

推荐架构

graph LR A[客户端] --> B[Gateway] B -->|Token 解析| B B -->|X-User-Id 透传| C[Auth Service 权限校验] C --> D[Order Service] C --> E[User Service]

读图导引: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 的响应式链路,核心优势在于线程复用

graph LR subgraph "Zuul 1.x 阻塞模型" A1[请求1] --> T1[线程1 阻塞 200ms] A2[请求2] --> T2[线程2 阻塞 200ms] A3[请求3] --> T3[线程3 阻塞 200ms] A4[请求4] --> T4[排队等待] Note: 200线程池,200并发即满载 end subgraph "Gateway 响应式模型" B1[请求1] --> E1[EventLoop 处理请求头] E1 -.->|等待响应| W1[下游 200ms] B2[请求2] --> E1[EventLoop 处理请求头] E1 -.->|等待响应| W2[下游 200ms] B3[请求3] --> E1 B4[请求4] --> E1 Note: 一个 EventLoop 可同时处理数千请求 end

读图导引:对比左右两个模型。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 的 HttpClientWiretapHttpServerWiretap 会记录所有请求/响应体,生产环境务必关闭:

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 / 数据库) + 事件通知保证各节点同步。
graph LR A[管理员] -->|调用 API| B[Gateway Node 1] B -->|写入| C[Nacos Config] C -->|长轮询推送| B C -->|长轮询推送| D[Gateway Node 2] C -->|长轮询推送| E[Gateway Node 3]

读图导引:Gateway 多节点部署时,路由变更通过共享配置中心(如 Nacos)同步。每个节点独立监听配置变更,各自刷新本地路由表。不需要节点间的直接通信,因为配置中心本身就是一致性保证。


总结

本文从"Zuul 1.x 线程池耗尽导致 503"的真实困境出发,穿透 Spring Cloud Gateway + Sentinel 的流量治理实现:

  1. Gateway 的核心链路:请求到达 Netty Server → DispatcherHandlerRoutePredicateHandlerMapping 匹配首个 Route → FilteringWebHandler 合并并排序 GlobalFilter + GatewayFilter → 递归执行 Filter 链 → NettyRoutingFilter 发送 HTTP 请求 → 下游响应后反向执行 Filter 后置逻辑。

  2. Predicate 与 Filter:Predicate 决定请求走哪条路由,Filter 决定请求怎么被处理。内置 13 种 Predicate 和 20+ 种 Filter,支持自定义扩展。Filter 按 Order 排序,前置正序、后置逆序。

  3. 网关限流双方案:Gateway 内置 RequestRateLimiter 基于 Redis 令牌桶,适合简单 IP/Route 限流;Sentinel Gateway Adapter 支持滑动窗口、热点参数、系统保护,适合复杂场景。生产环境建议 Sentinel 做粗粒度保护 + RequestRateLimiter 做 IP 细粒度限流。

  4. 动态路由:通过自定义 RouteDefinitionRepository + Nacos Config 监听,实现路由的实时刷新,无需重启网关。这是蓝绿发布和灰度路由的基础设施。

  5. 鉴权与灰度:Gateway 做 JWT 解析 + 用户信息透传,具体 RBAC 校验下沉到 Auth Service 或业务服务。灰度发布支持 Weight Predicate(权重分流)和 Header Predicate(白名单/特征路由)。

  6. Gateway 的边界:Gateway 的职责应严格限定为"路由 + 限流 + 鉴权 + 日志",数据聚合用 BFF 服务,复杂业务逻辑下沉到业务服务。大文件上传考虑直传 OSS。

面试一句话:Gateway 是微服务的流量入口,用 WebFlux + Netty 解决了 Zuul 的线程阻塞问题;Predicate 匹配路由,Filter 处理逻辑,Sentinel 做网关限流,Nacos Config 做动态路由——四者组合构成完整的流量治理方案。