从 “黑盒“ 到 “透明“:SkyWalking 实战指南 —— 让微服务问题无所遁形
在微服务架构盛行的今天,一个请求往往需要穿越多个服务才能完成。当系统出现问题时,开发者就像在迷宫中寻找出口,面对成百上千的服务实例和日志,定位问题变得异常艰难。Apache SkyWalking 作为一款优秀的分布式追踪系统,能帮助我们打开微服务的 "黑盒",让系统运行状态一目了然。本文将从理论到实践,全方位解析 SkyWalking 的核心原理与实战技巧,让你轻松掌握分布式系统的可观测性建设。
一、SkyWalking 前世今生:为什么需要分布式追踪?
随着微服务架构的普及,系统变得越来越复杂,传统的监控手段已经难以满足需求。想象一下,当用户投诉某个功能响应缓慢时,你需要排查 API 网关、多个微服务、数据库、缓存等多个组件,这个过程如果没有有效的工具支持,简直是一场噩梦。
1.1 分布式系统的可观测性挑战
分布式系统面临的可观测性挑战主要体现在三个方面:
- 调用链追踪困难:一个请求可能经过多个服务,传统日志分散在各个服务中,难以串联起来分析。
- 性能瓶颈定位难:系统响应慢,到底是哪个服务、哪个方法、哪个数据库查询出了问题?
- 服务依赖关系复杂:随着服务数量增加,服务间的依赖关系变得错综复杂,难以梳理。
这些挑战催生了分布式追踪系统的发展,而 SkyWalking 正是其中的佼佼者。
1.2 SkyWalking 简介
Apache SkyWalking 是一个开源的可观测性平台,专为微服务、云原生和容器化应用设计。它提供了分布式追踪、服务网格遥测分析、度量聚合和日志收集等功能,帮助开发者洞察分布式系统的运行状态。
SkyWalking 的核心优势:
- 多语言自动探针,支持 Java、.NET、Node.js 等多种语言
- 自动发现服务拓扑和依赖关系
- 强大的度量分析和告警能力
- 支持多种存储后端(Elasticsearch、MySQL、TiDB 等)
- 丰富的可视化界面
- 高性能,对业务代码侵入性小
1.3 分布式追踪的核心概念
在深入学习 SkyWalking 之前,我们需要了解几个分布式追踪的核心概念:
- 追踪(Trace):代表一个完整的请求链路,由多个跨度组成,用一个全局唯一的 Trace ID 标识。
- 跨度(Span):代表一个服务处理请求的单元,包含开始时间、结束时间、操作名称等信息,用 Span ID 标识。
- 标签(Tag):键值对,用于记录跨度的详细信息,如 HTTP 方法、URL、数据库语句等。
- 注解(Annotation):用于记录关键事件,如客户端发送请求(Client Send)、服务端接收请求(Server Receive)等。
- 采样率(Sampling Rate):由于高并发场景下追踪所有请求会带来性能开销,通常会设置采样率,只追踪一部分请求。
二、SkyWalking 架构深度解析
SkyWalking 采用模块化设计,各组件职责清晰,便于扩展和定制。理解其架构有助于我们更好地部署和使用 SkyWalking。
2.1 整体架构
SkyWalking 的架构可以分为四个主要部分:
- 探针(Agent):嵌入到应用程序中,负责收集追踪数据和度量信息,无需修改业务代码。
- 服务端(OAP Server):接收探针发送的数据,进行分析、聚合和存储,并提供查询接口。
- 存储(Storage):存储分析后的追踪数据、度量信息和告警数据,支持多种存储后端。
- UI:提供可视化界面,用于展示和查询追踪数据、服务拓扑、性能指标等。
2.2 数据流转过程
SkyWalking 的数据流转过程如下:
- 应用程序运行时产生服务调用
- Agent 拦截这些调用,生成 Span 和度量数据
- Agent 将数据批量发送到 OAP Server
- OAP Server 对数据进行分析和聚合
- 处理后的数据存储到指定的存储后端
- 用户通过 UI 查询和可视化数据
2.3 核心技术原理
SkyWalking 的实现基于字节码增强技术,这也是它能做到对业务代码零侵入的关键:
- 字节码增强:Agent 通过 Java Instrumentation API 在类加载时修改字节码,插入追踪代码。
- 跨进程传播:通过在服务间传递 Trace ID 和 Span ID,实现跨服务的调用链追踪。
- 服务发现:通过分析服务间的调用关系,自动发现服务拓扑。
- 指标计算:基于追踪数据计算各种性能指标,如响应时间、吞吐量、错误率等。
三、SkyWalking 环境搭建实战
接下来,我们将一步步搭建 SkyWalking 环境,包括 OAP Server、UI 和存储后端的部署。
3.1 环境准备
本次实战使用以下版本:
- SkyWalking 9.7.0(最新稳定版)
- Elasticsearch 8.11.3(存储后端)
- JDK 17(运行环境)
- Spring Boot 3.2.0(演示应用)
3.2 部署 Elasticsearch
SkyWalking 推荐使用 Elasticsearch 作为存储后端,我们先来部署 Elasticsearch:
- 下载 Elasticsearch 8.11.3:
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.11.3-linux-x86_64.tar.gz
tar -zxvf elasticsearch-8.11.3-linux-x86_64.tar.gz
cd elasticsearch-8.11.3
- 修改配置文件 config/elasticsearch.yml:
cluster.name: skywalking-es-cluster
node.name: node-1
path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
network.host: 0.0.0.0
http.port: 9200
discovery.seed_hosts: ["localhost"]
cluster.initial_master_nodes: ["node-1"]
# 关闭安全特性(仅用于演示环境)
xpack.security.enabled: false
xpack.security.enrollment.enabled: false
xpack.security.http.ssl.enabled: false
xpack.security.transport.ssl.enabled: false
- 启动 Elasticsearch:
# 创建专用用户(Elasticsearch不允许root用户运行)
useradd esuser
chown -R esuser:esuser elasticsearch-8.11.3
su esuser
# 启动
./bin/elasticsearch -d
- 验证 Elasticsearch 是否启动成功:
curl http://localhost:9200
如果返回类似以下内容,说明启动成功:
{"name" : "node-1","cluster_name" : "skywalking-es-cluster","cluster_uuid" : "xxxxxxxxxxxxxxxxxxxx","version" : {"number" : "8.11.3","build_flavor" : "default","build_type" : "tar","build_hash" : "f561230f37a86c8929ca1a400f2d00f7128049","build_date" : "2023-12-08T11:33:43.978089850Z","build_snapshot" : false,"lucene_version" : "9.8.0","minimum_wire_compatibility_version" : "7.17.0","minimum_index_compatibility_version" : "7.0.0"},"tagline" : "You Know, for Search"
}
3.3 部署 SkyWalking OAP Server
- 下载 SkyWalking 9.7.0:
wget https://archive.apache.org/dist/skywalking/9.7.0/apache-skywalking-apm-9.7.0.tar.gz
tar -zxvf apache-skywalking-apm-9.7.0.tar.gz
cd apache-skywalking-apm-bin
- 修改配置文件 config/application.yml,配置 Elasticsearch 作为存储:
storage:selector: elasticsearchelasticsearch:namespace: skywalkingclusterNodes: localhost:9200protocol: http# 其他配置保持默认
- 启动 OAP Server:
# Linux
bin/oapService.sh start
# Windows
bin/oapService.bat
3.4 部署 SkyWalking UI
SkyWalking UI 已经包含在下载的安装包中,无需单独下载:
- 修改 UI 配置文件 webapp/application.yml(可选):
server:port: 8080 # UI端口,默认8080collector:path: /graphqlribbon:ReadTimeout: 10000# OAP Server地址listOfServers: localhost:12800
- 启动 UI:
# Linux
bin/webappService.sh start
# Windows
bin/webappService.bat
- 访问 UI:打开浏览器,访问http://localhost:8080,看到 SkyWalking 的登录界面即表示部署成功(默认无需登录)。
四、Spring Boot 应用集成 SkyWalking
接下来,我们将创建一个 Spring Boot 应用,并集成 SkyWalking Agent,实现分布式追踪。
4.1 创建 Spring Boot 项目
首先创建一个 Spring Boot 项目,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.2.0</version><relativePath/></parent><groupId>com.example</groupId><artifactId>skywalking-demo</artifactId><version>0.0.1-SNAPSHOT</version><name>skywalking-demo</name><description>Demo project for SkyWalking integration</description><properties><java.version>17</java.version><mybatis-plus.version>3.5.5</mybatis-plus.version><lombok.version>1.18.30</lombok.version><fastjson2.version>2.0.32</fastjson2.version><guava.version>32.1.3-jre</guava.version><springdoc.version>2.1.0</springdoc.version><mysql.version>8.0.33</mysql.version></properties><dependencies><!-- Spring Boot核心 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 数据库 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>${mysql.version}</version><scope>runtime</scope></dependency><!-- MyBatis-Plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version><optional>true</optional></dependency><!-- FastJSON2 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>${fastjson2.version}</version></dependency><!-- Guava --><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>${guava.version}</version></dependency><!-- Swagger3 --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>${springdoc.version}</version></dependency><!-- 测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></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>
4.2 创建数据库表
创建一个简单的用户表用于演示:
CREATE DATABASE IF NOT EXISTS skywalking_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;USE skywalking_demo;CREATE TABLE IF NOT EXISTS `user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',`username` varchar(50) NOT NULL COMMENT '用户名',`password` varchar(100) NOT NULL COMMENT '密码',`email` varchar(100) DEFAULT NULL COMMENT '邮箱',`age` int DEFAULT NULL COMMENT '年龄',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';-- 插入测试数据
INSERT INTO `user` (`username`, `password`, `email`, `age`) VALUES
('test1', '123456', 'test1@example.com', 20),
('test2', '123456', 'test2@example.com', 25),
('test3', '123456', 'test3@example.com', 30);
4.3 配置文件
创建 application.yml 配置文件:
spring:datasource:url: jdbc:mysql://localhost:3306/skywalking_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver# MyBatis-Plus配置
mybatis-plus:mapper-locations: classpath*:mapper/**/*.xmltype-aliases-package: com.example.skywalkingdemo.entityconfiguration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl# 日志配置
logging:level:com.example.skywalkingdemo: debug# 服务器配置
server:port: 8081servlet:context-path: /user-service
4.4 实体类
创建 User 实体类:
package com.example.skywalkingdemo.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;/*** 用户实体类** @author ken*/
@Data
@TableName("user")
public class User {/*** 主键ID*/@TableId(type = IdType.AUTO)private Long id;/*** 用户名*/private String username;/*** 密码*/private String password;/*** 邮箱*/private String email;/*** 年龄*/private Integer age;/*** 创建时间*/@TableField(value = "create_time", fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT)private LocalDateTime createTime;/*** 更新时间*/@TableField(value = "update_time", fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}
4.5 Mapper 接口
创建 UserMapper 接口:
package com.example.skywalkingdemo.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.skywalkingdemo.entity.User;
import org.apache.ibatis.annotations.Mapper;/*** 用户Mapper接口** @author ken*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
4.6 服务层
创建 UserService 接口和实现类:
package com.example.skywalkingdemo.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.example.skywalkingdemo.entity.User;
import java.util.List;/*** 用户服务接口** @author ken*/
public interface UserService extends IService<User> {/*** 创建用户** @param user 用户信息* @return 创建的用户*/User createUser(User user);/*** 根据ID获取用户** @param id 用户ID* @return 用户信息*/User getUserById(Long id);/*** 获取所有用户** @return 用户列表*/List<User> getAllUsers();/*** 更新用户** @param user 用户信息* @return 是否更新成功*/boolean updateUser(User user);/*** 删除用户** @param id 用户ID* @return 是否删除成功*/boolean deleteUser(Long id);
}
package com.example.skywalkingdemo.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.skywalkingdemo.entity.User;
import com.example.skywalkingdemo.mapper.UserMapper;
import com.example.skywalkingdemo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import java.util.List;/*** 用户服务实现类** @author ken*/
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {/*** 创建用户** @param user 用户信息* @return 创建的用户*/@Override@Transactional(rollbackFor = Exception.class)public User createUser(User user) {log.info("创建用户: {}", user);if (ObjectUtils.isEmpty(user)) {throw new IllegalArgumentException("用户信息不能为空");}baseMapper.insert(user);return user;}/*** 根据ID获取用户** @param id 用户ID* @return 用户信息*/@Overridepublic User getUserById(Long id) {log.info("根据ID获取用户: {}", id);if (ObjectUtils.isEmpty(id)) {throw new IllegalArgumentException("用户ID不能为空");}return baseMapper.selectById(id);}/*** 获取所有用户** @return 用户列表*/@Overridepublic List<User> getAllUsers() {log.info("获取所有用户");return baseMapper.selectList(new QueryWrapper<>());}/*** 更新用户** @param user 用户信息* @return 是否更新成功*/@Override@Transactional(rollbackFor = Exception.class)public boolean updateUser(User user) {log.info("更新用户: {}", user);if (ObjectUtils.isEmpty(user) || ObjectUtils.isEmpty(user.getId())) {throw new IllegalArgumentException("用户ID不能为空");}return baseMapper.updateById(user) > 0;}/*** 删除用户** @param id 用户ID* @return 是否删除成功*/@Override@Transactional(rollbackFor = Exception.class)public boolean deleteUser(Long id) {log.info("删除用户: {}", id);if (ObjectUtils.isEmpty(id)) {throw new IllegalArgumentException("用户ID不能为空");}return baseMapper.deleteById(id) > 0;}
}
4.7 控制层
创建 UserController:
package com.example.skywalkingdemo.controller;import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.skywalkingdemo.entity.User;
import com.example.skywalkingdemo.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.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.util.ObjectUtils;
import java.util.List;/*** 用户控制器** @author ken*/
@Slf4j
@RestController
@RequestMapping("/api/users")
@Tag(name = "用户管理", description = "用户CRUD操作")
public class UserController {private final UserService userService;public UserController(UserService userService) {this.userService = userService;}/*** 创建用户** @param user 用户信息* @return 创建的用户*/@PostMapping@Operation(summary = "创建用户", description = "添加新用户到系统")public ResponseEntity<User> createUser(@Parameter(description = "用户信息", required = true) @RequestBody User user) {User createdUser = userService.createUser(user);return ResponseEntity.ok(createdUser);}/*** 根据ID获取用户** @param id 用户ID* @return 用户信息*/@GetMapping("/{id}")@Operation(summary = "根据ID获取用户", description = "通过用户ID查询用户详情")public ResponseEntity<User> getUserById(@Parameter(description = "用户ID", required = true) @PathVariable Long id) {User user = userService.getUserById(id);if (ObjectUtils.isEmpty(user)) {return ResponseEntity.notFound().build();}return ResponseEntity.ok(user);}/*** 获取所有用户** @return 用户列表*/@GetMapping@Operation(summary = "获取所有用户", description = "查询系统中所有用户")public ResponseEntity<List<User>> getAllUsers() {List<User> users = userService.getAllUsers();return ResponseEntity.ok(users);}/*** 分页获取用户** @param page 页码* @param size 每页数量* @return 分页用户列表*/@GetMapping("/page")@Operation(summary = "分页获取用户", description = "分页查询系统中的用户")public ResponseEntity<IPage<User>> getUsersByPage(@Parameter(description = "页码", required = true) @RequestParam(defaultValue = "1") Integer page,@Parameter(description = "每页数量", required = true) @RequestParam(defaultValue = "10") Integer size) {Page<User> userPage = new Page<>(page, size);IPage<User> users = userService.page(userPage);return ResponseEntity.ok(users);}/*** 更新用户** @param id 用户ID* @param user 用户信息* @return 更新结果*/@PutMapping("/{id}")@Operation(summary = "更新用户", description = "根据ID更新用户信息")public ResponseEntity<Boolean> updateUser(@Parameter(description = "用户ID", required = true) @PathVariable Long id,@Parameter(description = "用户信息", required = true) @RequestBody User user) {user.setId(id);boolean updated = userService.updateUser(user);return ResponseEntity.ok(updated);}/*** 删除用户** @param id 用户ID* @return 删除结果*/@DeleteMapping("/{id}")@Operation(summary = "删除用户", description = "根据ID删除用户")public ResponseEntity<Boolean> deleteUser(@Parameter(description = "用户ID", required = true) @PathVariable Long id) {boolean deleted = userService.deleteUser(id);return ResponseEntity.ok(deleted);}
}
4.8 配置类
创建 MyBatis-Plus 分页插件配置类:
package com.example.skywalkingdemo.config;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** MyBatis-Plus配置类** @author ken*/
@Configuration
public class MyBatisPlusConfig {/*** 配置分页插件** @return MybatisPlusInterceptor*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分页插件,指定数据库类型为MySQLinterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}
}
4.9 启动类
创建项目启动类:
package com.example.skywalkingdemo;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;/*** SkyWalking示例项目启动类** @author ken*/
@SpringBootApplication
@MapperScan("com.example.skywalkingdemo.mapper")
@OpenAPIDefinition(info = @Info(title = "SkyWalking实战示例API",version = "1.0",description = "SkyWalking集成示例项目的API文档")
)
public class SkywalkingDemoApplication {public static void main(String[] args) {SpringApplication.run(SkywalkingDemoApplication.class, args);}
}
4.10 集成 SkyWalking Agent
要让应用程序被 SkyWalking 监控,需要在启动时指定 SkyWalking Agent:
- 从 SkyWalking 安装包中复制 agent 目录到合适的位置,例如项目根目录
- 修改 agent/config/agent.config 配置文件,设置应用名称和 OAP Server 地址:
# 应用名称,将在SkyWalking UI中显示
agent.service_name=${SW_AGENT_NAME:user-service}
# OAP Server地址
collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:localhost:11800}
# 采样率,1表示100%采样
agent.sample_n_per_3_secs=${SW_AGENT_SAMPLE:1}
- 使用以下命令启动 Spring Boot 应用(添加 - javaagent 参数):
java -javaagent:/path/to/agent/skywalking-agent.jar \-jar target/skywalking-demo-0.0.1-SNAPSHOT.jar
如果使用 Maven 插件启动,可以在 pom.xml 中配置:
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><jvmArguments>-javaagent:/path/to/agent/skywalking-agent.jar</jvmArguments><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration>
</plugin>
然后使用 Maven 命令启动:
mvn spring-boot:run
4.11 测试服务
启动应用后,可以通过 Swagger 文档测试接口:访问 http://localhost:8081/user-service/swagger-ui/index.html
调用几个接口后,我们就可以在 SkyWalking UI 中看到追踪数据了。
五、SkyWalking UI 功能详解
SkyWalking UI 提供了丰富的可视化功能,帮助我们分析和诊断分布式系统问题。
5.1 仪表盘(Dashboard)
仪表盘是 SkyWalking 的首页,展示了系统的关键指标:
- 服务吞吐量(Service Throughput):单位时间内的请求数
- 平均响应时间(Average Response Time):服务处理请求的平均时间
- 服务成功率(Service Success Rate):成功请求占总请求的比例
- 慢查询(Slow Traces):响应时间较长的请求
- 拓扑图(Topology):服务之间的依赖关系
5.2 服务(Services)
服务页面展示了所有被监控的服务列表,点击某个服务可以查看该服务的详细信息:
- 服务性能指标:响应时间、吞吐量、成功率随时间的变化曲线
- 实例列表:该服务的所有实例
- 端点列表:该服务的所有 API 端点
- 数据库访问:该服务访问的数据库及相关指标
- 缓存访问:该服务访问的缓存及相关指标
5.3 追踪(Traces)
追踪页面展示了所有被追踪的请求,我们可以通过以下方式筛选追踪数据:
- 按服务名称筛选
- 按响应时间筛选(可筛选慢查询)
- 按状态筛选(成功 / 失败)
- 按时间范围筛选
点击某个追踪可以查看详细的调用链:
- 每个跨度的响应时间
- 服务间的调用关系
- 每个跨度的详细信息(如 HTTP 方法、URL、数据库语句等)
5.4 拓扑图(Topology)
拓扑图展示了服务之间的依赖关系,以及服务与数据库、缓存等中间件的连接关系。通过拓扑图,我们可以直观地了解系统的整体架构。
拓扑图中的每个节点代表一个服务或中间件,节点之间的连线表示它们之间的调用关系,连线上的数字表示调用的 QPS。
5.5 告警(Alarms)
SkyWalking 支持基于指标设置告警规则,当指标超过阈值时会触发告警。告警页面展示了所有触发的告警信息,包括:
- 告警时间
- 告警级别
- 告警信息
- 关联的服务
六、SkyWalking 高级特性实战
6.1 自定义追踪上下文
有时候我们需要在追踪中添加自定义信息,SkyWalking 提供了 API 来实现这一功能:
package com.example.skywalkingdemo.service.impl;import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.skywalkingdemo.entity.User;
import com.example.skywalkingdemo.mapper.UserMapper;
import com.example.skywalkingdemo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import java.util.List;/*** 扩展用户服务实现类,添加SkyWalking自定义追踪** @author ken*/
@Slf4j
@Service
public class UserServiceImplWithTrace extends ServiceImpl<UserMapper, User> implements UserService {/*** 创建用户,添加自定义追踪信息** @param user 用户信息* @return 创建的用户*/@Override@Transactional(rollbackFor = Exception.class)public User createUser(User user) {// 获取当前Trace IDString traceId = TraceContext.traceId();log.info("创建用户,Trace ID: {}", traceId);if (ObjectUtils.isEmpty(user)) {// 记录错误信息到追踪中ActiveSpan.error(new IllegalArgumentException("用户信息不能为空"));throw new IllegalArgumentException("用户信息不能为空");}// 添加自定义标签ActiveSpan.tag("username", user.getUsername());ActiveSpan.tag("user_info", JSON.toJSONString(user));// 添加自定义日志ActiveSpan.log("开始插入用户数据");baseMapper.insert(user);ActiveSpan.log("用户数据插入完成,ID: " + user.getId());return user;}// 其他方法实现...@Overridepublic User getUserById(Long id) {log.info("根据ID获取用户: {}", id);if (ObjectUtils.isEmpty(id)) {ActiveSpan.error(new IllegalArgumentException("用户ID不能为空"));throw new IllegalArgumentException("用户ID不能为空");}ActiveSpan.tag("user_id", id.toString());return baseMapper.selectById(id);}@Overridepublic List<User> getAllUsers() {log.info("获取所有用户");return baseMapper.selectList(new QueryWrapper<>());}@Override@Transactional(rollbackFor = Exception.class)public boolean updateUser(User user) {log.info("更新用户: {}", user);if (ObjectUtils.isEmpty(user) || ObjectUtils.isEmpty(user.getId())) {ActiveSpan.error(new IllegalArgumentException("用户ID不能为空"));throw new IllegalArgumentException("用户ID不能为空");}ActiveSpan.tag("user_id", user.getId().toString());return baseMapper.updateById(user) > 0;}@Override@Transactional(rollbackFor = Exception.class)public boolean deleteUser(Long id) {log.info("删除用户: {}", id);if (ObjectUtils.isEmpty(id)) {ActiveSpan.error(new IllegalArgumentException("用户ID不能为空"));throw new IllegalArgumentException("用户ID不能为空");}ActiveSpan.tag("user_id", id.toString());return baseMapper.deleteById(id) > 0;}
}
要使用 SkyWalking 的自定义追踪 API,需要添加以下依赖:
<dependency><groupId>org.apache.skywalking</groupId><artifactId>apm-toolkit-trace</artifactId><version>9.7.0</version>
</dependency>
6.2 分布式追踪实战:多服务调用
为了演示分布式追踪,我们再创建一个订单服务,调用前面的用户服务:
- 创建订单服务项目,pom.xml 与用户服务类似,额外添加 Spring Cloud OpenFeign 依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId><version>4.1.0</version>
</dependency>
- 创建订单实体类:
package com.example.orderservice.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;/*** 订单实体类** @author ken*/
@Data
@TableName("order")
public class Order {/*** 主键ID*/@TableId(type = IdType.AUTO)private Long id;/*** 用户ID*/private Long userId;/*** 订单金额*/private BigDecimal amount;/*** 订单状态:0-待支付,1-已支付,2-已取消*/private Integer status;/*** 创建时间*/@TableField(value = "create_time", fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT)private LocalDateTime createTime;/*** 更新时间*/@TableField(value = "update_time", fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}
- 创建 Feign 客户端调用用户服务:
package com.example.orderservice.feign;import com.example.orderservice.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 = "http://localhost:8081/user-service")
public interface UserServiceFeignClient {/*** 根据ID获取用户** @param id 用户ID* @return 用户信息*/@GetMapping("/api/users/{id}")User getUserById(@PathVariable("id") Long id);
}
- 创建订单服务的控制器和服务类,在服务中调用用户服务:
package com.example.orderservice.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.orderservice.entity.Order;
import com.example.orderservice.entity.User;
import com.example.orderservice.feign.UserServiceFeignClient;
import com.example.orderservice.mapper.OrderMapper;
import com.example.orderservice.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;/*** 订单服务实现类** @author ken*/
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {private final UserServiceFeignClient userServiceFeignClient;public OrderServiceImpl(UserServiceFeignClient userServiceFeignClient) {this.userServiceFeignClient = userServiceFeignClient;}/*** 创建订单,同时查询用户信息** @param order 订单信息* @return 创建的订单*/@Override@Transactional(rollbackFor = Exception.class)public Order createOrder(Order order) {log.info("创建订单: {}", order);if (ObjectUtils.isEmpty(order) || ObjectUtils.isEmpty(order.getUserId())) {ActiveSpan.error(new IllegalArgumentException("用户ID不能为空"));throw new IllegalArgumentException("用户ID不能为空");}// 调用用户服务查询用户信息log.info("查询用户信息,用户ID: {}", order.getUserId());User user = userServiceFeignClient.getUserById(order.getUserId());if (ObjectUtils.isEmpty(user)) {ActiveSpan.error(new IllegalArgumentException("用户不存在,ID: " + order.getUserId()));throw new IllegalArgumentException("用户不存在,ID: " + order.getUserId());}log.info("查询到用户信息: {}", user);// 创建订单baseMapper.insert(order);return order;}// 其他方法实现...@Overridepublic Order getOrderById(Long id) {log.info("根据ID获取订单: {}", id);if (ObjectUtils.isEmpty(id)) {ActiveSpan.error(new IllegalArgumentException("订单ID不能为空"));throw new IllegalArgumentException("订单ID不能为空");}return baseMapper.selectById(id);}
}
- 配置订单服务的 SkyWalking Agent,设置服务名为 order-service:
agent.service_name=${SW_AGENT_NAME:order-service}
- 分别启动用户服务和订单服务,调用订单服务的创建订单接口,然后在 SkyWalking UI 中查看完整的调用链。
6.3 告警配置
SkyWalking 支持基于各种指标设置告警规则,配置文件位于 OAP Server 的 config/alarm-settings.yml:
rules:# 服务响应时间告警service_resp_time_rule:metrics-name: service_resp_timeop: ">"threshold: 1000period: 10count: 3silence-period: 5message: "服务 {name} 响应时间超过1000ms,当前值: {value}ms"# 服务错误率告警service_error_rate_rule:metrics-name: service_error_rateop: ">"threshold: 0.05period: 10count: 2silence-period: 5message: "服务 {name} 错误率超过5%,当前值: {value}"# 端点响应时间告警endpoint_resp_time_rule:metrics-name: endpoint_resp_timeop: ">"threshold: 500period: 10count: 3silence-period: 5message: "端点 {name} 响应时间超过500ms,当前值: {value}ms"# 告警接收器配置
receivers:default:# 控制台输出- console:# 可以添加邮件、Slack等其他接收器#- email:# host: smtp.example.com# port: 587# username: alert@example.com# password: password# from: alert@example.com# to: admin@example.com# subject: "SkyWalking 告警"
配置说明:
- metrics-name:要监控的指标名称
- op:比较运算符(>、<、>=、<=、==)
- threshold:阈值
- period:检测周期(分钟)
- count:连续多少次超过阈值才触发告警
- silence-period:告警沉默期,在此期间不再发送相同的告警
- message:告警消息
修改配置后需要重启 OAP Server 使配置生效。
6.4 日志集成
SkyWalking 可以与日志框架集成,将 Trace ID 添加到日志中,方便将日志与追踪关联起来。
- 添加 logback 依赖:
<dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.4.8</version>
</dependency>
- 创建 logback-spring.xml 配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><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{36} - %msg%n</Pattern></layout></encoder></appender><root level="INFO"><appender-ref ref="STDOUT" /></root>
</configuration>
- 添加 SkyWalking 日志工具依赖:
<dependency><groupId>org.apache.skywalking</groupId><artifactId>apm-toolkit-logback-1.x</artifactId><version>9.7.0</version>
</dependency>
这样配置后,日志中会包含 [TID:xxx] 字段,记录当前的 Trace ID,方便将日志与 SkyWalking 中的追踪关联起来。
七、SkyWalking 性能优化与最佳实践
7.1 性能优化建议
SkyWalking 本身的性能开销很小,但在高并发场景下,仍需要进行适当的优化:
-
调整采样率:在高并发场景下,可以降低采样率,减少数据量
# 每3秒最多采样10个追踪 agent.sample_n_per_3_secs=10
-
选择合适的存储:生产环境推荐使用 Elasticsearch,并根据数据量进行集群配置
-
调整 JVM 参数:为 OAP Server 分配足够的内存
# 在bin/oapService.sh中调整 JAVA_OPTS="-Xms2g -Xmx2g -XX:+UseG1GC"
-
定期清理数据:配置 Elasticsearch 的索引生命周期管理,定期清理旧数据
-
使用异步 reporter:Agent 默认使用异步方式发送数据,确保此配置未被修改
agent.reporter.type=grpc
7.2 最佳实践
-
为所有服务配置有意义的名称:方便在 UI 中识别和筛选服务
agent.service_name=order-service
-
合理设置告警阈值:根据业务场景设置合适的告警阈值,避免告警风暴
-
集成日志与追踪:将 Trace ID 添加到日志中,方便问题排查
-
定期分析慢查询:利用 SkyWalking 的慢查询追踪功能,定期分析并优化慢查询
-
监控数据库性能:关注数据库操作的响应时间,及时发现数据库性能问题
-
跟踪服务依赖变化:通过拓扑图关注服务依赖关系的变化,及时发现不合理的依赖
-
结合业务指标:除了系统指标,还可以通过自定义指标监控业务指标
八、常见问题与解决方案
8.1 Agent 无法连接到 OAP Server
可能原因:
- OAP Server 未启动或端口错误
- 网络问题,Agent 无法访问 OAP Server
- 防火墙阻止了连接
解决方案:
- 检查 OAP Server 是否正常运行:
ps -ef | grep oap
- 检查 OAP Server 端口配置(默认 11800)
- 测试网络连通性:
telnet oap-server-ip 11800
- 检查防火墙规则,确保 11800 端口开放
8.2 UI 中看不到服务数据
可能原因:
- Agent 未正确配置或未随应用启动
- Agent 与 OAP Server 连接失败
- 应用未产生任何请求
- 采样率设置过低,未采集到数据
解决方案:
- 检查应用启动命令,确保 - javaagent 参数正确
- 查看应用日志,寻找与 SkyWalking 相关的错误信息
- 发送请求到应用,确保有流量产生
- 提高采样率:
agent.sample_n_per_3_secs=100
8.3 追踪数据不完整
可能原因:
- 部分服务未集成 SkyWalking Agent
- 服务间调用未正确传递 Trace 上下文
- 异步调用未正确处理 Trace 上下文
解决方案:
- 确保所有服务都集成了 SkyWalking Agent
- 检查服务间调用方式,确保支持 Trace 上下文传递(SkyWalking 对常见框架如 Spring Cloud、Dubbo 等都有支持)
- 对于异步调用,使用 SkyWalking 提供的工具类传递上下文:
import org.apache.skywalking.apm.toolkit.trace.TraceContext; import org.apache.skywalking.apm.toolkit.trace.TraceCrossThread;// 异步任务 @TraceCrossThread public class AsyncTask implements Runnable {@Overridepublic void run() {// 异步任务逻辑} }
8.4 OAP Server 内存占用过高
可能原因:
- 数据量过大
- JVM 内存配置不足
- 存储后端性能不佳,导致数据堆积
解决方案:
- 增加 OAP Server 的 JVM 内存配置
- 降低采样率,减少数据量
- 优化存储后端性能,如为 Elasticsearch 增加节点
- 配置数据保留策略,及时清理旧数据
九、总结与展望
Apache SkyWalking 作为一款优秀的分布式追踪系统,为微服务架构提供了强大的可观测性支持。通过本文的学习,我们了解了 SkyWalking 的核心概念、架构原理,掌握了环境搭建、应用集成和高级特性的使用方法。
SkyWalking 不仅能帮助我们追踪分布式系统中的请求流向,还能提供丰富的性能指标和告警功能,让我们能够及时发现和解决系统中的性能瓶颈和潜在问题。在实际项目中,合理使用 SkyWalking 可以显著提高系统的可靠性和可维护性。
希望本文能帮助你快速掌握 SkyWalking 的使用,让你的微服务系统从 "黑盒" 变为 "透明",让问题排查不再困难。