从 “黑盒“ 到 “透明“:SkyWalking 实战指南 —— 让微服务问题无所遁形

在微服务架构盛行的今天,一个请求往往需要穿越多个服务才能完成。当系统出现问题时,开发者就像在迷宫中寻找出口,面对成百上千的服务实例和日志,定位问题变得异常艰难。Apache SkyWalking 作为一款优秀的分布式追踪系统,能帮助我们打开微服务的 "黑盒",让系统运行状态一目了然。本文将从理论到实践,全方位解析 SkyWalking 的核心原理与实战技巧,让你轻松掌握分布式系统的可观测性建设。

一、SkyWalking 前世今生:为什么需要分布式追踪?

随着微服务架构的普及,系统变得越来越复杂,传统的监控手段已经难以满足需求。想象一下,当用户投诉某个功能响应缓慢时,你需要排查 API 网关、多个微服务、数据库、缓存等多个组件,这个过程如果没有有效的工具支持,简直是一场噩梦。

1.1 分布式系统的可观测性挑战

分布式系统面临的可观测性挑战主要体现在三个方面:

  1. 调用链追踪困难:一个请求可能经过多个服务,传统日志分散在各个服务中,难以串联起来分析。
  2. 性能瓶颈定位难:系统响应慢,到底是哪个服务、哪个方法、哪个数据库查询出了问题?
  3. 服务依赖关系复杂:随着服务数量增加,服务间的依赖关系变得错综复杂,难以梳理。

这些挑战催生了分布式追踪系统的发展,而 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 的架构可以分为四个主要部分:

  1. 探针(Agent):嵌入到应用程序中,负责收集追踪数据和度量信息,无需修改业务代码。
  2. 服务端(OAP Server):接收探针发送的数据,进行分析、聚合和存储,并提供查询接口。
  3. 存储(Storage):存储分析后的追踪数据、度量信息和告警数据,支持多种存储后端。
  4. UI:提供可视化界面,用于展示和查询追踪数据、服务拓扑、性能指标等。

2.2 数据流转过程

SkyWalking 的数据流转过程如下:

  1. 应用程序运行时产生服务调用
  2. Agent 拦截这些调用,生成 Span 和度量数据
  3. Agent 将数据批量发送到 OAP Server
  4. OAP Server 对数据进行分析和聚合
  5. 处理后的数据存储到指定的存储后端
  6. 用户通过 UI 查询和可视化数据

2.3 核心技术原理

SkyWalking 的实现基于字节码增强技术,这也是它能做到对业务代码零侵入的关键:

  1. 字节码增强:Agent 通过 Java Instrumentation API 在类加载时修改字节码,插入追踪代码。
  2. 跨进程传播:通过在服务间传递 Trace ID 和 Span ID,实现跨服务的调用链追踪。
  3. 服务发现:通过分析服务间的调用关系,自动发现服务拓扑。
  4. 指标计算:基于追踪数据计算各种性能指标,如响应时间、吞吐量、错误率等。

三、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:

  1. 下载 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

  2. 修改配置文件 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

  3. 启动 Elasticsearch:

    创建专用用户(Elasticsearch不允许root用户运行)

    useradd esuser
    chown -R esuser:esuser elasticsearch-8.11.3
    su esuser

    启动

    ./bin/elasticsearch -d

  4. 验证 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

  1. 下载 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

  2. 修改配置文件 config/application.yml,配置 Elasticsearch 作为存储:

    storage:
    selector: elasticsearch
    elasticsearch:
    namespace: skywalking
    clusterNodes: localhost:9200
    protocol: http
    # 其他配置保持默认

  3. 启动 OAP Server:

    Linux

    bin/oapService.sh start

    Windows

    bin/oapService.bat

3.4 部署 SkyWalking UI

SkyWalking UI 已经包含在下载的安装包中,无需单独下载:

  1. 修改 UI 配置文件 webapp/application.yml(可选):

    server:
    port: 8080 # UI端口,默认8080

    collector:
    path: /graphql
    ribbon:
    ReadTimeout: 10000
    # OAP Server地址
    listOfServers: localhost:12800

  2. 启动 UI:

    Linux

    bin/webappService.sh start

    Windows

    bin/webappService.bat

  3. 访问 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=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    
# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath*:mapper/**/*.xml
  type-aliases-package: com.example.skywalkingdemo.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

# 日志配置
logging:
  level:
    com.example.skywalkingdemo: debug

# 服务器配置
server:
  port: 8081
  servlet:
    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 用户信息
     */
    @Override
    public User getUserById(Long id) {
        log.info("根据ID获取用户: {}", id);
        if (ObjectUtils.isEmpty(id)) {
            throw new IllegalArgumentException("用户ID不能为空");
        }
        return baseMapper.selectById(id);
    }
    
    /**
     * 获取所有用户
     *
     * @return 用户列表
     */
    @Override
    public 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
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件,指定数据库类型为MySQL
        interceptor.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:

  1. 从 SkyWalking 安装包中复制 agent 目录到合适的位置,例如项目根目录

  2. 修改 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}

  3. 使用以下命令启动 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 ID
        String 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;
    }
    
    // 其他方法实现...
    @Override
    public 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);
    }
    
    @Override
    public 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 分布式追踪实战:多服务调用

为了演示分布式追踪,我们再创建一个订单服务,调用前面的用户服务:

  1. 创建订单服务项目,pom.xml 与用户服务类似,额外添加 Spring Cloud OpenFeign 依赖:

    <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>4.1.0</version> </dependency>
  2. 创建订单实体类:

    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;
        }
  3. 创建 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);
        }
  4. 创建订单服务的控制器和服务类,在服务中调用用户服务:

    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;
        }

      // 其他方法实现...
      @Override
      public 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);
      }
      }

  5. 配置订单服务的 SkyWalking Agent,设置服务名为 order-service:

    agent.service_name=${SW_AGENT_NAME:order-service}

  6. 分别启动用户服务和订单服务,调用订单服务的创建订单接口,然后在 SkyWalking UI 中查看完整的调用链。

6.3 告警配置

SkyWalking 支持基于各种指标设置告警规则,配置文件位于 OAP Server 的 config/alarm-settings.yml:

复制代码
rules:
  # 服务响应时间告警
  service_resp_time_rule:
    metrics-name: service_resp_time
    op: ">"
    threshold: 1000
    period: 10
    count: 3
    silence-period: 5
    message: "服务 {name} 响应时间超过1000ms,当前值: {value}ms"
  
  # 服务错误率告警
  service_error_rate_rule:
    metrics-name: service_error_rate
    op: ">"
    threshold: 0.05
    period: 10
    count: 2
    silence-period: 5
    message: "服务 {name} 错误率超过5%,当前值: {value}"
  
  # 端点响应时间告警
  endpoint_resp_time_rule:
    metrics-name: endpoint_resp_time
    op: ">"
    threshold: 500
    period: 10
    count: 3
    silence-period: 5
    message: "端点 {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 添加到日志中,方便将日志与追踪关联起来。

  1. 添加 logback 依赖:

    <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.8</version> </dependency>
  2. 创建 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>
  3. 添加 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 本身的性能开销很小,但在高并发场景下,仍需要进行适当的优化:

  1. 调整采样率:在高并发场景下,可以降低采样率,减少数据量

    复制代码
    # 每3秒最多采样10个追踪
    agent.sample_n_per_3_secs=10
  2. 选择合适的存储:生产环境推荐使用 Elasticsearch,并根据数据量进行集群配置

  3. 调整 JVM 参数:为 OAP Server 分配足够的内存

    复制代码
    # 在bin/oapService.sh中调整
    JAVA_OPTS="-Xms2g -Xmx2g -XX:+UseG1GC"
  4. 定期清理数据:配置 Elasticsearch 的索引生命周期管理,定期清理旧数据

  5. 使用异步 reporter:Agent 默认使用异步方式发送数据,确保此配置未被修改

    复制代码
    agent.reporter.type=grpc

7.2 最佳实践

  1. 为所有服务配置有意义的名称:方便在 UI 中识别和筛选服务

    复制代码
    agent.service_name=order-service
  2. 合理设置告警阈值:根据业务场景设置合适的告警阈值,避免告警风暴

  3. 集成日志与追踪:将 Trace ID 添加到日志中,方便问题排查

  4. 定期分析慢查询:利用 SkyWalking 的慢查询追踪功能,定期分析并优化慢查询

  5. 监控数据库性能:关注数据库操作的响应时间,及时发现数据库性能问题

  6. 跟踪服务依赖变化:通过拓扑图关注服务依赖关系的变化,及时发现不合理的依赖

  7. 结合业务指标:除了系统指标,还可以通过自定义指标监控业务指标

八、常见问题与解决方案

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 {
        @Override
        public void run() {
            // 异步任务逻辑
        }
    }

8.4 OAP Server 内存占用过高

可能原因

  • 数据量过大
  • JVM 内存配置不足
  • 存储后端性能不佳,导致数据堆积

解决方案

  • 增加 OAP Server 的 JVM 内存配置
  • 降低采样率,减少数据量
  • 优化存储后端性能,如为 Elasticsearch 增加节点
  • 配置数据保留策略,及时清理旧数据

九、总结与展望

Apache SkyWalking 作为一款优秀的分布式追踪系统,为微服务架构提供了强大的可观测性支持。通过本文的学习,我们了解了 SkyWalking 的核心概念、架构原理,掌握了环境搭建、应用集成和高级特性的使用方法。

SkyWalking 不仅能帮助我们追踪分布式系统中的请求流向,还能提供丰富的性能指标和告警功能,让我们能够及时发现和解决系统中的性能瓶颈和潜在问题。在实际项目中,合理使用 SkyWalking 可以显著提高系统的可靠性和可维护性。

希望本文能帮助你快速掌握 SkyWalking 的使用,让你的微服务系统从 "黑盒" 变为 "透明",让问题排查不再困难。

相关推荐
ArabySide7 小时前
【ASP.NET Core】分布式场景下ASP.NET Core中JWT应用教程
分布式·后端·asp.net core
小马爱打代码9 小时前
zookeeper:一致性原理和算法
分布式·zookeeper·云原生
Cikiss11 小时前
图解 bulkProcessor(调度器 + bulkAsync() + Semaphore)
java·分布式·后端·elasticsearch·搜索引擎
小马爱打代码11 小时前
zookeeper:架构原理和使用场景
分布式·zookeeper·架构
Logintern0912 小时前
【学习篇】Redis 分布式锁
redis·分布式·学习
写代码的小阿帆16 小时前
Java体系总结——从基础语法到微服务
java·微服务·学习方法
Light601 天前
领码方案|微服务与SOA的世纪对话(5):未来已来——AI 驱动下的智能架构哲学
微服务·智能双生体·ai 增强 ddd·自驱动 mesh·预测型 ci/cd·自演进闭环
赴前尘1 天前
Go 微服务框架排行榜(按 GitHub Star 排序)
微服务·golang·github
失散131 天前
分布式专题——33 一台新机器进行Web页面请求的历程
分布式·tcp/ip·http·路由器·交换机