
1. 在线教育项目里Gateway网关到底解决了一个什么真问题先交代一下背景。我之前参与过一个在线教育网站项目技术栈就是标题里说的这一套后端用 SpringCloud 做微服务前端用 Vue 写单页应用课程、订单、用户、支付这些模块各自拆成了独立服务。项目初期一切顺利但随着业务模块越拆越细问题开始一件接一件往外冒。最典型的一个场景前端页面每次请求都要拼不同的服务地址课程接口指向课程服务的地址订单接口指向订单服务的地址用户登录要指向用户服务。每个服务的 IP 和端口都直接暴露在浏览器的网络请求里。一开始服务不多还好后来服务数量上了两位数前端维护这一堆地址就想骂人。更麻烦的是好几个模块都需要做登录校验结果每个服务里都写了一份解析、校验 Token 的逻辑代码重复得一塌糊涂改一次校验规则要动四五个服务。那时候项目组天天在加班填坑终于有同事提出不能再这么裸奔下去了需要搞一个统一的入口层把路由转发、鉴权过滤、跨域处理这些破事先挡在外面。这就是我们决定整合 SpringCloud Gateway 网关的起因。很多刚接触微服务的同学会把网关当成一个可有可无的转发器觉得无非就是把请求从 A 挪到 B。实际上网关在一个 SpringCloud Vue 的项目里扮演的角色远不止转发这么简单。它至少要承担四件事第一统一入口。前端只需要记一个网关注册地址至于请求背后的服务在哪里由网关负责解析。第二横切逻辑下沉。登录校验、权限判断、请求日志、限流、跨域这些每个服务都要做一遍的工作收拢到网关这一层做掉服务内部专心写业务。第三保护后端服务。服务不再是裸奔状态外部请求先经过网关这道屏障起码多了一层缓冲。第四让前后端联调变舒服。前端开发的时候只需要面对一个稳定的入口地址后端服务随便迁移、扩缩容都不影响前端。我们最终敲定的架构是这样的前端 Vue 项目通过 axios 统一请求到 SpringCloud Gateway网关根据路径规则把请求转发到注册在 Nacos 上的各个微服务。用户服务负责登录和发放 JWT其他服务不再各自校验身份统一在网关的全局过滤器里解析认证信息。划线部分后面会详细展开这里先记住一个结论网关不是锦上添花而是微服务化进入深水区之后的一个绕不开的节点。2. 路由配置与 Nacos 联动让网关认得每一道服务网关的第一件正事就是指路。指路这件事看起来简单但里面的门道不少尤其是当服务数量变多、路径规则变复杂之后配置设计得好不好直接决定后续维护的体验。2.1 微服务拆分的路径规划思路网关路由是什么通俗地说就是一张请求路径 → 目标服务的映射表。收到一个请求看它的路径前缀决定要转给哪个服务。我们项目里的服务划分是这样的用户服务处理登录、注册、个人信息课程服务课程列表、课程详情、分类检索订单服务购物车、下单、订单列表支付服务支付下单、支付回调学习服务学习记录、进度同步、视频播放地址获取对应到网关的路径规划上我们的做法是给每个服务定一个统一的一级路径前缀服务路径前缀用户服务/api/user/**课程服务/api/course/**订单服务/api/order/**支付服务/api/pay/**学习服务/api/learn/**这样规划的意图很直白所有业务接口统一走 /api 前缀然后看第二段路径区分服务。前端不用关心每个服务背后的 IP 和端口发给网关就够了。而且未来如果新增服务只需要再加一个一级前缀不会影响已有路由的匹配逻辑。2.2 集成 Nacos 而不是写死 URL路由分为静态路由和动态路由。静态路由就是一个固定的 uri 指向某个地址比如 http://localhost:8081。这种做法在开发联调阶段能用但到了生产环境就不太合适了因为服务地址可能随时变化负载均衡也不好做。我们用的是 Nacos 作为注册中心Gateway 和 Nacos 打通之后路由的目标地址不用写死而是通过 lb:// 协议去 Nacos 里找。这一步的核心依赖这样配spring: cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true开启 locator 之后网关会自动根据 Nacos 里的服务名生成路由。比如 Nacos 里注册了一个叫 course-service 的服务网关会自动识别出一条 /course-service/** 的路由规则。这个功能调试时很方便但直接用在生产环境有隐患因为默认路由规则太粗暴路径上带着服务名很容易暴露内部的拓扑结构所以我们最终没有靠 locator 自动路由而是把它作为兜底具体路由规则还是手动写好再用 lb 协议指向服务。spring: cloud: gateway: routes: - id: course-route uri: lb://course-service predicates: - Path/api/course/** filters: - StripPrefix1这里 lb://course-service 的意思就是让网关通过负载均衡的方式从 Nacos 注册中心找到名为 course-service 的服务实例然后转发过去。一个小细节是 StripPrefix1。因为前端请求过来的时候路径是 /api/course/list而后端 course-service 的 Controller 接口定义的是 /course/list多出来的 /api 这一层如果不剥掉后端服务根本接收不到正确路径。所以网关在转发之前要去掉第一级前缀。这个路径对齐的问题非常容易踩坑后面章节会专门讲。2.3 网关自身的服务注册网关本身也应注册到 Nacos 里。这样做有几个实际好处网关和所有微服务在同一个注册中心里便于运维统一监控服务状态如果网关做了集群部署可以通过 Nacos 做服务发现和负载均衡后续如果有其他内部服务需要调用网关接口也方便。网关的注册配置很简单就是一个标准的 Spring Cloud 服务注册spring: application: name: gateway-service cloud: nacos: discovery: server-addr: 192.168.1.10:8848配置完成后启动网关服务在 Nacos 控制台的服务列表里就能看到 gateway-service 这个实例。这时候再去 Nacos 里看整个服务体系注册中心里躺着的就是网关 用户 课程 订单 支付 学习这几个服务。它们的名字要提前规划好因为后面所有路由规则都依赖这些服务名。这里要特别说一句路由的 uri 里的服务名必须和 Nacos 里的服务名完全一样大小写都要精确匹配。我们当时就因为 Nacos 里服务名是 CourseServiceuri 里写成了 course-service结果网关怎么都转发不过去一直报找不到实例。后来把服务名统一改成小写横杠风格才解决。2.4 健康检查和负载均衡的一次实际验证配置完成后建议做一次转发验证确认网关走的是 Nacos 的服务发现而不是本地写死的地址。我们当时的验证方式是先后台启动两个 course-service 实例一个端口 8081一个端口 8082两个实例的接口分别打印各自的端口号。然后用网关地址连续请求 /api/course/list 十次观察结果是否在两个实例之间交替返回。分发结果大致是这样的请求次数被调用的实例1808128082380814808258081680827808188082看到请求在两个实例之间轮询分发说明 Gateway 从 Nacos 拿到实例列表之后负载均衡已经生效。这一步验证特别重要因为它证明网关和注册中心的联动是通的而不是自己在某个瞬间碰巧能转发。3. 全局过滤器把登录校验从各个服务里抽出来网关最核心的价值之一就是把那些横跨多个服务的通用逻辑集中到网关这一层来处理。其中最有代表性的就是登录校验。在线教育网站里登录、注册、课程浏览这些接口对外是开放的但下单购买、获取学习记录、修改个人信息这些操作必须登录后才能访问。如果每个微服务各写一份校验逻辑不仅代码重复一旦校验逻辑要变就得挨个服务改一遍极其痛苦。3.1 GlobalFilter 的 JWT 统一校验实现我们的方案是在网关里写一个全局过滤器拦截所有经过网关的请求对需要登录的路径做 JWT 校验。核心逻辑是这样的前端登录成功后后端返回一个 JWT 字符串前端存在本地后续每次请求在 Header 里带上 Authorization: Bearer 这个 Token网关的全局过滤器拦截请求先从 Header 里取出 Token再去验证签名和过期时间。具体实现采用 Spring Cloud Gateway 的 GlobalFilter 和 Ordered 接口Component public class AuthGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String path request.getURI().getPath(); // 白名单路径直接放行 if (allowPass(path)) { return chain.filter(exchange); } // 从请求头中获取认证信息 String token request.getHeaders().getFirst(Authorization); if (!checkToken(token)) { ServerHttpResponse response exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } return chain.filter(exchange); } }这里要重点说说白名单路径的处理。在线教育项目里有些接口是所有人都能访问的比如查看课程列表、搜索课程、获取课程详情。这些请求如果也强制校验 Token游客用户连浏览课程都做不到业务上就说不过去了这跟在门口装个闸机却把所有人卡住没什么区别。白名单路径我们用了一个列表来管理private boolean allowPass(String path) { if (path.startsWith(/api/user/login) || path.startsWith(/api/user/register)) { return true; } if (path.startsWith(/api/course/list) || path.startsWith(/api/course/detail)) { return true; } return false; }3.2 Token 校验的几个容易漏掉的边界JWT 校验不是简单判断一下字符串非空就行我们实际踩过不少坑挑三个最有代表性的说一下。第一个是 Token 过期问题。JWT 默认生成的过期时间如果太长用户登录一次就一直有效安全性不够太短用户操作到一半就提示登录超时体验又很差。我们最终把 Token 有效期设为 2 小时同时在登录接口登录成功后同时返回一个 refreshToken7 天有效网关校验 Token 过期时不会直接拒绝而是看请求头里有没有刷新令牌有的话调用用户服务续期再放行。第二个是 Token 里包含的用户信息。我们在生成 JWT 的时候把用户 ID、昵称、角色这些信息放进 Token 的 payload 里网关校验通过后把这些信息解析出来并写入转发的请求头比如 X-User-Id、X-User-Name。后端服务拿到这些请求头就能识别用户不用自己去解析 Token。这步非常实用。因为微服务各写各的如果在用户服务里生成 Token 时放进了用户 ID但订单服务不解析 Token 就不知道当前用户是谁。网关统一解析把用户身份塞进请求头透传给后端每个服务直接用请求头里的 X-User-Id 往下走流程比各自解析方便太多。第三个是校验失败时的响应格式。网关返回 401 的时候前端拿到的是一个纯状态码没有统一的结构化错误信息前端处理起来很别扭。后来的做法是统一返回一个 JSON 格式的错误响应{ code: 401, message: 登录状态已过期请重新登录, data: null }这么做的原因是 Vue 前端拿到的响应结构永远是统一的无论成功失败都有 code、message、data 三层结构前端只需要判断 code 就能走完所有逻辑。3.3 网关里做鉴权时不能和 Servlet 思维混淆写 Gateway 过滤器的时候很多人会下意识按照以前写拦截器、写 Servlet Filter 的习惯去写很容易写到一半发现某些 API 根本不存在。这是因为 SpringCloud Gateway 底层不是传统的 Servlet 模型而是基于 Spring WebFlux 的响应式模型。整个请求处理过程是异步的、非阻塞的API 风格也完全不一样。具体表现在哪里呢比如想获取请求参数Servlet 里用 HttpServletRequest 就能拿到但在 WebFlux 里没有这个东西要用 ServerWebExchange 和 ServerHttpRequest。再比如要读取请求体内容因为请求体在响应式流里不能像以前那样直接 getInputStream() 一行行读需要借助 DataBuffer 和缓存机制才能拿到。这个认知差异不仅影响过滤器编写也影响后续排错。我们在排查一些奇怪问题的时候很多思路都会往响应式模型这个方向去想而不是套用以前的 Servlet 经验。4. CORS 跨域与前端联调Vue 项目接入 Gateway 的实战细节后端网关搭好了前端 Vue 要接上来这中间最容易出问题的就是跨域。前后端分离项目的经典痛点前端跑在 8080 端口网关跑在 8088 端口浏览器直接发请求必然触发跨域拦截。4.1 从前端代理到网关统一处理跨域网上很多教程解决跨域的办法是在 Vue 的 devServer 里配置 proxy 代理把 /api 开头的请求代理到后端地址。这个方案在本地开发没问题但有个隐患生产环境构建之后如果需要继续用代理就得依赖 Nginx 转发多出一个中间环节。而且如果公司内部有多个前端团队、多个项目每个项目都要在各自的环境配置里去写代理规则重复劳动实在太多。我们的做法是网关层统一处理跨域前端不用管代理所有环境开发、测试、生产都直接请求网关地址跨域问题在网关层解决。具体做法是在 Gateway 里配置全局 CORSConfiguration public class CorsConfig { Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsWebFilter(source); } }配置写了三个星号允许任意来源、任意方法、任意请求头。从功能角度这样最简单但安全性上有点粗糙。编码规范上建议用具体的域名而不是 *尤其配置了 allowCredentials 之后浏览器对通配符来源的处理会更严格。我们后来是把生产环境的前端域名加了白名单开发环境才用通配。4.2 前端 axios 的统一请求配置Vue 项目里我们把网络请求封装成了一个统一的 request 文件核心就做了三件事设置基础地址、自动携带 Token、统一处理响应。前端代码的请求封装大致长这样import axios from axios const service axios.create({ baseURL: process.env.VUE_APP_GATEWAY_URL, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { return response.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default servicebaseURL 从环境变量里取不同环境配不同网关地址这个非常关键。不然每次切换环境都要改代码重新构建效率太低。401 的处理也统一扔在这里跳回登录页同时清掉本地失效的 Token。这个逻辑在多个页面里都要用收拢到一个地方的体验非常好。4.3 联调时遇到的三个跨域肉疼问题问题一预检请求处理不当。我们的支付接口通过网关转发后返回 200但前端浏览器控制台一直报跨域错误。查了半天的原因是网关的 CORS 配置对整个请求路径生效了但支持 CORS 的过滤器在实现的时候对复杂请求的预检 OPTIONS 请求没有正确响应。后来把 CorsWebFilter 配置改成了显式支持 OPTIONS 方法并在全局过滤器里把 OPTIONS 请求直接放行问题就消失了。问题二traceId 和自定义请求头泛滥。某些内部调试的请求头在跨域时被浏览器拦截浏览器会认为这个自定义头超出了允许范围。需要把项目里用到的所有自定义请求头都在 allowedHeaders 里加进去否则前端带了也白带。问题三Cookie 跨域。如果后续业务想用 Cookie 来维持会话除了 allowCredentials(true) 之外前端的 axios 也要配置 withCredentials: true同时允许的 Origin 不能是 *必须精确指定。这个坑一时半会儿不会踩到但要心里有数。5. 网关限流与超时配置削峰填谷和系统保护在线教育网站的业务特点是明显的波峰波谷。晚上八点到十点用户集中看课、下单白天上班时间访问量少。如果所有流量都直接打到后端的数据库和业务服务上高峰期服务压力会非常大。网关这一层的限流配置可以看作一道水坝把这些流量先削一削避免后端被瞬时高峰冲垮。5.1 RequestRateLimiter 令牌桶限流的落地SpringCloud Gateway 内置了 RequestRateLimiter 过滤器实现的是当年经典的令牌桶算法。令牌桶维护了一个令牌概念请求到达时先从桶里取一个令牌有令牌才放行。如果桶令牌空了请求直接被拒从而实现对流量速率的限制。使用这个过滤器需要 Redis 支持因为网关的限流数据比如某个用户的访问计数要存在 Redis 里多实例的时候能够共享计数器。依赖加好后路由配置里增加限流参数spring: redis: host: 192.168.1.20 port: 6379 cloud: gateway: routes: - id: course-route uri: lb://course-service predicates: - Path/api/course/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 key-resolver: #{userKeyResolver}这里两个核心参数的含义是replenishRate平均每秒允许的请求数相当于下一秒往桶里放的令牌数量。burstCapacity桶的最大容量代表短时间内允许的最大突发流量。比如我配了 replenishRate10、burstCapacity20意思就是桶里最多能积攒 20 个令牌突然来 20 个请求都能直接放行但如果持续有流量进来平均每秒最多放行 10 个。突发流量超过了桶里已有的令牌后续的请求就会被限流策略拦下来。key-resolver 是限流维度的关键。我们项目里做了一个简单的 KeyResolver根据请求里的用户标识来区分访问者没有登录的用户按 IP 区分Bean public KeyResolver userKeyResolver() { return exchange - { String userId exchange.getRequest().getHeaders().getFirst(X-User-Id); if (userId null || userId.isEmpty()) { String ip exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); return Mono.just(ip); } return Mono.just(userId); }; }这个做法有一个立竿见影的效果单个用户每秒请求次数被限制住以后恶意刷接口的请求根本到不了后端服务就被拦在门口。课程详情接口之前被爬虫高频访问加了限流之后带宽资源和数据库压力都明显缓解。5.2 网关转发超时和重试设置网关转发到微服务如果后端处理得慢前端一直等就是很糟糕的体验。所以我们给网关设置了转发超时核心参数就两个connect-timeout连接超时和 response-timeout响应超时。spring: cloud: gateway: httpclient: connect-timeout: 3000 response-timeout: 5s连接超时用来管连不上的情况服务挂了、地址不对、网络不通3 秒没连上就直接失败不用傻等。响应超时用来管连上了但迟迟不返回的情况接口 SQL 写得烂、线程阻塞5 秒没返回直接断。重试机制我们一开始也没加直到有一次订单服务因为数据库连接被占满连续几分钟内返回了一批超时错误前端用户就看到一整段时间的报错。后来给网关配了重试策略spring: cloud: gateway: routes: - id: order-route uri: lb://order-service predicates: - Path/api/order/** filters: - name: Retry args: retries: 2 statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE重试次数设置的 2重试对象是 502 和 503 这类网关常见的错误状态码重试机制发起重试时 Gateway 会重新走一次负载均衡选实例的逻辑。不过这里要小心重试只建议对 GET 等安全请求启用POST 下单这类写操作如果重试很可能导致用户被创建多笔订单。我们一开始没意识到这个把重试加在了支付回调接口上结果回调慢的时候重复下单出现了好几回后来才在 Retry 配置里限制了 method 为 GET。5.3 网关调用链路上的超时三兄弟实际调优过程中测试人员经常反馈接口响应慢到底是谁慢前端等了很久报超时是网关超时、后端服务超时还是红线这里我把整个链路上的超时配置拉一张表出来方便对照检查链路层位置超时配置前端请求Vue axiostimeout: 10000网关连接Gateway httpclientconnect-timeout: 3000网关响应Gateway httpclientresponse-timeout: 5s后端服务SpringCloud OpenFeignconnectTimeout: 3000, readTimeout: 5000这张表格的核心语义是越靠前的超时时间应该更长越靠后的超时时间应该更短。前端等待时间要覆盖网关和后端的全部耗时否则就会出现后端还在处理、前端已经放弃等待的情况。网关超时也要大于后端处理接口的时间否则后端即使能在 4 秒内返回网关在 5 秒后等不到也会报错。6. 上线后的排错实录Gateway 的坑和排查路径网关上线之后排错工作并没有随着上线而结束反而多了不少只有网关层才会出现的幺蛾子。这里挑几个实战中遇到的典型问题把排查思路完整地梳理一遍因为我发现很多问题其实很有共性排查思路是可以直接复用的。6.1 网关返回 503 Service Unavailable 的排查链路现象很直接前端请求网关间歇性返回 503过一阵又自己恢复。第一次遇到这个问题组里好几个人都以为是后端服务挂了但去 Nacos 控制台看所有服务都注册在线健康检查也都正常。排查的第一步我们打开了网关的 Debug 日志看看到底卡在哪一环。日志里出现了一行关键信息No instances available for course-service。这说明网关在路由转发时找不到可用的服务实例。为什么 Nacos 里明明有实例网关却找不到继续往下查发现两个问题叠加在了一起。第一个问题某些服务实例下线时没有正常反注册。服务部署时发生了强制杀进程实例挂了但 Nacos 注册中心里的临时实例没有及时清除还在假活状态。网关拿到注册列表去路由时选中的恰恰是这些已经死掉的实例。第二个问题Nacos 客户端默认的发送心跳周期和网关的负载均衡的更新机制之间存在时间差。服务实例挂掉之后Nacos 要确认心跳超时才会把它摘掉这个过程短则 10 秒长则 30 秒。在这段时间里Gateway 的负载均衡器持有的是一个过期的实例列表轮询到死实例就必然报 503。解决方式分两步。第一步给服务加优雅下线策略服务进程收到停止信号时主动反注册别等着 Nacos 去判断心跳超时第二步在网关的负载均衡配置里减小对服务列表的刷新间隔让网关更快感知实例变化。6.2 同一个接口走网关报错、直连服务正常的原因另一种很常见的情况是同一个接口使用 Postman 直连后端服务的端口一切正常但通过网关访问就返回异常结果。这种问题的本质是网关在转发过程中修改了某些请求内容。我们遇到过一次很典型的中文乱码问题。课程搜索接口传入中文关键词直连后端时返回结果正常但通过网关访问时搜出来的结果是乱码。排查到后来发现网关转发请求时把请求头 Content-Type 里的 charset 信息丢掉了后端收到请求后用默认编码解析请求体中文参数就变了形。解决办法是在网关的过滤器里转发请求时显式补充请求头ServerHttpRequest mutatedRequest request.mutate() .header(Content-Type, application/json; charsetUTF-8) .build(); exchange exchange.mutate().request(mutatedRequest).build();这类问题确实很隐蔽因为直连服务时后端能拿到精确的编码格式但走了网关链路多了一道传输动作编码信息就可能丢。所以碰到直连正常、过网关异常的接口先检查请求头和响应头有没有发生变化能省下大量排查时间。6.3 网关转发后请求参数丢失的元凶还有一个我们踩得很深的坑GET 请求带一个分页参数 pageSize直连服务能正确读取通过网关转发后后端拿到的 pageSize 变成了 null。最终的结论这是微服务网关最经典的一个坑路径重写过滤器 StripPrefix 和 RewritePath 的匹配规则写的不对。我们的配置里 StripPrefix1 是固定写法但某些接口的 Controller 映射路径和路由前缀有重叠一层前缀剥离之后路径对不上参数也跟着解析不出来。排查方式在网关层加了一个临时日志过滤器把交换对象的原始 URL 和最终转发的 URL 都打出来。日志显示前端请求 /api/course/detail?id5剥离前缀后变成 /course/detail?id5参数明明还在。但后端服务实际收到的路径变成了 /course/detail?pageSizenull。问题出在后面后来有人给课程服务的路由多加了一条 RewritePath 过滤器把 /api/course/(.*) 重写成了 /course/$1和前面的 StripPrefix 叠加导致原始请求的参数被重复处理。去掉那条多余的 RewritePath 之后参数恢复。这个教训告诉我们路由过滤器不要叠太多层StripPrefix 和 RewritePath 能用一个就绝不用两个实在要用也要准确理解每一步对 URL 的处理结果。6.4 网关集群部署时的一个配置陷阱后期流量大了之后我们决定把网关从单实例扩展成多实例部署搭了个最简单的两节点网关集群。原本以为只要加一台服务器就行结果上线后发现一个现象个别用户请求偶尔出现登录态丢失重新登录后过一会又丢失。查了很久最后定位到网关的会话保持问题。用户登录后 Token 是存在浏览器端的每次请求都会带上本来不该有会话问题。但我们的网关配置了基于 IP 的限流策略前端的请求在 Nginx 层做了负载均衡同一用户的请求被分到了不同网关实例上而限流计数器是按网关实例内存来统计的导致同一个用户在不同网关上看到的请求频率不一致个别实例触发了限流拒绝前端就误以为登录失效。解决思路是让 Nginx 层按照用户维度做会话黏着配置 ip_hash 负载均衡策略让同一个 IP 尽量走同一个网关实例。另外一个更彻底的方案是限流依赖 Redis 做集中存储而不是内存存储。这两个方案我们都实施了之后这个用户移失会话的问题就不再出现了。7. 网关上线后的日常扩展这些事可以继续做网关整合完毕只是第一步后面随着业务迭代网关还能承担更多职责这里说说我们后续陆续加上的几个实用功能。一个是统一的日志搜集。网关是流量的统一入口天然适合在这里记录每个请求的路径、耗时、状态码、用户信息。我们把网关层访问日志直接输出到日志平台后续做分析报表的时候直接从网关日志里出数据比从前端埋点收集靠谱得多。另一个是灰度发布的入口。我们做课程服务的版本迭代时希望先让一小部分用户试用新版本稳定之后再全量放开。这个需求如果只靠服务本身做很难因为要判断用户特征。借助网关就方便很多网关解析出请求里的用户信息根据特定规则把请求转发到不同版本的服务实例这就是在入口层做灰度分流。还有链路追踪的串联。网关转发请求时给每个请求分配一个链路 ID透传给下游服务后端服务也记录这个链路 ID后续排查问题就能把网关、服务、数据库整个调用链串起来看。我们用的是 Spring Cloud Sleuth 加 Zipkin 这套组合接入的时候网关里要做的只是把链路信息放进请求头下游服务自动就传递了。这些都是把网关用好之后的自然扩展。网关的设计本质就是流量入口加策略层路由转发只是它的基本盘真正有价值的是在入口层把鉴权、限流、日志、灰度这些横切能力都沉淀下来。我在这个项目里最深的体会是网关整合的难点从来不是配置本身而是对整个微服务架构的理解。路由要清楚每个服务拆在哪、路径怎么规划才合理过滤器要清楚哪些逻辑该在这一层做、哪些必须留给服务层排错要清楚整条链路里的每一个环节从前端到网关再到后端服务中间任何一段都可能出问题。把这些想明白了Gateway 带来的收益才会真正体现出来。