Spring Cloud Alibaba整合Gateway与Nacos:动态路由、鉴权限流实战与避坑指南

发布时间:2026/10/14 2:50:03
Spring Cloud Alibaba整合Gateway与Nacos:动态路由、鉴权限流实战与避坑指南
网关是微服务流量入口的守门员在Spring Cloud Alibaba生态里Gateway和Nacos的组合基本是标准配置了。这篇文章我从选型理由、环境搭建、路由配置、动态刷新到鉴权限流把整合过程完整捋一遍顺便把那些网上很少写明白的坑也点出来。1. 为什么微服务网关选Gateway而不是Zuul先说结论如果项目已经确定使用Spring Cloud Alibaba这套体系网关层直接用Spring Cloud Gateway别犹豫。这不是说Zuul完全不能用而是从技术演进和生态契合度两个角度看Gateway都更合适。Spring Cloud Gateway基于WebFlux底层是Netty的Reactor模型处理请求用的是非阻塞I/O。这意味着同样的硬件资源Gateway能扛住的并发连接数比基于Servlet的Zuul 1.x高一个量级。Zuul 1.x每个请求占用一个线程线程池耗尽之后请求就只能排队而Gateway用的是事件驱动模型线程数量可以压得很低却能同时处理大量长连接请求。可能有人觉得业务并发没那么高选什么都无所谓但网关层是所有请求的必经之路哪怕平时并发只有几十一旦某个下游服务变慢线程模型差的网关会迅速被拖垮而事件驱动模型在这种场景下只是让请求排队等待响应不会把网关自身的线程池打满。再说生态。Nacos在Spring Cloud Alibaba里既当注册中心又当配置中心Gateway要动态感知服务实例列表直接通过Nacos的服务发现就能搞定。Spring Cloud Gateway配合Nacos做动态路由刷新只写一个实现RouteDefinitionRepository接口的类就够了ZooKeeper、Eureka、Consul也都能做服务发现但Github上的Spring Cloud Alibaba项目组对Gateway和Nacos的组合做过大量适配各种版本兼容性踩坑案例也最多社区方案相对成熟遇到问题能搜到解决方案的概率高得多。从部署运维角度讲Gateway是独立的网关服务它和业务服务完全解耦。团队里的人只关心自己的服务怎么注册到Nacos网关配置怎么改路由规则怎么调不需要为了网关再去维护一套Zuul的Filter体系。后面的章节我会用一个实际可运行的项目把整合过程完整串一遍从零开始搭环境到跑通第一个路由再到把路由配置挪到Nacos动态刷新最后加上JWT鉴权和限流。2. 环境准备与版本组合这一步最容易翻车Spring Cloud Alibaba整合Gateway九成以上的报错都出在版本不匹配上。不是代码的问题不是配置的问题就是版本组合不对。宁可先花十分钟把版本选好也不要启动报错后靠百度盲猜。2.1 版本选型对照表我整理了目前在用的稳定组合按主流的Spring Boot版本做了三套相关人员可以直接对着选Spring BootSpring CloudSpring Cloud AlibabaSpring Cloud GatewayJDK2.4.22020.0.12021.12.2.6.RELEASE1.82.4.132020.0.52021.0.1.02.2.9.RELEASE1.82.6.132021.0.52021.0.5.03.1.51.8这里有个关键点Spring Cloud Alibaba 2021.0.1.0对应的是Spring Cloud 2020.0.5不是2021.0.x。它的版本命名规则是Spring Cloud Alibaba版本号后面跟的是对应的Spring Cloud版本的小版本号。我见过有人Spring Boot 2.6.13配Spring Cloud Alibaba 2021.0.1.0结果Nacos注册一直失败就是因为Spring Cloud 2020.0.x和Boot 2.6.x兼容性有问题换成Spring Cloud Alibaba 2021.0.5.0却刚好对应Spring Cloud 2021.0.5反而能跑通。注意Spring Cloud Gateway 2.2.x版本之间差异不大但3.x开始包名和部分API有变化主要是javax到jakarta的命名空间迁移。如果你用第三套组合写过滤器时注意引入的是jakarta.servlet还是javax.servlet这个坑后面还会提到。2.2 Maven依赖到底该怎么引Gateway的依赖搭建比普通Spring Boot项目稍复杂因为Spring Cloud全家桶有一堆BOM需要管理。下面是pom.xml的核心部分我直接贴可用的配置dependencyManagement dependencies dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version2020.0.5/version typepom/type scopeimport/scope /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version2021.0.1.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies !-- 网关核心 -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency !-- Nacos注册中心客户端 -- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency !-- Nacos配置中心 -- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId /dependency !-- LB负载均衡Gateway转发时靠它选实例 -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-loadbalancer/artifactId /dependency !-- Redis后面限流要用 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis-reactive/artifactId /dependency /dependencies注意spring-cloud-starter-gateway和spring-boot-starter-web不能在同一个项目里共存否则启动直接报错。Gateway基于WebFlux和Servlet体系冲突。如果你在网关项目里引入了spring-boot-starter-webwebflux的自动配置会被跳过网关根本起不来。2.3 网关的配置文件结构先把最基础的application.yml写出来后面再逐步加内容。配置文件里的bootstrap.yml和application.yml要分清楚bootstrap.yml负责从配置中心拉取配置application.yml放本地的、跟应用自身相关的配置。网关项目里我习惯把spring.application.name、nacos地址、profiles写在bootstrap.yml路由规则写在application.yml后面再做Nacos动态刷新再挪走。# bootstrap.yml spring: application: name: gateway-server cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 namespace: public file-extension: yml # 优先加载的配置文件后面的路由规则放这里 shared-configs: -># application.yml server: port: 8080 spring: cloud: gateway: discovery: locator: # 默认false开启后自动根据注册中心的服务名生成路由 enabled: true lower-case-service-id: true routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix2 - id: auth-service uri: lb://auth-service predicates: - Path/api/auth/** filters: - StripPrefix2discovery.locator.enabledtrue这个配置有争议它的作用是网关自动根据Nacos注册的服务名生成/serviceName/**这样的路由。对于快速验证有用但正式环境不建议开启因为会把你所有内部服务全部暴露出去端口扫描一下就知道你的服务结构。还是用自己的routes控制白名单更稳妥。3. 从Nacos服务发现到第一跳路由核心配置与运行验证网关存在的意义不是自己处理业务而是把请求转发到背后的服务。Spring Cloud Alibaba场景下Gateway从Nacos拿到注册的服务实例列表通过LoadBalancer选出一个实例来转发。三个角色之间是怎么配合的我尽量说透。3.1 lb协议背后的工作原理在Gateway的配置文件里uri字段写的是lb://order-service这个lb是loadbalancer的缩写代表告诉Gatewayorder-service不是域名也不是IP而是去注册中心里找服务名为order-service的所有实例然后通过负载均衡策略选一个出来转发。处理流程是这样的客户端请求到达Gateway端口Gateway根据Predicate断言匹配到路由项发现uri是lb://开头触发LoadBalancerClient从Nacos拉取order-service的实例列表IP端口按负载均衡策略选一个实例把请求原始信息转发给这个实例这个机制意味着order-service即使有多个实例新增实例或者实例下线Gateway都能感知到并不需要重启。因为Nacos客户端在实例列表变化时会推送新的列表给订阅方LoadBalancer会自动更新。实际生产环境里服务扩容缩容都是常态这个动态感知能力是在Nacos加Gateway整合中价值最大的点之一。3.2 路由断言怎么写才靠谱路由断言是Gateway转发前的匹配条件常见的有Path、Method、Header、Query、Host、Cookie等。实际项目中推荐组合使用。比如routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** - MethodGET,POST - HeaderX-Client-Type, app filters: - StripPrefix2Path/api/order/**路径以/api/order/开头的请求MethodGET,POST限定请求方法HeaderX-Client-Type, app请求头里包含X-Client-Type且值为appPath是最核心的Method和Header建议也加上可以挡掉一部分异常请求进入后续链路。熟悉Spring的人可能觉得这像是拦截器但GateWay的断言是内置的路由级匹配器性能更好。3.3 StripPrefix过滤器别让路径带上一堆前缀网关在转发时默认把原始路径原封不动转发给下游。假设路由匹配的Path是/api/order/**但下游order-service的controller路径是/order/list怎么办这时候要用StripPrefix过滤器。StripPrefix2表示转发时去掉原始路径的前三段比如/api/order/list转发到下游时变成/list。如果下游controller的路径就是/order/list应该用StripPrefix1只去掉api。这里很容易搞混建议先拿一个请求实际测试一下再定参数。3.4 第一次跑通路由后的验证方法准备工作就绪后启动Gateway再启动一个简单的order-service注册到同一个Nacos。验证步骤给order-service写一个/order/list接口返回订单列表JSON调用curl http://127.0.0.1:8080/api/order/list观察返回结果应该是order-service的响应如果返回404排查优先级路由ID是否匹配、StripPrefix参数是否正确、order-service是否真的注册到Nacos、LoadBalancer是否能拿到实例列表。检查服务注册最简单的方式是打开Nacos控制台的服务列表页面看order-service是否在线。注意第一次跑通别急着加鉴权、限流、过滤器先把基础路由走通后面的问题单独排查会轻松很多。4. 把路由配置搬到Nacos实现动态刷新不用重启Gateway的路由配置如果写在application.yml里每次改路由都要重启网关。微服务场景下网关一重启所有依赖网关的请求全部中断这种代价基本不可接受。把路由配置托管到Nacos配置中心修改配置后自动推送网关无需重启路由即刻生效。4.1 为什么Spring Cloud Gateway原生不支持动态路由看到这可能有人问Spring Cloud Gateway不是号称支持动态路由吗怎么还要自己写代码需要区分两种动态基于注册中心的服务实例动态感知服务上下线路由自动感知基于路由规则的路由动态更新路由的Path、转发目标等规则变化时自动加载Spring Cloud Gateway开箱支持的是前者后者要靠RouteDefinitionRepository接口自己实现。好消息是不难。Spring Cloud Gateway启动时加载路由调用的就是RouteDefinitionRepository的getRouteDefinitions方法把路由配置交给RouteDefinitionRouteLocator转换成Route对象。只要实现这个接口动态路由就有了解法。4.2 实现一个基于Nacos的RouteDefinitionRepository我写了一个NacosRouteDefinitionRepository核心逻辑是监听Nacos配置变化后刷新内存中的路由定义Component public class NacosRouteDefinitionRepository implements RouteDefinitionRepository { private static final Logger log LoggerFactory.getLogger(NacosRouteDefinitionRepository.class); Autowired private NacosConfigManager nacosConfigManager; private static final String DATA_ID gateway-routes.yml; private static final String GROUP DEFAULT_GROUP; // 用volatile保存最新路由定义避免并发问题 private volatile ListRouteDefinition routeDefinitions new ArrayList(); PostConstruct public void init() { try { // 先读取一次配置 String config nacosConfigManager.getConfigService().getConfig(DATA_ID, GROUP, 5000); if (config ! null config.trim().length() 0) { routeDefinitions parseRouteDefinitions(config); log.info(init nacos config route definitions: {}, routeDefinitions.size()); } // 注册监听配置变更时回调 nacosConfigManager.getConfigService().addListener(DATA_ID, GROUP, new Listener() { Override public Executor getExecutor() { return null; // 用Nacos默认的线程池 } Override public void receiveConfigInfo(String configInfo) { log.info(receive nacos config change, refresh routes); routeDefinitions parseRouteDefinitions(configInfo); } }); } catch (NacosException e) { log.error(load nacos config error, e); } } Override public FluxRouteDefinition getRouteDefinitions() { return Flux.fromIterable(routeDefinitions); } // Nacos配置只做占位实现空方法 Override public MonoVoid save(MonoRouteDefinition route) { return Mono.empty(); } Override public MonoVoid delete(MonoString routeId) { return Mono.empty(); } private ListRouteDefinition parseRouteDefinitions(String config) { // 用Yaml工具把配置解析成RouteDefinition集合 Yaml yaml new Yaml(); MapString, Object map yaml.load(config); ListRouteDefinition definitions new ArrayList(); if (map null || map.get(spring) null) return definitions; MapString, Object springMap (MapString, Object) map.get(spring); MapString, Object cloudMap (MapString, Object) springMap.get(cloud); MapString, Object gatewayMap (MapString, Object) cloudMap.get(gateway); if (gatewayMap null) return definitions; ListMapString, Object routes (ListMapString, Object) gatewayMap.get(routes); if (routes null) return definitions; for (MapString, Object route : routes) { RouteDefinition definition new RouteDefinition(); definition.setId((String) route.get(id)); definition.setUri(URI.create((String) route.get(uri))); ListPredicateDefinition predicates new ArrayList(); ListString predicateStrs (ListString) route.get(predicates); if (predicateStrs ! null) { for (String predicate : predicateStrs) { predicates.add(new PredicateDefinition(predicate)); } } definition.setPredicates(predicates); ListFilterDefinition filters new ArrayList(); ListString filterStrs (ListString) route.get(filters); if (filterStrs ! null) { for (String filter : filterStrs) { filters.add(new FilterDefinition(filter)); } } definition.setFilters(filters); definitions.add(definition); } return definitions; } }思路是应用启动后从Nacos读取gateway-routes.yml把它解析成RouteDefinition列表然后注册一个Listener监听该配置的变更事件一旦Nacos推送配置变化就重新解析并更新内存中的路由定义。getRouteDefinitions()返回最新列表路由定位器会基于它刷新路由缓存。4.3 Nacos中路由配置文件的格式与格式约束在Nacos控制台新建配置Data ID必须是gateway-routes.ymlGroup是DEFAULT_GROUP配置内容建议这样写spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix2值得提醒的坑Data ID和Group必须和代码里一致不然Listener监听不到配置文件内容缩进严格用空格不要用Tab修改配置后Nacos推送是秒级的但日志里可能会看到request to Nacos server failed之类的警告先别慌如果receive nacos config change这句日志打了说明监听生效了4.4 为什么我用代码而不是spring-cloud-starter-alibaba的配置同步有人会说bootstrap.yml里配置了shared-configs: gateway-routes.yml并且refresh: true不是也能感知配置变更吗这个配置确实能让Gateway读取到Nacos里的配置文件但它的作用机制是把gateway-routes.yml作为Spring配置的一部分加载进EnvironmentSpring Cloud Gateway的RouteDefinitionRouteLocator只会处理配置属性路由并不会自动监听这个配置的变更再刷新Route对象。实测下来改配置后并不能自动刷新路由还是需要重启。真正要让路由动态刷新官方推荐的做法是重新发布RefreshRoutesEvent事件或者直接用RouteDefinitionRepository方案。我这里用了RouteDefinitionRepository实现每次配置变更后让getRouteDefinitions返回新列表RouteDefinitionRouteLocator会检测到变化并刷新路由缓存这是最省事的做法。如果是Spring Cloud 2020.0.x版本还有个小坑routeDefinitionLocator缓存了路由刷新事件触发时才会重建缓存所以代码里用volatile字段够用了不会有并发读旧路由的问题。5. 两个高频实战场景JWT统一鉴权与RequestRateLimiter限流网关能做的事远不止转发统一鉴权、限流、灰度发布、日志记录都是在网关层实现的。这一节讲两个最常见的JWT鉴权和基于Redis的限流。这两个场景覆盖了Gateway全局过滤器、自定义过滤器、内置限流过滤器三个核心扩展点搞懂之后网关功能就基本通透。5.1 用全局过滤器做JWT统一鉴权网关统一鉴权的核心逻辑是请求进来先判断路径是否需要鉴权不需要的直接放行需要校验的则解析JWT验证有效后放行无效返回401。关键点鉴权不该在每个微服务里各做一遍网关统一做了下游服务只信任网关传过去的用户信息。我写的全局过滤器Component public class AuthGlobalFilter implements GlobalFilter, Ordered { // 排除白名单登录、注册、健康检查等不需要鉴权 private static final ListString WHITE_LIST Arrays.asList( /api/auth/login, /api/auth/register, /api/health ); Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getURI().getPath(); // 白名单路径直接放行 if (WHITE_LIST.contains(path) || path.startsWith(/api/auth/login)) { return chain.filter(exchange); } // 从请求头获取token String token exchange.getRequest().getHeaders().getFirst(Authorization); if (token null || !token.startsWith(Bearer )) { return unauthorized(exchange); } token token.substring(7); // 去掉Bearer 前缀 // 校验JWT try { Claims claims Jwts.parser() .setSigningKey(secretKey) // 密钥来自配置 .parseClaimsJws(token) .getBody(); // 把解析出的用户信息放到请求头传递给下游服务 ServerHttpRequest newRequest exchange.getRequest().mutate() .header(X-User-Id, String.valueOf(claims.get(userId))) .header(X-User-Name, String.valueOf(claims.get(userName))) .build(); return chain.filter(exchange.mutate().request(newRequest).build()); } catch (JwtException | IllegalArgumentException e) { return unauthorized(exchange); } } private MonoVoid unauthorized(ServerWebExchange exchange) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } Override public int getOrder() { // 数字越小优先级越高放在限流之后转发之前 return -100; } }几个实际注意事项白名单直接写成path.startsWith(/api/auth/login)比在列表里写完整路径更灵活比如登录接口带了额外的query参数匹配不了精确字符串。JWT库我用的jjwt版本选0.9.1这个版本的API是Jwts.parser().setSigningKey()比较直观。如果用了jjwt 0.11.xAPI变成Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws()注意区分。签发的密钥不要在代码里写死建议放到Nacos配置中心管理定期轮换。轮换时向下兼容多个密钥比如校验时先试新的再试旧的这个后面可以展开。5.2 RequestRateLimiter限流的配置姿势限流的做法很多最简单有效的是用Gateway内置的RequestRateLimiter过滤器它基于Redis的Token Bucket算法实现不需要自己写Redis脚本。先从Redis环境讲起RequestRateLimiter必须依赖Reactive Redis所以网关项目里要引入spring-boot-starter-data-redis-reactive然后在application.yml里配置Redis连接否则限流过滤器一启用就报错。接着写一个KeyResolver决定按什么维度限流比如按用户维度、按IP维度、按接口维度。这里是按用户维度Component public class UserKeyResolver implements KeyResolver { Override public MonoString resolve(ServerWebExchange exchange) { String userId exchange.getRequest().getHeaders().getFirst(X-User-Id); if (userId null || userId.isEmpty()) { return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); } return Mono.just(userId); } }然后在配置文件里给需要限流的接口加上过滤器和参数routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix2 - name: RequestRateLimiter args: # 令牌桶容量单位时间内允许的最大请求数 redis-rate-limiter.replenishRate: 10 # 每秒补充的令牌数 redis-rate-limiter.burstCapacity: 20 # 按用户维度限流 key-resolver: #{userKeyResolver}参数含义replenishRate向令牌桶中补充令牌的速率单位是每秒补充多少个burstCapacity令牌桶的最大容量代表突发流量时能通过的请求上限key-resolver一个SpEL表达式指定限流维度的Bean名称假设replenishRate为10burstCapacity为20那么每个用户每秒最多能通过10个请求突发最多20个。需要注意burstCapacity必须大于等于replenishRate否则限流效果会不稳定。限流后的默认响应是429 Too Many Requests如果想统一返回JSON格式的错误信息自定义一个错误处理逻辑实现ServerWebExchange的响应写出就可以了。5.3 限流和鉴权的执行顺序过滤器链的顺序机制网关过滤器是有执行顺序的全局过滤器实现了Ordered接口getOrder()返回值决定优先级返回值越小越先执行。负数是正常的不是Bug。Ordered接口的默认排序是所有内置过滤器都实现了数字优先级比如RequestRateLimiter的order是-100路由转发的order是RouteToRequestUrlFilter实际上是-1所以自定义鉴权过滤器如果想先于限流执行order取-200如果想后于限流执行取-50。我的做法是去白名单放行的路径直接结束鉴权需要校验token的路径先让RequestRateLimiter生效再走鉴权。所以AuthGlobalFilter的order是-100RequestRateLimiter的order是-1000实际上内置限流的order是-40我把鉴权设成-30就能确保限流先执行。实际操作中用差值的绝对值大的负数简单省事但别把所有过滤器都设成同一个order否则执行顺序依赖类名排序会出现不确定性。6. 整合过程中的高发问题与完整排查链路这部分把我自己踩过的坑和团队里别人踩过的坑都拿出来按现象-排查-结论的方式整理。网关出问题不像普通业务逻辑那么好排查因为链路长从浏览器到网关到下游服务至少三跳每一步都可能出问题。6.1 启动报错ClassNotFound错误关于reactor相关类这是最典型的环境问题spring-cloud-gateway和spring-boot-starter-web冲突导致的典型表现之一。启动日志中会出现类似Caused by: java.lang.ClassNotFoundException: reactor.core.publisher.Mono的报错但Mono明明在依赖里为什么找不到实际上是因为spring-boot-starter-web的ServletIoC容器和Gateway的WebFlux容器冲突导致Reactor相关类没有被正确注册。排查步骤执行mvn dependency:tree查看依赖树找找有没有spring-boot-starter-web有的话在pom里exclusion掉另外确认自己没把spring-boot-starter-web当成普通web依赖误加还有种情况业务服务里用到了spring-boot-starter-web网关项目也引用了同一个父pom导致传递依赖把starter-web带进来了。6.2 路由404断言匹配与StripPrefix的连锁反应路由404的排查顺序是固定的按这个顺序来基本一次能定位先确认网关有没有加载到路由开源可视化的方式是在日志中看RouteDefinitionRepository相关日志或者自己写一个端点返回当前路由列表确认请求路径确实匹配了断言如果Path写的是/api/order/**请求是/api/order/list那匹配没问题但如果服务端点了/order/list网关却用/api/order/**匹配那就永远匹配不上确认StripPrefix参数这个最隐蔽因为匹配成功但路径转发错了下游返回404。前端看是404直觉会以为是网关路由没匹配实际是转发路径不对。先把StripPrefix去掉或改成0用原始路径测一次能通就说明是前缀问题最后再查服务注册和负载均衡另一种典型是order-service没注册到同一个Nacos namespace网关去服务发现里找不到order-service实例返回503 Service Unavailable这种情况和404要区分开。6.3 OpenFeign低版本与Spring Cloud Gateway兼容性冲突微服务之间内部调用一般都用OpenFeign但如果你在网关项目里同时引入了spring-cloud-starter-openfeign版本低于Spring Cloud 2020.0.x启动时可能会报LoadBalancerWanning: Feign clients that dont have a URL configured的警告或者直接启动失败。根因是Spring Cloud 2020.0.x开始Ribbon被移除了Spring Cloud Gateway内置的负载均衡改成了基于Spring Cloud LoadBalancer。OpenFeign如果还在用Ribbon的自动配置就会和Gateway的LoadBalancer冲突。解决方案确认pom里没有显示的ribbon依赖把spring-cloud-starter-openfeign版本升级到对应Spring Cloud版本网关项目尽量不引入OpenFeignFilter里需要调其他服务就手动用WebClient调用6.4 Nacos 2.0的客户端端口问题Nacos服务端升级到2.0之后客户端除了连接8848还要额外开放一个gRPC端口9848来维持长连接。如果服务器的安全组或防火墙只开放了8848网关启动时注册能成功但过一段时间可能出现连接断开、服务下线的情况日志里报caused by: java.io.IOException: Connection reset by peer。排查步骤确认Nacos版本2.0以上必查确认防火墙和云安全组的9848端口是开放的客户端线程Dump看看是不是有大量gRPC连接创建失败的异常如果服务器网络环境不允许再开端口临时方案是Nacos客户端配置nacos.core.grpc.port.reusetrue让gRPC复用8848端口但官方建议还是开放端口长期复用会影响性能。6.5 跨域配置失效的陷阱跨域是Web项目绕不开的问题尤其前端用了Vue或者React之后。在Gateway配置跨域spring: cloud: gateway: globalcors: cors-configurations: [/**]: allowedOrigins: http://localhost:3000 allowedMethods: - GET - POST - PUT - DELETE allowedHeaders: * allowCredentials: true容易踩的坑allowCredentials为true时allowedOrigins不能写*必须写具体域名。否则浏览器会在控制台报the value of the Access-Control-Allow-Origin header...这是浏览器安全机制和网关没关系。allowedOrigins支持用http://localhost:3000这种精确写法如果用*且allowCredentials为true等于给了安全漏洞任何来源都能带cookie来访问。另一个坑是如果你在网关里配置了多个路由跨域配置却只对部分路由生效。globalcors里的[/**]理论上匹配所有路径但如果某个Filter在CORS处理之前就拦截了带预检OPTIONS的请求会直接返回跨域就失效了。在自定义全局过滤器里对OPTIONS请求直接放行加跨域头。6.6 配置了Nacos配置中心后配置不生效使用nacos-config之后有时候改了配置中心的内容日志里也提示Refresh config success但路由规则还是老样子。这是配置中心的优先级问题。如果你在bootstrap.yml里通过shared-configs加载了网关路由配置那么在Nacos里的配置文件会作为配置源加载进Environment但Spring Cloud Gateway的RouteDefinitionRouteLocator只在启动和RefreshRoutesEvent触发时去读配置。修改Nacos配置后没有触发RefreshRoutesEvent路由自然不刷新。如果你用了上一章我写的RouteDefinitionRepository因为监听器直接更新内存列表RouteDefinitionRouteLocator能感知到变化所以路由立即刷新。这里也是两种方案的根本区别。7. 进阶玩法网关层灰度发布与配置管理经验网关层除了鉴权和限流灰度发布是微服务架构里很有价值的能力也是网关层最适合做的一件事。实现思路是服务发布新版本时让新版本实例注册到Nacos时带上元数据标识比如versionv2然后网关在转发时根据请求头或参数选择特定版本实例其余请求都走旧版本。Nacos里给实例打版本标签的方式spring: cloud: nacos: discovery: metadata: version: v2然后写一个自定义的LoadBalancer过滤器比如VersionGrayFilter在转发前根据请求头的X-Gray-Tag决定解析哪个版本的实例。这里建议不要直接修改RouteDefinition里的uri而是写一个重写ReactiveLoadBalancer的过滤器。大致思路是public class VersionLoadBalancer implements ReactorServiceInstanceLoadBalancer { private final ObjectProviderServiceInstanceListSupplier serviceInstanceListSupplierProvider; private final String serviceId; private final String version; public VersionLoadBalancer(ObjectProviderServiceInstanceListSupplier serviceInstanceListSupplierProvider, String serviceId, String version) { this.serviceInstanceListSupplierProvider serviceInstanceListSupplierProvider; this.serviceId serviceId; this.version version; } Override public MonoResponseServiceInstance choose(Request request) { // 从请求中获取灰度标识 String grayVersion ((DefaultRequestDefaultHttpHeaders) request).getHeaders().get(X-Gray-Version); ServiceInstanceListSupplier supplier serviceInstanceListSupplierProvider.getIfAvailable(); return supplier.get().next().map(instances - { // 优先选择带版本元数据的实例 if (grayVersion ! null) { ListServiceInstance grayInstances instances.stream() .filter(instance - grayVersion.equals(instance.getMetadata().get(version))) .collect(Collectors.toList()); if (!grayInstances.isEmpty()) { return Response.getInstance(grayInstances.get(ThreadLocalRandom.current().nextInt(grayInstances.size()))); } } return Response.getInstance(instances.get(ThreadLocalRandom.current().nextInt(instances.size()))); }); } }这个方案的优势是不修改路由配置不改断言只需要在Nacos服务实例元数据里打上版本标签然后在负载均衡层做版本筛选。发布新版本时灰度流量逐渐放大验证没问题后把旧版本实例下线即可。比基于路由的灰度更灵活因为路由规则是全局的而基于负载均衡的灰度是实例粒度的。灰度网关上线前建议先在测试环境跑一周灰度规则本身也可能有bug控制不住流量的时候能快速回退。回退方法很简单把请求头X-Gray-Version删掉或者把该版本的实例全部下线。配置管理方面Nacos配置中心里网关配置文件的命名我建议多留一份备份网关配置修改之前先导出避免误操作后无配置可用。Nacos控制台的命名空间也可以按环境区分dev、test、prod各一个namespace网关通过spring.cloud.nacos.config.namespace指定。注意bootstrap.yml里写的是namespace: public如果你改了namespaceRights。日志这块Gateway默认访问日志不够详细生产环境建议自己加一个LoggingGlobalFilter记录请求路径、方法、响应状态、耗时。这个比在业务服务里加日志更全面因为网关是入口能拿到全链路的第一手信息。为了不拖累性能日志级别用info注意序列化JSON的字段不要太多。8. 压测与实际性能指标Gateway到底能扛多大并发聊性能之前先定个调网关的性能上限和机器配置、路由复杂度、下游响应速度、限流算法开销都有关单测出来的数字没有绝对意义但能反映趋势和容量规划参考。我拿一个简单场景压过单条路由转发到本地一个echo服务Gateway部署在2核4G的云服务器上客户端用wrk在另一台机器上打压力。压测结果记录如下仅供参考并发连接数QPS近似的平均时延错误率1001.8万3ms0%2002.6万5ms0%5002.4万12ms0.1%10001.3万25ms2.5%个人体会是Gateway在并发500以内都能保持不错的稳定性和时延超过1000之后QPS反而下降这是因为线程、内存以及下游服务的负载到了瓶颈Netty的线程模型再好也扛不住下游响应变慢。如果只压网关不压下游指标会好看很多但真实场景里下游肯定有业务逻辑。压测得到的QPS可以作为后续容量规划的参考但生产环境的性能瓶颈往往不在网关本身而在网关下游的第一个服务。如果下游服务端耗时从5ms涨到50ms网关的线程资源和内存消耗会明显增长所以上线前务必做好下游服务的超时配置建议给Gateway配置全局的Http超时时间spring: cloud: gateway: httpclient: connect-timeout: 1000 response-timeout: 5sconnect-timeout连接超时设1000msresponse-timeout响应超时设5s是保守但安全的设定。如果下游某些接口确实需要超过5秒再针对特定路由配置更大的超时时间。超时设置不当网关在高峰期会被慢接口拖垮这个代价很惨烈我之前就吃过亏下游某个报表接口要跑10秒没设超时网关线程池被占满所有接口全部超时故障持续时间远超预期。后来才补上超时和熔断降级这类教训值得记下来。熔断降级也可以放在网关层做Spring Cloud Gateway虽然没有内置的熔断过滤器但可以通过集成Resilience4J的熔断器实现这个展开又是一大篇先不展开。核心思路是对下游服务加上熔断阈值失败比例达到阈值时一定时间窗口内的请求直接走降级逻辑不再打到下游。