1. 从一次"接口突然404"说起:为什么需要Spring Cloud Gateway
先讲一个我印象很深的场景。某天下午,同事急匆匆跑来说线上某个老接口突然返回404,前端一堆报错。我第一反应是"服务挂了?"结果一看注册中心,服务实例健康得很。再查网关日志,发现请求被路由规则直接丢弃了——原因是那段时间我们在调整网关的路由配置,新旧规则叠加,某个带有公共前缀的路径被更具体的路由抢先匹配,老接口就"消失"了。
这件事让我意识到,很多人把网关当成一个"转发工具"来用,配几条route就完事,实际上Spring Cloud Gateway是一个完整的、基于WebFlux的异步非阻塞网关框架,它不只是转发请求,还承担了路由规则匹配、跨服务聚合、过滤器链处理、限流熔断等一大堆职责。你把它用好了,整个微服务架构的入口就稳了;用不好,它就是第一个背锅的组件。
这篇文章我会从Spring Cloud Gateway的核心概念入手,带你把路由、断言、过滤器这几个最基础的机制彻底搞清楚,然后给出完整的工程落地示例,包括路由如何配置、断言怎么写、过滤器怎么用、如何做局部和全局过滤、如何在网关层面做限流。最后会重点聊聊我在实践中踩过的坑,比如路由匹配顺序、路径重写陷阱、CORS配置、WebFlux下获取真实IP等。适合刚开始接触Spring Cloud Gateway,或者已经在用但遇到各种"玄学问题"的同学参考。
2. 路由、断言、过滤器:Gateway的三个底层概念
2.1 路由是"表",断言是"条件",过滤器是"动作"
Spring Cloud Gateway中最核心的三个概念就是Route(路由)、Predicate(断言)、Filter(过滤器)。很多人觉得"这不就是三个名词吗",但实际把三者关系想清楚,后面配路由就顺手很多。
**路由(Route)**是整个网关的基本单元,它定义了一条完整的转发规则:当满足什么条件(断言)时,把请求转发到哪个目标地址(URI),并且经过哪些处理(过滤器链)。你可以把它理解成一张"路由表",每条记录都独立完整。
**断言(Predicate)**本质上是"匹配条件",它决定"什么请求会命中这条路由"。Spring Cloud Gateway内置了很多种断言工厂,比如按路径匹配的Path、按请求头匹配的Header、按参数匹配的Query、按时间匹配的After/Before/Between、按请求方法匹配的Method、按Cookie匹配的Cookie,还有按Host匹配的Host等。断言是一个组合条件,可以同时配置多个,相当于"并且"的关系。
**过滤器(Filter)**则是对请求和响应做处理的动作。它在路由匹配之后执行,同一个请求可能经过多个过滤器。过滤器的类型分两种:GatewayFilter和GlobalFilter。GatewayFilter是某个路由专属的,你把它配置在某个路由下面,就只对这个路由生效;GlobalFilter则是全局的,所有请求都会经过它。
我把这三者的关系打个比方:路由就像高速公路上的出口标识牌,断言就是"当前车速、车型、时间是否满足驶出条件",过滤器则是"驶出后经过的收费站、检查站、消毒站"。三者组合起来,才构成一条完整的通行路径。
2.2 一个最简单的路由配置长什么样
先来看一段最基础的YAML配置,感受一下三者如何配合。
spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=2这里面的含义是:
id:路由的唯一ID,建议语义化命名,比如order-service一眼就知道是订单服务的路由。uri:目标地址。这里写的是lb://order-service,意思是通过负载均衡器从注册中心找到名为order-service的服务实例,然后转发过去。如果服务没接入注册中心,也可以直接写http://localhost:8081这样的固定地址。predicates:匹配条件。Path=/api/order/**表示只有请求路径以/api/order/开头的请求才会走这条路由。filters:过滤动作。StripPrefix=2表示转发时把原始路径的前两段剥离掉。比如请求是/api/order/create,剥离后变成/order/create再发给下游服务。
别小看这段配置,它已经涵盖了网关最基础的用法。平时我们接手一个项目,先看它的routes配置,基本就能明白网关在整体架构中承担了哪些转发职责。
2.3 断言工厂的常用姿势与匹配优先级
内置断言工厂比较多,我把日常最常用的几个挑出来讲,每个都配合实际场景。
Path断言是最常用的。它支持Ant风格的通配符:*代表任意一层,**代表任意多层。例如:
predicates: - Path=/api/order/**,/api/pay/**同一个Path断言里可以配置多个路径,用逗号分隔,表示这些路径都能匹配。
但是这里有个容易被忽略的细节:Path断言的匹配是"前缀匹配",不是"全等匹配"。也就是说/api/order和/api/order/效果可能不同,/api/order/**能匹配/api/order,但如果你写的是/api/order,它只能精确匹配这个路径本身。所以配置路径断言时,尽量统一风格,避免出现"明明路径差不多,却没接住请求"的情况。
Method断言用于限定HTTP方法:
predicates: - Method=GET,POST这个在接口设计上很有用。比如某些写操作只允许POST,读操作只允许GET,直接在网关层就拦掉一批不合法请求,省得打到业务层再判断。
Header断言用于校验请求头:
predicates: - Header=X-Request-Id, \d+第二个参数是正则表达式,表示X-Request-Id的值必须满足数字格式。这样做可以保证整个链路有统一的链路追踪ID,防止调用方乱传。
Query断言用于校验请求参数:
predicates: - Query=source, app表示请求必须带source参数,且值等于app。
时间断言用于灰度发布场景:
predicates: - After=2024-01-01T00:00:00+08:00[Asia/Shanghai]这条路由只有在指定时间之后才生效。比如你打算凌晨2点切流量到新版本,又不想手动干预网关,就可以在发布前配置好时间断言,到点自动生效。
这里需要特别强调的是多个断言之间是"且"的关系。比如:
predicates: - Path=/api/user/** - Method=GET - Header=X-Version, v1请求必须同时满足路径前缀、GET方法、请求头X-Version等于v1,才会命中这条路由。
另外,当多个路由都能匹配同一个请求时,Gateway的匹配顺序遵循Spring的RoutePredicateHandlerMapping,断言匹配成功后会按路由声明的顺序返回第一个。这意味着,路由配置的顺序非常重要,顺序错了,路由就"串"了。后面我会专门讲这个坑。
3. 过滤器链:内置过滤器与自定义过滤器的配合
3.1 内置过滤器的分类与常用清单
Spring Cloud Gateway内置了几十个GatewayFilter工厂,覆盖了请求重写、路径处理、重试、熔断、限流、CORS等常见场景。我不打算把全部列出来,只挑实际项目里出现频率最高的几类。
路径重写类是使用率最高的。最典型的是RewritePath和StripPrefix。StripPrefix前面我们已经说过,剥离前N段;RewritePath则用正则替换来实现更灵活的路径改写:
filters: - RewritePath=/api/order/(?<segment>.*), /$\{segment}这段配置表示把/api/order/xxx重写为/xxx。注意,YAML中${segment}的$符号需要用$\{segment}来转义,否则会被当成占位符解析。这是一个新手极容易踩的坑。
请求头与响应头处理类:AddRequestHeader、AddResponseHeader、RemoveRequestHeader、RemoveResponseHeader、SetRequestHeader。比如要求所有请求带上内部调用标识:
filters: - AddRequestHeader=X-Internal-Call, gateway这样下游服务可以识别哪些请求是从网关进来的,在权限校验、日志记录时做区分。
重试与熔断类:Retry和FallbackHeaders。Retry可以配置重试次数、重试状态码、重试方法等。比如对GET请求做最多3次重试:
filters: - name: Retry args: retries: 3 methods: GET statuses: BAD_GATEWAY注意,重试要谨慎,写操作(POST/PUT/DELETE)不要在网关层轻易重试,否则可能造成重复提交,需要在下游做幂等。
限流类:RequestRateLimiter,这是网关内置的令牌桶限流过滤器,依赖Redis,后面我会单独展开讲。
3.2 自定义GatewayFilter:一个可落地的示例
内置过滤器覆盖了大部分通用场景,但业务始终是千奇百怪的。实际项目中,自定义过滤器往往比内置过滤器用得更频繁。最常见的自定义场景包括:接口签名校验、灰度标签透传、统一请求头清洗、黑白名单拦截等。
自定义GatewayFilter有两种实现方式。第一种是实现GatewayFilter接口或继承AbstractGatewayFilterFactory;第二种是通过配置类的形式。这里我写一个典型的场景:校验请求中是否存在token,且token前缀必须是"Bearer "。
@Component public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.Config> { public AuthGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); }; } public static class Config { // 可以留空,也可以加一些可配置参数 } }然后在路由配置中这样使用:
filters: - name: Auth args: {}工厂类的命名规则是:XxxGatewayFilterFactory,在配置中写Xxx即可。这个约定要记住,Gateway会去掉GatewayFilterFactory后缀来识别过滤器名称。
3.3 GlobalFilter:解决所有请求都需要处理的共性问题
GatewayFilter只作用在配置了它的路由上,如果你希望所有经过网关的请求都执行某些逻辑,那就必须用GlobalFilter。
GlobalFilter接口的定义很简洁,核心方法:
public interface GlobalFilter { Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain); }我举一个很实用的例子:给所有请求添加链路追踪ID。在微服务架构中,我们需要一个trace ID贯穿一次完整的调用链,网关作为入口,最适合在入口处生成或透传这个ID。
@Component public class TraceIdGlobalFilter implements GlobalFilter, Ordered { private static final String TRACE_ID_HEADER = "X-Trace-Id"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String traceId = exchange.getRequest().getHeaders().getFirst(TRACE_ID_HEADER); if (traceId == null || traceId.isEmpty()) { traceId = UUID.randomUUID().toString().replace("-", ""); } ServerWebExchange mutatedExchange = exchange.mutate() .request(exchange.getRequest().mutate() .header(TRACE_ID_HEADER, traceId) .build()) .build(); return chain.filter(mutatedExchange); } @Override public int getOrder() { return -100; } }这里有两个细节值得注意。
第一,exchange.mutate()创建的是新的ServerWebExchange实例,直接修改原exchange的header是无效的,因为请求对象不可变。这就是WebFlux响应式编程中"不可变性"的体现。
第二,getOrder()决定了GlobalFilter的执行顺序。返回值越小越先执行。通常我们希望链路追踪ID最先加进去,所以给一个负数,比如-100。
很多人在自定义GlobalFilter时栽过跟头:在filter里直接修改请求、直接获取响应body、或者使用exchange.getRequest().getBody()去读body后再传给下游,发现body被消费了,下游拿不到数据。这些都是WebFlux的"坑",我会在第5部分专门讲。
4. 从零搭建一个带路由、限流、熔断的网关服务
4.1 工程结构与依赖引入
理论知识讲得再多,不动手永远是空的。下面我用一个完整的工程示例,把网关从零到可用跑一遍。先看pom.xml中必须引入的关键依赖。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> </dependencies>这里的依赖选择是有讲究的。spring-cloud-starter-gateway是核心;因为要通过注册中心发现下游服务,所以引入nacos-discovery;同时我引入了spring-cloud-starter-loadbalancer,因为Spring Cloud 2020版本之后,Ribbon被移除了,默认使用LoadBalancer做客户端负载均衡,必须显式引入。限流需要Redis,且必须是reactive版本,因为网关本身是响应式WebFlux应用,不能使用普通的spring-boot-starter-data-redis,否则会引入Tomcat的Web容器,破坏网关的reactive特性。
4.2 路由配置的完整示例
再看application.yml中的核心配置。
server: port: 8080 spring: application: name: gateway-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: routes: - id: user-service uri: lb://user-service predicates: - Path=/api/user/** filters: - StripPrefix=2 - AddRequestHeader=X-Gateway-Route, user-service - id: order-service uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=2 - id: auth-service uri: lb://auth-service predicates: - Path=/api/auth/** filters: - StripPrefix=2 globalcors: add-to-simple-url-handler-mapping: true cors-configurations: '[/**]': allowedOriginPatterns: "*" allowedMethods: "*" allowedHeaders: "*" allowCredentials: true这段配置里有几个我要特别说明的地方。
第一,uri: lb://user-service中的lb://前缀,表示从注册中心按服务名负载均衡转发。Gateway会根据user-service这个名字去注册中心找实例列表,然后通过LoadBalancer选一个实例,把请求转发过去。
第二,加了AddRequestHeader=X-Gateway-Route, user-service这个过滤器,目的就是给下游一个标记,让它们知道请求是从哪个路由进来的。这在排查问题时非常有用。
第三,globalcors这段配置解决的是跨域问题。add-to-simple-url-handler-mapping: true用于处理预检请求(OPTIONS)的映射,否则很多浏览器的CORS请求会在网关被拦。
4.3 网关级限流:RequestRateLimiter的落地细节
限流是网关的核心能力之一。Spring Cloud Gateway内置的限流过滤器RequestRateLimiter,底层用Redis实现令牌桶算法。
先看配置:
spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/order/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 key-resolver: "#{@userKeyResolver}"releaseNotes含义:
replenishRate:令牌桶每秒填充的令牌数,也就是平均每秒允许的请求数。burstCapacity:令牌桶的容量,也就是瞬时允许的突发请求数。key-resolver:限流的维度。这里用了#{@userKeyResolver}引用一个Bean,用来确定限流的key,比如按用户限流、按IP限流、按接口限流。
userKeyResolver是一个自定义Bean,返回KeyResolver:
@Bean public KeyResolver userKeyResolver() { return exchange -> { String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id"); if (userId == null || userId.isEmpty()) { userId = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); } return Mono.just(userId); }; }这样实现的逻辑是:优先按请求头X-User-Id限流,如果用户ID不存在,则退化为按IP限流。
这里我想额外提醒一个坑:RequestRateLimiter在压测环境下特别容易踩到Redis连接超时。因为网关是响应式的,Redis连接数有限,当突发流量远大于burstCapacity时,Redis连接等待会导致大量超时错误。解决思路有两个:一是调整Redis连接池参数,二是限流配置的burstCapacity不要设得过大,避免瞬间涌入太多请求打爆Redis连接。实际生产环境,我一般把burstCapacity设为replenishRate的2倍左右,既能容忍一定突发,又不至于压垮Redis。
4.4 熔断兜底:集成Sentinel或Resilience4j的取舍
网关层做熔断,本质上是不希望某个下游服务出问题时,整个网关入口被拖死。Spring Cloud Gateway提供了fallback的扩展点:当路由转发失败或超时,可以转发到一个兜底接口。
官方推荐的方式是配合Spring Cloud CircuitBreaker使用,底层默认是Resilience4j。配置如下:
spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/order/** filters: - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/order然后在网关服务中写一个@RestController提供/fallback/order接口,返回友好的提示信息。
至于为什么不直接用Sentinel而推荐Resilience4j:Spring Cloud Gateway官方对Resilience4j的适配是默认的,开箱即用;Sentinel适配网关需要额外的依赖和规则配置,虽然控制台更丰富,但对中小团队来说维护成本偏高。我自己在项目里两边都用过,结论是:如果团队已有Sentinel控制台,用Sentinel网关适配没问题;如果只是单纯想要一个熔断兜底,直接用官方的CircuitBreaker过滤器就够了,别为了炫技引入一套新系统。
5. 高频率翻车现场:WebFlux环境下最容易踩的5个坑
5.1 路由顺序导致的"接口迷路"
这是我在文章开头提到的那个坑。Gateway在匹配路由时,如果多个路由的断言都能匹配同一个请求,会取第一个注册的路由。一旦你把一条泛匹配的路由放在前面,后面所有具体路由就都"失效"了。
举个例子:
routes: - id: default-route uri: http://example.com predicates: - Path=/** - id: user-service uri: lb://user-service predicates: - Path=/api/user/**这种情况下,任何请求都会命中default-route,user-service路由永远不会生效。
解决方案很简单:把具体路由放在前面,泛匹配的兜底路由放在最后。同时,尽量使用精确的前缀来缩小匹配范围,不要一开始就写/**。
5.2 StripPrefix和RewritePath的"改过头"
很多人分不清StripPrefix和RewritePath的区别。StripPrefix固定剥离前N段,更简单直白;RewritePath用正则重写,更灵活。但问题是,容易把路径改得面目全非,下游接口根本接收不到正确路径。
我建议的思路是:先明确下游服务的ContextPath,再决定怎么改。比如下游user-service的接口路径是/user/info,网关收到的前端请求是/api/user/info,那么StripPrefix=2就能正确转成/user/info。
如果下游接口路径是/api/user/info,而网关收到的是/api/user/info,就什么都不需要改。很多新手一上来就加StripPrefix,结果路径变成/info,导致404。建议加完过滤器后,打印一下实际转发的路径,不要凭感觉。
5.3 获取真实客户端IP:getRemoteAddress 不等于原始IP
这是一个非常隐蔽的问题。当经过多层代理或负载均衡后,exchange.getRequest().getRemoteAddress()拿到的是上一跳(可能就是Nginx)的IP,而不是真实客户端IP。正确做法是从请求头X-Forwarded-For中取,但要小心伪造。
下面是我常用的一段代码:
private String getClientIp(ServerWebExchange exchange) { String forwarded = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For"); if (forwarded != null && !forwarded.isEmpty()) { String first = forwarded.split(",")[0].trim(); if (isIpv4(first)) { return first; } } return exchange.getRequest().getRemoteAddress() != null ? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() : "unknown"; }注意,X-Forwarded-For可能存在多个IP,从左到右依次是"客户端IP, 代理1IP, 代理2IP",所以取第一个。同时要小心客户端伪造该头,如果网关直接暴露在公网,建议在更高层(如Nginx)的配置中覆盖或清洗该请求头。
5.4 在Gateway Filter里读取和修改RequestBody
在WebFlux中,请求Body是一个Flux<DataBuffer>,只能被读取一次。如果你在GlobalFilter里读了Body,下游Service就收不到Body了。所以很多人在做"请求验签"或"参数预处理"时,都会遇到Body被消费的问题。
正确的做法是缓存Body,然后重建请求。思路比较绕,但能解决问题:
在过滤器里读取完整的Body字符串,处理完成之后,将处理好的数据重新包装成一个新的BodyInserter,再通过exchange.mutate()替换请求。
一个简化版的实现思路如下:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); return DataBufferUtils.join(request.getBody()) .flatMap(dataBuffer -> { byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); DataBufferUtils.release(dataBuffer); String bodyStr = new String(bytes, StandardCharsets.UTF_8); // 处理bodyStr... String newBody = handleBody(bodyStr); return chain.filter(exchange.mutate() .request(exchange.getRequest().mutate() .body(newBody) .build()) .build()); }); }这里需要你自己封装一个ServerHttpRequest,重写getBody()方法,返回新的Flux<DataBuffer>。
这个方案对新手来说会比较绕。我的个人建议是:别轻易在网关层做复杂的Body处理和验签,能放到业务层就放到业务层。网关的核心职责是路由和过滤,而不是业务逻辑。如果实在要在网关层做Body处理,建议先跑通"缓存-读取-重建"三步链路,并用单元测试覆盖,避免上线后出现POST接口大量报错。
5.5 网关层CORS配置与自定义GlobalFilter的冲突
还有一类问题很隐蔽:前端发OPTIONS预检请求,结果在网关直接被自定义过滤器拦截,返回了401/403。原因是你的GlobalFilter没有放行OPTIONS请求。
解决方案很简单,在GlobalFilter里加一个判断:
if (exchange.getRequest().getMethod() == HttpMethod.OPTIONS) { return chain.filter(exchange); }同时,确保globalcors.add-to-simple-url-handler-mapping: true。这一步不做,浏览器预检永远过不了,你会看到各处都在报跨域错。
6. 本地调试与压测验证:确保网关上线前不"裸奔"
6.1 开启Gateway调试日志
配置网关后,最痛苦的事情是"配置对了没有?"。建议在开发环境打开调试日志:
logging: level: org.springframework.cloud.gateway: DEBUG打开后,请求进入网关时,控制台会打印详细的路由匹配过程,包括命中了哪个Route、经过了哪些过滤器、转发到哪个URI。这个信息量非常大,定位问题比看业务日志快得多。
另外还有一个更直观的工具:Spring Boot Actuator暴露了网关的gateway端点,可以查看当前所有的路由配置。
management: endpoints: web: exposure: include: gateway然后访问/actuator/gateway/routes,就能看到网关当前加载的所有路由、断言、过滤器信息。这在排查"为什么新加的路由不生效"时非常关键——先确认路由是否被加载,再去匹配逻辑找问题。
6.2 用Postman和脚本模拟多条件下发
调试网关时,我通常会用Postman做三类测试:
第一类:正常转发测试。直接请求/api/user/info,看是否能正确到达下游服务,并返回预期结果。
第二类:断言拦截测试。比如配置了Header=X-Version, v1,就分别用携带正确头和错误头的请求测试,确认断言生效。
第三类:限流测试。写一个小脚本循环请求100次,观察前20次可能成功,后面的请求返回429 Too Many Requests,这表示限流生效。
下面是一个简单的脚本片段,便于模拟快速请求:
for i in $(seq 1 100); do curl -s -o /dev/null -w "%{http_code}\n" \ -H "X-User-Id: test001" \ "http://localhost:8080/api/order/list" done如果limit配置的是replenishRate=10, burstCapacity=20,你会看到前20个左右的响应是200,后续大量返回429。这说明限流规则已经按照预期在运行。
6.3 网关的高可用:多实例部署时无状态是前提
Spring Cloud Gateway本身是无状态的,这意味着你可以随意横向扩容,在多个实例前面挂一个负载均衡器(比如Nginx或云负载均衡)对外提供服务。但在多实例部署时,有一个前提必须满足:不要在网关的内存中保存任何业务状态。
比如如果你把某个用户的登录态直接存在网关本地Map里,那负载均衡把请求分发到不同实例时,数据就丢了。正确的做法是把状态放到外部存储,比如Redis;网关只负责转发和校验,不保存业务数据。
另外,多实例部署时,限流依赖Redis这一点反而成了优势——因为限流计数器是集中在Redis中的,所以多个网关实例的限流是"全局排队",而不是各算各的。我之前见过一个团队,限流用的本地内存实现,部署两个网关实例后,实际通过的流量是配置阈值的两倍,这个问题直到压测才暴露出来。所以,限流过滤器的Redis依赖不仅是功能要求,更是多实例部署下正确性的保证。
7. 网关之外的思考:它解决不了什么问题
很多人学了Spring Cloud Gateway之后,容易陷入一种"万物都可网关化"的误区。我在这里泼一点冷水,把网关的边界说清楚。
第一,网关不是业务聚合层。不要在网关里写大量的业务逻辑,比如字段拼接、多次调用下游服务再合并结果。这样做会让网关变得极其笨重,拖垮整体性能。真正的业务聚合应该在BFF层或专门的聚合服务中做。
第二,网关不是持久化层。不要在网关里访问数据库、缓存业务数据。网关应该保持"无状态+轻量"。
第三,网关的过滤器链如果过长,会明显增加请求延迟。每多一个过滤器,就多一次请求处理的开销。我曾经在一个项目里见到一条路由挂了十来个过滤器,压测时延迟翻了一倍。建议是:能通过路由断言解决的,就不要用过滤器;能通过下游解决的,就不要在网关做。
最后再分享一个经验:上线前一定要做一次"路由全面测试",使用一个覆盖所有业务模块的调用清单,逐个验证网关转发是否正确。这种回归测试不需要太复杂的自动化框架,甚至用shell脚本循环curl就能完成,但非常有必要。网关是流量的总入口,它出问题,影响的不是某个服务,而是整个系统。
如果你正在搭建或者维护Spring Cloud Gateway,希望这篇文章能帮你少踩几个坑。遇到奇怪的问题,先开DEBUG日志,先看路由匹配记录,不要盲目改配置,问题其实都不难定位。