SkyWalking + Spring Cloud Gateway 链路追踪接入与实战解析
发布时间:2026/10/17 12:18:53 锦皓数字建站

做网关链路追踪这件事我真正意识到SkyWalking的价值不是在看文档的时候而是在某次线上故障里对着Spring Cloud Gateway的日志无从下手的时候。一个入口请求究竟被路由到了哪个服务、中间调用了什么、耗时花在哪单靠网关日志和监控图表根本说不清楚。后来把SkyWalking接进Gateway才第一次看清了每条路由请求的完整行踪。这篇文章就围绕SkyWalking Spring Cloud Gateway这条主线把接入、Trace结构、自定义Tag、跨服务传递和典型问题一次讲透。1. 为什么要在网关入口做全链路观测1.1 一次请求在网关内部的黑盒Spring Cloud Gateway是所有后端服务的统一入口这是它的职责也是它最让人头疼的地方。一个请求到了网关网关要做断言匹配、过滤器链处理、路由转发、响应回写。外部看就是进去了、出来了、状态码200或500但中间发生了什么其实是黑盒。我记得某次线上排查A服务调用方反馈某个接口变慢了从每秒5000请求直接跌到3000。网关Grafana面板上能看到流量下跌、P99从200ms升到1.2s但没有一个图表能告诉我慢在哪。日志里只有access log能看到路径、状态码、耗时但看不到内部进一步拆分——到底是网关过滤器慢还是下游服务慢还是网络来回慢。登录下游服务器一条条翻日志用时间戳手动对折腾了大半天才隐约怀疑是某台实例Full GC。事后复盘如果当时已经接上SkyWalking打开Trace面板几秒钟就能锁定是哪个Span耗时异常。这不是个例。网关层面的可观测性盲区本质上是指标告诉你坏了但链路告诉你为什么坏。指标能反映趋势日志能记录事件但只有Trace能把一次请求经过的所有节点串成一条有因果关系的时间线。1.2 指标、日志和Trace三者的分工很多团队在网关已经配了比较完整的指标监控和日志采集仍然会漏掉链路追踪原因是觉得重复。实际上三者完全不冲突指标适合做告警和趋势判断比如QPS、P99、错误率。它回答的是现在系统是否健康。日志适合事后翻查回答某个具体时间点发生了什么。Trace适合聚焦单个请求回答这个请求完整经过了哪些环节每一环花了多少时间。在网关场景里Trace还有一个额外价值因为网关是入口它天然是跨服务链路的起点。只要网关这一层能拿到全局TraceId再把TraceId一路传到下游后面所有微服务的日志、性能数据就能围绕同一条请求线关联起来。这是单独看网关日志做不到的。1.3 SkyWalking对Gateway场景的适配逻辑SkyWalking之所以适合做这件事倒不是因为功能花哨而是因为它能用Java Agent以字节码增强的方式接入业务代码基本不用改。对于Spring Cloud Gateway这种基于WebFlux和Reactor Netty的组件SkyWalking提供了专门的插件支持能理解Gateway的异步反应式模型而不是简单地把它当成普通HTTP服务处理。这里有个容易搞混的点Spring Cloud Gateway不是传统Servlet应用它是响应式编程模型线程模型和Tomcat完全不同。同一个请求的处理可能跨越多个Reactor线程ThreadLocal这一套在反应式环境下并不好使。SkyWalking为Reactor提供了专门的上下文传播机制这也是它能真正追踪到Gateway路由请求的关键。后面我会专门讲这个坑。2. 接入前的准备版本、插件与最小启动配置2.1 Agent、OAP与Gateway的版本匹配接入之前第一步不是下载Agent而是先确认版本关系。SkyWalking整体分三块Java Agent、OAP Server、UI。这三部分的版本最好保持一致比如都用8.9.1或9.x系列。混用版本不是完全不能跑但容易出现上报协议不兼容的怪问题排查起来非常痛苦。Spring Cloud Gateway本身也有版本差异。SkyWalking对Gateway的支持很早就有插件但不同版本插件的适配粒度不同。比如Gateway 3.x时代插件会单独识别Route、过滤器链等关键环节如果还在用Gateway 2.x建议去查当前Agent版本对应的插件列表。最稳妥的做法是打开agent/plugins目录确认是否存在apm-spring-cloud-gateway-3.x-plugin这类文件。没有对应插件Agent就算装上也只是把Gateway当一个普通WebFlux应用记录路由信息丢失很多。2.2 Gateway插件和WebFlux插件到底谁在干活这是接入时最容易被误解的点。Spring Cloud Gateway底层是WebFlux所以SkyWalking同时存在apm-spring-webflux系列插件和apm-spring-cloud-gateway系列插件。两者不是二选一的关系而是协同关系。按我的理解WebFlux插件负责WebFlux框架层面的入口Span、响应式线程上下文传播Gateway插件则在此基础上识别Gateway特有的组件比如路由过滤器链、Route信息。也就是说如果只加载了WebFlux插件而缺失Gateway插件你依然能在UI里看到一条TraceSpan基本完整但缺少Gateway路由层的细节自定义Route标签更加无从谈起。建议接入后先检查Agent日志查看插件加载情况。在logs/skywalking-api.log里一般能看到加载了哪些插件如果Gateway插件激活失败日志里通常会有对应提示。2.3 启动接入与验证的常用套路接入很简单不需要改业务代码启动参数加一个Agent即可。典型的启动命令如下java -javaagent:/opt/skywalking/agent/skywalking-agent.jar \ -Dskywalking.agent.service_nameapi-gateway \ -Dskywalking.collector.backend_service10.10.0.8:11800 \ -jar gateway.jar几个参数说明一下-javaagent指向Agent Jar路径。注意路径里的空格要处理生产环境建议把Agent放到无空格的目录。service_name是服务在SkyWalking里的名字建议与Spring Cloud应用名区分开比如统一叫api-gateway方便拓扑图里识别。collector.backend_service是OAP的gRPC地址。以前老版本用的是backend_service环境变量8.x之后逐步改成collector.backend_service参数名别搞混。也可以把这些配置写进agent/config/agent.config但命令行参数优先级更高适合不同环境差异化配置。启动之后验证三件事OAP控制台或UI上能看到api-gateway实例心跳。Agent日志无明显报错。手动请求一个经过Gateway的接口UI的追踪页面能查到对应Trace。我习惯先不接下游服务只让Gateway自己单跑把入口Trace确认干净了再逐步接入下游这样问题边界清晰。3. 读一条真实Trace路由请求的Span长什么样3.1 入口Span与网关内部Span接入生效后到SkyWalking UI的追踪页面搜一个请求比如GET /api/orders展开后是一条Span树。最上面一层通常是入口Span标签会显示HTTP方法、URL、状态码。Gateway插件还会加上部分内部组件信息。如果请求在网关内部就被拦截比如断言失败、过滤器抛异常Trace会停留在Gateway内部Span数量很少。这种情况下重点看异常信息存放在哪个Span的Log里SkyWalking会把异常堆栈自动附加到对应Span上。我遇到过很多次网关层面直接返回401或404业务侧又不打印日志的情况这时Trace里的异常信息几乎是唯一线索。3.2 从Gateway到下游服务的跨进程Span一次典型的成功路由转发Trace树会包含下游服务的那一段。以api-gateway为起点树下面挂着一个对order-service的HTTP调用SpanPeer地址是下游服务实例地址。如果下游也是Java服务且接了Agent那么这个Span下还会继续展开下游服务内部的数据库调用、Redis访问等更多子Span。在拓扑图上Gateway节点和下游服务节点之间会出现一条连线连线上的数字通常代表调用次数和平均耗时。如果看到连线说明跨进程链路已经打通如果看不到连线说明下游服务没接Agent或者传播Header在中间被丢掉了。3.3 UI读图时如何快速定位慢在哪一环读Trace最忌讳看整条耗时。比如整条Trace耗时1秒先看父Span与子Span的耗时占比层常见耗时来源排查方向入口Span自身耗时长网关过滤器、线程切换、序列化检查自定义GlobalFilter分析网关CPU/GC下游Span自身耗时长下游业务逻辑、数据库查询打开下游服务独立Trace网络耗时占比高跨机房、多跳路由检查实例分布和网络链路子Span缺失Agent插件未覆盖、异步丢失排查插件加载和线程上下文传播实际操作中我一般先看自持时间(Self Duration)也就是当前Span总耗时减去子Span耗时。如果网关入口Span自持时间占大头问题十有八九在Gateway内部如果下游服务Span自持时间占大头重点去查下游应用。这样三分钟就能把责任边界划出来。4. 让Trace说清楚路由到了哪里自定义Tag与日志联动4.1 默认信息有哪些缺口SkyWalking默认记录的HTTP Span信息主要是方法、URL、状态码足够回答慢不慢但不够回答这个请求被路由到了哪个后端。尤其在网关存在几十个Route的场景里只看默认Tag你只能看到一个归一化路径类似/api/{id}无法知道实际命中哪个Route、转发目标URI是什么。解决办法很简单在Gateway的GlobalFilter里读取Route信息再通过SkyWalking Toolkit把关键信息写入当前Span的Tag。这样每次查Trace时只需看Tag就能还原来自哪个入口、命中哪个路由、转发到哪个地址。4.2 通过GlobalFilter给当前Span打上Route标签先引入Toolkit依赖版本与你用的Agent主版本对齐dependency groupIdorg.apache.skywalking/groupId artifactIdapm-toolkit-trace/artifactId version${skywalking.version}/version /dependency再写一个过滤器Component public class RouteTagGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { Route route exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); if (route ! null) { Span span TracerManager.INSTANCE.getActiveSpan(); if (span ! null) { span.tag(gateway.route, route.getId()); span.tag(gateway.target, route.getUri().toASCIIString()); span.tag(gateway.request.id, exchange.getRequest().getId()); } } return chain.filter(exchange); } Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }这里有两个细节需要说明exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR)是获取当前路由的推荐方式。这个属性在Gateway完成路由匹配后就会放到Exchange里所以在GlobalFilter里能拿到。TracerManager.INSTANCE.getActiveSpan()取当前线程的活跃Span。由于Gateway请求处理初期入口Span已经创建这个调用在多数情况下是有值的。如果遇到为空的情况通常是Reactor线程切换导致后面的坑位会专门分析。Tag命名建议加上gateway.前缀这样与框架自带的http.method、url、status_code放在一起时一眼就能分辨查询时也更方便。4.3 把TraceId印进Gateway业务日志只把TraceId留在SkyWalking UI里还不够排查问题的时候大家最习惯的还是先看日志。如果日志里没有TraceId登录到不同机器上翻日志时依然没法把同一请求串起来。最简单的方式是使用SkyWalking Toolkit的Logback适配在日志格式里增加TraceId占位符。以Logback为例appender nameSTDOUT classch.qos.logback.core.ConsoleAppender encoder classch.qos.logback.core.encoder.LayoutWrappingEncoder layout classorg.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{tid}] %-5level %logger{36} - %msg%n/pattern /layout /encoder /appender接入之后每行日志都会带上[TraceId]可以和SkyWalking UI里的Trace ID直接对应。我建议网关的访问日志也尽量带上这个TraceId做法是在GlobalFilter里异步读取响应状态后打印一条摘要日志把状态码、耗时、Route标签和TraceId放进同一行。4.4 采样率与健康检查路径的收敛网关流量大全量采样在存储上是很贵的。而且像/actuator/health、/metrics这类探活请求根本没有追踪价值会污染Trace列表。Agent配置里可以设定采样率agent.sample_n_per_3_secs每3秒采样N条。-1表示全采样但网关环境我通常不建议直接全采。agent.ignore_url忽略指定路径多个路径用逗号分隔。我的经验是先全量跑一个压测周期确认所有关键路径的Trace都正常再收敛采样率同时一定把健康检查类路径加进忽略列表否则页面会被探活请求刷屏真正需要关注的慢请求反而不容易找到。5. 跨服务链路是如何自动接上的sw8协议与下游衔接5.1 sw8消息头里装了什么SkyWalking跨进程传播不是靠把TraceId拼在URL上而是通过HTTP Header。不同版本协议名称有差异主流是sw8。这个Header里的核心内容大概包含协议版本、时间戳、全局TraceId、父SpanId、采样标记、Peer地址。它对业务代码完全透明Java Agent在发起下游调用时会自动把当前上下文写入Header下游服务接Agent的话也会自动解析Header并接续链路。对Gateway而言这非常关键。Gateway默认通过Reactor Netty转发请求Agent的HTTPClient插件会识别这次出站调用并注入sw8 Header。只要下游服务同样接了Agent你在UI上就能看到Gateway - 下游服务的完整链路而不需要任何手工传参。5.2 下游不是Java服务时怎么处理如果下游服务是Python、Go或者其他非SkyWalking Agent接入的技术栈自动传播就断了。这时可以在网关日志里输出TraceId让下游开发手动把这个TraceId写入自己的日志系统至少能做到两边日志可对齐。更精细的做法是在Gateway层解析sw8 Header取出其中的TraceId然后放到自定义请求头或者业务参数里传给下游。比如可以封装一个GlobalFilter从当前上下文中取出TraceId写入下游请求头String traceId TraceContext.traceId(); ServerHttpRequest newRequest exchange.getRequest().mutate() .header(X-Trace-Id, traceId null ? : traceId) .build();注意这属于补充传播并不能替代SkyWalking本身的跨进程协议。下游如果再接Agent建议还是以sw8为主避免两套Header互相干扰。5.3 关于外来sw8头的一个安全性提醒这一点很多人会忽略。网关是对外暴露的外部客户端如果知道sw8的构造规则可以自行伪造一个Header让一个请求冒充某个已有TraceId或者把大量请求打到同一个TraceId下面造成链路数据污染。我见过的做法是在网关最外层把上游传来的sw8 Header清掉让Agent只信任自己创建的上下文。实现上可以借助ServerHttpRequestDecorator移除指定Header或者在前面一层代理直接剥离。工程上不一定每个团队都做但网关这类流量入口最好还是处理一下反正成本不高。6. 接入网关后最容易踩的几个坑6.1 Trace断在Gateway不再往下走这是最典型的问题Trace在Gateway能看到但下游服务页面上没有对应Span。按下面步骤排查先确认Gateway确实把请求发出去了。看下游访问日志如果有请求且状态正常说明不是转发问题。检查Agent是否有报错尤其关注Ignored trace、Async相关日志。确认下游服务已经接入Agent并且上报到了同一个OAP集群。很多人把Gateway接进A环境下游接进B环境自然看不到。确认Gateway使用的HTTP客户端类型。推荐使用Spring WebClient或Netty默认转发链路如果业务代码里自己new了一个HttpClient、OkHttpClient或版本太新的客户端插件不一定会自动注入sw8头。我排查过一起案例最后发现是下游服务Agent的service_name配置重复两个实例共用同一个服务名UI上只显示一个节点看起来像Trace断掉了实际上只是节点合并了。6.2 自定义Filter里拿不到当前Span有同学照着我上面的代码写了RouteTagGlobalFilter结果getActiveSpan()返回nullTag打不进去。这种现象多半发生在Reactor线程切换之后。SkyWalking虽然为Reactor提供了上下文传播但也不是万能的尤其遇到publishOn、subscribeOn切换线程池时ThreadLocal里的上下文可能在切换点丢失。几个有效对策尽量在请求入口后面的第一次过滤器执行中读取Span不要在异步回调里再读。如果做的是响应式链路上的回调优先用exchange属性传值而不是依赖上下文中当前Span永远可用。确认Agent加载了Reactor插件。查看plugins目录里是否有apm-reactor-3.x-plugin这一类文件没有就补上。如果自定义Filter自己创建线程池需要自己负责上下文传递比如先用外层Context做快照再在子线程里恢复。这一块没有万金油写法本质是让自己记住反应式编程下上下文不是自动跟着对象跑的它需要框架插件帮忙搬运。6.3 改启动参数后链路全黑的排查过程有一段时间我们Gateway集群某几个节点突然在UI上消失Trace也搜不到。第一时间怀疑Agent和插件但其实只是启动脚本调整时引入了两个参数问题一个是-Dskywalking.collector.backend_service写错了OAP地址Agent连不上后端本地日志里全是连接超时。另一个是多个实例复用了同一个Agent目录导致Agent日志、缓存文件互相覆盖实例标识混乱。后来把Agent目录按实例独立化并且启动脚本里用变量显式注入服务名和OAP地址问题不再出现。这里提醒一点SkyWalking的service_name如果配错成同一个UI拓扑会把多个实例合并成一个节点排查时非常容易误判。6.4 特别针对网关流量特点的落地建议按我实际接入的经验给网关做链路追踪最忌一把梭全量采样。网关是流量入口QPS天然高全量Trace对存储和OAP压力都很明显。可以先按场景分批开放第一批只采集核心交易链路相关Route忽略健康检查、静态资源、探活。第二批加上自定义Tag观察RouteID和target分布。第三批再考虑日志TraceId联动和告警。其次建议把TraceId日志联动放在接入当天就做完。链路追踪工具再直观最后落到排查行动上还是要靠日志系统。两者打通价值才会翻倍。我在实际运维中最常用的一条线索就是拿SkyWalking UI里的TraceId回到Gateway日志里看同一行在那一瞬间还发生了什么。有了这个闭环网关才真正变成可观测的入口而不是又一个数据孤岛。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。