SpringBoot 集成 SkyWalking 实现分布式链路追踪
一、SkyWalking 是什么?
SkyWalking 是一个开源的、用于观测分布式系统(特别是微服务、云原生和容器化应用)的平台。它提供了对分布式系统的追踪、监控和诊断能力。
二、SkyWalking 与 JDK 版本的对应关系
SkyWalking 8.x 版本要求 Java 版本至少为 8(即 JDK 1.8), SkyWalking 9.x 版本则要求 Java 版本至少为 11(即 JDK 11)
选择时需要注意 JDK 版本。
三、SkyWalking 下载
SkyWalking 官网下载地址 https://skywalking.apache.org/downloads/
- 其他版本的 APM 地址:https://archive.apache.org/dist/skywalking/
- 其他 Java 版本的 Agents 地址:https://archive.apache.org/dist/skywalking/java-agent/
注意点: 7.x 及以下版本 APM 包里面包括 Agents,但是 8.x 的就发现被分开了,所以 8.x 及以上的就需要 Agents 也得下载。
目前该文选择下载 APM 8.9.1 和 Agents 8.9.0 后解压。
四、SkyWalking 数据存储
SkyWalking 存在多种数据存储:
- h2(默认的存储方式,重启后数据会丢失)
- Elasticsearch(最常用的数据存储方式)
- MySQL
- TiDB
- …
相关文件 OAP 配置文件(config/application.yml),关于设置存储方式的部分如下:
storage:
selector: ${SW_STORAGE:h2}
elasticsearch:
namespace: ${SW_NAMESPACE:""}
clusterNodes: ${SW_STORAGE_ES_CLUSTER_NODES:localhost:9200}
protocol: ${SW_STORAGE_ES_HTTP_PROTOCOL:"http"}
connectTimeout: ${SW_STORAGE_ES_CONNECT_TIMEOUT:500}
socketTimeout: ${SW_STORAGE_ES_SOCKET_TIMEOUT:30000}
numHttpClientThread: ${SW_STORAGE_ES_NUM_HTTP_CLIENT_THREAD:0}
user: ${SW_ES_USER:""}
password: ${SW_ES_PASSWORD:""}
trustStorePath: ${SW_STORAGE_ES_SSL_JKS_PATH:""}
trustStorePass: ${SW_STORAGE_ES_SSL_JKS_PASS:""}
secretsManagementFile: ${SW_ES_SECRETS_MANAGEMENT_FILE:""}
dayStep: ${SW_STORAGE_DAY_STEP:1}
indexShardsNumber: ${SW_STORAGE_ES_INDEX_SHARDS_NUMBER:1}
indexReplicasNumber: ${SW_STORAGE_ES_INDEX_REPLICAS_NUMBER:1}
superDatasetDayStep: ${SW_SUPERDATASET_STORAGE_DAY_STEP:-1}
superDatasetIndexShardsFactor: ${SW_STORAGE_ES_SUPER_DATASET_INDEX_SHARDS_FACTOR:5}
superDatasetIndexReplicasNumber: ${SW_STORAGE_ES_SUPER_DATASET_INDEX_REPLICAS_NUMBER:0}
bulkActions: ${SW_STORAGE_ES_BULK_ACTIONS:5000}
flushInterval: ${SW_STORAGE_ES_FLUSH_INTERVAL:15}
concurrentRequests: ${SW_STORAGE_ES_CONCURRENT_REQUESTS:2}
五、SkyWalking 的启动
进入 apache-skywalking-apm-8.9.1/apache-skywalking-apm-bin/in,双击运行 startup.bat(用管理员方式启动),会开启两个命令行窗口。
- (1)Skywalking-Collector:追踪信息收集器,通过 gRPC/Http 收集客户端的采集信息。Http 默认端口 12800,gRPC 默认端口 11800。(如需要修改,可前往 apache-skywalking-apm-bin/config/application.yml 进行修改)
- (2)Skywalking-Webapp:管理平台页面 默认端口 8080(如需要修改,可前往 apache-skywalking-apm-bin/webapp/webapp.yml 进行修改)
启动图如下:

接着浏览器访问 SkyWalking:http://localhost:8080/。这个右边有个自动刷新的按钮,一定要启动起来,不然到时候 SpringBoot 工程启动以后,你以为没有连接成功(F5 刷新页面是没有用的)。

六、部署探针
前提:Agents 8.9.0 放入项目工程
建议将 Agent 文件放入项目目录,后续使用更为便利。

方式一:IDEA 部署探针
修改启动类的 VM options(虚拟机选项)配置。


配置的 JVM 参数如下:
-javaagent:D:/ideaObject/reactBootspringboot-full/src/main/skywalking-agent/skywalking-agent.jar -Dskywalking.agent.service_name=woqu-ndy -Dskywalking.collector.backend_service=127.0.0.1:11800
javaagent:表示 skywalking-agent.jar 的本地磁盘的路径(建议放到项目里面了)
-Dskywalking.agent.service_name:表示在 SkyWalking 上显示的服务名-Dskywalking.collector.backend_service:表示 SkyWalking 的 collector 服务的 IP 及端口 注意:-Dskywalking.collector.backend_service可以指定远程地址,但是javaagent必须绑定你本机物理路径的skywalking-agent.jar
方式二:Java 命令行启动方式
java -javaagent:D:/ideaObject/reactBootspringboot-full/src/main/skywalking-agent/skywalking-agent.jar=-Dskywalking.agent.service_name=service-myapp,-Dskywalking.collector.backend_service=localhost:11800 -jar service-myapp.jar
方式三:编写 sh 脚本启动(Linux 环境)
#!/bin/bash
# 设置 SkyWalking Agent 的路径
AGENT_PATH="/home/yourusername/Desktop/apache-skywalking-apm-6.6.0/apache-skywalking-apm-bin/agent"
# 设置 Java 应用的 JAR 文件路径
JAR_PATH="/path/to/your/service-myapp.jar"
# 设置 SkyWalking 服务名称和 Collector 后端服务地址
SERVICE_NAME="service-myapp"
COLLECTOR_BACKEND_SERVICE="localhost:11800"
# 构造 Java Agent 参数
JAVA_AGENT="-javaagent:$AGENT_PATH/skywalking-agent.jar -Dskywalking.agent.service_name=$SERVICE_NAME -Dskywalking.collector.backend_service=$COLLECTOR_BACKEND_SERVICE"
# 启动 Java 应用
java $JAVA_AGENT -jar $JAR_PATH
七、SpringBoot 的启动
IDEA 部署探针方式启动
启动后,控制台日志输出开头出现了以下的记录,就表示连接上 SkyWalking 了。

再看 SkyWalking(http://localhost:8080/)页面那边,你就会发现有个这个图(表示连接上了)。

我们再请求一下 Controller 的接口,就会发现捕获了相关接口记录(但是目前,还是没有接口具体详细的日志入参或者出参的)。


SkyWalking 进行日志配置
为 log 日志增加 SkyWalking 的 traceId(追踪 ID)。便于排查。
首先引入 Maven 依赖:
<!-- SkyWalking 的日志工具包 -->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>9.0.0</version>
</dependency>
接着在 resources 文件夹下创建 logback-spring.xml 文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="D:/logs/" ></property>
<!-- 彩色日志 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<!--控制台日志,控制台输出 -->
<appender name="STDOUT">
<encoder>
<layout>
<!--格式化输出:%d表示日期,%thread 表示线程名,%-5level:级别从左显示 5 个字符宽度%msg:日志消息,%n 是换行符-->
<pattern>%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} [%X{tid}] %clr([%-10.10thread]){faint} %clr(%-5level) %clr(%-50.50logger{50}:%-3L){cyan} %clr(-){faint} %msg%n</pattern>
</layout>
</encoder>
</appender>
<!--文件日志,按照每天生成日志文件(只能是 由 Logger 或者 LoggerFactory 记录的日志消息哦)-->
<!--以下关于 日志文件的 pattern 需要去掉颜色,防止出现 ANSI 转义序列-->
<appender name="FILE">
<rollingPolicy>
${LOG_HOME}/%d{yyyy-MM-dd}/pro.log
30
%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%-10.10thread] %-5level %-50.50logger{50}:%-3L - %msg%n
10MB
%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%thread] %-5level %logger{36} -%msg%n
请求接口就可以发现 TID 的输出(在这里是 882c67dc859046c398fbfc5725df9de0.109.17288962842340001)。

然后把它放到 追踪 栏目的追踪 id,可以查到记录。

然后把它放到 日志 栏目的追踪 id,可以查到记录。

实现入参、返参都可查看
方式一:通过 Agent 配置实现(存在局限性)
首先,你需要确认 SkyWalking 的 Agent 配置。 SkyWalking 的 Agent 在启动时会读取配置文件,通常是 agent.config。 默认情况下,请求参数的采集是关闭的,你需要手动开启。 具体步骤如下: 在你的 SkyWalking Agent 配置文件 agent.config 中,找到 plugin 部分,确保以下配置项设置为 true:
plugin.tomcat.collect_http_params=${SW_PLUGIN_TOMCAT_COLLECT_HTTP_PARAMS:true}
plugin.springmvc.collect_http_params=${SW_PLUGIN_SPRINGMVC_COLLECT_HTTP_PARAMS:true}
plugin.httpclient.collect_http_params=${SW_PLUGIN_HTTPCLIENT_COLLECT_HTTP_PARAMS:true}
缺点:以上设置,只能开启 GET 请求的入参采集,POST 无法获取到,该方式存在局限性。
方式二:通过 trace 和 Filter 实现
一、引入追踪工具包
<!-- SkyWalking 追踪工具包 -->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>9.0.0</version>
</dependency>
二、使用 HttpFilter 和 ContentCachingRequestWrapper
知识小贴士:为什么不用 HttpServletRequest? 如果直接把 HttpServletRequest 中的 InputStream 读取后输出日志,会导致后续业务逻辑读取不到 InputStream 中的内容,因为流只能读取一次。
package com.example.springbootfull.quartztest.Filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Component
public class ApmHttpInfo extends HttpFilter {
//被忽略的头部信息
private static final Set<String> IGNORED_HEADERS;
static {
Set<String> ignoredHeaders = new HashSet<>();
ignoredHeaders.addAll(
java.util.Arrays.asList(
"Content-Type", "User-Agent", "Accept", "Cache-Control", "Postman-Token", "Host", "Accept-Encoding", "Connection", "Content-Length"
).stream()
.map(String::toUpperCase)
.collect(Collectors.toList())
);
IGNORED_HEADERS = ignoredHeaders;
}
IOException, ServletException {
(request);
(response);
{
filterChain.doFilter(requestWrapper, responseWrapper);
} {
{
()
.append().append(request.getMethod())
.append().append(request.getRequestURL().toString());
(StringUtils.hasLength(request.getQueryString())) {
sb.append().append(request.getQueryString());
}
Enumeration<String> headerNames = request.getHeaderNames();
(headerNames.hasMoreElements()) {
headerNames.nextElement();
(!IGNORED_HEADERS.contains(headerName.toUpperCase())) {
sb.append().append(headerName).append().append(request.getHeader(headerName)).append();
}
}
(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
(StringUtils.hasLength(body)) {
sb.append().append(body).append();
}
ActiveSpan.tag(, sb.toString());
(responseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
ActiveSpan.tag(, responseBody);
} (Exception e) {
log.warn(, e);
} {
responseWrapper.copyBodyToResponse();
}
}
}
}
效果如下(GET 请求):

效果如下(POST 请求):

方式三:通过 trace 和 AOP 去实现
此处不再赘述,这也是一种方案。

