Spring Cloud Gateway 从入门到实战

举报
yd_236998400 发表于 2026/11/01 10:47:48 2026/11/01
【摘要】 Spring Cloud Gateway 从入门到实战:微服务架构下的统一网关本文基于 Spring Boot 3.x + Spring Cloud 2023.x(Gateway 4.x)+ Java 17 编写。如果你还在使用 Spring Boot 2.7 / Spring Cloud 2021.x(Gateway 3.x),文中绝大多数配置和代码完全兼容。📌 版本配套速查Sprin...

Spring Cloud Gateway 从入门到实战:微服务架构下的统一网关

本文基于 Spring Boot 3.x + Spring Cloud 2023.x(Gateway 4.x)+ Java 17 编写。如果你还在使用 Spring Boot 2.7 / Spring Cloud 2021.x(Gateway 3.x),文中绝大多数配置和代码完全兼容。

📌 版本配套速查

Spring Boot Spring Cloud Gateway 说明
2.7.x 2021.0.x(Jubilee) 3.1.x 老项目常见组合,配置基本一致
3.2.x 2023.0.x(Leyton) 4.1.x 本文基准
3.3.x ~ 3.4.x 2024.0.x(Moorgate) 4.2.x 官方拆分出 Gateway WebMVC 版本
3.5.x 2025.0.x(Northfields) 4.3.x 当前最新稳定线

升级前请以 Spring Cloud 官网 Projects 页面的 Learn 栏版本对照表为准。

📑 目录

基础篇

  • 一、为什么需要 API 网关
  • 二、Spring Cloud Gateway 是什么
  • 三、三大核心概念
  • 四、5 分钟上手
  • 五、常用断言(Predicate)
  • 六、常用过滤器(Filter)
  • 七、集成注册中心:从写死地址到服务发现
  • 八、自定义过滤器实战
  • 九、限流:Redis 令牌桶
  • 十、熔断降级:CircuitBreaker + Resilience4j
  • 十一、跨域(CORS)统一处理
  • 十二、生产环境避坑指南 ⭐

进阶篇

  • 十三、自定义断言工厂
  • 十四、动态路由(配置中心驱动)
  • 十五、修改请求体 / 响应体
  • 十六、灰度发布实战
  • 十七、全链路 TraceId 过滤器
  • 十八、监控与性能调优
  • 十九、高频面试题速查
  • 二十、写在最后

一、为什么需要 API 网关

在单体应用时代,前端直接调用后端接口就够了。但当系统拆分成几十个微服务后,客户端会面临一堆现实问题:

  • 服务地址分散,客户端需要知道每个服务的 IP 和端口;
  • 每个服务都要重复实现鉴权、限流、日志、跨域等横切逻辑;
  • 内部服务结构直接暴露给外部,存在安全风险;
  • 缺乏统一的流量入口,灰度发布、熔断降级无从下手。

API 网关就是为了解决这些问题而生的:它作为系统的统一入口,把"转发"这件小事和"鉴权、限流、监控"这些横切能力收敛到一处。

flowchart LR
    C[客户端] --> G[API 网关<br/>Spring Cloud Gateway]
    G --> A[用户服务]
    G --> B[订单服务]
    G --> D[商品服务]

网关的典型职责:路由转发、统一鉴权、限流熔断、日志监控、跨域处理、灰度分流。

二、Spring Cloud Gateway 是什么

Spring Cloud Gateway 是 Spring 官方推出的网关组件,基于 Spring WebFlux、Project Reactor 和 Netty 构建的高性能异步非阻塞网关。它的前身 Zuul 1.x 基于 Servlet 同步阻塞模型,每个请求占用一个线程,高并发下线程开销大;而 Gateway 采用响应式模型,用少量线程支撑大量并发连接,性能天然占优。

核心特性:

  • 动态路由:支持按路径、方法、请求头等多种条件匹配,可随注册中心动态刷新;
  • 内置丰富的断言(Predicate)和过滤器(Filter);
  • 与 Spring Cloud 生态无缝集成(Nacos、Eureka、Resilience4j、Micrometer 等);
  • 支持 WebSocket、HTTP/2,可集成 Redis 实现分布式限流。

💡 提示:Spring Cloud 2024.x 之后,官方还提供了 spring-cloud-starter-gateway-mvc(Gateway Server WebMVC),让网关可以跑在 Servlet 技术栈上,适合不想引入响应式编程的团队。本文仍以主流的 WebFlux 版本为例。

三、三大核心概念

理解这三个概念,就理解了 Gateway 的一切:

概念 说明
Route(路由) 网关的基本单元,由 ID、目标 URI、一组断言和一组过滤器组成。断言全部匹配才走这条路由
Predicate(断言) Java 8 的 Predicate,判断请求是否满足条件(路径、方法、Header、参数……)
Filter(过滤器) 在请求转发前后对请求/响应进行修改,是 Gateway 的核心扩展点

请求的处理流程如下:

flowchart LR
    A[客户端请求] --> B{Handler Mapping<br/>按断言匹配路由}
    B -- 匹配成功 --> C[Gateway Web Handler]
    C --> D[过滤器链 pre 逻辑]
    D --> E[转发到目标服务]
    E --> F[过滤器链 post 逻辑]
    F --> G[返回响应]
    B -- 无匹配 --> H[404]

关键点:过滤器在转发前(pre)修改请求,在转发后(post)修改响应,请求和响应都会按 Ordered 顺序穿过整条过滤器链。这与 Servlet 的 Filter Chain 思想一致,只是 API 变成了响应式。

四、5 分钟上手

1. 引入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!-- actuator 可选,用于查看路由信息 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

⚠️ 划重点:Gateway 基于 WebFlux,千万不要引入 spring-boot-starter-web,否则启动直接报错。

2. 配置路由

server:
  port: 9000

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      routes:
        - id: user-service-route          # 路由唯一标识
          uri: http://localhost:8081      # 目标地址
          predicates:
            - Path=/api/user/**          # 断言:路径匹配
          filters:
            - StripPrefix=1              # 过滤器:去掉第一段路径前缀
        - id: order-service-route
          uri: http://localhost:8082
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1

StripPrefix=1 的作用:把 /api/user/get/1 转发为 http://localhost:8081/user/get/1,避免下游服务看到网关加的前缀。

3. 启动并验证

@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

访问 http://localhost:9000/api/user/get/1,请求会被转发到用户服务。一个最简网关就跑起来了。

五、常用断言(Predicate)

Gateway 内置了十几种断言,全部形如"名称 + 参数"写在配置里。常用的有:

断言 示例 说明
Path Path=/api/user/** 路径匹配,最常用
Method Method=GET,POST 匹配 HTTP 方法
Query Query=token, ^abc. 匹配请求参数,可加正则
Header Header=X-Request-Id, \d+ 匹配请求头
Cookie Cookie=channel, ^pc$ 匹配 Cookie
Host Host=**.example.org 匹配主机名
After / Before / Between After=2026-01-01T00:00:00+08:00[Asia/Shanghai] 按时间匹配,可用于限时活动
Weight Weight=gray-group, 8 按权重分流,灰度发布利器
RemoteAddr RemoteAddr=192.168.1.0/24 匹配来源 IP 段

多个断言是 AND 关系,同时满足才能命中。例如"灰度期间只允许 GET 请求走新版本服务":

- id: user-service-gray
  uri: lb://user-service-gray
  predicates:
    - Path=/api/user/**
    - Method=GET
    - Weight=gray-group, 2    # 20% 流量进灰度

六、常用过滤器(Filter)

过滤器分为路由级(GatewayFilter)和全局级(GlobalFilter)。常用的路由级过滤器:

过滤器 示例 说明
StripPrefix StripPrefix=2 去掉路径前 N 段
PrefixPath PrefixPath=/api 给路径加前缀
RewritePath RewritePath=/api/(?<seg>.*), /$\{seg} 正则重写路径
AddRequestHeader AddRequestHeader=X-Source, gateway 添加请求头
AddResponseHeader AddResponseHeader=X-Response-Time, 100 添加响应头
Retry 见下文 失败重试
CircuitBreaker 见下文 熔断降级
RequestRateLimiter 见下文 请求限流

RewritePath 是 StripPrefix 的正则加强版,更灵活,推荐配合命名捕获组使用(注意 YAML 中 $ 需转义为 $\{seg})。

如果希望某些过滤器对所有路由生效,可以用默认过滤器:

spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=X-Source, gateway

七、集成注册中心:从写死地址到服务发现

上面 uri: http://localhost:8081 写死了目标地址,服务一旦扩容或多实例就失效了。正确姿势是结合注册中心(以 Nacos 为例):

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service      # lb = LoadBalancer,按服务名负载均衡
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1

lb://服务名 会通过 ReactiveLoadBalancer 从 Nacos 拉取实例列表并负载均衡转发,服务上下线自动感知。

Gateway 也支持根据注册中心自动生成路由(服务名即路径前缀),适合本地调试,生产环境一般不建议:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true   # 允许 http://网关:9000/user-service/xxx 直接按服务名路由

八、自定义过滤器实战

GlobalFilter 是全局过滤器接口,与路由无关、对所有请求生效,是做鉴权、日志、链路追踪的标配扩展点。

先看一个最常用的:接口耗时统计。

@Component
public class TimingGlobalFilter implements GlobalFilter, Ordered {

    private static final Logger log = LoggerFactory.getLogger(TimingGlobalFilter.class);
    /** 用 exchange 的 attributes 在 pre/post 阶段之间传递数据,别用 ThreadLocal */
    private static final String START_TIME = "requestStartTime";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            Long start = exchange.getAttribute(START_TIME);
            if (start != null) {
                long cost = System.currentTimeMillis() - start;
                log.info("[网关] {} {} 耗时 {} ms",
                        exchange.getRequest().getMethod(),
                        exchange.getRequest().getURI().getPath(),
                        cost);
            }
        }));
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

再看一个统一鉴权过滤器:白名单放行 + 无 Token 直接返回 401 JSON。

@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private static final List<String> WHITE_LIST = List.of(
            "/api/auth/login", "/api/user/register"
    );

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
            return chain.filter(exchange);
        }

        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (token == null || token.isBlank()) {
            return writeUnauthorized(exchange, "未登录或令牌缺失");
        }
        // 实际项目中在这里解析并校验 JWT,校验通过后可把用户信息放入请求头传给下游
        return chain.filter(exchange);
    }

    private Mono<Void> writeUnauthorized(ServerWebExchange exchange, String message) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        String body = "{\"code\":401,\"message\":\"" + message + "\"}";
        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }

    @Override
    public int getOrder() {
        return -100; // 数值越小优先级越高,鉴权要早于大多数业务过滤器执行
    }
}

两个要点:

  1. getOrder() 决定过滤器执行顺序,鉴权、限流这类前置校验应设小值,让它们尽早执行;
  2. 多个过滤器串在一条链上,请求按 order 升序穿过 pre 阶段,响应按降序穿过 post 阶段。

九、限流:Redis 令牌桶

Gateway 内置 RequestRateLimiter 过滤器,基于 Redis + Lua 实现的令牌桶算法,天然支持多实例共享限流数据。

第一步,引入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

第二步,定义限流 key 的维度(按 IP、按用户、按接口都可以):

@Configuration
public class RateLimiterConfig {

    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.justOrEmpty(
                Optional.ofNullable(exchange.getRequest().getRemoteAddress())
                        .map(addr -> addr.getAddress().getHostAddress())
                        .orElse("unknown"));
    }
}

第三步,在路由上启用:

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10    # 每秒补充 10 个令牌 = 稳态 10 QPS
                redis-rate-limiter.burstCapacity: 20    # 桶容量 20 = 允许突发 20 个请求
                redis-rate-limiter.requestedTokens: 1   # 每次请求消耗 1 个令牌
                key-resolver: "#{@ipKeyResolver}"       # 按 IP 维度限流

超限的请求会收到 429 Too Many Requests。如果想给被限流的用户一个友好的降级响应,可以搭配下面的熔断降级做统一处理。

十、熔断降级:CircuitBreaker + Resilience4j

下游服务挂掉或变慢时,网关应该快速失败并返回兜底数据,避免故障蔓延。引入 Resilience4j:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>

路由配置:

resilience4j:
  circuitbreaker:
    instances:
      user-service-cb:
        slidingWindowSize: 10          # 统计窗口内最近 10 次请求
        failureRateThreshold: 50       # 失败率超 50% 熔断
        waitDurationInOpenState: 10s   # 熔断 10 秒后尝试半开恢复

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
          filters:
            - name: CircuitBreaker
              args:
                name: user-service-cb
                fallbackUri: forward:/fallback/user

网关内提供 fallback 接口:

@RestController
public class FallbackController {

    @RequestMapping("/fallback/user")
    public Map<String, Object> userFallback() {
        return Map.of("code", 503, "message", "用户服务繁忙,请稍后重试");
    }
}

十一、跨域(CORS)统一处理

有了网关,跨域配置收敛到一处即可:

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origin-patterns: "*"
            allowed-methods: "*"
            allowed-headers: "*"
            allow-credentials: true
            max-age: 3600

⚠️ 注意:如果下游服务自己也配了 CORS,浏览器会收到两个 Access-Control-Allow-Origin 响应头而直接报错。跨域只在网关配置一处,下游服务删掉 @CrossOrigin 和 WebMvcConfigurer 里的 CORS 配置。

十二、生产环境避坑指南 ⭐

以下是 Gateway 使用中最高频的坑,每一条都值得细看:

1. 引入 spring-boot-starter-web 直接启动失败

报错:Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time.

Gateway 是 WebFlux 技术栈,与 MVC 互斥。检查依赖树 mvn dependency:tree,把传递引入的 web starter 排除掉。

2. 请求体(Body)只能读一次

WebFlux 的请求体是流,被一个过滤器消费后,下游就收不到 body 了。在过滤器中读取 body 的正确方式是用内置的 ReadBodyRoutePredicateFactory,或通过 DataBufferUtils.join 聚合并重新包装请求:

return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
    byte[] bytes = new byte[dataBuffer.readableByteCount()];
    dataBuffer.read(bytes);
    DataBufferUtils.release(dataBuffer);
    // 缓存到 attributes 供后续使用,并重建请求体往下传
    ServerHttpRequest newRequest = exchange.getRequest().mutate()
            .body(Flux.just(exchange.getResponse().bufferFactory().wrap(bytes)))
            .build();
    return chain.filter(exchange.mutate().request(newRequest).build());
});

注意这会把整个 body 缓存在内存,大文件上传场景不适用。

3. 不要用 ThreadLocal 传递上下文

响应式模型下,一个请求的多个处理阶段可能在不同线程上执行,ThreadLocal 会神秘失效。统一使用 exchange.getAttributes().put/get 传递数据(见第八节耗时过滤器的写法)。

4. 过滤器里严禁阻塞调用

在过滤器里调 JDBC、RestTemplate、Thread.sleep,会卡死 Netty 的 EventLoop 线程,把整个网关拖垮。必须调用阻塞资源时,用 Schedulers.boundedElastic() 隔离,或换成响应式客户端(R2DBC、WebClient)。

5. 超时必须显式配置

默认不配响应超时,下游一旦 hang 住,网关线程和连接就会被慢慢占满:

spring:
  cloud:
    gateway:
      httpclient:
        connect-timeout: 1000    # 连接超时 1s
        response-timeout: 5s     # 响应超时 5s

个别慢接口可在路由的 metadata 里单独放宽(response-timeout: 30000)。

6. 转发到下游的 Header 注意点

  • Host 头默认会改写为下游地址,如需保留原始 Host:spring.cloud.gateway.preserve-host-header=true;
  • 建议在全局过滤器里给下游追加 X-Request-Id、X-Real-IP 等头,方便链路追踪。

7. actuator 路由端点是个安全隐患

GET /actuator/gateway/routes 能看到全部路由,而 POST /actuator/gateway/routes/{id} 甚至能动态修改路由定义。生产环境要么关闭,要么必须纳入鉴权体系:

management:
  endpoint:
    gateway:
      enabled: true   # 仅内网/受保护环境开启
  endpoints:
    web:
      exposure:
        include: health, gateway

8. 路由不生效时的排查三板斧

  1. 开 debug 日志:logging.level.org.springframework.cloud.gateway=DEBUG;
  2. 访问 GET /actuator/gateway/routes 核对路由是否加载、断言是否如预期;
  3. 检查多个路由的断言是否重叠——Gateway 按配置顺序匹配,顺序错了会被前面的路由"截胡"。

十三、进阶:自定义断言工厂

内置断言不满足需求时,可以自定义断言工厂。命名必须以 RoutePredicateFactory 结尾,Gateway 会通过命名约定自动发现它。例如实现"匹配指定 API 版本"的断言:

@Component
public class ApiVersionRoutePredicateFactory
        extends AbstractRoutePredicateFactory<ApiVersionRoutePredicateFactory.Config> {

    public ApiVersionRoutePredicateFactory() {
        super(Config.class);
    }

    /** 定义 shortcut 写法中参数的顺序 */
    @Override
    public List<String> shortcutFieldOrder() {
        return List.of("version");
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            String version = exchange.getRequest().getHeaders().getFirst("X-Api-Version");
            return config.getVersion().equalsIgnoreCase(version);
        };
    }

    @Validated
    public static class Config {
        @NotEmpty
        private String version;
        // getter / setter 省略
    }
}

配置中就能像内置断言一样使用(shortcut 写法):

predicates:
  - Path=/api/user/**
  - ApiVersion=v2     # 只放行携带 X-Api-Version: v2 的请求

十四、进阶:动态路由(配置中心驱动)

RouteDefinitionWriter 是 Gateway 提供的路由定义读写接口,配合 Nacos 配置监听,就能做到改配置即改路由,不用重启网关。

第一步,封装路由的增删改:

@Component
public class DynamicRouteService {

    private final RouteDefinitionWriter routeDefinitionWriter;
    private final ApplicationEventPublisher publisher;
    /** 本地缓存已保存的路由定义,便于删除 */
    private final Map<String, RouteDefinition> cache = new ConcurrentHashMap<>();

    public DynamicRouteService(RouteDefinitionWriter routeDefinitionWriter,
                               ApplicationEventPublisher publisher) {
        this.routeDefinitionWriter = routeDefinitionWriter;
        this.publisher = publisher;
    }

    /** 新增或更新路由 */
    public String save(RouteDefinition definition) {
        cache.put(definition.getId(), definition);
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        publisher.publishEvent(new RefreshRoutesEvent(this)); // 触发路由表刷新
        return "ok";
    }

    /** 删除路由 */
    public String delete(String routeId) {
        if (cache.remove(routeId) == null) {
            return "路由不存在";
        }
        routeDefinitionWriter.delete(Mono.just(routeId)).subscribe();
        publisher.publishEvent(new RefreshRoutesEvent(this));
        return "ok";
    }

    /** 全量替换:先删旧、再存新 */
    public void replaceAll(List<RouteDefinition> routes) {
        cache.keySet().forEach(this::delete);
        routes.forEach(this::save);
    }
}

第二步,监听 Nacos 配置变化(需引入 spring-cloud-starter-alibaba-nacos-config):

@Component
public class NacosRouteEventListener implements ApplicationRunner {

    private final DynamicRouteService dynamicRouteService;
    private final NacosConfigManager nacosConfigManager;

    public NacosRouteEventListener(DynamicRouteService dynamicRouteService,
                                   NacosConfigManager nacosConfigManager) {
        this.dynamicRouteService = dynamicRouteService;
        this.nacosConfigManager = nacosConfigManager;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        nacosConfigManager.getConfigService().addListener(
                "gateway-routes.json", "DEFAULT_GROUP",
                new AbstractListener() {
                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        // 配置内容就是 RouteDefinition 的 JSON 数组
                        ObjectMapper mapper = new ObjectMapper();
                        List<RouteDefinition> routes = mapper.readValue(configInfo,
                                new TypeReference<List<RouteDefinition>>() {});
                        dynamicRouteService.replaceAll(routes);
                    }
                });
    }
}

两个细节:

  • RefreshRoutesEvent 发布后,Gateway 会重新把 RouteDefinition 编译成可执行的路由,这一步是必须的;
  • 内存里存的动态路由不持久化,网关重启后会丢失,所以启动时应主动 getConfig 拉一次全量配置再注册监听。

十五、进阶:修改请求体 / 响应体

有些场景需要网关"加工"报文:入参补全、响应脱敏、协议适配。内置的 ModifyRequestBody / ModifyResponseBody 过滤器配合 RewriteFunction 即可实现,无需手写字节流处理。

@Component
public class MaskRewriteConfig {

    /** 把响应 JSON 中的手机号做脱敏处理 */
    @Bean
    public RewriteFunction<String, String> maskMobileRewrite() {
        return (exchange, body) -> Mono.just(
                body.replaceAll("(\"mobile\":\\s*\"?)(\\d{3})\\d{4}(\\d{4})", "$1$2****$3"));
    }
}
filters:
  - name: ModifyResponseBody
    args:
      inClass: '#{T(java.lang.String)}'
      outClass: '#{T(java.lang.String)}'
      rewriteFunction: '#{@maskMobileRewrite}'

ModifyRequestBody 用法相同,只是作用于请求方向。注意:一旦涉及 body 读写,就会触发"请求体只能读一次"的问题(第十二节第 2 条),这两个内置过滤器已经替你处理了重发逻辑,但大报文会被完整载入内存,只适合 JSON 类小报文。

十六、进阶:灰度发布实战

利用 Weight 断言做按比例分流,两条路由加入同一个权重组:

spring:
  cloud:
    gateway:
      routes:
        - id: user-service-stable
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
            - Weight=user-group, 90     # 90% 流量走稳定版
        - id: user-service-gray
          uri: lb://user-service-gray
          predicates:
            - Path=/api/user/**
            - Weight=user-group, 10     # 10% 流量走灰度版

想支持"指定用户必进灰度",可以在前面加一条优先级更高的路由(Header / Cookie 命中即短路):

- id: user-service-gray-force
  uri: lb://user-service-gray
  predicates:
    - Path=/api/user/**
    - Cookie=gray, true      # 特定用户加了灰度 Cookie,必定进灰度

十七、进阶:全链路 TraceId 过滤器

分布式排查问题第一步是对齐日志。在网关生成/透传 TraceId 是最低成本的做法:

@Component
public class TraceIdGlobalFilter implements GlobalFilter, Ordered {

    public static final String TRACE_ID = "X-Request-Id";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String traceId = Optional
                .ofNullable(exchange.getRequest().getHeaders().getFirst(TRACE_ID))
                .orElse(UUID.randomUUID().toString().replace("-", ""));

        ServerHttpRequest request = exchange.getRequest().mutate()
                .header(TRACE_ID, traceId)
                .build();

        return chain.filter(exchange.mutate().request(request).build())
                .then(Mono.fromRunnable(() ->
                        exchange.getResponse().getHeaders().set(TRACE_ID, traceId)));
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE; // 最早执行,让后续过滤器都能拿到 TraceId
    }
}

请求头会带着 X-Request-Id 传给下游,下游日志框架(Logback MDC、Log4j2 等)只需在 pattern 中加入该字段;响应头返回给前端,用户反馈问题时一查便知全链路。生产级方案可以在此基础上接入 SkyWalking / OpenTelemetry。

十八、进阶:监控与性能调优

监控接入

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
management:
  endpoints:
    web:
      exposure:
        include: health,prometheus
  metrics:
    tags:
      application: ${spring.application.name}

Prometheus 抓取 /actuator/prometheus,Grafana 配面板即可。重点关注的指标:

  • http.server.requests:接口 QPS、P99 延迟、状态码分布;
  • spring.cloud.gateway.requests:Gateway 自带指标,按路由 ID、断言、状态码打标签,能直接看出哪条路由慢;
  • JVM 指标:重点关注堆外内存(Netty direct memory)使用量。

性能调优清单

spring:
  cloud:
    gateway:
      httpclient:
        pool:
          max-connections: 2000     # 连接池上限(fixed 模式)
          acquire-timeout: 3000     # 获取连接的等待上限
          max-idle-time: 30s        # 空闲连接回收,防止被 LB 中间件掐断
  • 超时三件套必配:connect-timeout、response-timeout、acquire-timeout,任何一环缺失都会导致请求堆积;
  • 过滤器要"早失败":鉴权、限流的 order 设小值,把无效请求挡在最前面,别让它们消耗下游资源;
  • 别在过滤器打印大 body 日志:读 body 意味着内存复制,QPS 高时是肉眼可见的性能损耗;
  • JVM 层面:JDK 17 + G1 默认即可;容器环境注意 -XX:MaxDirectMemorySize,Netty 大量使用堆外内存,配小了会 OutOfDirectMemoryError。

十九、高频面试题速查

问题 要点
Gateway 和 Zuul 的区别? Zuul 1.x 基于 Servlet 同步阻塞,一请求一线程;Gateway 基于 WebFlux + Netty 异步非阻塞,线程少、吞吐高,且是 Spring 官方亲儿子
三大核心概念? Route(路由)、Predicate(断言)、Filter(过滤器),断言定"谁能过",过滤器定"怎么处理"
GlobalFilter 和 GatewayFilter 的区别? GlobalFilter 对所有路由生效,GatewayFilter 绑定具体路由;二者都按 Ordered 排序后在同一条链上执行
为什么请求体只能读一次? WebFlux 中 body 是流式 DataBuffer,消费即释放;需要多次读必须缓存重建(ReadBodyRoutePredicateFactory / DataBufferUtils.join)
网关怎么实现限流? 内置 RequestRateLimiter:Redis + Lua 令牌桶,多实例共享计数;KeyResolver 决定限流维度(IP/用户/接口)
路由如何动态刷新? RouteDefinitionWriter 修改定义 + 发布 RefreshRoutesEvent;来源可以是注册中心、Nacos 配置监听或 actuator 端点
lb:// 的原理? ReactiveLoadBalancerClientFilter 把 lb://服务名 解析成实际实例地址,从注册中心拉列表并负载均衡
怎么做灰度发布? Weight 断言按比例分流,配合 Header/Cookie 断言实现"指定用户进灰度"

二十、写在最后

回顾整篇文章(基础 + 进阶):

  • 骨架是 Route + Predicate + Filter,一切功能都围绕这三者展开;
  • 基础四件套:注册中心负载均衡(lb://)+ 全局过滤器(鉴权/日志)+ Redis 限流 + Resilience4j 熔断;
  • 进阶三板斧:动态路由让配置流动起来,灰度发布让上线更从容,TraceId 全链路让问题无处遁形;
  • 最重要的心法:忘掉 Servlet 的思维,拥抱响应式——不用 ThreadLocal、不做阻塞调用、请求体只能读一次。

建议把 Spring Cloud Gateway 官方文档(Spring 官网 Docs 板块内搜索 “Spring Cloud Gateway”)当作工具书备查。

如果本文对你有帮助,欢迎点赞、收藏、转发三连~ 有问题欢迎在评论区交流。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。