先讲一段真实经历。当时我负责的SpringBoot服务已经拆成了五六个微服务,某天用户反馈下单很慢,但每台机器上的日志都干干净净没有任何异常。我把网关、订单、库存、用户服务各自的日志文件按时间戳一条条对齐,来回翻了快一个小时,才在一堆无关日志里发现是某个第三方渠道的接口超时。如果当时系统里有链路跟踪,从拓扑图上点一下,三五分钟就能锁定问题。那次之后我就下定决心,必须把Skywalking用起来——无侵入、Java Agent方式接入、SpringBoot项目几乎零改造,适合绝大多数微服务团队上手。
这篇聊聊SpringBoot集成Skywalking链路跟踪的完整过程,包括环境部署、Agent挂载、Dashboard数据解读、生产落地配置和我在实际接入中踩过的坑。内容偏向实操,适合正在给SpringBoot项目做可观测性建设,或者被"接口慢但查不出在哪一环"折腾过的同学参考。
1. 微服务慢请求排查之痛:为什么我最终选了Skywalking
1.1 一次线上问题,让我意识到日志对齐的极限
那次线上排查让我印象特别深刻。用户反馈下单慢,但单次接口的耗时分布看起来并没有明显恶化,只是有小部分请求超过了3秒。我们的微服务链路是网关 -> 订单服务 -> 库存服务 -> 用户服务,中间还穿插了Redis缓存和几个外部HTTP调用。每个服务都打了日志,但每个服务的时间戳都有几毫秒到几十毫秒的偏差,请求在服务之间传递时也没有一个全局唯一的标识,全靠订单号去各个日志文件里grep。
更麻烦的是,那个慢请求并不是每次都会出现,偶发性能问题在日志里往往表现为"查不到异常,但就是慢"。我把四个服务的日志拉到一起,对着时间戳画时间线,画到第二页纸的时候才猜到了可能是第三方渠道超时。事后我就在想,如果有一套工具能自动把一次请求跨服务的调用关系画出来,把每个环节的耗时标出来,这个排查时间能压缩到原来的十分之一。这就是链路跟踪要解决的核心问题。
1.2 Skywalking的技术定位:无侵入的APM系统
链路跟踪的概念大家都不陌生:一次请求从入口开始,经过网关、各个微服务、数据库、消息队列,每一步都记录下来,形成一个完整的调用链路。但实现方式差别很大。
有些方案需要业务代码里手动埋点,在关键方法上加注解、在调用处传上下文,侵入性很强,老项目改造起来简直是噩梦。Skywalking走的是另一条路:Java Agent技术,利用字节码增强,在JVM加载类的时候动态改写字节码,自动拦截HTTP请求、数据库访问、消息队列等常见组件的调用,不需要修改一行业务代码。
对SpringBoot项目来说,这个优势非常关键。业务代码零改动,只需要在启动JVM时加一个-javaagent参数,应用就像什么都没发生一样继续跑,但Skywalking已经在后台把所有关键调用链路记录下来了。对于已经上线、不方便大改的老项目,这是最友好的接入方式。除了链路追踪,Skywalking还包含指标监控、拓扑分析、告警等功能,定位上属于完整的APM系统,不只是"链路跟踪"这一个点。
1.3 对比Zipkin和Pinpoint,选型结论
我在选型时重点看了三个开源方案:Zipkin、Pinpoint、Skywalking。简单对比一下各自的差异。
| 对比项 | Zipkin | Pinpoint | Skywalking |
|---|---|---|---|
| 接入方式 | HTTP/消息中间件上报,需配合Brave等埋点 | Java Agent字节码增强 | Java Agent字节码增强 |
| 代码侵入性 | 需要引入依赖并配置埋点 | 无侵入 | 无侵入 |
| UI能力 | 偏链路查询,功能单一 | 拓扑图、调用链、状态面板丰富 | 拓扑图、调用链、指标、告警、性能剖析都有 |
| 存储依赖 | MySQL/ES/Cassandra | HBase | H2/ES/MySQL等 |
| 学习成本 | 低 | 中 | 中 |
| 社区活跃度 | 一般 | 一般 | 活跃,国内使用广泛 |
Zipkin胜在轻量,但功能边界比较窄,基本就是一个Trace查询工具;Pinpoint功能很强,Agent做的很细,但依赖HBase存储,运维成本不低,而且有时候Agent对字节码的改动太激进,出现过跟业务框架冲突的情况。Skywalking各方面比较均衡:Agent插件机制灵活,OAP支持多种存储,UI开箱即用,再加上中文文档和社区都比较完善,所以最终选了它。
2. 环境准备:把Skywalking OAP与UI先跑起来
2.1 版本到底选8.x还是9.x
第一次安装Skywalking的人最容易忽略一个点:OAP服务端和Agent的版本兼容性。直接说结论:
- SkyWalking 8.x:OAP要求JDK 8+,对老环境友好。
- SkyWalking 9.x:OAP要求JDK 11+,UI和OAP的部署方式有一些调整。
如果你生产环境的SpringBoot应用是JDK 8,不用担心Agent侧,Agent本身是JDK 8编译的,支持在JDK 8~17的JVM上运行。需要关注的是OAP服务端所在机器的JDK版本。如果你手头只有JDK 8的机器,就老老实实用8.x的Agent和OAP;如果能提供JDK 11+,直接用9.x也是顺理成章的选择。
我的建议是:Agent和OAP尽量用同一个版本,至少大版本保持一致。跨大版本连接时,gRPC接口的兼容性虽然官方会尽量保证,但谁也不想在排查问题的时候先排查监控系统本身,所以安装时直接从官网下载同一个发行包,Agent就用包里的那个agent目录。
2.2 解压后的目录,每个角色负责什么
下载发行包解压后,目录结构是这样的:
apache-skywalking-apm-bin/ ├── agent/ # Java Agent,用于挂载到SpringBoot应用 ├── bin/ # OAP和UI的启动脚本 ├── config/ # OAP的配置文件(application.yml、alarm-settings.yml) ├── oap-libs/ # OAP服务端的依赖库 └── webapp/ # UI前端应用Agent、OAP、UI三个角色各管一摊:Agent部署在业务应用里,负责采集数据;OAP Server负责接收Agent上报的数据、分析计算,并写入存储;UI负责把数据可视化,供人查看。发行包里这几个组件都齐了,一套包就能把环境搭起来,这也是Skywalking上手快的原因之一。
2.3 启动OAP:默认H2存储与切换Elasticsearch
OAP的配置在config/application.yml里,核心两部分:端口和存储。
如果是本地调试或者小型项目,OAP默认用H2存储,开箱即用,不用装任何数据库。启动前先看一下端口配置:
core: default: gRPCPort: 11800 # Agent上报数据的gRPC端口 httpPort: 12800 # UI和后端查询接口使用的HTTP端口这两个端口要记住:11800是给Agent上报用的,12800是给UI查询用的。生产环境建议把存储切换到Elasticsearch,改动配置里的存储选择器:
storage: selector: ${SW_STORAGE:elasticsearch} elasticsearch: clusterNodes: ${SW_STORAGE_ES_CLUSTER_NODES:localhost:9200} namespace: ${SW_NAMESPACE:""}切换存储的原因很现实:H2是嵌入式数据库,不适合保存大量历史监控数据,而且查询性能在多服务长链路场景下会明显下滑。Elasticsearch本身就是为检索和分析而生的,监控数据持续采集、按时间范围聚合查询的场景跟它非常匹配。小团队一开始用H2跑POC完全没问题,但在准备让Skywalking承担真正的生产可观测性之前,尽早切到ES是更稳妥的路线。
2.4 启动UI并验证两个端口
在bin目录下执行启动脚本,可以同时启动OAP和UI:
bin/startup.sh也可以分开启动:
bin/oapService.sh bin/webappService.shUI默认端口是8080,配置文件在webapp目录下(不同版本文件名可能是application.yml或webapp.yml)。如果UI和OAP不在同一台机器,需要把UI配置里的OAP地址指向真正的OAP服务器,默认是http://localhost:12800。
启动完成后,浏览器访问http://localhost:8080就能看到Skywalking的首页。此时系统里还没有任何业务数据,页面是空的,这正常。先用下面的命令确认服务都活过来了:
netstat -tlnp | grep -E "11800|12800|8080"看到这三个端口都在监听,说明OAP的gRPC、OAP的HTTP、UI三个服务都已经就绪,可以开始接应用了。
3. SpringBoot挂载Skywalking Agent的三种姿势
3.1 IDEA本地调试:VM options里加一行就够
开发调试阶段就想看链路的话,用IDEA跑SpringBoot最方便。打开Run/Debug Configurations,找到你的SpringBoot启动类,在VM options里加一行:
-javaagent:/path/to/apache-skywalking-apm-bin/agent/skywalking-agent.jar -Dskywalking.agent.service_name=order-service -Dskywalking.collector.backend_service=127.0.0.1:11800几个参数说明一下:
-javaagent指定Agent入口jar的路径,注意要指向agent目录下的skywalking-agent.jar,不能只复制这一个jar到别处,因为Agent的插件和配置都在agent目录里,它们要靠这个路径定位。-Dskywalking.agent.service_name是服务名,会显示在Skywalking的拓扑图和Trace列表里,建议跟SpringBoot应用名保持一致。-Dskywalking.collector.backend_service是OAP的地址和gRPC端口,默认值是127.0.0.1:11800,本地调试不用改,但如果OAP在远程机器上,这里改成OAP服务器IP:11800。
配置好后正常启动SpringBoot,应用的日志和业务功能不会受任何影响。这时候去Skywalking UI的服务列表里刷新一下,应该能看到你刚定义的服务名出现了。
3.2 生产部署:JVM启动参数挂载全流程
生产环境本质上跟IDEA是一样的,只是把参数写到启动脚本里。假设Agent目录放在/opt/skywalking/agent,SpringBoot应用包是order-service.jar,启动命令这样写:
java -javaagent:/opt/skywalking/agent/skywalking-agent.jar \ -Dskywalking.agent.service_name=order-service \ -Dskywalking.collector.backend_service=10.0.0.10:11800 \ -Xms512m -Xmx512m \ -jar order-service.jar这里有两个我自己踩过坑后的习惯:
- 服务名一定不要带随机后缀,比如
order-service-01,同一个服务的多个实例应该共用同一个服务名。Skywalking的拓扑图是按服务名聚合的,如果你给每个实例起了不同的名字,拓扑图会变成一团乱麻。 backend_service不要配多个地址,除非你明确知道自己在做负载均衡,否则配多个地址反而可能让Agent在选择节点时出现奇怪的报错。按实际部署的OAP地址配一个就好。
3.3 Docker容器部署:挂载目录而不是打进镜像
现在很多SpringBoot应用都在Docker里跑,Agent的挂载思路要调整为"挂载目录,而不是把Agent打包进镜像"。
我的做法是把Agent目录放在宿主机上,启动容器时挂载进去,再用环境变量把参数传给JVM:
docker run -d \ --name order-service \ -v /opt/skywalking/agent:/opt/skywalking/agent:ro \ -e JAVA_OPTS="-javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.service_name=order-service -Dskywalking.collector.backend_service=10.0.0.10:11800" \ -p 8080:8080 \ my-registry/order-service:1.0.0不过要注意,这个方案要求你的镜像启动命令能识别JAVA_OPTS环境变量,具体看Dockerfile里ENTRYPOINT怎么写的。如果用的是官方openjdk镜像直接java -jar,那就需要把JAVA_OPTS手动拼进启动命令。还有一点,挂载Agent目录时用:ro只读模式,避免Agent在容器里写日志时产生奇怪的文件权限问题。
如果你的基础设施是Kubernetes,思路一样,把Agent目录放到一个共享存储或者直接打进sidecar镜像里,通过YAML里的command参数追加-javaagent。重点是保持一致:Agent和OAP版本一致,服务名规范一致,上报地址可达。
3.4 如何确认Agent已经生效
接完Agent后别急着看UI,先在应用侧确认Agent加载成功。SpringBoot启动日志里会有一段Agent输出的内容,类似:
SkyWalking agent started successfully.同时Agent的工作目录下会有logs目录,里面生成了skywalking-agent.log,这个日志记录了Agent加载了哪些插件、连接OAP的状态。如果Agent启动失败或者连不上OAP,先看这个文件。
然后是UI侧。等几十秒后去Skywalking界面,左侧菜单点服务列表,应该能看到你配置的服务名出现了。刚接入时服务列表可能只有一个节点、没有拓扑连线,这是正常的,等有业务请求进来后,调用关系才会慢慢生成。
4. 看懂Dashboard:服务拓扑与链路明细实战解读
4.1 拓扑图:服务之间的"地图"
一次接口调用从网关进来,经过订单、库存、用户等多个服务,Skywalking会把它们画成一张拓扑图。图中的每个圆圈代表一个服务节点,连线和箭头代表调用方向,线的颜色和状态代表着健康度。比如正常流量是绿色,出现错误率上升会变红,一眼扫过去哪里有问题就一目了然。
我第一次看拓扑图时有个直觉性的误解:以为它是实时动态的。实际上它展示的是选定时间范围内的聚合关系。假设你选了最近15分钟,那图上展示的就是这15分钟内所有服务间的调用关系、平均响应时间、吞吐量。看问题的时候先选对时间范围,再根据颜色异常的节点往下钻取,效率会高很多。
4.2 Trace明细:一次请求的完整拆解
拓扑图帮你看"哪个服务有问题",Trace列表则帮你看"这次请求到底经历了什么"。在追踪菜单里,每一行就是一条请求记录,关键信息包括服务名、接口路径、总耗时、状态码、开始时间。
点进某一条Trace,你会看到这次请求被拆成一段段Span,展开后就像看一个微型的时间轴:网关收到HTTP请求,网关调用订单服务,订单服务查Redis,订单服务调库存服务,每个Span都有开始时间、结束时间、耗时、操作类型,还能看到具体的SQL语句、HTTP URL、方法名。有时候别人问我"你们那个接口为什么慢",我已经养成了一个习惯:不猜,直接打开Skywalking,按TraceId搜一下,几秒钟就能看到瓶颈在哪个Span上。
这里有个很实用的技巧:在Trace列表页看耗时分布,如果大部分Span都很快,只有一个Span耗时特别长,那基本就是这个环节的问题。如果所有Span都慢,更可能是网络层的问题或者某个共享资源(数据库连接池、线程池)被占满了。不同类型的慢,排查思路完全不一样。
4.3 定位慢SQL和慢调用的实战思路
Skywalking对数据库调用有内置插件,MySQL、PostgreSQL、Redis这些常见组件都能自动识别。在Trace明细里,数据库访问会单独作为一个Span显示,里面有执行时间,还能看到SQL语句。
我实际排查过这样一个案例:一个列表接口在压测时偶发超时,接口本身逻辑很简单,就是一次数据库查询加一次Redis缓存。从Trace里看到Redis读取有时能到800ms,这就很反常了,因为单次Redis读取正常情况都是零点几毫秒。顺着这个线索查下去,发现是Redis连接池在部分场景下被慢查询占满了,请求都堆在池子里等连接。如果当时没有Skywalking,这种问题靠猜可能要猜很久。
还有一点值得注意:Skywalking的数据库Span里记录的SQL是Agent自动捕获的,包含参数值时可能有敏感信息。生产环境建议关注一下Agent的插件配置,必要时关闭SQL参数采集,避免敏感数据落到监控存储里。
5. 接入过程中的坑:从排查思路到解决方案
5.1 版本不匹配与数据空白的排查链路
我见过最多的问题就是:Agent配好了,SpringBoot也启动正常,但UI上就是什么都没有。
我的排查顺序是这样的:
- 先看Agent日志
skywalking-agent.log,确认Agent是否成功启动、是否成功连接到OAP的gRPC端口。如果日志里有Connection refused,说明Agent到OAP的网络不通,检查collector.backend_service地址和防火墙。 - 再确认端口。
11800是gRPC端口,Agent上报用的是它,不是12800。有同事把backend_service配成了12800,看起来"端口是通的",但Agent用gRPC协议连HTTP端口,根本注册不上。 - 然后看OAP侧日志。
logs/skywalking-oap-server.log里如果有报错,一般会直接指出问题,比如存储初始化失败、端口被占用。 - 最后看版本。Agent和OAP版本差太多,比如Agent是8.6,OAP是9.2,gRPC的接口定义变化可能导致Agent的数据上报被拒绝或解析失败。虽然日志里不一定有明显报错,但数据就是上不去。最稳妥的做法是同一个发行包里的Agent和OAP配套使用。
这条链路走完,95%的"数据空白"问题都能定位。
5.2 端口通信链路:11800和12800分别干什么
很多新手对Skywalking的端口职责含糊,我再说透一点。
11800:OAP的gRPC端口,Agent采集到的数据走这个端口上报。12800:OAP的HTTP端口,UI查询数据走这个端口,也开放给外部API调用。8080:Skywalking UI的页面端口。
正常部署时,业务应用所在机器只要能访问OAP的11800端口即可;UI所在机器只要能访问OAP的12800即可。安全组和防火墙的规则按这个逻辑来开,不要图省事把三个端口全放给所有机器。之前有一次我们自己搭测试环境,OAP和UI不在同一台机器,UI白屏打不开数据,排查了半天发现是UI机器访问不到OAP的12800端口,在云安全组里加上规则就好了。
5.3 异步线程链路断裂:@Async场景的修复
这个问题在SpringBoot项目里非常典型:主线程的链路是完整的,但一进入异步线程,Skywalking的Trace就断了,拓扑图上表现为一个服务调用了另一个服务,但调用链没有延续,TraceId也对不上。
原因不复杂:链路上下文默认存在ThreadLocal里,新线程不会继承主线程的上下文。项目里只要用了@Async、自建线程池、或者CompletableFuture这类异步手段,链路就可能在那个边界断开。
解决办法有两个方向。官方toolkit包里提供了包装好的线程池实现:
ExecutorService executorService = new TracingThreadPoolExecutor( 5, 10, 1000L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100));把原来的new ThreadPoolExecutor替换成TracingThreadPoolExecutor,任务提交时就会自动传递链路上下文,集成成本非常低。如果是Spring的@Async,可以在自定义的TaskExecutor里用这个包装。另一个方案是手动做上下文传递:在提交任务前用ContextManager.capture()抓取快照,在任务执行开头用ContextManager.continued(snapshot)恢复。这个方案灵活但侵入性稍强,适合无法替换线程池类型的场景。
我自己的经验是:与其到处缝缝补补,不如在代码规范阶段就定好规则,统一使用一个被TracingThreadPoolExecutor包装的线程池Bean,避免每个开发各自new线程池导致漏处理。
5.4 健康检查接口刷屏:用trace-ignore插件过滤
SpringBoot项目基本都会配健康检查、接口探活,/actuator/health这类路径每几秒就被探一次,如果它们进了Skywalking,Trace列表里会被大量健康检查请求占满,真正需要关注的业务请求反而被淹没了。
Skywalking有一个trace-ignore插件,专门干这个。默认插件不在启用列表,需要手动激活:
cp agent/optional-plugins/apm-trace-ignore-plugin-*.jar agent/plugins/然后在agent/config目录下新建apm-trace-ignore-plugin.config,配置你想忽略的路径:
trace.ignore_path=/actuator/health,/actuator/info,/actuator/metrics重启应用后,这些路径的请求就不会上报到OAP了。别小看这个配置,尤其在高频探活场景下,它能帮你省掉大量无用数据的存储和展示开销。
6. 工程化落地:采样率、日志关联与告警
6.1 采样率配置:从全量采到按比例采
Skywalking默认是100%采集所有链路。流量小的项目没问题,但一旦QPS比较高,全量采样对存储和OAP性能的压力会迅速增长。这跟业务系统做日志采集是同一个道理,监控数据也要控制成本。
Agent的采样率在agent/config/agent.config里配置:
agent.sample_ratio=${SW_AGENT_SAMPLE_RATE:1}这里sample_ratio的值不是百分比,而是分母。默认为1,表示每1条Trace采1条,即全量采样;改成1000,表示每1000条采1条,也就是千分之一采样率。生产环境可以根据业务量和监控需求来权衡,一般的经验是:核心链路和排查高频问题的服务可以适当提高采样率,非核心服务用千分之一甚至万分之一都够。
不过要注意,采样率是Agent本地决定的,OAP端无法干预。调整配置后必须重启应用才能生效。
6.2 日志与TraceId关联:问题定位的最后一步
链路跟踪有一个很关键的配套能力:让业务日志带上TraceId。有了它,排查问题时的流程就变成:Skywalking里找到慢请求的TraceId,然后去日志系统里过滤这个TraceId,把这次请求的所有日志全捞出来。没有这一步,链路跟踪的价值会打折扣。
实现方式很简单,引入官方toolkit依赖:
<dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-logback-1.x</artifactId> <version>与Agent版本保持一致</version> </dependency>然后在logback的pattern里用%tid输出TraceId:
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%tid] [%thread] %-5level %logger{39} - %msg%n</pattern> </layout> </encoder>新版Agent本身也会尝试把TraceId写入MDC,key是tid,所以有些项目直接用%X{tid}也能拿到。我建议固定用toolkit方式,它对版本的兼容性更稳定。配置完成后重启应用,随便打一条日志看效果,你会看到每行日志前面都带着一长串TraceId,跟Skywalking里的是同一个值。
6.3 告警规则:让Skywalking主动发现问题
链路跟踪不只是被动的排查工具,它还能主动发现问题。Skywalking OAP内置了告警能力,配置文件在config/alarm-settings.yml。
默认的告警规则里包含一些实用的维度,比如端点平均响应时间超过阈值、服务错误率升高、数据库慢调用等。生产环境一般会根据自己的业务特点调整阈值。举个例子,把响应时间超过1秒的接口在3次采样中持续超时定义为需要告警:
rules: endpoint_avg_response_time: metrics-name: endpoint_avg_response_time threshold: 1000 op: ">" period: 10 count: 3 message: Endpoint {name} response time over 1000ms in recent 2 minutes告警消息发到哪里?Skywalking支持配置Webhook地址,把告警推给内部的消息平台或群机器人:
webhooks: - http://你的告警接收服务地址/notify收到告警后,再去Skywalking里查对应的链路,就能快速确定"哪个服务、哪个端点、什么时间段出了问题"。这套组合拳下来,慢接口的发现从"用户投诉后被动排查"变成了"系统主动通知你",体验完全不一样。
我自己实际用下来的感受是:Skywalking不是装上去就完事的工具,它真正的价值在于持续使用过程中形成的排查习惯——遇到性能问题先看拓扑,再看Trace,最后用TraceId回捞日志,三步下来基本能把问题范围缩小到很具体的环节。如果你的SpringBoot项目还在靠人工翻日志定位问题,真心建议花半天时间把Skywalking搭起来,这个时间投入会从第一次实战排查开始回本。