首页 / 资讯中心 / 文章详情

Spring Cloud Gateway整合Nacos:微服务网关集成到上线全攻略

Spring Cloud Gateway整合Nacos:微服务网关集成到上线全攻略 ★ FEATURED ARTICLE
这是微服务实战系列的第六篇。前面我们把服务注册、配置管理、远程调用、负载均衡一一捋顺之后网关成了绕不开的一环。不少团队走到这一步就开始掉头发——Spring Cloud Gateway本身不难难的是它同时要对接Nacos的配置中心和注册中心启动顺序、路由写法、版本兼容、过滤器顺序任何一个环节没对齐你的网关不是起不来就是起来之后请求全部503。这篇我把Gateway Nacos从集成到上线的完整链路讲一遍适合已经能跑通单个微服务、正准备给系统加统一入口的开发者和架构师。我不会把官方文档复述一遍重点讲集成时要打通的关键点以及文档里没写清楚的坑。1. 网关在微服务里解决什么问题先搞懂它和Nacos的配合1.1 没有网关的年代每个服务都要自己处理公共逻辑网关不是锦上添花而是微服务规模到了一定程度后的必然产物。最早做微服务时客户端要调某个接口得自己维护一堆服务地址用户登录、接口鉴权、访问频控、参数校验这些逻辑每个服务都得写一遍。更要命的是一旦鉴权规则要改所有服务都要重新打包上线。这个阶段还能忍是因为服务数量少等服务到了七八个以上你会发现团队大部分时间不是在做业务而是在重复造公共功能的轮子。网关的核心价值就是把这类横切逻辑从业务服务里剥出来统一收敛到入口层。以我们当时的支付订单服务为例它既要处理自己的下单核心逻辑又要做登录态校验、黑白名单过滤、调用频次控制代码里超过一半都在处理跟业务无关的流程。后来我们把这些逻辑全部挪到网关层业务服务瘦身效果非常明显发布频率也降下来了。网关本质上就是微服务架构里的门卫统一收门票、验身份、控流量而不是让每栋楼都自己雇一个保安。1.2 网关与Nacos各管一段注册中心负责“找到人”网关负责“带路”这个阶段Nacos同时承担两个角色很多初学者会把它们搞混。作为注册中心Nacos负责管理所有业务服务的实例信息服务A要调服务B去Nacos拿B的地址列表然后自己做负载均衡。作为配置中心Nacos负责下发所有服务的配置文件包括网关自己的路由规则。网关本身不保存服务地址表。收到请求后它先按路由配置找到服务名再到Nacos拉取该服务的实例列表选一个目标地址发过去。所以你可以这样理解注册中心负责“找到人”网关负责“带路”。搞清楚这个分工后面看问题就能少犯迷糊。很多人排错时纠结“为什么Gateway拿不到服务地址”其实不是Gateway不拿而是它需要通过注册中心客户端去查这个查询链路上的任何一个环节断了表现都是“路由不通”。1.3 什么项目应该接网关不过也不是所有项目都得立刻上网关。我的判断标准很简单第一服务数量超过三四个而且互相调用关系变得复杂第二有的接口必须做统一鉴权、统一限流散到各服务做明显不划算第三对外暴露的入口需要和内部服务结构解耦比如内网服务和公网服务要走不同的规则。单体应用或者只有两个服务的场景强行上网关反而增加链路复杂度一顿操作猛如虎最后发现收益几乎为零。2. 为什么是Gateway而不是Zuul技术选型要看到三年后2.1 Zuul的两次转身与Gateway的诞生用过老Spring Cloud的人都知道早期官方推荐的网关是Zuul。Zuul 1.x是基于Servlet的同步阻塞模型每个请求占用一个线程从进入到返回全程霸着不放。后面社区一直等它的大版本重写但发布节奏拖了很久Spring官方等不及了干脆基于Spring WebFlux自己做了Spring Cloud Gateway。所以Gateway不是简单的“又一个网关”它是Spring生态在响应式编程上的一次落地。当初我们选型讨论过要不要等Zuul的新版本最后还是选择了Gateway。原因很简单Spring官方主推、社区活跃度更高、基于响应式模型更符合后续技术演进方向。做技术选型不能只看今天够不够用还得看三年后这个组件还有没有人维护、有没有持续迭代。2.2 生产环境真正介意的是线程模型为什么阻塞模型在生产环境会出事想象一个餐厅同步阻塞的服务员接待一桌客人后就一直站在这桌旁边等他们点完菜、吃完、结账这期间其他桌再多人他也顾不上。高并发一来线程池被打满后面请求要么排队要么超时服务没有崩溃但和崩溃没什么区别。Gateway基于Netty和Reactor用的是事件驱动模型请求到达后拆成事件I/O在等待时线程可以去处理别的事情少量线程就能扛住大量并发。这就是为什么Gateway在流量高峰期更稳的根本原因。2.3 Gateway的代价底层WebFlux意味着什么不过Gateway的响应式底层也不是白送的。它默认跑在Netty上不兼容Servlet标准所以你以前在Spring MVC里写的那套过滤器、拦截器基于HttpServletRequest的工具不能原样搬过来。刚开始集成时我想把老项目里一个基于Servlet的日志过滤器直接复制到Gateway结果发现一堆API没法用。这不是配置问题是编程模型的问题。如果你后续要用网关做鉴权、日志、限流得按WebFlux的方式写或者干脆在全局过滤器里拿ServerHttpRequest来处理。3. Nacos配置中心回落Gateway启动要先过配置这一关3.1 bootstrap.yml里最关键的三行配置用Nacos做配置中心后踩的第一个坑是Spring Cloud 2020.0之后bootstrap默认不加载了。以前我们习惯在bootstrap.yml里写Nacos配置中心的地址升级后发现Gateway启动根本不读这个文件自然也就不会去Nacos拉配置。解决方案是在pom里显式加入spring-cloud-starter-bootstrap依赖让bootstrap上下文重新生效。这一步不做后面所有Nacos配置都不会进到Gateway里。dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-bootstrap/artifactId /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-loadbalancer/artifactId /dependencybootstrap.yml里最核心的配置如下spring: application: name: gateway-service cloud: nacos: server-addr: ${NACOS_ADDR:127.0.0.1:8848} config: file-extension: yml group: DEFAULT_GROUP namespace: public shared-configs: ->spring: cloud: gateway: routes: - id: order-route uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix2 - id: user-route uri: lb://user-service predicates: - Path/api/user/** filters: - StripPrefix2这里的StripPrefix也要说清楚数字代表去掉几段路径。比如请求打到网关是/api/order/list配置StripPrefix2转发到下游时路径就是/list。如果你只想去掉一层就写1要根据你实际的前缀设计调整不是照抄别人的配置就能通的。4.2 别忘了spring-cloud-starter-loadbalancer但注意上面说的是“如果有负载均衡客户端的话”。Spring Cloud 2020.0之后Gateway的依赖不再自动带负载均衡能力以前Ribbon那套也彻底退场了。如果你pom里只加了spring-cloud-starter-gateway和nacos-discovery没有单独加spring-cloud-starter-loadbalancer那么所有lb://路由都会返回503。这个坑非常隐蔽因为依赖不报错服务也注册成功了就是请求转发不过去。排查到最后往往只是一行依赖的缺失。我在前文给的pom里已经把spring-cloud-starter-loadbalancer加进去了这里再强调一次。版本方面我用的是一套当时比较稳的组合Spring Boot 2.6.13、Spring Cloud 2021.0.6、Spring Cloud Alibaba 2021.0.5.0。如果你用Spring Boot 3.x对应的是Spring Cloud 2022.0.x以上的版本依赖坐标虽然一样但底层的API变化比较大一定要先查清楚版本对应关系再动手否则启动就会抛NoSuchMethodError。4.3 Nacos配置变更后路由能否动态刷新routes写在Nacos里修改后Gateway会自动生效吗以我的实测经验Nacos配置变更触发RefreshEvent后Gateway的RouteDefinitionRouteLocator会重新从Environment读取路由定义大部分情况下路由是能动态刷新的不需要重启。但这里面有几个边界要特别小心一是只改了Nacos配置但Gateway没有订阅刷新的事件那就要检查refresh配置是否正确二是自定义全局过滤器里缓存的路由表可能不会跟着自动刷新。总之修改后最好用curl实测一下别拿“配置中心能刷新”当万能结论。5. 断言、过滤器与跨域配置网关日常开发最常用的三件套5.1 断言请求什么条件才配到这个路由路由能不能匹配靠的是Predicate。Path是用的最多的但实际业务里多半不会只有Path。比如你想把10%的流量引到新版本可以用Weight断言想限制某些请求必须带特定Header可以用Header断言。这些断言可以组合全部满足才会走这个路由。我见过不少配置不生效的案例查到最后往往不是代码问题而是几个断言互相矛盾比如Path限定了范围Header又要求一个不可能同时存在的条件结果路由永远匹配不上。几个常用断言写在下面方便对照predicates: - Path/api/order/** - HeaderX-Request-Id, \d - Queryflag, gray - Weightcanary, 10Weight断言常用于灰度分流两条路由使用同一个分组名权重加起来等于100网关会按比例分发请求。Header和Query断言则用于细粒度控制比如内部调用和外部调用通过不同Header区分再走不同的路由。5.2 自定义GlobalFilter做统一登录鉴权网关最常见的需求是统一鉴权。我们会在Gateway里放一个全局过滤器处理所有请求先判断请求路径是否在白名单里不在白名单就校验Token通过之后把用户ID塞进Header再转发给下游服务。这个做法的好处是下游服务不用再解析Token拿到网关传的X-User-Id直接用减少重复验签。Component RefreshScope ConfigurationProperties(prefix gateway.auth) public class AuthProperties { private ListString ignoreUrls new ArrayList(); public ListString getIgnoreUrls() { return ignoreUrls; } public void setIgnoreUrls(ListString ignoreUrls) { this.ignoreUrls ignoreUrls; } }Component public class AuthFilter implements GlobalFilter, Ordered { private final AuthProperties authProperties; public AuthFilter(AuthProperties authProperties) { this.authProperties authProperties; } Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getURI().getPath(); if (authProperties.getIgnoreUrls().contains(path)) { return chain.filter(exchange); } String token exchange.getRequest().getHeaders().getFirst(Authorization); if (token null || !token.startsWith(Bearer )) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } try { String userId parseUserId(token); ServerHttpRequest mutatedRequest exchange.getRequest().mutate() .header(X-User-Id, userId) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } catch (Exception e) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } private String parseUserId(String token) { // 这里用网关的秘钥做验签解析出userId return 1001; } Override public int getOrder() { return -100; } }关键点是过滤器的order值。order越小越先执行。鉴权过滤器一般放在比较前面的位置但要放在跨域处理之后否则预检请求会被卡住。白名单配置放Nacos里通过RefreshScope的配置属性类加载调整白名单后不用重启网关这一点在运维时特别方便。5.3 跨域前后端分离场景的网关级解法跨域问题在网关集成前都是各服务自己配网关集成后统一在网关配省心很多。GlobalCors配置比较直接但有两点要注意一是allowedOriginPatterns和allowCredentials要配合好通配符配不好浏览器会直接报跨域错误二是必须确认你的鉴权过滤器没有拦截OPTIONS预检请求。我们线上就出过这事前端明明配好了跨域但实际请求还是失败一看日志预检请求被鉴权过滤器拦了。spring: cloud: gateway: globalcors: cors-configurations: [/**]: allowedOriginPatterns: * allowedMethods: - GET - POST - PUT - DELETE - OPTIONS allowedHeaders: * allowCredentials: true配置本身不难难的是和其他过滤器的协作顺序。如果你在全局过滤器里对所有请求都做鉴权别忘了放行OPTIONS方法或者把跨域配置的优先级提到过滤器前面去。6. 网关真实部署里的坑位清单6.1 503 Service Unavailable的真相Gateway部署完成后最经典的现象是请求到网关返回503。我总结下来核心原因有三种现象最可能的根因排查动作所有lb://路由都503缺少spring-cloud-starter-loadbalancer依赖检查pom依赖确认版本单个路由503路由里的服务名和Nacos注册名不一致Nacos控制台核对服务名目标服务所有实例都503实例全部下线或未注册成功查看Nacos服务详情和健康状态排查顺序我固定下来给大家参考先看Nacos服务列表里有没有这个服务再确认Gateway本地是否引入了负载均衡依赖最后看路由配置里lb://后面是不是写对了名字。大多数情况按这个顺序走能在五分钟内定位问题。6.2 超时、连接池与响应大小服务发现和路由都正常之后还会遇到一个隐蔽问题网关默认的HttpClient配置不一定适合你的业务。调用下游很慢时超过默认的响应超时时间网关会主动断开客户端看到504或超时错误。这时需要调大connect-timeout和response-timeout。spring: cloud: gateway: httpclient: connect-timeout: 3000 response-timeout: 5s同时如果你的业务有文件下载注意网关默认对响应Buffer的大小限制大文件会被截断需要把spring.codec.max-in-memory-size适当调大。spring: codec: max-in-memory-size: 10MB这两个参数属于“不遇到问题根本想不起来配置”的类型建议在网关上线前就根据业务预期设置好别等线上文件下载失败再救火。6.3 链路追踪与请求头透传微服务链路排障请求ID是命根子。Gateway作为第一站一般会在全局过滤器里生成TraceId放到Header传给下游。如果直接用框架的链路追踪功能需要注意网关的响应式线程模型和传统线程模型在透传方式上略有差异。我们当时图省事自己在过滤器里生成ID然后写测试脚本验证整条链路都能取到同一个值确保日志能串起来。实践上要做两件事一是生成请求ID二是透传。生成可以在GlobalFilter里做透传则是把请求ID放到Header里转发给下游服务。下游服务的日志里也打印这个Header值排障时按请求ID一搜整条链路一目了然。如果你们已经接了链路追踪系统那就忽略这条不过最好也在网关层确认Header被正确传递。6.4 Swagger聚合和IP白名单网关前面通常还有一层运维入口但很多团队只想让网关对外把多个服务的Swagger聚合到一个地址。大致思路是给网关加一个Swagger资源Provider收集下游服务的Swagger资源路径再通过路由转发。另一个实用场景是IP白名单有些管理后台接口不希望暴露在公网可以在全局过滤器里加一层IP判断白名单配置放Nacos里运维改配置就能生效。这两块都属于让网关变得“好用”的运维向功能。7. 网关限流与灰度发布的进阶思路7.1 RequestRateLimiter Redis限流网关做限流比在业务服务里做要合理因为流量还没进业务系统挡在入口更干净。Gateway内置的RequestRateLimiter基于令牌桶算法需要配合Redis实现。实现思路是定义一个KeyResolver从请求里拿到用户ID或IP把它作为限流的key然后给对应路由挂上RequestRateLimiter过滤器设置补充速率和桶容量。高并发场景下超过容量的请求会直接返回429。filters: - name: RequestRateLimiter args: key-resolver: #{userKeyResolver} redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20Bean public KeyResolver userKeyResolver() { return exchange - { String userId exchange.getRequest().getHeaders().getFirst(X-User-Id); if (userId ! null !userId.isEmpty()) { return Mono.just(userId); } return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); }; }要注意的是KeyResolver的选择会直接决定限流的粒度。如果你按IP限同一出口多人共享IP时容易被误伤按用户限则必须先有用户身份上下文所以它通常要放在鉴权过滤器之后去配置使用。7.2 灰度发布的网关思维灰度发布是网关的另一个高价值场景。最简单的做法是用Weight断言按比例切流量比如新老版本权重分别设成90和10。进阶一点的做法是利用Nacos里的实例metadata标记版本网关读请求里的灰度标识如果带了灰度标签就路由到灰度实例否则走正常实例。这两种思路各有适用场景按比例适合平滑放量按标识适合内部测试环境。不管哪种灰度发布的关键都是网关要有足够的灵活性这也是为什么我们坚持把路由配置放Nacos而不是写死在本地代码里。8. 从Gateway集成里沉淀的运维体会8.1 先跑通一条路由再丰富功能每次接新项目我的习惯都是先不加任何全局过滤器用一个最简单的Path路由把网关和某个服务跑通确认Nacos配置、注册发现、lb协议这一整条链路是通的再加鉴权、跨域、限流。这样问题定位会非常快否则全局过滤器、断言、超时配置混在一起排查成本直接翻倍。如果你现在正被网关集成搞得焦头烂额不妨把配置全部先清空只留一条路由和一个服务跑通了再逐步往上加。8.2 网关变更要遵守上线习惯网关是全队服务的入口一挂全挂。所以网关的配置变更、过滤器升级我都会当成重要变更处理。Nacos里改路由虽然理论上能热生效但我也会挑流量低峰期改完先在测试环境验证一轮。上线后盯一段时间日志重点关注503、429、超时这三类状态码的数量变化。这套习惯帮我们避过好几次差点酿成事故的操作。最后再分享一个细节Gateway的日志级别默认比较克制排查问题时把org.springframework.cloud.gateway调成DEBUG你会看到每个请求的路由匹配过程和过滤器执行顺序这比瞎猜高效得多。等路由、断言、过滤器都验证稳定了再调回INFO。这是我在整个Gateway集成过程中最后悔没早点知道的一个小技巧希望对正在做微服务网关集成的你有帮助。
阅读完成 · 觉得有帮助?
咨询建站