简介这是面向 Spring Cloud 微服务开发者的一套 SkyWalking 全链路追踪集成示例工程面向有一定微服务基础、希望提升系统可观测性的读者解决分布式环境下服务调用链难以追踪、故障定位效率低等痛点。源码包共 78 个文件以 Java 源码22 个、编译后 class、XML 与 YML/YAML 配置文件为主清晰划分了网关、用户、订单等微服务模块并配备了启动与配置文件便于快速对照学习。包体方面24 个 XML 多数属于 Maven 工程描述与 IDE 配置6 个 YML 和 2 个 YAML 用于定义 OAP 服务器地址、服务名、采样率等关键参数整体仅 75KB轻量易读非常适合直接导入工程做二次改造。目前已有 1157 人学习代码覆盖 SkyWalking Agent 接入、自定义拦截器编写以及调用链数据上报等关键环节启动后可在 SkyWalking Web UI 中直观查看服务拓扑、调用耗时与异常定位是掌握 Spring Cloud 可观测性实战的优质参考资源。1. 一次支付超时却定位不到根因SpringCloud 该上链路追踪了用户的支付请求在 3 秒后返回 504日志系统里 order、pay、account 三个服务各有一段记录按时间对齐后只能得出“远程调用慢”的结论却看不出来是哪一个节点拖垮了整条链路。这是 SpringCloud 微服务从“能跑通”走向“能运维”时必然遇到的坎服务间通过 OpenFeign、Gateway 和 MQ 通信日志在各服务本地是碎片只有把一次请求跨进程的逻辑串联起来才能定位瓶颈。SkyWalking 是链路追踪里非常务实的选择Java agent 在启动时挂载到 JVM自动给 Controller、Feign、RestTemplate、JDBC 织入追踪逻辑业务代码不需要改一行。下面从链路模型开始到把 SpringCloud 项目完整接进 SkyWalking再讲生产环境必须调整的参数和让日志与链路联动的技巧。2. SkyWalking 的链路模型与 SpringCloud 自动埋点原理2.1 Trace、Segment、Span 这三个概念先定住链路追踪不只是在日志里拼一个 traceId。一次完整的业务请求是一整条 Trace它由一个全局唯一的字符串标识贯穿所有服务每个服务进程内处理的部分是一个 Segment在 Segment 内部一次 RPC、一次 SQL、一次消息发送会生成 Span负责记录时间、状态和调用关系。Span 不是平铺的它们通过 parentSpanId 组成一棵调用树。入口处收到请求时生成 EntrySpan表示接收端点要调用下游时生成 ExitSpan表示出站调用。举个例子gateway 收到请求后通过 Feign 调 order-serviceorder-service 再去查 MySQL这一过程会产生三条典型 Span阶段Span 类型关键信息接入用户请求EntrySpanHTTP 方法、URL、状态码Feign 调用下游ExitSpan下游服务地址、接口路径、耗时执行 SQLExitSpanSQL 摘要、数据库实例名、影响行数只看 traceId 的日志只能告诉你“这几个日志属于同一条请求”却回答不了“这三个调用谁的耗时占比最高”。Span 上的起止时间、状态、上下游地址才是成本分析和故障定位需要的。SkyWalking 的 agent 在做字节码增强时会从一次请求进入 Service 的第一个插件开始生成 EntrySpan在后续每个组件插件上生成 ExitSpan并利用 HTTP Header 中的sw8字段把上下文传给下一个进程。这个 Header 对业务是透明的所以常见的 SpringCloud 项目不需要为此改任何代码。2.2 SpringCloud 组件在 agent 里被“无侵入”埋的点做 SpringCloud 项目搭建时团队通常已经记录了网关、Nacos、OpenFeign 这些核心组件而 SkyWalking 之所以接入成本低是因为它的插件体系恰好覆盖了 SpringCloud 核心组件。SpringCloud 核心组件对应 agent 插件链路图上能看到的内容Spring Cloud Gatewayapm-spring-cloud-gateway路由匹配、转发目标、HTTP 状态码OpenFeignapm-spring-feign目标服务名、方法名、连接耗时RestTemplateapm-spring-resttemplate远端地址、HTTP 方法Nacos Clientnacos 相关插件服务发现、注册配置的 RPC 调用Spring MVCapm-springmvc-annotationController 映射、请求耗时JDBC 数据源apm-jdbcSQL 摘要、数据库实例名这里有一个常被误解的点agent 插桩走的是字节码增强目标是类加载过程不是在 Spring 容器里装配拦截器所以 SpringCloud 工程里不需要写Bean来注册链路组件。只要 agent 启动时加载了对应插件插件会在类被 JVM 加载时改写字节码把 span 采集逻辑织入原有方法。这也是 SkyWalking 能做到“零代码接入”的原因。2.3 为什么很多 SpringCloud 团队优先选 SkyWalking链路追踪方案并不少常见的是自研埋点和接入通用 APM。自研埋点的优势是可控但缺点是覆盖面严重依赖开发纪律最常见的遗漏在于异步线程里忘了传递上下文或者只给入口 Controller 埋点Feign 内部根本没采到数据排障时链路依然是断的。Zipkin 单独做链路收集本身不提供指标和告警需要另外搭一套Pinpoint 在 JVM 上也做到无侵入但服务端组件较重。SkyWalking 的定位更接近“可观测性平台”链路、服务拓扑、服务指标、告警都在一套体系里OAP 存储和查询独立出来一个组件前端只是负责展示。对 SpringCloud 这种本身就是多组件协作的技术栈来说“一套后端、所有服务统一接入”的架构更省维护成本。很多照着“SpringCloud 学习笔记尚硅谷”那种教程搭完五大组件后不知道下一步做什么的项目把链路监控接进来是一个投入产出比很高的延续。3. 从零接入SpringCloud 服务通过 Java agent 挂进 SkyWalking3.1 先准备一个能打通调用链的 SpringCloud 工程接入 SkyWalking 本身不要求改造工程但要验证链路效果至少要有一个服务间调用的最小场景。这里以 Nacos 作为注册中心创建 user-service 和 order-service 两个服务order-service 通过 OpenFeign 调用 user-service。pom 里的关键依赖如下dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependencyNacos 和 OpenFeign 是 SpringCloud 里最常见的远程调用组合SkyWalking 的插件会主动探测这两个组件的类所以只要启动参数带上 agent链路会自动生成不需要额外引入链路相关的依赖。版本上要注意 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者的一致性具体以你项目里的统一 BOM 为准。order-service 的application.yml按常规配置注册中心和 Feign 的扫包路径即可SkyWalking 不做参与。3.2 启动 SkyWalking 后端OAP 与 UI 的端口分工后端分两个部分OAP 负责接收 agent 上报数据并写入存储UI 负责把数据展示成拓扑和追踪视图。最简方式从官方发行版解压后执行bin/startup.sh或 Windows 上的bin/startup.bat脚本会先拉起 OAP再拉起 Web UI。端口分工在对接排障时非常关键11800是 agent 上报数据的 gRPC 端口12800是 UI 查询数据和调用 GraphQL 接口的端口8080是 UI 的 Web 端口。如果你用 Docker Compose 启动配置环境变量会更直观services: oap: image: apache/skywalking-oap-server:9.6.0 container_name: skywalking-oap ports: - 11800:11800 - 12800:12800 environment: SW_STORAGE: h2 ui: image: apache/skywalking-ui:9.6.0 container_name: skywalking-ui ports: - 8080:8080 depends_on: - oap environment: SW_OAP_ADDRESS: http://oap:12800这里把存储指定为 h2最小环境可以跑但只适合验证生产环境会在后面的章节讲怎么换。启动后 OAP 还需要一段初始化时间先不要急着立刻打开 UI观察 OAP 日志出现存储初始化完成再继续。如果本机 8080 已经被 SpringCloud 的网关或别的服务占用UI 的端口映射需要换成一个空闲端口否则会直接把本机端口冲突。3.3 用一段 JVM 命令把服务挂进链路启动 order-service 时在 JVM 参数里指定 agent 即可不需要改 SpringCloud 的任何代码。Linux 和 macOS 下的启动命令java -javaagent:/data/skywalking/agent/skywalking-agent.jar \ -Dskywalking.agent.service_nameorder-service \ -Dskywalking.collector.backend_service127.0.0.1:11800 \ -jar order-service.jarWindows 下用 CMD 启动时合成一行写java -javaagent:D:\skywalking\agent\skywalking-agent.jar -Dskywalking.agent.service_nameorder-service -Dskywalking.collector.backend_service127.0.0.1:11800 -jar order-service.jar参数的含义先说清楚。-javaagent指向 agent 的 jar 包路径一定不要带引号路径中存在空格时按各操作系统的转义规则处理。agent.service_name决定这条服务在 SkyWalking UI 里显示的名字如果不设置agent 会尝试从启动类名推断但结果不可控建议每个服务显式指定。collector.backend_service是 OAP 的 gRPC 地址多台 OAP 时用逗号分隔写多个地址比如oap1:11800,oap2:11800。user-service、gateway 等其余服务用同样方式启动只改service_name。这里有一点容易踩坑-Dskywalking.agent.service_name的优先级高于 agent 配置文件和系统环境变量所以不同环境复用同一套脚本时要确认这个参数不会被覆盖成同一个名字否则所有实例会在 UI 里聚合为一个服务看不到实例级别的数据。3.4 在代码层补一段自定义 Span 和跨线程包装自动插桩能覆盖大部分 SpringCloud 组件但总有些场景插件覆盖不到比如调用一个内部 SDK、一个没有插件的 HTTP 客户端或者某段业务逻辑本身需要被单独度量。这时候在工程里引入 toolkit 依赖用注解手动埋点dependency groupIdorg.apache.skywalking/groupId artifactIdapm-toolkit-trace/artifactId version9.6.0/version /dependency代码里在需要观测的方法上加Trace并在入口取一次 traceIdimport org.apache.skywalking.apm.toolkit.trace.Trace; import org.apache.skywalking.apm.toolkit.trace.TraceContext; Trace(operationName OrderService#createOrder) public Order createOrder(OrderDTO dto) { String traceId TraceContext.traceId(); log.info(create order traceId{}, traceId); return orderRepository.save(dto); }operationName建议写成类名#方法名便于在 UI 的 Endpoint 列表里聚合如果方法内部逻辑很长把Trace加在方法上等于把整段逻辑放进一个 Span。无参构造不行这里要明确的是Trace支持在方法上直接注解但不建议给声明周期很长的 Service 类整体加因为粒度太大时反而看不清内部哪一步慢。如果业务里用了线程池比如CompletableFuture.runAsync或自定义的 Executor子线程里通常拿不到父线程的 traceId自动插桩在这里会断。SkyWalking 的 toolkit 提供了现成的包装类import org.apache.skywalking.apm.toolkit.trace.RunnableWrapper; CompletableFuture.runAsync(RunnableWrapper.of(() - { inventoryService.deduct(dto.getSkuId()); }), executorService);RunnableWrapper.of会把当前线程的 TraceContext 传给子线程子线程内生成的 Span 会挂到同一条 Trace 上。这个包装器解决的是“跨线程上下文传递”不是“创建一个新 Trace”所以不要在包装后的 Runnable 内部去主动修改 traceId。4. 生产环境必须调好的一组 SkyWalking 参数4.1 服务名链路图里的名字和 Nacos 注册列表要对齐SpringCloud 里服务注册名通常来自spring.application.name而 SkyWalking 里的服务名来自上面的agent.service_name这是两套不同的名字体系。如果某个服务忘了设置agent.service_nameUI 里会出现一个莫名其妙的“服务”而 Nacos 控制台里对应的服务别名却很正常排查的时候非常容易误判。比较稳妥的做法是在启动参数里把服务名写成“业务域-环境-服务”的格式例如trade-prod-order-service、trade-prod-user-service。环境标识加在服务名里有明显的三分法好处测试环境的链路不会和生产环境混在一起同一个服务在多环境部署时UI 里可以直接按名字筛选。如果团队习惯在 Nacos 的服务列表里按环境分命名空间那么 SkyWalking 侧也做同样的环境后缀即可。另外一个容易忽略的是实例名。agent.instance_name默认由 agent 生成格式通常是服务名加上 PID例如order-service1234。Kubernetes 环境下 Pod 重建后 PID 会变UI 里的实例列表会不断累积新实例看起来像服务不稳定实际只是实例名变了。可以在启动命令里固定实例名-Dskywalking.agent.instance_name${POD_NAME}这样实例名稳定对应 Pod 名重启后也是可预期的标识排查“某个实例内存是否异常”时能直接对应到具体 Pod。4.2 采样率、忽略路径和 Span 上限的参数取舍SkyWalking agent 的默认采样率是一个容易带来误解的配置。它不像常见系统那样用百分比而是用“每 3 秒采样 N 条”来表达。agent.sample_n_per_3_secs默认值是 1意味着每 3 秒最多采集 1 条链路压测时从 UI 上看会有大量请求“消失”这不是链路断了是采样被默认值限制住了。定位问题或做压测对比时建议临时把采样率调成全量agent.sample_n_per_3_secs-1 trace.ignore_path/actuator/health,/actuator/info,/error agent.span_limit_per_segment300sample_n_per_3_secs设成 -1 表示全量采样设成普通的正整数如 3表示每 3 秒保留 3 条。生产环境最终值要按 QPS 和存储容量平衡一般不需要全量但排查线上问题时全量一分钟再恢复原值是快速拿到根因的有效手段不必长期开启。trace.ignore_path用逗号分隔多个路径规则用来处理/actuator/health这类健康检查请求。健康检查在容器平台上频率很高如果不忽略过一段时间存储里全部是这个接口的记录真正的业务链路反而被淹没。span_limit_per_segment默认 300当一次长调用在单个 Segment 内 Span 数超过上限时超出部分的 Span 会被丢弃UI 上会表现为“部分调用数据丢失”。正常情况下 300 够用但如果业务里在一个接口内批量循环调用很多次下游就需要把这个值调大并且同步检查 OAP 单条 Trace 的存储上限。4.3 存储从 h2 换到更可靠的后端h2 只适合本地验证把 SkyWalking 教到生产环境继续用 h2 会遇到很现实的问题数据只能存在单机容量受限重启 OAP 后历史数据可能丢失。生产比较常见的存储后端是 ElasticsearchOAP 通过环境变量切换environment: SW_STORAGE: elasticsearch SW_STORAGE_ES_CLUSTER_NODES: es-01:9200,es-02:9200OAP 启动后会自动创建所需索引并写入模板ES 用户需要具备索引管理权限。切换存储后的验证方式和 h2 没有区别但要注意一点存量数据不会自动迁移。从 h2 切到 ES 之前如果还要保留历史链路数据得在切换前先做一次导出否则切换后 UI 里只能看到新写入的数据。ES 本身也有自己的容量策略SkyWalking 的索引建议按天分片配合 ES 的 ILM 生命周期策略做定期清理避免索引无限膨胀拖慢查询。4.4 网关与 MQ 场景下高频断链的两个检查点SpringCloud 网关同步转发场景通常没问题但 Spring Cloud Gateway 版本和 SkyWalking 插件版本不匹配时会出现“网关看到入口下游看不到网关传过去的上下文”。这种断链最直接的排查方式是看两个服务的 Header 里是否有sw8。如果网关转发后请求头里没有sw8说明 Gateway 插件没有正常生效优先核对网关版本是否在 agent 插件支持范围内必要时升级网关版本或更换兼容的 agent 版本。异步线程和消息队列是断链的高发地带。线程池场景用前面提到的RunnableWrapper包一层可以解决。消息队列场景要注意语义MQ 的生产者和消费者本身处于不同处理时机SkyWalking 不会把两端硬合成同一条 Trace如果业务上需要把“发送消息”和“消费消息”串起来可以在消息体里写入发送端的 traceId消费端在入口处用这个值做关联聚合。不要期望 agent 自动把 MQ 两端接成一条链路这是模型决定的不是配置问题。5. 把 traceId 落进业务日志让日志和链路双向跳转链路追踪的最终价值不只是 UI 上看图还要落到排障效率上。实际运维中我们经常遇到这样的流程先看到错误日志再从日志里找 traceId再去 SkyWalking 里搜这条链路。这个流程能走通的前提是业务日志里必须能拿到 traceId。手动在每个方法调TraceContext.traceId()写进 MDC 不是不行但侵入面太大。更简洁的做法是利用 SkyWalking 提供的 logback 插件在日志格式里直接输出一个%tid占位符。首先引入依赖dependency groupIdorg.apache.skywalking/groupId artifactIdapm-toolkit-logback-1.x/artifactId version9.6.0/version /dependencylogback.xml里的 appender 改成带 traceId 布局的写法appender nameCONSOLE 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} [%tid] [%thread] %-5level %logger{36} - %msg%n/Pattern /layout /encoder /appender配置生效后日志每行会多出一个[TID.xxx.xxx]字段这个值就是 SkyWalking 的全局 traceId。拿到它以后可以在 SkyWalking UI 的“追踪”页面按 Trace ID 直接查询快速定位到该请求在各服务上的完整 Span 列表。反过来在 UI 看某条链路很慢也可以把这个 traceId 拿到日志系统里 grep 同一请求的所有输出把“链路图”和“业务日志”两张视图对上。还有一个实用技巧是反向利用 traceId 做范围耗时对比。线上巡检时选择一个时间段后进入拓扑图点击某个慢服务查看该时间段内耗时 Top 的 Endpoint再进追踪列表按“全部状态、耗时倒序”排列点开耗时最长的 Trace看它的 Segment 列表。耗时占比最大的那一段 Span 旁边通常标着组件类型和远端地址比如“MySQL-查询”或“Feign-调用”这样可以直接定位瓶颈组件省去逐个服务翻日志的时间。如果希望更精细地观察某段代码的内部耗时不要只依赖自动插桩回到 3.4 节说的Trace手动埋点在关键方法上打点再配合日志里的同名 traceId就能把代码执行时间和链路图完全对应起来。本文还有配套的精品资源点击获取
