☰
微服务网关高可用实践:限流、熔断与灰度发布全解析
2026/10/23 3:30:50 网站建设 项目流程

从项目上线前的那个下午说起。当时某跨平台系统的网关还是单实例裸奔,上线大促前一星期压测,网关线程被打满,整条链路雪崩,用户端直接超时白屏。事后复盘,问题就出在网关层只做了最基础的路由转发,没有限流、没有熔断、没有超时控制,更别提灰度发布。后来我重写了整条网关链路,从调优到限流、熔断、灰度一步步落地,压测从勉强撑住变成了从容应对。这篇就把整套方案完整拆开,全是可直接抄走的配置和代码。

1. 高可用方案的整体设计思路

1.1 为什么网关高可用是整套系统的命门

网关在微服务架构里的位置很特殊。它是所有流量的入口,也是唯一一个不可替代的公共节点。服务挂了可以靠注册中心摘除,数据库挂了可以切主从,但网关一旦挂掉,后面所有业务接口全部瘫痪,连个报错页面都出不来。所以网关的高可用设计,本质上是在回答一个问题:当流量、依赖、代码质量都不确定的时候,怎么让入口保持稳定。

我见过不少团队在网关上的态度,能转发就行,性能参数全是默认,限流熔断一概不配。平时接口量小看不出问题,一旦遇到大促、活动、爬虫攻击,第一个死的就是网关。而且网关挂了之后排查还特别麻烦,因为它是异步非阻塞模型,线程池被打满的表现跟普通Web应用完全不同,新手盯着CPU和内存看半天也定位不到原因。

另外,网关层的高可用有个容易被忽视的价值:它是全链路里最好做治理的位置。在这里统一做限流、熔断、灰度、鉴权,每个服务都不用重复实现一套自己的防护逻辑。如果放弃网关这层,把限流熔断下放到各个业务服务,每个团队的实现水平参差不齐,出事的概率反而更高。

1.2 高可用的五大维度划分

在动手改之前,我把网关高可用拆成了五个维度,所有工作都围绕这五块展开:

维度解决的核心问题落地手段
性能调优单机吞吐跑不满、连接池耗尽线程模型、连接池、缓冲区、超时控制
全局限流瞬时流量洪峰打垮依赖服务令牌桶算法 + Redis 分布式计数
接口级熔断单个慢接口拖垮整个网关信号量隔离 + 熔断器模式
灰度发布新版本上线风险不可控权重路由 + Header 匹配 + 实例级灰度
安全防护恶意请求、跨域滥用、爬虫CORS 收敛 + 私有 Header 鉴权

这个划分顺序其实就是落地顺序。先让网关跑得动、跑得稳,再给它加保护能力,最后再考虑上线策略。跳过调优直接做限流,会出现一种尴尬情况:限流阈值设得很保守,因为单机性能根本撑不住,结果正常业务流量都被误限了。

1.3 技术选型说明

整套方案基于 Spring Cloud Gateway 2020.0.x 版本,搭配 Spring Boot 2.4.x 和 Resilience4j。为什么用这个组合而不是 Sentinel?原因很简单:团队当时已经深度依赖 Spring Cloud 体系,和 Nacos、OpenFeign 的兼容性需要最小化改造成本。Resilience4j 是 Spring Cloud Gateway 官方推荐的熔断实现,配起来不费劲。如果你用的是 Sentinel,思路完全相同,只是 API 不同,不影响整体方案理解。

依赖版本上有个细节需要注意,Spring Cloud Alibaba 和 Spring Cloud 官方的组件版本一定要对应好,否则会出现 RouteDefinition 加载失败或者过滤器不生效这类诡异问题。具体的依赖我用的是 Spring Cloud 2020.0.4 + Spring Boot 2.4.13 + Resilience4j 1.7.1,这套组合实测下来兼容性最稳。

2. 网关高性能调优的六个关键参数

2.1 线程池与 Netty 线程模型调优

Spring Cloud Gateway 底层是 WebFlux + Netty,天生就是异步非阻塞模型。很多刚上手的人以为异步模型不用调线程池,这是误区。Netty 的 worker 线程池是处理所有请求的公共线程池,默认线程数是 CPU 核数的两倍。对网关这种纯 IO 密集型的应用,这个值偏小,尤其是容器环境里限制了 CPU 配额的时候,worker 线程会成为最先被打满的资源。

我的调整方式是通过spring.application.reactor.netty.worker-thread-count或者 JVM 参数-Dreactor.netty.ioWorkerCount来控制。实际项目里 8 核机器我直接设置成 32,16 核机器设成 64。这个值的参考公式是 CPU 核数乘以 4,但上限不建议超过 64,因为网关的每个请求最终要转发给下游服务,下游的线程模型能承受的并发是有天花板的,worker 线程设得再大,瓶颈只会往后端转移。

另一个容易漏掉的是reactor.netty.pool.max-connections,这个是 WebClient 转发时到下游服务的连接池上限。默认值是 CPU 核数乘以 2,这个数太小了。我遇到过一种现象:网关 CPU 只有 20%,但响应时间持续飙高,排查半天才发现是到下游某服务的连接池被占满,新的请求都在排队等连接。这个参数我根据下游服务数量做了调整,单个下游实例的连接数上限放在 500 到 1000 之间,同时把acquire-timeout设成了 5 秒,避免无限等待。

2.2 路由配置里的超时与重试陷阱

路由超时是另一个高频事故点。Gateway 的全局超时默认是 long max,等价于没有超时。也就是说下游服务如果一直不返回,网关会一直等,worker 线程一直被占着,最终整个网关被拖死。所以超时设置不是可选项,是必选项。

每一组路由都应该单独配置超时,因为不同服务的响应时间差异很大。核心交易链路我设的是 5 秒,报表查询类接口是 15 秒,文件上传类接口是 30 秒。配置路径是spring.cloud.gateway.routes[].metadata.response-timeout和connect-timeout,单位是毫秒。

重试这块我踩过很深的坑。Gateway 内置的RetryGatewayFilter默认会对 GET 请求做重试,但在网关层重试是把双刃剑。如果下游接口不存在幂等性,POST 请求被自动重试就会产生重复下单、重复扣款这类严重问题。所以我的建议是:网关层不要配置自动重试,重试逻辑下沉到业务服务里,由业务自己控制幂等和重试策略。如果一定要在网关配重试,必须把retry的方法列表明确限制为 GET,并且设置retries=1,绝对不要用默认值。

2.3 缓冲区与转发性能细节

网关的请求体缓冲默认大小是 256 KB,文件上传场景必然超出。spring.codec.max-in-memory-size这个参数我设成了 10 MB,配合上传接口的独立路由使用。但要注意,调这个参数要谨慎,每个请求都会占用对应大小的内存缓冲,网关是异步模型,并发量大的时候内存会涨得很凶。如果不是必须用ReadBody或者ModifyRequestBody这类会读取完整请求体的过滤器,建议保持小缓冲区,走流式转发。

另外一个小细节是 GZIP 压缩。在网关层开CompressResponse过滤器能把响应体压缩到原来的 30% 左右,对带宽占用和传输时间优化非常明显。但要注意对图片、视频这类已经压缩过的资源不要再压,白耗 CPU。我的做法是用content-type正则只匹配常见的文本类 MIME。

2.4 性能验证:用压测数据说话

调优不能拍脑袋,最终要用数据说话。我用压测工具先跑了调整前的基线:8 核 16 G 的机器,网关单实例极限大概在 1800 QPS 左右,响应时间 P99 接近 800 毫秒。调整完上述参数之后,同样配置下单实例能到 3200 QPS,P99 稳定在 200 毫秒以内。这个提升主要来自三块:worker 线程数翻倍、下游连接池扩容、超时兜底减少线程长时间占用。

压测的时候有个细节分享下:不要用单机压网关,一定要从多台施压机同时压,否则施压机本身的性能瓶颈会被误判成网关瓶颈。另外压测数据要看 P99 而不是平均值,平均值在偶发尖峰下完全没有参考价值。

把网关的连接池和超时调好比加任何框架都管用,这两个是基础中的基础。我之前接手一个系统,啥都没动,只加了超时和调大连接池,整体可用性就提升了一个量级。

3. 全局限流方案的 Redis + 令牌桶落地

3.1 RequestRateLimiter 的工作原理

Spring Cloud Gateway 内置的限流过滤器是RequestRateLimiterGatewayFilterFactory,底层用的是令牌桶算法,计数数据放在 Redis 里。令牌桶的核心思想是:系统以固定速率往桶里放令牌,请求要执行必须先从桶里取一个令牌,桶空了就拒绝请求。这样既允许一定程度的突发流量,又能保证长期平均速率不超限。

这里有两个关键参数,redis-rate-limiter.replenishRate是每秒向桶里补充的令牌数,也就是平均速率上限;redis-rate-limiter.burstCapacity是桶的容量,也就是允许的突发流量上限。还有一个requestedTokens参数表示每次请求消耗几个令牌,默认是 1。

注意一个常见误区:burstCapacity不能设为 0。如果设为 0,所有请求都会被拒绝,看起来像是限流配置成功了,实际上是把流量全掐断了。这个参数至少要大于等于replenishRate。

3.2 KeyResolver 的完整实现与配置

限流必须基于键来区分用户或者来源。我用的是KeyResolver,分别实现了按用户 ID 限流和按 IP 限流两种策略。用户 ID 从 Header 里取,取不到就回退到 IP。下面是完整的代码:

@Configuration public class RateLimiterConfig { @Bean public KeyResolver userKeyResolver() { return exchange -> { ServerHttpRequest request = exchange.getRequest(); String userId = request.getHeaders().getFirst("X-User-Id"); if (userId == null || userId.isEmpty()) { String ip = request.getRemoteAddress(); if (ip == null) { return Mono.just("unknown"); } return Mono.just(ip.getAddress().getHostAddress()); } return Mono.just(userId); }; } @Bean public KeyResolver ipKeyResolver() { return exchange -> { String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); return Mono.just(ip); }; } }

要注意KeyResolver返回的字符串就是 Redis 里 key 的一部分,如果返回的值维度太粗,比如所有人都是同一个固定的 user id,那限流就变成了全局限流,所有用户共享同一个令牌桶,会误伤大面积用户。所以在用的时候,一定要确认从 Header 取到的值有没有真实的区分度。

3.3 限流参数的计算与配置

限流阈值怎么定?我的方法是先压测出网关单实例的性能上限,取一个安全比例,再乘上实例数,推算出集群总阈值。比如压测得单实例能扛 2000 QPS,取 80% 的安全系数,就是 1600 QPS 作为单实例的限流阈值,集群 5 个实例的总限流阈值就是 8000 QPS。

配置长这样:

spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/order/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1600 redis-rate-limiter.burstCapacity: 2400 key-resolver: "#{@ipKeyResolver}"

burstCapacity我通常设成replenishRate的 1.5 倍。这样既能平滑日常流量波动,又能给瞬时峰值留缓冲。如果设得太大,突发流量放进来之后下游服务可能扛不住;设得太小,日常的流量毛刺就会被频繁 429。

限流被触发时默认返回 429 Too Many Requests,但返回体是空的。我自定义了一个全局异常处理器,把 429 的响应体统一写成标准格式的 JSON,方便前端识别处理。

@Component public class RateLimitExceptionHandler implements ErrorWebExceptionHandler { @Override public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { ... ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); ... // 返回 {"code": 429, "message": "请求过于频繁,请稍后再试"} } }

3.4 Redis 的高可用与 Lua 脚本原子性

限流组件依赖 Redis,Redis 的高可用直接决定限流本身是否可靠。RequestRateLimiter 的取令牌逻辑是用 Lua 脚本实现的,Redis 执行 Lua 脚本是原子操作,不会出现并发取令牌时计数错乱的问题。但要注意,如果 Redis 是单节点,这个节点挂了,整个限流能力就没了,网关会退化成无保护状态。

所以 Redis 至少要做主从加哨兵,或者直接用云上的 Redis 集群。限流数据的 key 可以设置过期时间,避免 Redis 内存被无效的 key 占满。默认的 key 结构里包含了 route id 和 key resolver 返回的值,长时间运行后旧的 user id 会积累,我给 Redis 设置了合理的淘汰策略和过期时间。

限流的本质是保护下游,不是限制用户。阈值设得太紧,用户刷个页面都被 429,这个平衡需要反复压测调整。我的经验是先设一个偏保守的值上线,观察监控一周,再逐步放宽。

4. 接口级熔断方案的 Resilience4j 集成实战

4.1 全局限流挡不住接口级雪崩

在网关层只做全局限流而不做接口级熔断,会漏掉一种典型故障:某个下游接口突然变慢,响应时间从 200 毫秒涨到 10 秒,虽然单接口的 QPS 不高,没触发全局限流,但它会占用大量网关连接和 worker 线程,导致其他正常接口也一起被拖垮。熔断要解决的就是这种局部故障被放大的问题。

我之前就遇到过这种事故:系统里一个报表查询接口的数据库索引失效,平均响应时间从 500 毫秒暴增到 20 秒,网关还没限流,因为 QPS 本身只有几十,但所有打到这个接口的请求都卡在等待响应,把到下游服务的连接池给占满了,整个系统的接口都开始陆陆续续超时。

4.2 Resilience4j 的参数配置与原理说明

Spring Cloud Gateway 2020.0.x 官方版本直接支持 Resilience4j 的 CircuitBreaker 和 TimeLimiter 整合。我用的配置如下:

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

核心的熔断参数在 Resilience4j 配置里:

resilience4j: circuitbreaker: instances: reportServiceCB: slidingWindowType: TIME_BASED slidingWindowSize: 30 minimumNumberOfCalls: 5 failureRateThreshold: 50 slowCallRateThreshold: 60 slowCallDurationThreshold: 3s waitDurationInOpenState: 20s permittedNumberOfCallsInHalfOpenState: 3 common: registerHealthIndicator: true allowHealthIndicatorToFail: false timelimiter: instances: reportServiceCB: timeoutDuration: 5s cancelRunningFuture: true bulkhead: instances: reportServiceCB: maxConcurrentCalls: 20 maxWaitDuration: 1s

每个参数的作用:

  • slidingWindowType滑动窗口类型。我用的 TIME_BASED,按最近 30 秒统计,而非固定次数。这种类型更贴近真实业务流量波动。
  • failureRateThreshold当 30 秒窗口内失败率超过 50% 时,熔断器打开。
  • slowCallDurationThreshold当调用时长超过 3 秒被记为慢调用,慢调用比例超过 60% 时也触发熔断。
  • waitDurationInOpenState熔断器打开后持续 20 秒,之后进入半开状态。
  • permittedNumberOfCallsInHalfOpenState半开状态下允许放 3 个试探请求,成功则关闭熔断器,失败则重新打开。
  • minimumNumberOfCalls窗口内最少 5 次调用才开始统计,因为样本太少时偶然失败不具备统计意义。
  • maxConcurrentCalls信号量隔离的最大并发数。默认值只有 10,对网关这个并发量级远远不够,我设成了 20,根据下游服务的实际处理能力再加大。
  • timeoutDuration单个请求的最大执行时间,超过就取消,和路由超时配合形成双重保护。

这组参数背后的逻辑是:熔断不是故障发生一瞬间就切断,而是用滑动窗口把一段时间内的健康度量化出来,用统计数据做决策。50% 的失败率阈值意味着如果只有一个实例出了问题,流量被负载均衡分散后,单实例的失败率可能达不到 50%,熔断不会被误触发;如果整个服务都出了问题,失败率轻松越过阈值,保护立刻生效。

4.3 熔断触发后的降级响应实现

熔断触发后流量会走fallbackUri,我实现了一个请求转发到本地的 fallback 接口:

@RestController public class FallbackController { @GetMapping("/fallback/report") public Mono<ServerResponse> reportFallback(ServerWebExchange exchange) { return ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) .bodyValue("{\"code\":503,\"message\":\"报表服务暂不可用,请稍后重试\"}"); } }

fallback 响应不能太随意,需要包含两个信息:错误码和用户能看到的重试提示。如果是 B 端内部系统,会在 message 里附带一个 traceId 便于排查。这些细节到了线上会显著减少咨询量,因为用户至少知道发生了什么,而不是看到一片白屏。

4.4 熔断状态的可观测性

熔断器状态要能实时看到。我开了 Resilience4j 的 Actuator 端点,用/actuator/metrics和自定义的 health indicator 把熔断器状态暴露出来:

management: endpoints: web: exposure: include: health, metrics, gateway

通过resilience4j.circuitbreaker.state指标可以实时看每个熔断器的状态变化。我平时盯的是OPEN和HALF_OPEN的状态转换频率,如果某个熔断器频繁开关,说明下游服务在反复崩溃和恢复,这种时候我会直接看这个服务的日志和数据库慢查询,而不是继续放流量进去试错。

熔断参数不是一次配好就完事。第一次上线时我把 failureRateThreshold 设为 50%,结果某天下游做了一次超过 10 秒的 Full GC,熔断器在 30 秒窗口内打开,很完美地保护了网关。但后来又发现,把窗口时间拉长到 60 秒,能减少误判,因为偶发的 10 秒 GC 在 60 秒窗口里占比会被稀释,不会导致熔断。

5. 灰度发布方案的 Spring Cloud LoadBalancer 实现

5.1 灰度方式选型:为什么用 LoadBalancer 而不是 Weight Route

灰度发布在网关层有几种做法。最简单的可以用 Gateway 自带的Weight路由谓词,按百分比在两组路由之间分流。这种方式的优点是零代码、配置即用,缺点是只能做到路由级别的按权重分发,不能按用户、按请求头做精确匹配,灰度粒度太粗。

我更推荐的做法是在 LoadBalancer 层做灰度。Spring Cloud LoadBalancer 支持自定义ServiceInstanceListSupplier,可以拿到当前请求的上下文信息,从服务列表里筛选出符合条件的实例。这样灰度策略就变成了:请求头里带灰度标记,就把流量打到灰度实例上;没带标记,就走稳定实例。

这个方式有一个历史原因:我最早是用 Nacos 的 metadata 来实现灰度的,在实例注册时打上 version 标签,再在 LoadBalancer 里按版本筛选。后来发现这个思路完全通用,不管注册中心是 Nacos、Consul 还是 Eureka,都能用同一套代码实现,所以就把它沉淀成了一套公共组件。

5.2 灰度 ID 的传递与实例筛选实现

灰度 ID 从请求头X-Gray-Tag进入网关,但我不能直接把它往下游透传,因为网关层在下游服务调用的时候,转发请求的 Header 默认不会带自定义头。所以做了两层处理:第一层在 Gateway Filter 里截获这个 Header,存到ServerWebExchange的 attribute 里;第二层在 LoadBalancer 的自定义 Supplier 里读这个 attribute,用它来筛选实例。

核心代码如下:

@Component public class GrayLoadBalancer implements ServiceInstanceListSupplier { private final String serviceId; private final ObjectProvider<ServiceInstanceListSupplier> delegate; public GrayLoadBalancer(String serviceId, ObjectProvider<ServiceInstanceListSupplier> delegate) { this.serviceId = serviceId; this.delegate = delegate; } @Override public Flux<ServiceInstance> get() { return Flux.deferContextual(contextView -> { String grayTag = contextView.getOrDefault("grayTag", ""); ServiceInstanceListSupplier supplier = delegate.getIfAvailable(); if (supplier == null) { return Flux.empty(); } return supplier.get().map(instances -> filterByGrayTag(instances, grayTag)); }); } private List<ServiceInstance> filterByGrayTag( List<ServiceInstance> instances, String grayTag) { if (grayTag == null || grayTag.isEmpty()) { return instances.stream() .filter(instance -> { Map<String, String> metadata = instance.getMetadata(); return metadata.getOrDefault("gray", "false").equals("false"); }) .collect(Collectors.toList()); } return instances.stream() .filter(instance -> grayTag.equals( instance.getMetadata().get("grayTag"))) .collect(Collectors.toList()); } }

这段代码要配合一个核心配置才能生效。Spring Cloud Gateway 在构建 LoadBalancer 时默认用的是DelegatingServiceInstanceListSupplier,要对它进行替换,用@LoadBalancerClient指定自定义配置:

@Configuration @LoadBalancerClient(name = "report-service", configuration = GrayLoadBalancerConfig.class) public class GrayLoadBalancerConfig { @Bean public ServiceInstanceListSupplier graySupplier( ObjectProvider<ServiceInstanceListSupplier> delegate, ObjectProvider<DiscoveryClient> discoveryClient) { ServiceInstanceListSupplier base = ServiceInstanceListSupplier.builder() .withBlockingDiscoveryClient() .withCaching() .build(delegate); return new GrayLoadBalancer("report-service", base); } }

实例侧需要在注册中心注册 metadata。Nacos 注册实例时配置:

spring: cloud: nacos: discovery: metadata: gray: "true" grayTag: "v2.0.0"

稳定版本不用配 metadata,或者显式配置gray: "false"。这样新版本的实例先只对带了灰度标记的请求可见,没问题后把 metadata 里的gray改成true,灰度流量比例就自动扩大。

5.3 灰度流量从 1% 到 100% 的平滑演进

灰度发布最怕的不是切不过去,而是切不回来。我会把灰度过程拆成四个阶段,每个阶段都有关卡和观察指标:

  • 第一阶段,灰度实例只对内部测试用户和带特定 Header 的请求开放,比例大概在 1% 以内,主要验证基础功能和接口兼容性。
  • 第二阶段,放量到 5% 到 10%,观察 P99 响应时间、错误率、GC 情况。这个阶段重点看的是灰度实例和稳定实例的指标对比。
  • 第三阶段,扩到 50%,做一次全链路的压力测试。此时如果灰度实例的性能劣化或者报错率升高,能直观地在监控上看到。
  • 第四阶段,100% 全量,此时灰度实例的graymetadata 已经设为true,稳定实例逐个摘除,完成版本切换。

实际执行的时候还要准备回滚预案。一旦第二阶段或第三阶段发现异常,立刻把gray全部改回false,并且让灰度实例下线,LoadBalancer 会自动把流量全部切回稳定实例。整个过程不需要重启网关,因为 LoadBalancer 的 metadata 是实时从注册中心拉取的。

灰度发布还有一个容易忽略的地方:灰度期间新旧两个版本的数据库结构可能不同。如果新版本有表结构变更,要考虑兼容旧版本的读写,否则灰度流量会污染数据库,回滚的时候数据已经回不去了。这块属于业务层面的事,但网关的灰度方案设计时必须把它作为前提条件写清楚。

灰度发布不是只给技术团队看的,要拉上业务方一起定灰度指标。比如订单量、支付成功率、用户投诉率这些业务指标,比技术指标更早暴露问题。我有一次灰度了 5% 的流量,技术指标全绿,但业务方发现灰度用户的支付成功率掉了 0.2 个百分比,立刻回滚,避免了一次线上事故。

6. 综合配置示例与常见问题排查实录

6.1 全套配置组合示例

把上面所有的配置整合成一份完整的application.yml,这份配置直接放到 Spring Boot 项目里就能跑,核心依赖只需要spring-cloud-starter-gateway、spring-boot-starter-data-redis-reactive、spring-cloud-starter-loadbalancer、spring-cloud-starter-circuitbreaker-reactor-resilience4j:

server: port: 8080 spring: application: name: api-gateway cloud: nacos: discovery: server-addr: ${NACOS_ADDR:127.0.0.1:8848} metadata: gray: "false" grayTag: "v1.0.0" gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/order/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1600 redis-rate-limiter.burstCapacity: 2400 key-resolver: "#{@ipKeyResolver}" - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/order metadata: response-timeout: 5000 connect-timeout: 3000 - id: report-service uri: lb://report-service predicates: - Path=/api/report/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 400 redis-rate-limiter.burstCapacity: 600 key-resolver: "#{@userKeyResolver}" - name: CircuitBreaker args: name: reportServiceCB fallbackUri: forward:/fallback/report metadata: response-timeout: 10000 connect-timeout: 5000 globalcors: cors-configurations: '[/**]': allowedOrigins: "https://*.example.com" allowedMethods: - GET - POST - PUT - DELETE allowedHeaders: - X-User-Id - X-Gray-Tag - Content-Type resilience4j: circuitbreaker: instances: orderServiceCB: slidingWindowType: TIME_BASED slidingWindowSize: 30 minimumNumberOfCalls: 5 failureRateThreshold: 50 slowCallRateThreshold: 60 slowCallDurationThreshold: 3s waitDurationInOpenState: 20s permittedNumberOfCallsInHalfOpenState: 3 reportServiceCB: slidingWindowType: TIME_BASED slidingWindowSize: 30 minimumNumberOfCalls: 5 failureRateThreshold: 50 slowCallRateThreshold: 60 slowCallDurationThreshold: 5s waitDurationInOpenState: 20s permittedNumberOfCallsInHalfOpenState: 3 management: endpoints: web: exposure: include: health, metrics, gateway, prometheus

网关的 CORS 配置要格外注意,allowedOrigins不要用*,否则任何站点都能跨域调用你的接口,爬虫和恶意脚本会更容易拿到数据。生产环境我用的是域名白名单,并且只放行必要的 Header。

6.2 常见问题排查实录

这一节我直接列一张排查速查表,全是实战中真实遇到过的问题和解决路径:

现象可能原因排查思路与解决方案
网关启动后所有路由返回 404路由断言 Path 配置多级前缀转发了两次检查StripPrefix过滤器。/api/order/**转发到lb://order-service后,如果下游接口是/order/info,需要配StripPrefix=1,否则就会 404
所有接口返回 503,含熔断 fallback 响应熔断器打开,但waitDurationInOpenState太短或太频繁通过/actuator/health查看熔断器状态。如果频繁 OPEN 且恢复又很快,延长waitDurationInOpenState到 30 到 60 秒,并排查下游服务稳定性
GET 请求正常,POST 请求大量超时下游连接池被响应慢的接口占用,POST 请求在连接队列等待调大max-connections和acquire-timeout,同时检查超时配置,隔离慢接口到独立路由
限流不生效,所有请求照常通过KeyResolver 没有被 Spring 管理,或返回的 key 是固定的检查key-resolver的 SpEL 表达式是否有效。ipKeyResolver必须注册为 Bean,且方法返回的 key 要能区分不同来源
限流生效但第一次请求总是失败连接 Redis 超时,等待过久检查 Redis 连接池配置,增加connect-timeout和so-timeout。限流过滤器对 Redis 故障应该有降级逻辑
灰度 Header 带了但流量还是打到老版本LoadBalancer 自定义 Supplier 没生效确认@LoadBalancerClient注解的 serviceId 和路由配置里的 serviceId 完全一致。大小写、连字符都不能错
动了配置之后路由不刷新网关启用了缓存,配置更新有延迟开启spring.cloud.gateway.enabled=true后配置默认会实时拉取,但如果用了静态路由定义,需要手动调用/actuator/refresh或重启实例

6.3 线上排障必备工具组合

网关的排障需要一套组合拳。除了常规的日志和监控面板,我日常用得最多的三个命令级工具是 Redis 的 MONITOR、Actuator 的/actuator/gateway/routes端点和 JFR。

定位限流问题,用 Redis MONITOR 看限流脚本的执行频率和 key 的分布。/actuator/gateway/routes能看到路由的完整匹配顺序和过滤器链,排查路由不生效比看日志直观得多。JFR 在高峰期的 CPU 热点分析上帮过大忙,它能直接看到哪些类的方法在消耗 CPU,发现了很多隐藏的序列化瓶颈和日志打印开销。

另外,强烈建议给网关单独拉一套 Grafana 面板,把这几个指标放上去:每秒请求数、P99 响应时间、熔断器状态变化计数、限流拒绝计数、下游连接池活跃连接数。这套面板搭好之后,每次发布新版本或者调配置,先看面板再下结论,能省掉大量猜测时间。

我个人的习惯是每次改动只改一个参数,然后观察 10 分钟。把三个参数一起改,出了问题根本不知道是哪个引起的,回滚也不知道回滚到哪一步。灰度配置也是同理,一步一步放量,不要一次跨两个阶段。

整套方案落地之后,最直观的变化是压测数据:单实例吞吐从 1800 QPS 提升到了 3200 QPS,限流、熔断、灰度全都在网关层完成,后面的业务服务不再需要自己实现一套重复的防护逻辑。实际运行中遇到过一次下游依赖服务宕机,熔断器在 30 秒内打开,网关整体零异常,接口全部快速返回降级提示,用户侧完全无感知这是一次服务故障。这种稳定状态,正是从一开始就把网关当高可用核心来打磨换来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询