ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Cloud Gateway企业级落地:路由、过滤器与限流实战

Spring Cloud Gateway企业级落地:路由、过滤器与限流实战 先讲一段我上一家公司的真实经历。微服务拆分做了一年多服务数量从个位数涨到三十多个前端联调时维护一长串 URL 列表后端每个服务各自做鉴权、限流、日志线上出问题都不知道该找谁。后来把 Spring Cloud Gateway 立成统一流量入口半年时间把所有横切逻辑全部收口到网关层这套体系才真正像个“中枢”的样子。但过程远没有文档里写的那么顺利——路由配置膨胀、请求体二次读取失效、过滤器顺序错乱、限流组件和网关版本不兼容……每个坑都踩了个遍。所以这篇东西不是官方文档的复述而是把我在企业级落地 Spring Cloud Gateway 的经验拆开揉碎了讲。核心围绕三件事网关到底该承担哪些职责深度定制有哪些标准化做法性能与稳定性在实践中如何权衡。适合正在用网关但觉得“只是配了个路由”的团队也适合准备把网关从玩具级升级到企业级的开发者。1. 为什么说网关是流量中枢而不是请求转发器很多团队对网关的理解停留在“反向代理 路由转发”这个层级——配几个 Path 断言把请求转到下游服务完事。这不是网关这是带壳的 Nginx。真正的企业级网关至少要承接下面这些能力。1.1 网关在微服务架构里的真实定位网关是所有外部请求进入系统的唯一入口是南北向流量的汇聚点。这个位置决定了它天然适合做几类事情统一鉴权与认证所有请求先过网关身份校验、Token 解析、权限判断不用在每个服务里重复实现。流量治理限流、熔断、降级、灰度路由在入口层做控制成本最低、效果最明显。协议转换与适配外部客户端可能用 HTTP、WebSocket下游服务可能暴露不同协议网关负责转换。可观测性埋点请求进入和离开网关时记录指标全局的流量视图就有了。安全防护防 SQL 注入、防 XSS、IP 黑名单、Bot 识别都能在网关层做前置拦截。把网关定位成“流量中枢”而不是“流量转发器”差别就在于前者的逻辑是主动治理流量后者只是被动搬运流量。我见过很多团队下游服务里塞满了各种 Filter 做鉴权和限流本质上是网关缺位导致的能力上移。1.2 选 Spring Cloud Gateway 而不是 Zuul、Nginx 的理由技术选型其实是个多维比较的过程我直接给结论。对比维度Spring Cloud GatewayZuul 1.xNginx底层模型Spring WebFlux Reactor Netty非阻塞Servlet 3.0 同步阻塞事件驱动C 语言实现扩展方式Java 代码级扩展谓词/过滤器模型Java FilterLua 脚本OpenResty动态配置支持配合配置中心可动态刷新支持度一般需 reload有连接闪断生态整合与 Nacos、Sentinel、SkyWalking 等无缝集成需要适配层需要额外开发学习曲线中等要求理解响应式编程较低需要懂 Lua 才能深度定制Nginx 在纯转发性能和静态负载均衡上确实很强但在做精细化流量治理时Lua 脚本的开发效率和 Java 生态相比差太多了。Zuul 1.x 是同步阻塞模型线程池容易被慢下游拖垮已经基本被边缘化。Spring Cloud Gateway 最大的好处是它是 Spring Cloud 生态的原生组件配置模型、扩展机制、配置刷新机制都和其他组件一致。团队只要会写 Java就能在网关里快速定制各种逻辑而不需要额外引入一门脚本语言。1.3 企业级网关定制的能力清单我梳理一个企业级网关应该具备的能力清单这篇文章后续的内容也基本围绕它展开路由层动态路由、灰度发布、多环境隔离、自定义断言。过滤器层统一鉴权、参数校验、请求响应转换、审计日志。流量防护层接口级限流、集群维度限流、熔断降级、热点防护。可观测层全链路 TraceId 透传、访问日志、指标监控。稳定性层超时控制、重试机制、优雅下线、压测调优。这个清单可以作为你团队网关建设的一个 CheckList。如果你们当前的网关只覆盖了其中两三项说明还有很大的优化空间。2. 路由层的深度定制从静态配置到动态路由路由是网关最基础也是最核心的能力。把路由玩明白网关就成功了一半。这一节我从基础配置讲起重点落在自定义谓词和动态路由上这两块是“深度定制”的起点。2.1 路由配置的底层逻辑一个路由由三部分组成Route ID路由唯一标识 Predicate匹配条件 Filter转发后的处理逻辑。这里的 Predicate 就是“断言”判断一个请求是否满足转发条件。看一下基础配置spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix1这个配置的意思是当请求路径匹配/api/order/**时转发到order-service这个服务lb://表示走负载均衡StripPrefix1表示把第一级路径前缀api剥掉再转发。但企业级场景远远不止这种固定路径匹配。我实际落过地的场景包括线上流量需要按比例灰度、特定用户需要路由到新版本服务、外部调用方需要按 AppId 路由到不同集群、某些请求需要临时切流到 mock 服务。这些需求内置谓词根本覆盖不了。2.2 自定义谓词工厂灰度发布的关键实现灰度发布是最典型的“深度定制”需求。实现思路是通过 Header、Cookie 或参数携带灰度标识网关根据标识决定路由到老版本还是新版本服务。Spring Cloud Gateway 提供了谓词工厂的扩展点我们可以自己实现一个GrayVersionRoutePredicateFactory。Component public class GrayVersionRoutePredicateFactory extends AbstractRoutePredicateFactoryGrayVersionRoutePredicateFactory.Config { public GrayVersionRoutePredicateFactory() { super(Config.class); } Override public ShortcutType shortcutType() { return ShortcutType.GATHER_LIST; } Override public PredicateServerWebExchange apply(Config config) { return exchange - { String version exchange.getRequest().getHeaders().getFirst(X-version); if (StringUtils.hasText(version) config.getVersions().contains(version)) { return true; } // 没有指定版本的请求默认不匹配这个灰度路由 return false; }; } Data public static class Config { private ListString versions; } }配置文件中这样使用spring: cloud: gateway: routes: - id: order-gray uri: lb://order-service-gray predicates: - Path/api/order/** - GrayVersionv2,v3这个方案的业务含义很清晰请求头里带X-version: v2或X-version: v3的请求会转发到灰度服务其他请求走默认路由。灰度发布策略就落地了。2.3 基于 Nacos 的动态路由方案静态配置在发布后改路由必须重启网关这在企业级是不能接受的。路由应该支持动态修改、实时生效。我用 Nacos 做配置中心实现了一套方案核心思路是路由配置存储在 Nacos 中配置变化时通过事件刷新 Gateway 的路由缓存。具体实现Component public class NacosDynamicRouteService implements ApplicationEventPublisherAware { Autowired private RouteDefinitionWriter routeDefinitionWriter; private ApplicationEventPublisher publisher; private static final MapString, RouteDefinition ROUTE_CACHE new ConcurrentHashMap(); public void updateRoute(RouteDefinition definition) { try { // 删除旧路由并发布刷新事件 this.routeDefinitionWriter.delete(Mono.just(definition.getId())).subscribe(); this.ROUTE_CACHE.remove(definition.getId()); // 写入新路由 this.routeDefinitionWriter.save(Mono.just(definition)).subscribe(); this.ROUTE_CACHE.put(definition.getId(), definition); // 通知路由刷新 this.publisher.publishEvent(new RefreshRoutesEvent(this)); } catch (Exception e) { log.error(动态更新路由失败: {}, definition.getId(), e); } } Override public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher publisher; } }然后在 Nacos 配置监听器里解析 JSON 格式的路由配置并调用updateRoute方法。这套方案我在生产环境稳定运行了一年多路由变更秒级生效没有任何重启操作。注意RouteDefinitionWriter是 Spring Cloud Gateway 提供的接口RefreshRoutesEvent是触发路由重新计算的机制。理解这两个类的协作关系是动态路由的核心。3. 过滤器链的深度定制统一鉴权、日志审计与请求体缓存路由解决了“流量往哪走”过滤器解决了“流量怎么被处理”。这一节讲的三个过滤器是我认为企业级网关里最常用、也最容易被写坏的。3.1 过滤器执行模型GlobalFilter 和 GatewayFilter 的区别先理清概念。Spring Cloud Gateway 里有两类过滤器GatewayFilter绑定在单个路由上只对该路由生效。GlobalFilter全局生效对所有路由生效。过滤器之间有执行顺序通过实现Ordered接口或加Order注解控制。顺序在网关里极其重要全局过滤器先执行路由级过滤器后执行同级别按 order 值从小到大排列。我自己画过一张执行链路图从请求进入开始GlobalFilter 链鉴权、限流、日志、透传→ 路由匹配 → GatewayFilter 链StripPrefix、RewritePath 等→ 转发下游。任何一环抛异常都会中断链路并走错误处理器。3.2 自定义统一鉴权 GlobalFilter网关统一鉴权最容易踩的坑是直接读 request body 做验签把 body 消费掉了下游服务拿到空 body。这个坑后面专门讲。先看正确实现Component Order(-100) public class AuthGlobalFilter implements GlobalFilter { Autowired private AuthService authService; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String token request.getHeaders().getFirst(Authorization); if (!StringUtils.hasText(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 异步调用鉴权服务不阻塞 Netty 事件循环线程 return authService.validate(token) .flatMap(userInfo - { // 把用户信息透传到下游 ServerHttpRequest mutatedRequest request.mutate() .header(X-user-id, userInfo.getUserId()) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); }) .onErrorResume(e - { exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().setComplete(); }); } }两个关键点一是用响应式写法不要用Thread.sleep或阻塞式调用否则会把 Netty 事件循环线程搞挂二是通过ServerHttpRequest.mutate()把用户信息加到 Header 透传给下游这样下游服务就不用再解析 Token性能更好也更统一。3.3 请求体缓存解决 Body 二次读取问题网关层面做验签、参数校验经常需要读请求体内容。但ServerHttpRequest.getBody()返回的 Flux 只能订阅一次第一次读取后就没了下游服务就会收到空 body。这是所有网关开发者都会撞上的问题。解决方案是“请求体缓存”过滤器环节把原始 body 缓存到内存或本地文件然后构造一个可重复读取 body 的请求对象后续过滤器和下游服务都能正常读取。Component public class CacheBodyGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); if (request.getHeaders().getContentType() null || !request.getHeaders().getContentType().includes(MediaType.APPLICATION_JSON)) { return chain.filter(exchange); } return DataBufferUtils.join(request.getBody()) .map(dataBuffer - { byte[] bytes new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); DataBufferUtils.release(dataBuffer); return bytes; }) .flatMap(bytes - { // 缓存 body 到 exchange 属性中供后续过滤器读取 exchange.getAttributes().put(CACHED_REQUEST_BODY_KEY, bytes); ServerHttpRequest mutatedRequest new ServerHttpRequestDecorator(request) { Override public FluxDataBuffer getBody() { return Flux.defer(() - Flux.just(exchange.getResponse() .bufferFactory().wrap(bytes))); } }; return chain.filter(exchange.mutate().request(mutatedRequest).build()); }); } Override public int getOrder() { // 保证在鉴权过滤器之前执行 return -200; } }这个过滤器通过ServerHttpRequestDecorator包装原始请求重写getBody()方法每次都返回缓存的字节数组实现 body 无限次读取。缓存的数据放在exchange.getAttributes()里后续过滤器通过 key 读取即可。注意DataBufferUtils.release(dataBuffer)必须调用否则会造成直接内存泄漏。这个泄漏肉眼看不见但压测时 GC 和堆外内存会飙升。3.4 请求响应审计日志过滤器企业级系统通常有审计需求谁在什么时间、调用了什么接口、传了什么参数、返回了什么结果。在网关层统一做审计日志是最合理的位置。我的实践是请求进来时记录开始时间和请求摘要响应返回时计算耗时并组装完整日志。为了避免大 body 把日志撑爆内容超过 1KB 就截断只记录前 1KB 加长度信息。Component Order(10) public class AuditLogGlobalFilter implements GlobalFilter { private static final int MAX_BODY_LOG_LENGTH 1024; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { long startTime System.currentTimeMillis(); return chain.filter(exchange).then(Mono.fromRunnable(() - { long cost System.currentTimeMillis() - startTime; String path exchange.getRequest().getURI().getPath(); String method exchange.getRequest().getMethod().name(); HttpStatusCode status exchange.getResponse().getStatusCode(); String traceId exchange.getRequest().getHeaders().getFirst(X-trace-id); log.info(audit | traceId{} | method{} | path{} | status{} | cost{}ms, traceId, method, path, status, cost); })); } }日志格式固定成结构化文本后面接 ELK 或 Loki 做检索就很方便。一套完整的审计日志一定是面向检索设计的字段越多 Future You 查问题越省事。4. 流量防护落地Spring Cloud Gateway 与 Sentinel 的整合实践网关作为流量中枢天然承受着所有外部流量。如果没有流量防护一个突刺的流量洪峰就可能把下游服务打垮。这一节讲我如何把 Sentinel 整合进网关实现限流、熔断与降级。4.1 网关层限流与业务层限流的边界限流可以在业务服务里做也可以在网关层做很多人不清楚两者的差异。限流层级优点缺点业务服务规则细腻可按用户维度精确限流每个服务都要接入重复建设网关层统一收口单点管控规则粒度偏粗需配合转发头透传我的实践建议是粗粒度限流放在网关层按接口、按来源 App细粒度限流放在业务层按用户、按订单维度。网关层先把洪峰削掉一部分业务层再处理更精细的策略。两层配合是性价比最高的方式。4.2 Sentinel 网关适配器的接入过程Sentinel 官方提供了sentinel-spring-cloud-gateway-adapter专门用于 Spring Cloud Gateway 的流量治理。接入过程其实很直接。Maven 依赖dependency groupIdcom.alibaba.csp/groupId artifactIdsentinel-spring-cloud-gateway-adapter/artifactId version1.8.7/version /dependency启动时注册 Sentinel 的过滤器并配置规则Configuration public class SentinelGatewayConfig { PostConstruct public void init() { // 注册 Sentinel 网关过滤器 GatewayCallbackManager.setBlockHandler((exchange, t) - { exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS); return exchange.getResponse().writeWith( Mono.just(exchange.getResponse().bufferFactory() .wrap({\code\:429,\msg\:\请求过于频繁\}.getBytes()))); }); // 加载网关限流规则 initGatewayRules(); } private void initGatewayRules() { SetGatewayFlowRule rules new HashSet(); // 按路由 ID 限流order-service 路由 QPS 不超过 1000 GatewayFlowRule rule1 new GatewayFlowRule(order-service) .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID) .setCount(1000) .setIntervalSec(1); // 按 API 分组限流/api/pay/** 的 QPS 不超过 500 GatewayFlowRule rule2 new GatewayFlowRule(pay_api) .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME) .setCount(500) .setIntervalSec(1); rules.add(rule1); rules.add(rule2); GatewayRuleManager.loadRules(rules); } }接入之后网关会拦截每一个请求根据路由 ID 或自定义 API 分组进行计数。超过阈值就触发BlockHandler中定义的逻辑直接返回 429 响应请求根本不会到达下游服务。4.3 网关限流的参数调优经验限流参数设多少没有标准答案要根据业务场景和压测数据来定。我这里分享几个实践原则初始阈值参考压测数据先对下游单服务压测拿到最大承受 QPS网关阈值设置为下游最大 QPS 的 80% 左右留 20% 缓冲。区分核心链路和非核心链路支付、下单等核心接口要设置独立且较严格的规则查询类非核心接口可以设置宽松阈值或直接不限。流量突发场景要配置排队等待Sentinel 支持匀速排队模式setControlBehavior设置为匀速排队适合秒杀等有突发流量但下游能慢慢消化的场景。限流触发后的降级响应要友好客户端收到 429 后应该能看懂并做重试或提示而不是一个空白的错误页面。我这边的实践是核心接口限流阈值设为压测最大 QPS 的 60%非核心接口设为 80%。给核心链路留更多冗余避免限流本身成为故障源。4.4 熔断降级规则在网关层的设计除了限流Sentinel 还支持熔断降级。这里说的熔断是当某个下游服务持续异常超时率或异常比例超过阈值网关直接切断对这个服务的调用快速失败返回降级结果而不是把请求继续打到已经故障的服务上。网关层配置熔断规则private void initDegradeRules() { SetDegradeRule rules new HashSet(); DegradeRule rule new DegradeRule(order-service) .setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) .setCount(0.5) // 异常比例超过 50% .setTimeWindow(10) // 熔断 10 秒 .setMinRequestAmount(100); // 触发熔断的最小请求数 rules.add(rule); DegradeRuleManager.loadRules(rules); }这段配置的含义是order-service 路由在 10 秒内请求量超过 100 个且异常比例达到 50%就熔断 10 秒。熔断期间请求直接走降级逻辑不给下游服务继续增加压力。熔断不是一个“锦上添花”的能力而是“保命”的能力。下游服务挂掉时如果没有熔断网关会变成请求的“放大器”把故障传递给所有调用方。这一点在核心链路上尤其重要。5. 性能与稳定性网关的线程模型、超时控制与优雅下线网关是流量的必经之路它的性能和稳定性直接决定了整个系统的可用性。这一节是我在压测和线上故障中总结出来的干货每条都是真金白银换来的经验。5.1 Spring Cloud Gateway 的线程模型为什么不能有阻塞操作Spring Cloud Gateway 底层是 Reactor Netty采用事件循环模型。核心特点是线程数量少每个线程处理大量并发请求线程绝对不能阻塞。这个模型下一个常见的性能杀手是在过滤器里调用了同步的RestTemplate或Thread.sleep()把事件循环线程卡住了。一旦卡住所有排队的请求都要等待网关吞吐量断崖式下跌。我在代码审查中见过不少这样的写法// 错误示范同步阻塞调用 User user restTemplate.getForObject(http://user-service/api/user/ id, User.class);正确做法是使用 WebClient 的异步调用或者将阻塞操作放到独立的线程池中隔离执行// 正确示范异步调用不阻塞事件循环 MonoUser userMono webClient.get() .uri(/api/user/{id}, id) .retrieve() .bodyToMono(User.class);5.2 关键参数配置超时、连接池和内存网关的参数配置我整理了一个我目前生产环境的参考配置spring: cloud: gateway: httpclient: connect-timeout: 3000 response-timeout: 10s pool: type: elastic max-connections: 1000 max-idle-time: 30s # 全局请求超时防止下游慢接口拖垮网关线程 httpclient: ssl: use-insecure-trust-manager: false codec: max-in-memory-size: 5MB几个关键参数的说明connect-timeout和response-timeout控制与下游建立连接和等待响应的超时时间。超时时间必须小于下游服务的超时时间否则网关会先被拖垮。max-connections连接池最大连接数决定了网关能支持的最大并发连接数需要压测后确认。max-in-memory-size控制请求体和响应体的最大缓存大小。默认 256KB如果业务有上传大 JSON 的场景需要调大否则会报DataBufferLimitException。5.3 优雅下线与平滑发版的实践网关作为入口如果重启或发版时直接杀掉进程正在处理的请求会直接中断客户端会收到连接重置错误。优雅下线是必须做的。Spring Boot 2.3 提供优雅停机支持server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 20s加上这段配置后应用收到关闭信号时会停止接收新请求等待已接收的请求处理完再退出最多等 20 秒。但在网关这种“流量中枢”场景光靠进程级优雅停机不够。我建议配合注册中心实现“摘流量后再下线”的流程先从注册中心反注册让新请求不再路由到当前网关实例。等待 10-15 秒让存量连接排空。再优雅关闭应用。我们当时用 K8s 的 preStop 钩子实现了这套逻辑preStop里先调用注册中心的下线接口然后sleep 10最后应用进程才收到 SIGTERM。这个流程上线后发版期间再也没收到过请求中断的告警。5.4 压测结果参考一次完整的网关性能验证分享一组我当时的压测数据硬件环境是 8C16G 的单节点网关压测工具是 wrk压测场景QPS平均响应时间P99 响应时间错误率空路由转发无过滤器680001.8ms8ms0%加鉴权过滤器520002.3ms12ms0%加限流过滤器490002.5ms14ms0%加完整日志 审计410003.1ms18ms0%结论是过滤器数量对性能有一定影响但合理设计下单机支撑几万 QPS 没有任何问题。让我意外的是日志和审计过滤器是最大的性能损耗点比鉴权和限流加起来还高。后来把同步打印改为异步批量写入后才稳定在 41000 QPS 这个水平。6. 排坑实录网关落地的五个高频问题与排查链路这一节是我认为对大家最有价值的。网关的坑很奇怪不踩一遍永远不知道踩过一遍下次就学会了。我按真实经历讲五个高频问题把排查过程完整呈现给大家。6.1 请求体被读空CachedBody 过滤器的必要性现象是网关里加了验签逻辑后下游服务收到的 body 是空的或者解析报错。排查链路是这样的先确认网关层是否读取过 body。通过日志或断点确认验签过滤器确实读了 body。意识到ServerHttpRequest.getBody()是 Flux 流只能被消费一次。第一次消费后流被关闭第二次读取返回空。最终方案就是前面讲的CacheBodyGlobalFilter把 body 缓存起来用ServerHttpRequestDecorator包装请求实现可重复读取。这个坑是网关开发的第一大坑几乎所有做网关的人都会踩。核心认知是一旦你在网关层读取了 body就必须缓存它否则下游必然会出问题。6.2 过滤器顺序导致鉴权被绕过现象是某些接口没有经过鉴权就访问到了下游服务。排查时发现路由配置里有个StripPrefix过滤器它的 order 值很小在鉴权 GlobalFilter 之前执行了导致匹配到的路由已经变了而鉴权过滤器又根据路由规则放行了部分请求。排查链路查看网关日志确认没有鉴权链路记录。检查GlobalFilter的 order 值和路由级 GatewaysFilter 的执行顺序。发现自定义鉴权过滤器 order 是 0而路由级 Filter 的 order 默认是 1竟然先执行了。修复方式把鉴权 GlobalFilter 的 order 调整为负数确保它在路由级过滤器之前执行。教训是网关里一切顺序问题都可能引发安全漏洞过滤器 order 值的设定一定要在项目初期就建立规范。6.3 动态路由未生效RefreshRoutesEvent 的失效场景动态路由更新后路由没有变化排查发现是没发RefreshRoutesEvent。RouteDefinitionWriter.save()只保存了路由定义但 RouteLocator 的缓存没有失效需要手动发布事件通知路由重新加载。排查链路确认 Nacos 配置已经变更且监听器已经触发。确认routeDefinitionWriter.save()执行没有报错。检查代理类生成的路由与期望不一致因为缓存没有刷新。确认通过ApplicationEventPublisher.publishEvent(new RefreshRoutesEvent(this))通知刷新后路由立即生效。这个问题的根因是理解偏差保存路由定义和刷新路由缓存是两个步骤缺一不可。6.4 WebFlux 与 Web MVC 依赖冲突现象是网关应用启动直接报错提示 Spring MVC found on classpath。这是因为网关基于 WebFlux如果项目里同时引入了spring-boot-starter-web两者就会冲突。原因很好理解Spring Cloud Gateway 必须运行在 WebFlux 环境下而 Web MVC 和 WebFlux 不能共存。很多从普通 Spring Boot 项目转过来的团队都会踩这个坑。排查链路检查启动日志确认是DispatcherServlet还是DispatcherHandler加载失败。检查 Maven 依赖树搜索spring-boot-starter-web。排掉这个依赖并重启应用。唯一修复方式就是移除 Web MVC 依赖。如果某个库强制依赖 Web MVC需要评估是否适合放进网关项目。6.5 网关线程池耗尽Netty 事件循环线程全被阻塞现象是网关压测时吞吐量上不去线程 dump 发现 Netty 事件循环线程全部处于 BLOCKED 状态。排查链路线程 dump 确认阻塞位置发现是某个过滤器在调用RestTemplate.postForObject同步接口。检查下游服务响应时间发现有接口响应 10 秒以上。大量线程因为等待响应而阻塞最终线程池耗尽所有请求都无法处理。修复方案是把同步调用替换为 WebClient 异步调用同时在网关层配置合理的响应超时防止下游慢接口拖垮整个网关。这件事让我深刻理解了一个道理在网关里写同步阻塞代码就是在给故障埋雷。7. 我的实践心得网关定制的三条铁律最后分享几点我从多个项目里沉淀出来的心得这些不是技术细节但比技术细节更能决定项目的成败。第一条网关逻辑要克制不能什么都在网关做。网关确实能做很多事情但不代表什么都应该放在网关里做。我见过有的团队把复杂的业务校验、数据聚合也写进网关结果网关变成了一个大泥球性能和可维护性都崩了。我的原则是能下沉到业务服务的逻辑就下沉网关只做与“流量”强相关的横切逻辑。第二条过滤器顺序和命名规范必须从第一天建立。等到有几十个过滤器再规范顺序就是巨大的重构成本。我们的规范是负 200 到负 100 处理请求体缓存与上下文初始化负 100 到 0 处理安全相关逻辑0 到 100 处理业务横切逻辑100 以上处理响应增强逻辑。每个过滤器在命名里带上顺序含义一清二楚。第三条网关必须有完整的可观测性和压测数据支撑。网关层出了故障影响面是所有业务。没有监控、没有压测数据、没有链路追踪出了问题就像在暗房里找东西。我们上线前必须确认访问日志有TraceId、核心指标有监控大盘、每季度的压测数据有报告留档。Spring Cloud Gateway 这套体系理解和定制并不难难的是用工程化的方式把它管起来。把上面的章节消化掉你们团队的网关就能从“能转发请求”进化到“真正的中枢”。如果你在实际落地中碰到别的问题欢迎带着具体场景来交流我这边还有不少案例可以拆开来细讲。
返回列表