微服务架构的普及,让一次用户请求可能跨越前端网关、业务服务、数据库、缓存、消息队列等数十个节点。传统的单点监控只能看到单个服务的运行状态,无法串联起完整的请求链路,一旦出现接口超时、服务异常、数据不一致等问题,研发人员只能在各个服务的日志中大海捞针,排查效率极低。全链路追踪技术正是为了解决这一痛点而生,而Apache SkyWalking作为国产开源的顶级APM(应用性能监控)产品,凭借无侵入接入、高性能、多语言支持、全场景覆盖的优势,成为了国内企业分布式可观测性建设的首选方案。
本文将从全链路追踪的核心理论出发,逐层拆解SkyWalking的底层实现原理,结合可落地的业务实战案例,手把手教你掌握分布式故障的定位方法,让你彻底吃透SkyWalking,告别分布式系统的盲调困境。
一、全链路追踪的核心理论基础
要理解SkyWalking,首先要掌握全链路追踪的核心模型与规范,这是所有追踪系统的底层基石。
1.1 核心概念定义
全链路追踪的核心思想来自Google Dapper论文,其核心是通过全局唯一的标识,将一次分布式请求中分散在各个服务节点的调用信息串联起来,还原出完整的调用链路。核心概念如下:
- Trace:一次完整的分布式请求,从用户发起请求到收到响应的全流程,对应唯一的TraceID。TraceID是整个链路的唯一标识,贯穿请求的全生命周期,就像快递的单号,通过单号就能查到快递的全流程中转信息。
- Span:链路中的最小执行单元,对应一次具体的操作,比如HTTP请求、RPC调用、数据库操作、本地方法执行。每个Span都有唯一的SpanID,同时记录了操作的开始时间、结束时间、操作名称、标签、事件日志等信息。
- Span上下文:包含TraceID、SpanID、ParentSpanID(父SpanID)等核心信息,是实现链路串联的关键。父Span与子Span形成树形结构,完整还原请求的调用关系。
- Tag:Span的属性标签,以键值对的形式记录操作的附加信息,比如HTTP请求的url、状态码、数据库操作的SQL语句、RPC调用的接口名等,用于过滤和定位问题。
- Log:Span内的事件日志,通常用于记录操作过程中的异常信息、关键事件,比如异常堆栈、业务关键节点日志,是定位问题的核心依据。
1.2 OpenTracing规范
OpenTracing是一套与平台无关、语言无关的全链路追踪规范,定义了统一的API和数据模型,解决了不同追踪系统之间的兼容性问题。SkyWalking完全兼容OpenTracing规范,同时针对分布式场景做了大量优化,核心是实现了上下文的跨进程、跨线程传播,保证链路的完整性。
1.3 链路串联的核心逻辑
一次分布式请求的链路串联,本质是上下文的持续传递:
- 请求进入第一个服务时,生成全局唯一的TraceID,创建入口Span,生成SpanID,ParentSpanID为空。
- 服务内发起本地方法调用时,创建子Span,ParentSpanID指向父Span的SpanID。
- 服务发起跨进程调用时,将TraceID、当前SpanID等上下文信息注入到请求头中,传递给下游服务。
- 下游服务接收到请求后,从请求头中解析出上下文,创建子Span,ParentSpanID指向上游服务的SpanID,继续传递上下文。
- 所有Span执行完成后,通过TraceID将所有Span聚合,还原出完整的树形调用链路。
二、SkyWalking 整体架构解析
SkyWalking采用分层架构设计,各模块职责清晰,解耦性强,支持水平扩展,可满足从个人测试到企业级大规模集群的部署需求。整体架构分为四大核心模块,架构图如下:
2.1 Agent探针模块
Agent是SkyWalking的数据采集端,以无侵入的方式挂载到业务应用中,负责采集链路追踪、JVM指标、服务实例信息等数据,经过预处理后上报给OAP Server。SkyWalking支持Java、Go、Python、C#等十多种语言的Agent,其中Java Agent功能最完善,性能最优,也是本文的核心讲解对象。
2.2 OAP Server模块
OAP(Observability Analysis Platform)可观测性分析平台,是SkyWalking的核心大脑,负责数据的接收、清洗、分析、聚合、存储与查询。核心分为两大核心流程:
- 流式处理流程:实时接收Agent上报的原始数据,解析链路模型,生成服务拓扑、实时指标、调用关系,写入存储。
- 批处理流程:按时间维度对历史数据进行聚合,生成小时级、天级的统计指标,用于长期趋势分析与容量规划。
OAP支持单机与集群两种部署模式,集群模式下通过注册中心(ZooKeeper/Nacos)实现服务发现与负载均衡,可水平扩展支撑数万服务实例的监控需求。
2.3 存储模块
存储模块负责SkyWalking所有数据的持久化,支持多种存储引擎,不同存储引擎的适用场景如下:
- Elasticsearch:生产环境首选,支持全文检索,高性能的读写能力,适合链路数据、日志数据的存储与查询,支持大规模集群部署。
- MySQL/PostgreSQL:适合测试环境、小规模部署场景,部署简单,运维成本低。
- TiDB:分布式关系型数据库,适合需要强事务、水平扩展的大规模部署场景。
- InfluxDB/Prometheus:时序数据库,适合指标数据的存储与分析,不适合链路明细数据的存储。
2.4 UI可视化模块
SkyWalking UI是基于React开发的可视化界面,提供了服务拓扑图、全链路追踪、指标监控、告警管理、日志分析等功能,通过GraphQL协议与OAP Server交互,直观展示分布式系统的运行状态,是研发人员排查问题的核心入口。
三、SkyWalking 核心底层原理深度拆解
这一部分是本文的核心,将逐层拆解SkyWalking的底层实现逻辑,用通俗的语言讲透无侵入采集、链路传播、数据处理的核心原理,让你不仅知其然,更知其所以然。
3.1 Java Agent无侵入采集原理
SkyWalking Java Agent的核心是基于JVM提供的Instrumentation API实现的字节码增强,无需修改任何业务代码,即可实现链路数据的采集,这也是其无侵入特性的核心来源。
3.1.1 Java Agent核心机制
JVM提供了两种Agent加载方式,SkyWalking均提供支持:
- premain模式:启动时加载,在应用主方法执行之前,通过-javaagent参数挂载Agent,JVM会优先执行Agent的premain方法,完成字节码增强的初始化。这是SkyWalking的默认加载方式,稳定性最高,适合生产环境。
- agentmain模式:运行时动态加载,通过JVM的Attach API,在应用运行过程中动态挂载Agent,无需重启应用。适合无法重启的生产服务紧急接入场景,稳定性略低于premain模式。
Instrumentation API提供了ClassFileTransformer接口,允许Agent在类加载的时候,拦截并修改类的字节码,SkyWalking正是通过这个接口,在目标类加载时,插入链路追踪的逻辑,实现无侵入的采集。
3.1.2 字节码增强实现:ByteBuddy
SkyWalking没有直接使用底层的ASM字节码操作框架,而是选择了ByteBuddy作为字节码增强的核心库。ByteBuddy提供了更易用的流式API,屏蔽了字节码的底层细节,开发效率更高,同时生成的字节码经过了优化,性能远超ASM的手写代码,也避免了手写字节码带来的稳定性问题。
SkyWalking字节码增强的完整流程如下:
3.1.3 插件化架构设计
SkyWalking的所有框架支持能力,都是通过插件实现的,核心框架只提供了字节码增强的内核与链路追踪的核心模型,所有的SpringMVC、Dubbo、MyBatis、Redis等框架的支持,都是以独立插件的形式存在,放在plugins目录下,Agent启动时自动加载。
这种插件化架构的优势非常明显:
- 按需加载:不需要的插件可以直接从plugins目录删除,减少字节码增强的范围,降低性能损耗。
- 易于扩展:用户可以根据业务需求,开发自定义插件,支持自研框架的链路追踪。
- 解耦性强:插件的升级与修改不影响核心框架,稳定性更高。
每个插件的核心是定义了两个部分:
- 类与方法匹配规则:定义需要增强的类名、方法名、方法参数,比如匹配org.springframework.web.servlet.DispatcherServlet的doDispatch方法。
- 拦截器逻辑:定义方法执行前、执行后、异常时的处理逻辑,比如方法执行前创建Span,执行后结束Span,异常时记录异常日志。
3.2 全链路上下文传播原理
链路能跨服务、跨线程串联起来,核心是上下文的正确传播。SkyWalking的上下文传播分为跨线程传播与跨进程传播两大场景,分别解决了服务内异步调用与服务间分布式调用的链路串联问题。
3.2.1 上下文核心模型
SkyWalking的上下文核心是ContextCarrier与ContextSnapshot两个类:
- ContextCarrier:用于跨进程的上下文传播,负责将Trace上下文序列化后注入到请求头中,在下游服务中反序列化还原上下文。
- ContextSnapshot:用于跨线程的上下文传播,负责在父线程中捕获当前的Trace上下文,在子线程中还原,实现异步场景的链路串联。
上下文的核心数据包括:TraceID、TraceSegmentID、SpanID、服务名、服务实例名、端点名、采样标记等,这些数据是链路串联的核心。
3.2.2 跨线程上下文传播
在Java应用中,大量使用了线程池、CompletableFuture等异步编程方式,默认情况下,ThreadLocal中的上下文无法传递到子线程中,会导致异步场景的链路断裂。
SkyWalking的解决方案是:通过字节码增强,包装了所有的线程池、Runnable、Callable、CompletableFuture等异步组件,在父线程提交任务时,捕获当前的ContextSnapshot,绑定到任务中;在子线程执行任务前,将ContextSnapshot中的上下文还原到子线程的ThreadLocal中,任务执行完成后清除上下文,避免上下文污染。
这种方式完美解决了异步场景的链路传播问题,无需修改业务代码,即可实现异步方法的链路追踪。
3.2.3 跨进程上下文传播
跨进程传播是分布式链路串联的核心,SkyWalking通过在请求协议中注入上下文头,实现TraceID的跨服务传递。针对不同的通信协议,SkyWalking提供了对应的插件,自动完成上下文的注入与解析:
- HTTP协议:通过HTTP请求头注入sw8上下文头,下游服务的SpringMVC插件自动从请求头中解析sw8,还原上下文。
- Dubbo RPC协议:通过Dubbo的Attachment传递上下文,Provider端自动从Attachment中解析上下文。
- MQ协议:通过消息的Properties传递上下文,消费端自动从Properties中解析上下文。
- 数据库协议:通过SQL注释传递上下文,实现数据库访问的链路串联。
其中sw8头是SkyWalking跨进程传播的标准格式,结构如下:
<version>-<trace-id>-<parent-segment-id>-<parent-span-id>-<parent-service>-<parent-service-instance>-<parent-endpoint>-<sampled>
所有字段均经过Base64编码,避免特殊字符导致的传输问题,保证跨网络、跨协议的传输兼容性。
3.3 Trace核心模型:Segment与Span
SkyWalking在OpenTracing规范的基础上,针对分布式场景做了优化,提出了Trace Segment的概念,形成了Trace -> Segment -> Span的三级模型,解决了纯Span模型在分布式场景下的性能与扩展性问题。
3.3.1 三级模型详解
- Trace:一次完整的分布式请求,由多个Trace Segment组成,通过全局唯一的TraceID关联。
- Trace Segment:一次请求在单个JVM进程内的链路片段,每个服务实例处理一次请求,会生成一个唯一的Trace Segment,对应唯一的SegmentID。Segment内包含了本次请求在该服务内的所有Span,以及上游服务的上下文信息。
- Span:链路的最小执行单元,对应一次具体的操作,每个Span都属于一个Trace Segment,通过SpanID唯一标识,通过ParentSpanID形成父子关系。
这种三级模型的优势在于:
- 分布式场景下,每个服务独立生成Segment,无需等待下游服务的返回,即可上报数据,降低了Agent的内存占用,提升了上报效率。
- 数据聚合时,OAP只需通过TraceID将多个Segment聚合,即可还原完整链路,无需处理海量Span的父子关系,提升了分析效率。
- Segment内包含了服务、实例、端点的完整信息,无需在每个Span中重复存储,降低了数据存储量。
3.3.2 Span的三大类型与使用场景
SkyWalking将Span分为三大类型,不同类型的Span对应不同的使用场景,这是很多用户容易混淆的核心知识点,这里做明确区分:
| Span类型 | 核心定义 | 典型使用场景 | 核心特点 |
|---|---|---|---|
| EntrySpan | 服务的入站请求Span,是链路在当前服务的入口 | HTTP请求接收、Dubbo Provider接口调用、MQ消息消费 | 一个Trace Segment中最多有一个EntrySpan,代表请求进入当前服务的起点 |
| ExitSpan | 服务的出站请求Span,是链路从当前服务流出的节点 | HTTP远程调用、Dubbo Consumer接口调用、数据库操作、Redis访问、MQ消息发送 | 对应一次跨进程/跨组件的出站操作,会注入上下文到请求中,传递给下游 |
| LocalSpan | 服务内的本地操作Span,不涉及跨进程通信 | 业务层本地方法调用、工具类方法执行、本地缓存操作 | 用于追踪服务内部的方法执行情况,不涉及上下文的传递 |
3.3.3 Span的生命周期
每个Span都有严格的生命周期,只有正确完成生命周期的Span,才会被采集并上报到OAP Server:
- 创建阶段:通过Tracer创建Span,设置Span类型、操作名称、开始时间,绑定到当前的Trace上下文。
- 数据记录阶段:在方法执行过程中,通过Tag记录操作的属性信息,通过Log记录事件与异常信息。
- 结束阶段:方法执行完成后,调用Span的end方法,设置结束时间,计算耗时,将Span添加到当前的Trace Segment中。
- 上报阶段:当前Segment的所有Span都执行完成后,Agent将Segment序列化为二进制数据,通过gRPC协议上报到OAP Server。
3.4 OAP Server核心处理原理
OAP Server是SkyWalking的大脑,负责处理Agent上报的海量数据,生成可查询的链路、指标、拓扑数据。其核心处理流程分为流式处理与批处理两大核心模块。
3.4.1 流式处理流程
流式处理是OAP的核心实时处理流程,基于LMAX Disruptor高性能内存队列实现,处理延迟在毫秒级,核心流程如下:
- 数据接收:Receiver模块通过gRPC服务接收Agent上报的Segment数据,进行格式校验与解析。
- 链路分析:解析Segment中的Span数据,提取服务、实例、端点、调用关系信息,生成链路明细数据。
- 拓扑生成:根据Span的上下游调用关系,生成服务之间的拓扑依赖关系,更新服务拓扑图。
- 指标聚合:按分钟维度,聚合服务、实例、端点的调用量、响应时间、成功率、异常率等核心指标。
- 数据写入:将链路明细数据、拓扑数据、分钟级指标数据写入存储引擎。
3.4.2 批处理流程
批处理流程用于处理历史数据的聚合,生成长期的统计指标,核心是按小时、天维度,对分钟级指标进行二次聚合,生成小时级、天级的指标数据,用于长期趋势分析、容量规划、报表统计。批处理流程通过定时任务触发,默认每小时执行一次小时级聚合,每天执行一次天级聚合。
3.4.3 采样机制
SkyWalking的采样机制在Agent端实现,核心是为了在大规模部署场景下,减少数据上报量,降低OAP与存储的压力。采样规则如下:
- 采样决策在Trace的入口处做出,一旦Trace被标记为采样,整个链路的所有Span都会被完整采集,不会出现半条链路的情况。
- 采样决策基于TraceID的哈希值实现,保证同一个Trace在所有服务中的采样决策一致,避免链路断裂。
- 支持自定义采样规则,比如按服务名、端点名、响应时间、异常状态设置不同的采样率,比如异常请求100%采样,正常请求按10%采样。
四、环境部署与业务接入实战
这一部分将带你从零开始,完成SkyWalking的部署与业务系统的接入,所有组件均采用最新稳定版本,步骤清晰,可直接落地。
4.1 环境准备
- 操作系统:Windows/Linux/macOS
- JDK:17+
- MySQL:8.0+
- Docker(可选,推荐)
4.2 SkyWalking OAP Server部署
这里提供两种部署方式,Docker部署(推荐)与二进制包部署。
4.2.1 Docker部署(推荐)
- 先创建MySQL数据库,用于存储SkyWalking的元数据与指标数据:
CREATE DATABASE skywalking DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'skywalking'@'%' IDENTIFIED BY 'Skywalking@123';
GRANT ALL PRIVILEGES ON skywalking.* TO 'skywalking'@'%';
FLUSH PRIVILEGES;
2. 启动OAP Server容器,配置MySQL存储:
docker run -d \
--name skywalking-oap \
-p 11800:11800 \
-p 12800:12800 \
-e SW_STORAGE=mysql \
-e SW_JDBC_URL=jdbc:mysql://你的MySQL地址:3306/skywalking?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true \
-e SW_DATA_SOURCE_USER=skywalking \
-e SW_DATA_SOURCE_PASSWORD=Skywalking@123 \
apache/skywalking-oap-server:10.3.0
3. 启动SkyWalking UI容器:
docker run -d \
--name skywalking-ui \
-p 8080:8080 \
-e SW_OAP_ADDRESS=http://skywalking-oap:12800 \
--link skywalking-oap:skywalking-oap \
apache/skywalking-ui:10.3.0
部署完成后,访问http://127.0.0.1:8080,即可进入SkyWalking UI界面,默认用户名/密码:admin/admin。
4.2.2 二进制包部署
- 下载Apache SkyWalking 10.3.0二进制包,解压到本地目录。
- 修改config/application.yml文件,将存储配置改为MySQL:
storage:
selector: ${SW_STORAGE:mysql}
mysql:
properties:
jdbcUrl: ${SW_JDBC_URL:"jdbc:mysql://127.0.0.1:3306/skywalking?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"}
dataSource.user: ${SW_DATA_SOURCE_USER:skywalking}
dataSource.password: ${SW_DATA_SOURCE_PASSWORD:Skywalking@123}
3. 启动OAP Server:bin/oapService.sh(Linux)/bin/oapService.bat(Windows) 4. 启动UI:bin/webappService.sh(Linux)/bin/webappService.bat(Windows)
4.3 业务系统接入实战
我们将创建两个Spring Boot服务:用户服务(user-service)与订单服务(order-service),订单服务通过OpenFeign调用用户服务,演示跨服务的全链路追踪,同时集成MyBatisPlus实现数据库操作,集成Swagger3实现接口文档。
4.3.1 父工程pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>skywalking-demo</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>user-service</module>
<module>order-service</module>
</modules>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.2</spring-cloud.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<mysql.version>8.0.39</mysql.version>
<lombok.version>1.18.30</lombok.version>
<fastjson2.version>2.0.52</fastjson2.version>
<guava.version>33.2.0-jre</guava.version>
<springdoc.version>2.5.0</springdoc.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.3.2 用户服务(user-service)
- 子模块pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jam.demo</groupId>
<artifactId>skywalking-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>user-service</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. 配置文件application.yml
server:
port: 8081
spring:
application:
name: user-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/skywalking_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
springdoc:
swagger-ui:
path: /swagger-ui.html
api-docs:
path: /v3/api-docs
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
3. 数据库建表语句
CREATE DATABASE skywalking_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE skywalking_demo;
CREATE TABLE t_user (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
username VARCHAR(64) NOT NULL COMMENT '用户名',
phone VARCHAR(11) NOT NULL COMMENT '手机号',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除 0-未删除 1-已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
INSERT INTO t_user (username, phone) VALUES ('zhangsan', '13800138000'), ('lisi', '13900139000');
4. 实体类User.java
package com.jam.demo.user.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体类
* @author ken
*/
@Data
@TableName("t_user")
@Schema(description = "用户实体")
public class User {
@TableId(type = IdType.AUTO)
@Schema(description = "用户ID", example = "1")
private Long id;
@Schema(description = "用户名", example = "zhangsan")
private String username;
@Schema(description = "手机号", example = "13800138000")
private String phone;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@TableLogic
@Schema(description = "是否删除")
private Integer deleted;
}
5. Mapper接口UserMapper.java
package com.jam.demo.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.user.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户Mapper接口
* @author ken
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
6. Service层接口与实现 UserService.java
package com.jam.demo.user.service;
import com.jam.demo.user.entity.User;
/**
* 用户服务接口
* @author ken
*/
public interface UserService {
/**
* 根据用户ID查询用户信息
* @param id 用户ID
* @return 用户实体
*/
User getUserById(Long id);
}
UserServiceImpl.java
package com.jam.demo.user.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.user.entity.User;
import com.jam.demo.user.mapper.UserMapper;
import com.jam.demo.user.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
/**
* 用户服务实现类
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
@Override
public User getUserById(Long id) {
if (ObjectUtils.isEmpty(id)) {
log.warn("用户ID为空");
return null;
}
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<User>()
.eq(User::getId, id)
.eq(User::getDeleted, 0);
User user = userMapper.selectOne(queryWrapper);
if (ObjectUtils.isEmpty(user)) {
log.info("用户不存在,ID:{}", id);
}
return user;
}
}
7. Controller层UserController.java
package com.jam.demo.user.controller;
import com.jam.demo.user.entity.User;
import com.jam.demo.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户信息查询接口")
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
@Operation(summary = "根据ID查询用户", description = "根据用户ID查询用户详细信息")
public User getUserById(
@Parameter(description = "用户ID", required = true, example = "1")
@PathVariable Long id) {
log.info("查询用户信息,ID:{}", id);
return userService.getUserById(id);
}
}
8. 启动类UserServiceApplication.java
package com.jam.demo.user;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 用户服务启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.user.mapper")
@EnableFeignClients
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
4.3.3 订单服务(order-service)
- 子模块pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jam.demo</groupId>
<artifactId>skywalking-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>order-service</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. 配置文件application.yml
server:
port: 8082
spring:
application:
name: order-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/skywalking_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
springdoc:
swagger-ui:
path: /swagger-ui.html
api-docs:
path: /v3/api-docs
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
3. 数据库建表语句
USE skywalking_demo;
CREATE TABLE t_order (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID',
order_no VARCHAR(64) NOT NULL COMMENT '订单编号',
user_id BIGINT NOT NULL COMMENT '用户ID',
amount DECIMAL(10,2) NOT NULL COMMENT '订单金额',
status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态 0-待支付 1-已支付 2-已取消',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除 0-未删除 1-已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
INSERT INTO t_order (order_no, user_id, amount, status) VALUES ('ORDER20240520001', 1, 99.99, 1), ('ORDER20240520002', 2, 199.99, 0);
4. Feign客户端UserFeignClient.java
package com.jam.demo.order.feign;
import com.jam.demo.order.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 用户服务Feign客户端
* @author ken
*/
@FeignClient(name = "user-service", url = "127.0.0.1:8081")
public interface UserFeignClient {
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long id);
}
5. 实体类 User.java(用于接收Feign返回的用户数据)
package com.jam.demo.order.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体类
* @author ken
*/
@Data
@Schema(description = "用户实体")
public class User {
@Schema(description = "用户ID", example = "1")
private Long id;
@Schema(description = "用户名", example = "zhangsan")
private String username;
@Schema(description = "手机号", example = "13800138000")
private String phone;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "是否删除")
private Integer deleted;
}
Order.java
package com.jam.demo.order.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体类
* @author ken
*/
@Data
@TableName("t_order")
@Schema(description = "订单实体")
public class Order {
@TableId(type = IdType.AUTO)
@Schema(description = "订单ID", example = "1")
private Long id;
@Schema(description = "订单编号", example = "ORDER20240520001")
private String orderNo;
@Schema(description = "用户ID", example = "1")
private Long userId;
@Schema(description = "订单金额", example = "99.99")
private BigDecimal amount;
@Schema(description = "订单状态 0-待支付 1-已支付 2-已取消", example = "1")
private Integer status;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@TableLogic
@Schema(description = "是否删除")
private Integer deleted;
}
6. Mapper接口OrderMapper.java
package com.jam.demo.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;
/**
* 订单Mapper接口
* @author ken
*/
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
7. Service层接口与实现 OrderService.java
package com.jam.demo.order.service;
import com.jam.demo.order.entity.Order;
/**
* 订单服务接口
* @author ken
*/
public interface OrderService {
/**
* 根据订单ID查询订单详情,包含用户信息
* @param id 订单ID
* @return 订单实体
*/
Order getOrderById(Long id);
}
OrderServiceImpl.java
package com.jam.demo.order.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.order.entity.Order;
import com.jam.demo.order.entity.User;
import com.jam.demo.order.feign.UserFeignClient;
import com.jam.demo.order.mapper.OrderMapper;
import com.jam.demo.order.service.OrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
/**
* 订单服务实现类
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
private final UserFeignClient userFeignClient;
@Override
public Order getOrderById(Long id) {
if (ObjectUtils.isEmpty(id)) {
log.warn("订单ID为空");
return null;
}
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<Order>()
.eq(Order::getId, id)
.eq(Order::getDeleted, 0);
Order order = orderMapper.selectOne(queryWrapper);
if (ObjectUtils.isEmpty(order)) {
log.info("订单不存在,ID:{}", id);
return null;
}
User user = userFeignClient.getUserById(order.getUserId());
if (ObjectUtils.isEmpty(user)) {
log.warn("订单对应的用户不存在,用户ID:{}", order.getUserId());
}
log.info("查询订单详情完成,订单号:{},用户名:{}", order.getOrderNo(), ObjectUtils.isEmpty(user) ? "未知" : user.getUsername());
return order;
}
}
8. Controller层OrderController.java
package com.jam.demo.order.controller;
import com.jam.demo.order.entity.Order;
import com.jam.demo.order.service.OrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 订单控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
@Tag(name = "订单管理", description = "订单信息查询接口")
public class OrderController {
private final OrderService orderService;
@GetMapping("/{id}")
@Operation(summary = "根据ID查询订单", description = "根据订单ID查询订单详细信息,包含关联的用户信息")
public Order getOrderById(
@Parameter(description = "订单ID", required = true, example = "1")
@PathVariable Long id) {
log.info("查询订单信息,ID:{}", id);
return orderService.getOrderById(id);
}
}
9. 启动类OrderServiceApplication.java
package com.jam.demo.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 订单服务启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.order.mapper")
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
4.3.4 Agent接入启动
- 下载SkyWalking Agent 10.3.0,解压到本地目录。
- 启动用户服务,添加JVM参数:
-javaagent:/path/to/skywalking-agent/skywalking-agent.jar
-Dskywalking.agent.service_name=user-service
-Dskywalking.collector.backend_service=127.0.0.1:11800
3. 启动订单服务,添加JVM参数:
-javaagent:/path/to/skywalking-agent/skywalking-agent.jar
-Dskywalking.agent.service_name=order-service
-Dskywalking.collector.backend_service=127.0.0.1:11800
4. 访问订单服务接口:http://127.0.0.1:8082/order/1,触发跨服务调用。 5. 打开SkyWalking UI,即可看到服务拓扑、链路追踪数据。
五、分布式故障定位实战方法
掌握了SkyWalking的原理与接入,核心是用它解决实际的分布式故障问题。这里总结了四大高频故障场景的定位方法,结合上面的实战项目,手把手教你快速定位问题。
5.1 慢请求定位实战
慢请求是分布式系统中最常见的问题,接口响应超时,用户体验差,传统方式很难定位到具体的慢环节,通过SkyWalking可以快速定位到慢的Span,找到根因。
定位步骤:
-
打开SkyWalking UI,进入「服务」菜单,选择order-service服务,查看端点的响应时间排行,找到响应时间最长的端点/order/{id}。
-
点击端点,进入「追踪」菜单,筛选该端点的请求,按耗时降序排序,找到耗时最长的Trace。
-
点击Trace,查看完整的链路瀑布图,每个Span的耗时一目了然,可以快速看到哪个环节耗时最长:
- 如果是数据库Span耗时最长,点击Span查看Tag中的SQL语句,分析是否是慢查询,是否缺少索引。
- 如果是Feign调用Span耗时最长,点击Span查看下游服务的端点,跳转到下游服务的链路,继续分析。
- 如果是本地方法Span耗时最长,查看方法名,定位到具体的业务代码,分析是否有循环、锁竞争、IO阻塞等问题。
5.2 异常请求定位实战
接口报错、返回500,是研发人员经常遇到的问题,分布式场景下,不知道是哪个服务抛出的异常,通过SkyWalking可以快速定位到异常的根因。
定位步骤:
- 打开SkyWalking UI,进入「服务」菜单,选择order-service服务,查看「异常」指标,找到异常请求的端点。
- 进入「追踪」菜单,筛选状态为「异常」的Trace,找到对应的请求。
- 点击Trace,查看链路中的Span,标红的Span就是抛出异常的Span。
- 点击标红的Span,查看「日志」标签,即可看到完整的异常堆栈信息,直接定位到抛出异常的代码行,快速解决问题。
5.3 全链路压测性能瓶颈定位
在系统上线前,我们会进行压测,验证系统的性能,传统的压测工具只能看到整体的QPS、响应时间,无法定位到性能瓶颈,通过SkyWalking可以结合压测,找到全链路的性能瓶颈。
定位步骤:
- 使用JMeter等压测工具,对订单服务的/order/{id}接口进行压测,持续施压。
- 打开SkyWalking UI,进入「服务」菜单,查看order-service和user-service的核心指标:QPS、响应时间、成功率、CPU使用率、JVM内存使用率。
- 进入「拓扑」菜单,查看服务之间的调用关系,每个服务的QPS、响应时间、异常率,找到响应时间最长的服务节点。
- 进入「追踪」菜单,查看压测过程中的Trace,分析每个Span的耗时占比,找到耗时占比最高的环节,比如数据库查询、Redis访问、第三方接口调用。
- 针对瓶颈环节进行优化,比如给数据库加索引、优化SQL、添加缓存、异步化处理,再次压测验证优化效果。
5.4 分布式事务问题定位
跨服务的分布式事务问题,比如订单创建成功,库存扣减失败,导致数据不一致,传统方式很难排查每个服务的执行情况,通过SkyWalking的全链路追踪,可以完整看到事务的每个环节的执行情况。
定位步骤:
- 拿到异常的订单号,从业务日志中找到对应的TraceID。
- 打开SkyWalking UI,进入「追踪」菜单,通过TraceID搜索到完整的链路。
- 查看链路中的每个Span,对应分布式事务的每个环节,比如订单创建、库存扣减、积分增加,查看每个Span的执行状态、耗时、异常信息。
- 找到执行失败的Span,查看异常日志,定位到事务回滚的原因,比如数据库锁等待超时、库存不足、网络超时等。
- 结合链路的执行顺序,分析分布式事务的一致性问题,比如是否是异步调用导致的时序问题,是否是事务传播机制配置错误等。
六、最佳实践与避坑指南
6.1 生产环境最佳实践
- 版本一致性:Agent版本必须与OAP Server版本完全一致,否则会出现数据不上报、解析失败的问题,这是最常见的踩坑点。
- 插件按需加载:plugins目录下的插件,只保留业务系统用到的插件,不需要的插件直接删除,减少字节码增强的范围,降低性能损耗,避免不必要的类冲突。
- 采样率合理配置:大规模部署场景下,不要使用全采样,根据业务需求配置合理的采样率,比如核心接口100%采样,非核心接口10%采样,异常请求100%采样,减少数据存储量,提升查询效率。
- OAP集群部署:生产环境必须使用集群模式部署OAP Server,通过负载均衡实现高可用,避免单点故障,同时根据数据量水平扩展OAP节点。
- 存储引擎选择:生产环境优先选择Elasticsearch作为存储引擎,配置合理的分片数与副本数,设置数据过期时间,避免存储无限增长,定期清理历史数据。
- 告警配置:配置合理的告警规则,比如接口响应超时、异常率突增、服务不可用、JVM内存溢出等,及时发现问题,提前预警。
6.2 常见踩坑与解决方案
-
Agent启动后,服务没有出现在SkyWalking UI中
- 原因:Agent与OAP版本不一致;OAP地址配置错误;防火墙拦截了11800端口;服务没有收到请求,没有生成Trace数据。
- 解决方案:核对Agent与OAP版本;检查backend_service配置是否正确;检查防火墙端口是否开放;调用服务接口,触发Trace生成。
-
跨服务调用,TraceID不传递,链路断裂
- 原因:使用了自定义的HTTP客户端,没有对应的SkyWalking插件;跨线程调用,没有传递上下文;请求头被网关过滤,sw8头被丢弃。
- 解决方案:添加对应HTTP客户端的插件;使用SkyWalking支持的异步组件,或者手动传递上下文;配置网关,放行sw8开头的请求头。
-
异步方法调用,链路丢失
- 原因:使用了自定义的线程池,没有被SkyWalking增强;手动创建线程,没有传递上下文。
- 解决方案:使用Spring管理的线程池,SkyWalking会自动增强;如果使用自定义线程池,需要手动包装Runnable/Callable,传递ContextSnapshot。
-
数据库操作,没有生成对应的Span
- 原因:使用了自定义的数据源,没有对应的插件;MyBatisPlus的版本与SkyWalking插件版本不兼容;SQL执行没有经过JDBC标准接口。
- 解决方案:添加对应数据源的插件;升级SkyWalking插件到最新版本,兼容MyBatisPlus;使用标准的JDBC接口操作数据库。
-
OAP Server内存溢出,频繁重启
- 原因:数据量过大,OAP的JVM内存配置不足;聚合规则过多,导致内存占用过高;存储引擎写入慢,导致数据堆积在OAP内存中。
- 解决方案:调大OAP的JVM堆内存;优化采样率,减少数据量;优化聚合规则,关闭不需要的聚合;优化存储引擎的写入性能,升级Elasticsearch集群。
总结
分布式系统的可观测性,是保障系统稳定性的核心能力,而全链路追踪是可观测性体系的核心支柱。SkyWalking凭借无侵入的接入方式、完善的多语言支持、高性能的数据处理能力、丰富的可视化功能,成为了分布式全链路追踪的首选方案。