尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Cloud Gateway 内置 Filter 实战:从路径重写到熔断限流

Spring Cloud Gateway 内置 Filter 实战:从路径重写到熔断限流 做微服务网关路由只是入口真正让网关有价值的其实是挂在路由上的那些 Filter。最近在整理项目里的 Gateway 配置时发现很多同事对内置 Filter 的理解停留在“会用 AddRequestHeader 给下游加个头”这个层面一旦碰到路径重写、重试策略、熔断降级就开始瞎试然后被 502、超时、重复请求折磨得焦头烂额。这篇文章我把 Spring Cloud Gateway 内置 Filter 里最常用的几个拿出来逐个拆一遍包括 AddRequestHeader、RewritePath、StripPrefix、Retry、CircuitBreaker、RequestRateLimiter 这些结合一个完整的实战配置讲讲它们的原理、配置方法和踩坑经验适合正在做网关方案、或者已经在用但想搞清楚配置细节的同学。1. 网关的本质是转发Filter 才是网关的“业务层”1.1 GatewayFilter 与 GlobalFilter先分清两种角色很多人第一次接触 Spring Cloud Gateway 会被 Filter 这个词搞晕因为一搜资料发现既有 GatewayFilter 又有 GlobalFilter每个 Filter 后面还跟着个 Factory配置的时候一会儿是- AddRequestHeaderxxx一会儿又是- name: Retry格式都不统一。这其实是没搞懂 Filter 的分工。Spring Cloud Gateway 里的过滤器分两类。一类是路由级 GatewayFilter它只对当前这个路由生效通过spring.cloud.gateway.routes[].filters配置这也是“内置 Filter”这个词真正指的东西。另一类是全局 GlobalFilter对所有路由生效通常用 Java 代码实现比如RouteToRequestUrlFilter、ReactiveLoadBalancerClientFilter、NettyRoutingFilter都属于这一类。框架内置了二三十个 GatewayFilter 工厂它们帮你把请求头改写、路径重写、重试、熔断、限流这些高频动作封装成了一个个可以直接声明式使用的组件。理解了这一点再看配置格式就顺了。AddRequestHeaderX-Header, value这种简洁写法本质上是把AddRequestHeaderGatewayFilterFactory的 name 和 args 合并成了一行工厂的顺序参数就是配置参数。而name args这种写法是展开式的适合 Retry、CircuitBreaker 这种参数多的过滤器。两者是同一个东西只是配置姿势不同。1.2 内置 Filter 工厂解决了哪几类共性问题我复盘了一下项目里真正用到的内置 Filter发现它们其实只干了四类事情请求头/参数改写AddRequestHeader、AddRequestParameter、AddResponseHeader、RemoveRequestHeader、MapRequestHeader、SetRequestHeader 等。用来对内网传参、加 traceId、剥掉敏感头。路径处理StripPrefix、RewritePath、PrefixPath、SetPath。解决网关路径前缀和下游服务路径之间的映射关系。路由健壮性Retry、CircuitBreaker、FallbackHeaders、RequestRateLimiter、RequestSize。这是网关作为流量入口的自我保护机制。HTTP 行为控制RedirectTo、SetStatus、SaveSession、PreserveHostHeader。处理一些特殊的协议和状态码场景。你把所有现有需求对照这个分类过一遍会发现 90% 的配置需求都能用内置 Filter 覆盖真正需要手写 GatewayFilter 或 GlobalFilter 的场景非常少。这也是为什么我建议先把内置 Filter 吃透再谈自定义——很多自定义过滤器其实就是内置 Filter 的排列组合只是顺序和参数没摆对。1.3 从一个最简路由看 Filter 如何插进去直接看一个最基础的配置spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/api/user/** filters: - StripPrefix1 - AddRequestHeaderX-Internal-Token, internal-token-123这条路由的意思是所有匹配/api/user/**的请求先被StripPrefix1剥掉第一段路径再被AddRequestHeader加一个内部认证头然后通过负载均衡转发到 user-service。请求进来之后Filter 是挂在 Predicate 匹配结果上的Predicate 决定“这个请求走不走这条路由”Filter 决定“走了之后要做什么手脚”。这个模型非常干净路由只管匹配Filter 只管处理。2. AddRequestHeader 与参数改写最常被低估的三种用法2.1 AddRequestHeader 的静态特性值写死时机要搞清AddRequestHeader 是大多数人最早接触的内置 Filter配置极其简单filters: - AddRequestHeaderX-Request-From, gateway - AddRequestHeaderX-User-Id, 9527它会在请求转发给下游之前在请求头里追加键值对。这个 Filter 最常见的用法是解决内网认证问题外网请求先经过网关做登录鉴权鉴权通过后网关把用户信息以约定好的 Header 传给下游服务下游服务直接信任这个 Header。类似还有标识请求来源、传递版本号、传递环境名等。这里有一个很多新手忽略的关键点AddRequestHeader 的值是在路由构建时就固定下来的静态字符串不是每次请求动态生成的。所以如果你想用它给每个请求加一个 UUID 作为 traceId写AddRequestHeaderX-Trace-Id, ${uuid}是行不通的——你拿不到请求上下文过滤器的值在路由初始化时已经被apply()方法写死进过滤器对象里了。提示需要每次请求动态生成 Header 的场景不用硬套内置过滤器直接写一个 GlobalFilter在exchange.getRequest().mutate().header(...)里动态生成就行。后面实战部分会给出代码。另一个容易忽略的是执行时机。路由内的多个过滤器按配置顺序执行而全局过滤器里的NettyRoutingFilter是排在整个过滤器链最后才真正发出 HTTP 请求的所以 AddRequestHeader 加的 Header 一定会在请求路径到达NettyRoutingFilter之前出现在请求头里不用担心顺序问题导致 Header 没加上。不过响应链路是反过来的。请求转发出去之后响应会沿着调用链倒序返回所以 AddResponseHeader 在响应路径上生效的顺序和 AddRequestHeader 是镜像对称的。这个特性在分析“为什么响应头没有按预期被修改”时会用到。2.2 AddRequestParameter 的同名参数坑AddRequestParameter 的作用是给转发请求追加 query string 参数filters: - AddRequestParameterchannel, mobile配置之后GET /api/product/list转发出去了下游实际收到的是GET /api/product/list?channelmobile。这个 Filter 有个非常隐蔽的坑它只会追加参数不会覆盖已有参数。假设原始请求是GET /api/product/list?page1你配置了AddRequestParameterpage, 2下游收到的 URL 是GET /api/product/list?page1page2。两个同名 page 参数会同时存在。至于下游拿到哪个值取决于框架的解析策略。Spring Boot 的RequestParam默认取第一个Node.js 的 Express 框架会把同名参数组装成数组Go 和 Python 多数取第一个。这个行为不一致会让下游在联调时产生一种“参数怎么改都没生效”的错觉。我的建议是加参数之前先确认下游接口不会因为同名参数产生歧义。如果只是想给下游加一个“默认值”且允许被原请求覆盖AddRequestParameter 的方案不成立应该改用自定义 filter 在路由阶段判断“原 URL 没带这个参数才追加”。顺带提醒一句官方内置 filter 里并没有 RemoveRequestParameter 这个工厂网上有一些文章提到它多半是笔误或自定义实现。真的要移除 query 参数得靠写 GlobalFilter 或 GatewayFilter 自己改 URL别指望开箱即用。2.3 AddResponseHeader / RemoveRequestHeader / RemoveResponseHeader 联动这三个过滤器放在一起说因为它们配合起来通常在做一件事控制哪些信息能流入下游、哪些信息能流回客户端。filters: - RemoveRequestHeaderX-User-Id - AddResponseHeaderX-Gateway-Version, 2.1.0 - RemoveResponseHeaderServerRemoveRequestHeader 常用于安全场景。比如前端可以自行构造一个X-User-Id头伪装身份如果网关鉴权后会把真实的用户信息放进另一个 Header比如X-Authenticated-User那就有必要先把前端传的X-User-Id剥掉防止下游业务误用。AddResponseHeader 则用于给所有响应统一打标记比如网关版本号、缓存策略、CORS 头。RemoveResponseHeader 用于隐藏下游框架自动带的敏感响应头最典型的就是把Server、X-Powered-By这类暴露技术栈的 Header 剥掉。还有一个冷门但好用的 DedupeResponseHeader。它是专门处理响应头重复的比如下游同时配置了多个 Path 路由每次经过网关可能会被加两次Set-Cookie用DedupeResponseHeaderSet-Cookie可以按策略去重。我见过不少项目因为响应头重复导致前端 cookie 异常最后就是靠这个过滤器解决的。2.4 动态值不适用TraceId 的真实场景前面提到 AddRequestHeader 的值是静态的这让很多人卡在“动态 traceId”这个需求上。我给出一个在实际项目中跑通的写法。先定义一个全局过滤器Component public class TraceIdGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String traceId Optional.ofNullable( exchange.getRequest().getHeaders().getFirst(X-Trace-Id)) .orElse(UUID.randomUUID().toString()); ServerHttpRequest request exchange.getRequest().mutate() .header(X-Trace-Id, traceId) .build(); return chain.filter(exchange.mutate().request(request).build()); } Override public int getOrder() { return -100; } }这个过滤器把 order 设为 -100确保它在所有路由级 GatewayFilter 之前执行。这样后面的 AddRequestHeader、RewritePath 等配置里的过滤器处理时X-Trace-Id 已经在请求头里了。实际用的时候还可以把 traceId 顺手放到exchange.getResponse().getHeaders()里这样客户端也能拿着这个 ID 来查日志排障会舒服很多。这个例子也说明一个问题内置 Filter 解决的是“静态、通用、配置化”的需求遇到“动态、业务化”的需求用 GlobalFilter 补充是更干净的做法不需要什么事都去自定义 GatewayFilterFactory。3. RewritePath、StripPrefix、SetPath路径改写不要凭感觉选3.1 StripPrefix只数段数是新手最容易理解错的StripPrefix 只接收一个整数参数表示去掉路径前几段filters: - StripPrefix1原始路径/api/user/1StripPrefix1 之后变成/user/1转发给下游的 URL 就是这个。StripPrefix2 则去掉/api和user两段路径变成/1。新手最容易理解错的是StripPrefix 不是按“固定的字符串前缀”去匹配而是按/分割后数段数从第一段开始丢弃。假设路径是/api/user/1/detailStripPrefix1 后是/user/1/detailStripPrefix2 后是/1/detail它关心的是路径层级完全不关心这一层的名字到底叫api还是foo。这个 Filter 适合“所有路由都带一个统一前缀而下游服务不关心这个前缀”的场景。例如前端喊口号“所有接口都带 /api”网关把/api剥掉再转发下游各个微服务不需要在自己的RequestMapping里写/api前缀路径职责清晰。3.2 RewritePath正则的完整匹配与命名分组RewritePath 是内置 Filter 里灵活度最高但也最容易被转义搞疯的一个filters: - RewritePath/api/user/(?segment.*), /$\{segment}这个配置会把/api/user/1重写成/1把/api/user/1/orders重写成/1/orders。注意这里的匹配规则是第一个参数是正则表达式第二个参数是替换表达式替换表达式里$开头的部分是正则捕获组引用Java 里叫“命名分组”或“序号分组”。这里有几个容易踩的坑。第一个坑是YAML 占位符冲突。${segment}这种写法在 YAML 里会被 Spring 当属性占位符解析如果环境里没有segment这个属性启动直接报错。所以官方文档里写的是$\{segment}用反斜杠转义掉$让替换表达式里的${segment}能作为正则捕获组引用传递给 Java 正则引擎。如果你用 Java DSL 写同样逻辑字符串里还得再转义一层f.rewritePath(/api/user/(?segment.*), /$\\{segment})复数的转义是第一次用 RewritePath 的人最容易卡住的地方没有之一。第二个坑是正则的匹配范围。RewritePath 的第一个参数是正则“全文匹配”不是“前缀匹配”。所以RewritePath/api/user/(.*), /$1只会重写完整符合/api/user/xxx的路径如果原始路径是/api/user/1/orders上半段的正则(.*)能匹配整个剩余部分是 OK 的但如果正则写的是/api/user而不是/api/user/(.*)那就连/api/user/1都替换不了因为完整字符串没被匹配上。第三个坑是命名分组比序号分组可读性好太多。用(?segment.*)$\{segment}在配置里一眼能看出捕获的是哪个变量用(.*)$1虽然也行但一个正则里塞三四个捕获组之后配置根本没法维护。建议项目里统一用命名分组。3.3 SetPath 与 PrefixPath 的模板占位符SetPath 和 RewritePath 解决的是同一类问题但用法完全不同。SetPath 使用 URI 模板语法而不是正则。它配合 Predicate 的路径变量用predicates: - Path/api/users/{userId} filters: - SetPath/users/{userId}Predicate 在匹配/api/users/123时已经从路径中提取出了userId123这个变量。SetPath 拿着这个变量把路径重写为/users/123。它的可读性比 RewritePath 强多了因为不需要写正则模板里的{userId}直接就是变量名。PrefixPath 则是最简单的前缀添加器filters: - PrefixPath/api请求/user/1变成/api/user/1。它和 StripPrefix 是一对反操作一个加前缀一个剥前缀。两者连用可以实现“替换前缀”的效果例如先 StripPrefix1 再 PrefixPath/v2就把/v1/user变成了/v2/user。3.4 三个方案的选择决策表我把这三类路径处理 Filter 的适用场景整理成一个表实际配置时直接对号入座过滤器机制典型场景注意点StripPrefix按路径段数去掉前 N 段所有接口统一带/api前缀只数段数不看名字RewritePath正则全文匹配 捕获组替换复杂路径映射、版本号改写正则匹配是整个路径YAML 需要转义${}SetPathURI 模板复用 Predicate 提取的变量根据路径变量精确重写必须配合能提取同名变量的 PredicatePrefixPath直接加前缀网关路径和下游路径风格不一致和 StripPrefix 配合可实现替换前缀在真实项目里StripPrefix 的使用频率最高因为大多数公司的 API 都有统一定义的网关前缀RewritePath 次之适合那种“下游接口路径和网关暴露路径没法一一对应”的存量系统SetPath 在纯新增项目里用得最少但在做 BFFBackend For Frontend层时非常顺手。4. Retry、CircuitBreaker、RequestRateLimiter让下游服务更扛揍4.1 Retry什么时候能重试比怎么重试更重要Retry 的配置并不复杂filters: - name: Retry args: retries: 3 statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE methods: GET backoff: firstBackoff: 200ms maxBackoff: 1000ms factor: 2核心参数就几个retries是最大重试次数statuses是触发重试的响应状态码methods是允许重试的 HTTP 方法backoff是退避策略第一次重试等 200ms之后按 2 倍指数增长封顶 1s。但我想强调的重点不在配置而在重试的幂等性红线。Retry 配置一旦放开意味着同一个请求可能被网关重复发送多次。GET 这种幂等请求问题不大但 POST、PUT、PATCH 这类可能产生副作用的请求一定要谨慎否则用户提交一次订单网关因为下游响应超时重试了三次结果下了四单这个事故背不起。提示实际项目里我一般只在 GET 请求上开启 RetryPOST 请求一律不配置重试或者只在明确幂等的内部接口上开。判断标准很简单下游收到这个请求两次结果是否可接受。另一个细节是series与statuses的选择。series按状态码大类匹配比如SERVER_ERROR会匹配所有 5xxstatuses精确到具体状态码。我建议用statuses精确匹配BAD_GATEWAY、SERVICE_UNAVAILABLE这类“服务临时不可用”的状态不要把INTERNAL_SERVER_ERROR也纳入重试范围——程序 bug 产生的 500重试多少次都一样是 500只会放大下游压力。4.2 CircuitBreaker 的 fallbackUri 设计CircuitBreaker 是在 Retry 之上的第二道保护它的作用是当下游服务持续失败时直接打开熔断器不再把请求转发到下游而是快速失败并走降级逻辑。filters: - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/order配置里name是熔断器实例名fallbackUri是兜底地址。注意这里有个很容易搞错的设计fallbackUri不是指向一个外部服务地址而是指向网关自己的某个路径用forward:协议落地时通常会配一条本地 fallback 路由- id: order-fallback uri: forward:/fallback/order predicates: - Path/fallback/order链路是这样的请求转发到 order-service 失败熔断器打开请求被网关内部转为forward:/fallback/order这条本地路由收到请求后在网关内生成一个降级响应返回给客户端。整个过程中没有真正的网络请求出去因此不会因为下游挂了而一直在那里干等连接超时。只配熔断器不配具体参数是远远不够的。默认的熔断参数比较宽实际项目中一定要通过 resilience4j 配置干预resilience4j: circuitbreaker: instances: orderServiceCB: slidingWindowSize: 10 minimumNumberOfCalls: 5 failureRateThreshold: 50 waitDurationInOpenState: 10s这些参数的含义是在滑动窗口内至少调用 5 次如果失败率超过 50%熔断器打开保持 10 秒后进入半开状态允许少量请求试探下游是否恢复。这里我想特别提醒waitDurationInOpenState不要太短否则下游还在抖动期熔断器反复开合效果很差也不要太长否则下游恢复了也要白白熔断很久。10 秒到 30 秒是比较常见的取值。4.3 RequestRateLimiter 的 Redis 与 KeyResolverRequestRateLimiter 是网关做接口限流的标准方案但它不是纯本地计算而是基于 Redis 的令牌桶实现所以引入它之前要先满足一个硬条件网关必须能连上 Redis。filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 key-resolver: #{userKeyResolver}replenishRate是令牌桶的填充速率代表平均每秒允许处理的请求数burstCapacity是桶容量代表瞬时允许的突发流量key-resolver是限流 key 的解析器告诉网关“按什么维度限流”。这三个参数缺一不可而key-resolver本身是一个 Spring BeanBean public KeyResolver userKeyResolver() { return exchange - Mono.just( exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() ); }示例里按客户端 IP 限流也可以按用户 ID、按请求路径等维度设计。KeyResolver 返回的字符串就是 Redis 里令牌桶的唯一标识维度设计得越细限流控制越精准但 Redis 的 key 数量也会越多需要在二者之间平衡。还有一个容易被忽略的点RequestRateLimiter 触发限流后默认返回 429 Too Many Requests。如果你希望客户端拿到一个更友好的错误结构可以用SetStatus和自定义 fallback 思路去包装或者干脆自定义一个全局异常处理器把限流响应统一成 JSON 格式。5. 一个真实网关配置实战YAML 与 Java DSL 双版本落地5.1 场景需求与路由规划前面讲的都是单个过滤器的用法这一节把它们串起来做一个接近真实业务的完整配置。假设现在有一个电商后台包含 user-service用户服务、order-service订单服务、product-service商品服务三个微服务前端统一走网关 8080 端口。要求如下外部访问统一带/api前缀网关转发前剥掉前缀网关转发前添加内部认证头X-Internal-Token下游服务凭借这个头识别来自网关的合法请求每次请求生成动态 traceId写入请求头贯穿全链路日志order-service 的 GET 接口做两次重试且开启熔断降级user-service 开启 IP 维度的 Redis 限流每 IP 每秒最多 10 个请求突发容量 20所有响应统一添加X-Gateway-Version: 1.0.0响应头这三个服务的路由规划很简单/api/user/**到 user-service/api/order/**到 order-service/api/product/**到 product-service。全部通过注册中心的服务名发现使用lb://前缀。5.2 YAML 配置逐段解读server: port: 8080 spring: application: name: gateway-service cloud: gateway: default-filters: - AddResponseHeaderX-Gateway-Version, 1.0.0 routes: - id: user-service uri: lb://user-service predicates: - Path/api/user/** filters: - StripPrefix1 - AddRequestHeaderX-Internal-Token, internal-token-2024 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 key-resolver: #{userKeyResolver} - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix1 - AddRequestHeaderX-Internal-Token, internal-token-2024 - name: Retry args: retries: 2 statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE methods: GET backoff: firstBackoff: 200ms maxBackoff: 1000ms factor: 2 - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/order - id: product-service uri: lb://product-service predicates: - Path/api/product/** filters: - StripPrefix1 - AddRequestHeaderX-Internal-Token, internal-token-2024 - id: order-fallback uri: forward:/fallback/order predicates: - Path/fallback/order data: redis: host: localhost port: 6379 resilience4j: circuitbreaker: instances: orderServiceCB: slidingWindowSize: 10 minimumNumberOfCalls: 5 failureRateThreshold: 50 waitDurationInOpenState: 10s逐段解读几个关键设计default-filters里的AddResponseHeader对所有路由生效用来做全局响应标记不用每个路由写一遍user-service 路由同时叠了StripPrefix、AddRequestHeader、RequestRateLimiter顺序是按配置依次执行先剥路径、再加头、再限流。顺序上没什么问题因为限流只依赖请求 KeyResolver不依赖前面两个过滤器产生的变化order-service 路由的 Retry 只对 GET 生效重试条件精确到 502 和 503。CircuitBreaker 的fallbackUri指向forward:/fallback/order和底下单独定义的order-fallback本地路由呼应order-fallback路由没有配置额外 Filter因为它的职责就是在网关内部直接返回降级响应真正的降级逻辑可以在代码层用 handler 实现5.3 Java DSL 配置逐段解读有些人喜欢把所有路由都放在 YAML 里但遇到 ModifyRequestBody 这类需要在过滤器中写 Java 代码的场景YAML 就非常别扭。这时候 Java DSL 是更好的选择。上面这套 YAML 配置用 Java DSL 写是这样Configuration public class GatewayRoutesConfig { Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(user-service, r - r.path(/api/user/**) .filters(f - f .stripPrefix(1) .addRequestHeader(X-Internal-Token, internal-token-2024) .requestRateLimiter(c - c.setReplenishRate(10) .setBurstCapacity(20) .setKeyResolver(remoteAddrKeyResolver()))) .uri(lb://user-service)) .route(order-service, r - r.path(/api/order/**) .filters(f - f .stripPrefix(1) .addRequestHeader(X-Internal-Token, internal-token-2024) .retry(config - config.setRetries(2) .setStatuses(HttpStatus.BAD_GATEWAY, HttpStatus.SERVICE_UNAVAILABLE) .setMethods(HttpMethod.GET) .setBackoff(Duration.ofMillis(200), Duration.ofSeconds(1), 2)) .circuitBreaker(config - config.setName(orderServiceCB) .setFallbackUri(URI.create(forward:/fallback/order)))) .uri(lb://order-service)) .route(product-service, r - r.path(/api/product/**) .filters(f - f .stripPrefix(1) .addRequestHeader(X-Internal-Token, internal-token-2024)) .uri(lb://product-service)) .route(order-fallback, r - r.path(/fallback/order) .uri(forward:/fallback/order)) .build(); } Bean public KeyResolver remoteAddrKeyResolver() { return exchange - Mono.just( exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() ); } }Java DSL 的好处是参数有类型提示Retry 的setStatuses直接传 HttpStatus 枚举setBackoff用 Duration 表达时间比 YAML 里拼字符串更不容易出错也方便做变量提取。如果你的网关路由逻辑比较简单YAML 足够一旦出现复杂的条件过滤、body 修改、按环境动态切换路由Java DSL 是更主流的选择。5.4 本地验证用 curl 模拟三条链路配置写完我会本地起服务验证三条链路是否正常。第一步验证最简单的前缀剥除和 Header 添加curl -i http://localhost:8080/api/user/1正常情况下user-service 的日志里能看到请求路径是/1请求头里带着X-Internal-Token: internal-token-2024响应头里能看到X-Gateway-Version: 1.0.0。第二步验证限流。连续快速请求 user-service 接口超过 20 次第 21 个请求会返回 429。如果没配RequestRateLimiter或者 Redis 没连上这个测试会直接暴露问题。第三步验证熔断。把 order-service 停掉连续请求 order-service 接口第一次可能返回 502重试后仍然失败等到熔断器窗口统计到失败率超过阈值后续请求会立刻返回 fallback 的降级响应而不是干等超时。提示本地验证熔断时优雅停服比 kill -9 要接近真实情况。kill -9 会让下游直接出现连接拒绝而不是正常的 5xx 响应两种失败的熔断口径不同别用极端测试得出错误结论。6. 排障实录502 排查链路与集群部署6.1 502 排查链路从端口、日志到 Header 篡改502 是网关场景里最常见的报错原因多到让人头大但排查链路是有章法的。我直接把成熟的排查顺序列出来。第一确认目标服务本身是否健康。绕过网关直接 curl 路由配置里的目标地址curl -v http://127.0.0.1:1572/health如果这条命令都不通网关报unexpected status 502 bad gateway: unknown error, url: http://127.0.0.1:1572就是在告诉你“目标地址根本没服务在监听”或者目标服务 TCP 都连不上问题大概率不在网关先把服务拉起来再说。这一步能过滤掉一半以上的 502尤其是本地开发环境经常是服务没启动、端口写错、IDE 里启动的还是旧进程。第二看网关日志里的底层异常。Spring Cloud Gateway 的 502 日志里通常会带 Netty 的异常信息比如Connection refused、Unexpected error、Read timed out。连接被拒绝说明端口不通读超时说明端口通但服务处理太慢如果是 TLS 握手问题日志里一般会出现 SSL 相关字样。这些异常信息会直接把你引向问题根源。第三检查路由 uri 是否写错。lb://开头的地址要走注册中心注册中心里如果没有对应服务或者服务实例的 IP 是注册中心所在机器私网 IP而网关在另一台机器上访问不到转发也会失败。http://开头的地址要重点看协议、端口、路径拼接。这里有个很容易被忽略的细节http://user-service:8080和http://user-service:8080/是两回事URI 末尾的斜杠会影响路径拼接结果有时候多一个斜杠下游就 404 或 502。第四确认过滤器有没有把请求改坏。最常见的是自定义 GlobalFilter 里把 Host 头覆盖了下游服务按虚拟主机路由收到错误的 Host 直接拒绝或者 RewritePath 把路径改成了某个不存在的接口导致下游返回 4xx被某些客户端统一展示成“bad gateway”。这种情况最坑因为你很难直接从网关日志里发现问题最好的办法是把网关所有过滤器临时全部关掉跑通基线再逐个打开缩小范围。第五检查超时配置。网关默认连接超时和响应超时时间可能很长但如果你在spring.cloud.gateway.httpclient里配置了很小的responseTimeout下游慢接口超过这个时间就会收到 502。这个属于“网关主动放弃等待”的情况本质上不是下游挂了而是你的超时阈值设得不合理。记住一个原则502 是网关层面的“上游不可达/响应异常”的统一表现网关本身只有一个职责——把请求转发出去并把响应带回来。所以排查 502 的核心思路就是逐步确认“出去”这一步哪一环断了以及“回来”的响应是不是异常。6.2 Spring Cloud Gateway 集群部署的前提条件网络热词里经常能看到“spring cloud gateway 能做集群吗”的疑问。直接给结论能而且它就是为集群而生的。Spring Cloud Gateway 本身不保存用户 Session路由配置一旦从配置中心拉取后只有本地缓存没有分布式状态所以部署多个实例完全没问题。但是“能集群”和“集群能正常工作”是两回事有三个配套条件必须提前到位。第一路由配置要统一管理。如果每个实例手工改 YAML分分钟配置漂移。实践中使用 Nacos Config、Apollo 或 Spring Cloud Config 把路由配置文件放到配置中心各实例启动时拉取同一份配置。路由变更时通过配置中心的发布机制配合POST /actuator/gateway/refresh触发刷新能实现不停机更新路由规则。要注意的是refresh 只能刷新动态配置Java DSL 里固化的路由不会热更新如果要支持运行时动态调整路由就需要把路由信息放到数据库或配置中心走 RouteDefinitionRepository 自定义实现。第二限流的 Redis 必须是共享的。RequestRateLimiter 如果每个实例用一套 Redis那限流就是“每实例限流”总量会随实例数量成倍放大。正确做法是所有网关实例连同一个 Redis令牌桶 key 自然成为多实例共享的计数器这才是真正的分布式限流。这一点在部署的时候就要规划好别等线上加完节点发现限流完全失效才往回查。第三熔断状态要以实例为单位去看。Resilience4j 的熔断器是进程内状态每个网关实例独立判断熔断与否。也就是说同一个下游服务挂了可能实例 A 已经熔断、实例 B 还在硬挺着转发请求。这不算 Bug但要心中有数。如果真的需要一个全局熔断状态那得把状态迁移到 Redis 或外部组件这属于高阶架构改造普通场景用不到。最后集群前面的负载均衡器Nginx 或云上 SLB/K8s Service要支持 WebSocket 和 SSE 这类长连接协议否则网关做了 WebSocket 代理之后集群模式下的连接升级会出问题。这些细节不多但每一件都能决定你集群是“能跑”还是“能可靠地跑”。回到 Filter 这个话题上我把这次的实战经验总结成一句话Spring Cloud Gateway 的配置不要背要理解。每一条 Filter 的本质就是“在请求进入这个路由之后、到达目标之前按顺序做一组动作”参数怎么填、顺序怎么排、什么时候用 YAML 什么时候用 Java DSL都基于这个理解去决策。先把 AddRequestHeader、RewritePath、StripPrefix 这几个高频过滤器玩明白再逐步啃 Retry、CircuitBreaker、RequestRateLimiter 这些稳定性组件网关这块的能力就能撑起绝大多数生产需求了。
返回列表