1 Skywalking概述
1.1 什么是APM系统?
1.1.1 APM系统概述
- APM(Application Performance Management)即应用性能管理系统,是对企业系统即时监控以实现对应用程序性能管理和故障管理的系统化的解决方案。应用性能管理,主要是指企业的关键业务进行监控、优化,提高企业应用的可靠性和质量,保证用户得到良好的服务,降低IT总的拥有成本。
- APM系统是可以帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题。
1.1.2 分布式链路追踪
- 随着分布式系统和微服务架构的出现,一次用户的请求会经过多个系统,不同服务之间的调用关系十分复杂,任何一个系统的出错都可能影响整个请求的处理结果。以往的监控系统往往只能知道单个系统的健康状况、一次请求的成功和失败,无法快速定位失败的根本原因。
- 除此之外,复杂的分布式系统也面临这如下的问题:
- 性能分析:一个服务依赖很多服务,被依赖的服务也依赖了其他的服务。如果某个接口耗时突然很长,那未必是直接调用的下游服务慢了,也可能是下游的下游服务变慢造成的,如何快速快速定位耗时变长的根本原因?
- 链路梳理:需求迭代很快,系统之间调用关系变化频繁,靠人工很难梳理出系统链路拓扑(系统之间的调用关系)。
- 为了解决这些问题,Google推出了一个分布式链路追踪系统
Dapper
,之后各个互联网公司都参照Dapper
的思想推出了自己的分布式链路追踪系统,而这些系统就是分布式系统下的APM系统。
1.1.3 什么是OpenTracing?
- 分布式链路追踪最先由Google在Dapper论文中提出的,而OpenTracing通过提供平台无关、厂商无关的API,使得开发人员能够方便的添加(或更换)追踪系统的实现。
- 下图是一个分布式调用的例子,客户端发起请求,请求首先到达负载均衡器,接着经过认证服务、订单服务,然后请求资源,最后返回结果。
- 虽然这种图对于看清各个组件的组合关系很有用,但是存在着如下的问题:
- 它不能很好的显示组件的调用时间,是串行调用还是并行调用,如果展现更复杂的调用关系,会更加复杂,甚至无法画出这样的图。
- 这种图也无法显示调用的时间间隔以及是否通过定时调用来启动调用。
- 一种有效的展现一个调用过程的图:
- 基于OpenTracing我们就可以很轻松的构建出如类似上面的图。
1.1.4 主流的开源APM产品
Pinpoint:
- Pinpoint是由一个韩国团队实现并开源,针对Java编写的大规模分布式系统设计,通过JavaAgent的机制做字节码的植入,实现加入tranceid和获取性能数据的目的,对应用代码零侵入。
- 官网:
Skywalking:
- Skywalking是apache基金会下面的一个开源APM项目,为微服务架构和云原生架构系统设计。它通过探针自动收集所需的指标,并进行分布式追踪。通过这些调用链路以及指标,Skywalking APM会感知应用间的关系和服务间的关系,并进行相应的指标统计。
- Skywalking支持链路追踪和监控应用组件基本涵盖主流框架和容器,如国产RPC Dubbo和motan等,国际化的SpringBoot、SpringCloud。
- 官网。
Zipkin:
- Zipkin是由Twitter开源,是分布式链路调用监控系统,聚合各业务系统调用延迟数据,达到链路调用监控跟踪。Zipkin基于Google的Dapper论文实现,主要完成数据的收集、存储、搜索和界面展示。
- 官网。
CAT:
- CAT是由大众点评开源的项目,基于Java开发的实时应用监控平台,包括实时应用监控、业务监控、可以提供几十张报表展示。
- 官网。
1.2 什么是Skywalking?
1.2.1 Skywalking概述
- 根据官方的解释,Skywalking是一个可观测分析平台(Observability Analysis Platform,简称OAP)和应用性能管理系统(Application Performance Management,简称APM)。
- 提供分布式链路追踪、服务网格(Service Mesh)遥测分析、度量(Metric)聚合和可视化一体化解决方案。
- 下面是Skywalking的几大特点:
- 多语言自动探针,Java、.NET、和NodeJS等。
- 多种监控手段、语言探针和Service Mesh。
- 轻量高效,不需要额外搭建大数据平台。
- 模块化架构,UI、存储、集群管理多种机制可选。
- 支持告警。
- 优秀的可视化效果。
- Skywalking整体架构如下:
- Skywalking提供Tracing和Metric数据的获取和聚合。
Metric的特点是,它是可累加的:它们都是原子性的,每个都是一个逻辑计量单元,或者一个时间段内的柱状图。例如:队列的当前深度可以被定义为一个计量单元,在写入或读取时被更新统计;输入HTTP请求的数量可以被定义为一个计数器,用于简单累加;请求的执行时间可以被定义为一个柱状图,在指定的时间片上更新和统计汇总。 Tracing的最大特点就是,它在单次请求的范围内,处理消息。任何的数据、元数据信息都被绑定到系统中的单个事务上。例如:一次调用远程服务的RPC执行过程,一次实际的SQL查询语句,一次HTTP请求的业务性ID。 总结:Metric主要用来进行数据的统计,比如HTTP请求数的计算;Tracing主要包含了某一次请求的链路数据。
- Skywalking的整体架构包含如下的三个组成部分:
探针(agent):负责进行数据的收集,包含了Tracing和Metric的数据,agent会被安装到服务所在的服务器上,以方便数据的获取。
可观测性分析平台OAP:接收探针发送的数据,并在内存中使用分析引擎进行数据的整合运算,然后将数据存储到对应的存储介质上,比如ElasticSearch、MySQL数据库、H2数据库等,同时OAP还使用查询引擎提供HTTP查询接口。
Skywalking提供单独的UI进行数据的查看:此时UI会调用OAP提供的接口,获取对应的数据然后进行展示。
1.2.2 Skywalking的优势
- Skywalking相比较其他的分布式链路监控工具,具有以下的特点:
社区相当活跃。kywalking已经成为apache顶级项目,开发者是国人,可以直接和项目发起人交流进行问题的解决。
Skywalking支持Java、.NET和NodeJS语言。相对于其他平台,如Pinpoint支持Java和PHP,具有较大的优势。
探针无倾入性。对于CAT具有倾入性的探针,优势较大。不修改原有项目的一行代码就可以进行集成。
探针性能优秀。有网友对Pinpoint和Skywalking进行过测试,由于Pinpoint收集的数据过多,所以对性能损耗较大,而Skywalking探针性能十分出色。
支持组件较多。特别是对RPC框架的支持,这是其他框架所不具备的。Skywalking对Dubbo、gRpc等有原生的支持,甚至连小众的motan和sofarpc都支持。
1.2.3 Skywalking的主要概念介绍
- Skywalking的主要概念包括:
- 服务(Service)。
- 端点(Endpoint)。
- 实例(Instance)。
- 上图中,我们编写了用户服务,这是一个web项目,在生产中部署了两个节点:192.168.1.100和192.168.1.101。
- 用户服务就是Skywalking的服务(Service),用户服务其实就是一个独立的应用(Application),在6.0之后的Skywalking将应用更名为服务。
- 用户服务对外提供的HTTP接口就是一个端点,端点就是对外提供的接口。
- 192.168.1.100和192.168.1.101这两个相同服务部署的节点就是实例,实例指同一服务可以部署多个。
1.3 环境搭建
1.3.1 概述及准备工作
- 我们在虚拟机中的CentOS7中搭建Skywalking的可观测性分析平台OAP环境。Skywalking默认使用H2内存数据库进行数据的存储,我们可以替换存储源为ElasticSearch保证其查询的高效和可用性。
- 本次使用的Skywalking的版本是8.3.0,下载地址(网络不行,请点这里apache-skywalking-apm-es7-8.3.0.tar.gz)。
- 本次使用的ElasticSearch的版本是7.10.0,下载地址(网络不行,请点这里elasticsearch-7.10.0-linux-x86_64.tar.gz)。
- 创建skywalking目录:
mkdir /usr/local/skywalking
建议将虚拟机内存设置为3G并且CPU设置为2核,防止资源不足。
- 进入skywalking目录,并下载Skywalking和ElasticSearch:
cd /usr/local/skywalking
wget https://ftp.wayne.edu/apache/skywalking/8.3.0/apache-skywalking-apm-es7-8.3.0.tar.gz
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.10.0-linux-x86_64.tar.gz
1.3.2 安装ElasticSearch
- 首先安装ElasticSearch,将压缩包解压:
tar -zxvf elasticsearch-7.10.0-linux-x86_64.tar.gz
- 修改Linux系统的限制配置,将文件创建数改为65536个:
vim /etc/security/limits.conf
#新增如下内容在limits.conf文件中
es soft nofile 65536
es hard nofile 65536
es soft nproc 4096
es hard nproc 4096
- 修改系统中允许应用最多创建多少文件等的限制权限。Linux默认来说,一般限制应用最多创建的文件是65535个。但是ES最少需要65536的文件创建数的权限。
- 修改系统中允许用户启动的进程开启多少个线程。默认的Linux限制root用户开启的进程任意数量的线程,其他用户开启的进程最多1024个线程。必须修改限制树为4096+,因为ES至少需要4096的线程池预备。
- 修改系统控制权限,ElasticSearch需要开辟一个65536字节以上空间的虚拟内存。Linux默认不允许用户和应用程序直接开辟这么大的虚拟内存。
vim /etc/sysctl.conf
#新增如下内容在sysctl.conf文件中,当前用户拥有的内存权限大小 vm.max_map_count=262144
vm.max_map_count=262144
#让系统控制权限配置生效
sysctl -p
- ES5之后不允许使用root用户启动,需要创建一个用户来启动ES:
# 创建用户
useradd es
# 修改上述用户的密码
passwd es
# 修改es的目录的拥有者
chown -R es /usr/local/skywalking/elasticsearch-7.10.0
- 默认情况下,ES是不支持跨域访问的,所以需要修改ES的配置文件elasticsearch.yml,添加如下的参数:
vim /usr/local/skywalking/elasticsearch-7.10.0/config/elasticsearch.yml
node.name: node-1
cluster.initial_master_nodes: ["node-1"]
network.host: 0.0.0.0
xpack.ml.enabled: false
http.cors.enabled: true
http.cors.allow-origin: /.*/
- 启动ES:
# 切换用户
su es
cd /usr/local/skywalking/elasticsearch-7.10.0/bin
# 后台启动
./elasticsearch -d
- 测试:
curl http://localhost:9200
1.3.3 安装Skywalking
安装Skywalking分为两个步骤:
- 安装Backend后端服务。
- 安装UI。
- 首先切回root用户,切换到skywalking目录下,解压skywalking:
# 切换到root用户
su root
# 切换到skywalking目录
cd /usr/local/skywalking
# 解压skywalking
tar -zxvf apache-skywalking-apm-es7-8.3.0.tar.gz
- 修改Skywalking的数据源配置:
cd apache-skywalking-apm-bin-es7/config
vim application.yml
- 我们可以发现默认是H2。
- 更改为ElasticSearch7:
storage:
selector: ${SW_STORAGE:elasticsearch7}
elasticsearch7:
nameSpace: ${SW_NAMESPACE:"elasticsearch"}
- 默认使用了localhost下的ES,所以我们不需要做任何处理,直接使用即可,启动OAP程序:
cd /usr/local/skywalking/apache-skywalking-apm-bin-es7/bin
./oapService.sh
- 这样Backend后端服务就已经安装完毕了,接下来需要安装UI,先看下UI的配置文件:
cd /usr/local/skywalking/apache-skywalking-apm-bin-es7/webapp
cat webapp.yml
- 目前的默认配置不需要修改就可以使用,启动UI程序:
cd /usr/local/skywalking/apache-skywalking-apm-bin-es7/bin
./webappService.sh
- 我们可以通过浏览器访问Skywalking的可视化页面,访问地址为:http://localhost:8080(假设虚拟机中CentOS7的IP是192.168.159.103,那么将lcoalhost改为192.168.159.103),如果出现下面的图,就代表安装成功了。
2 Skywalking基础
2.1 agent的使用
- agent探针可以让我们不修改代码的情况下,对Java应用上使用到的组件进行动态监控,获取运行数据发送到OAP上进行统计和存储。
- agent探针在Java中是使用Java agent技术实现的,不需要更改任何代码,Java agent会通过虚拟机(JVM)接口来在运行期更改代码。
- agent探针支持JDK1.6-12的版本,agent探针所有的文件在Skywalking的agent文件夹下,文件目录如下:
部分插件在使用上会影响整体的性能或者由于版权问题放置在可选插件包上,不会直接加载,如果需要使用,将可选插件中的jar包复制到plugins包下即可。
- 由于没有修改agent探针中的应用名,所以默认显示的是Your_ApplicationName,我们修改下应用名,让其显示更加准确。编辑agent的配置文件:
cd /usr/local/skywalking/apache-skywalking-apm-bin-es7/agent/config
vim agent.config
- 找到如下的一行:
agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}
- 这里的配置含义是可以读取到SW_AGENT_NAME的配置属性,如果该属性没有指定,那么默认名称就是Your_ApplicationName。我们这里将Your_ApplicationName改为skywalking_boot。
2.2 Skywalking和Springboot的集成使用
2.2.1 准备SpringBoot项目
- 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>2.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot2</artifactId>
<version>1.0</version>
<name>springboot2</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 启动类:
package com.example.springboot2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Springboot2Application {
public static void main(String[] args) {
SpringApplication.run(Springboot2Application.class, args);
}
}
- 业务类:
package com.example.springboot2.web;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 许大仙
* @version 1.0
* @since 2021-01-12 09:18
*/
@RestController
public class HelloController {
@RequestMapping(value = "/hello")
public String hello() {
return "你好啊";
}
@RequestMapping(value = "/exception")
public String exception(){
int i = 10 /0;
return "你怎么可以这个样子";
}
}
2.2.2 通过工具将SpringBoot项目打包,并上传至/usr/local/skywalking目录
- 略。
2.2.3 使用命令启动SpringBoot项目
- 启动命令:
cd /usr/local/skywalking
java -javaagent:/usr/local/skywalking/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar -Dserver.port=8082 -jar springboot2-1.0.jar &
- 使用jar包启动的项目如果需要集成Skywalking,需要添加-javaagent参数,参数值为agent的jar包的绝对路径。
- -Dserver.port参数用于指定端口号,防止和Skywalking端口冲突。
- 末尾添加&后台运行模式启动SpringBoot项目。
2.2.4 访问Skywalking的UI页面
2.3 Rocketbot的使用
2.3.1 概述
- Skywalking的监控页面为Rocketbot,我们可以通过8080端口进行访问,由于8080端口很容易冲突,可以吸怪webapp/webapp.yaml来更改启动端口。
server:
port: 9010 # 修改为9010
- 我们修改为9010端口防止冲突。
2.3.2 仪表盘
2.3.3 拓扑图
- Skywalking提供拓扑图,直观的查看服务之间的调用关系。
2.3.4 追踪
- 在Skywalking中,每一次用户发起一个请求,就可以视为一条追踪数据,每条追踪数据携带有一个ID值,追踪数据可以在追踪页面进行查询。
- 左侧是追踪列表,也可以通过上方的追踪ID来进行查询。点击追踪列表的某一条记录之后,右侧会显示出这条追踪的详细信息。有三种显示结果:列表、树结构和表格。
- 可以很好的展现这表追踪的调用链情况上的每个节点,可以通过左键点击节点,查看详细信息:
- 当前的接口是HTTP的GET请求,相对比较简单,如果出现异常情况或者数据库的访问,也会打印出异常信息、堆栈甚至详细的SQL语句。
2.3.5 告警
- Skywalking中的告警功能相对比较简单,在到达告警阈值之后会生成一条告警记录,在告警页面上进行展示。
3 Skywalking高级
3.1 MySQL调用监控
3.1.1 前提条件
- 虚拟机中已经安装好了Docker(略)。
- SpringBoot使用Spring Data JPA操作MySQL。
3.1.2 Docker命令启动MySQL
- Docker命令启动MySQL:
docker run -id -p 3306:3306 --name mysql5.7 -v /var/mysql5.7/conf:/etc/mysql/conf.d -v /var/mysql5.7/logs:/logs -v /var/mysql5.7/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --restart=always mysql:5.7 --lower_case_table_names=1
3.1.3 sql脚本
create database if not exists skywalking;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `user` VALUES (1, '张三');
INSERT INTO `user` VALUES (2, '李四');
INSERT INTO `user` VALUES (3, '王五');
3.1.4 Spring Data JPA访问MySQL
- 可以直接使用刚才创建的SpringBoot的项目,只需要导入MySQL和Spring Data JPA相关的依赖即可。
<!-- 增加的依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
- 在resources目录下新建application.yml文件:
spring:
# 配置数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.159.103:3306/skywalking?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
password: 123456
# Hikari 连接池配置
hikari:
# 最小空闲连接数量
minimum-idle: 5
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
# 连接池最大连接数,默认是10
maximum-pool-size: 1000
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
# 连接池名称
pool-name: HikariCP
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认30秒,即30000
connection-timeout: 30000
connection-test-query: SELECT 1
data-source-properties:
useInformationSchema: true
- 新建实体类:
package com.example.springboot2.domain;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;
/**
* @author 许大仙
* @version 1.0
* @since 2021-01-20 14:55
*/
@Entity
@Table(name = "`user`")
@DynamicInsert
@DynamicUpdate
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
- 新建Dao:
package com.example.springboot2.dao;
import com.example.springboot2.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
* @author 许大仙
* @version 1.0
* @since 2021-01-20 14:58
*/
public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User> {
}
- 新建Service:
package com.example.springboot2.service;
import com.example.springboot2.dao.UserRepository;
import com.example.springboot2.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.List;
/**
* @author 许大仙
* @version 1.0
* @since 2021-01-20 15:01
*/
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 查询所有用户信息
*
* @return
*/
public List<User> findAll() {
return userRepository.findAll();
}
}
- 新建Controller:
package com.example.springboot2.web;
import com.example.springboot2.domain.User;
import com.example.springboot2.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author 许大仙
* @version 1.0
* @since 2021-01-20 15:03
*/
@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/findAll")
public List<User> findAll(){
return userService.findAll();
}
}
3.1.5 部署方式
- 将SpringBoot项目打包,上传到/usr/local/skywalking的目录中(略)。
- 切换到/usr/local/skywalking,启动SpringBoot:
cd /usr/local/skywalking
java -javaagent:/usr/local/skywalking/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar -Dserver.port=8082 -jar springboot2-1.0.jar &
3.1.6 调用接口
3.1.7 打开skywalking查看mysql调用的监控情况
- 数据库仪表盘:
- 拓扑图:
- 追踪:
- 点击MySQL的调用,可以看到详细的SQL语句。
- 这样就可以很好的定位问题产生的原因,特别是在某些SQL语句执行慢的情况下。
3.2 配置覆盖
3.2.1 概述
- 我们每次部署应用都需要到agent中修改服务名,如果部署多个应用,那么只能复制agent以便产生隔离,但是这样非常麻烦。我们可以用Skywalking提供的配置覆盖功能通过启动命令动态的指定服务名,这样agent就只需要部署一份即可。
- Skywalking支持以下几种配置:
系统配置。
探针配置。
系统环境变量。
配置文件中的值(上面的案例中我们就是这样使用)。
3.2.2 系统配置(System Properties)
- 使用
skywalking.
+配置文件中的配置名作为系统配置项来进行覆盖。 - 例如:通过命令行启动的时候加上如下的参数可以进行
agent.service_name
的覆盖
-Dskywalking.agent.service_name=skywalking_mysql
3.2.3 探针配置(Agent Options)
- 可以指定探针的时候加上参数,如果配置中包含分隔符(,或=),就必须使用引号(‘’)包裹起来。
-javaagent:/usr/local/skywalking/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar=[option]=[value],[option]=[value]
- 例如:
-javaagent:/usr/local/skywalking/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar=agent.service_name=skywalking_mysql
3.2.4 系统环境变量
- 案例:
- 由于agent.service_name配置项如下所示
agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}
- 可以在环境变量中设置SW_AGENT_NAME的值来指定服务名。
3.2.5 覆盖的优先级
- 探针配置>系统配置>系统环境变量>配置文件中的值。
- 简而言之,推荐使用
探针方式
或系统配置
的方式。
3.3 获取追踪的ID
3.3.1 概述
- Skywalking提供了Trace工具包,用于在追踪链路的时候进行信息的打印或者获取对应的追踪ID。
3.3.2 应用示例
- 在pom.xml中增加Trace工具包的坐标:
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>8.3.0</version>
</dependency>
- 修改Controller.java
package com.example.springboot2.web;
import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 许大仙
* @version 1.0
* @since 2021-01-12 09:18
*/
@RestController
public class HelloController {
@RequestMapping(value = "/hello")
public String hello() {
return "你好啊";
}
/**
* TraceContext.traceId() 可以打印出当前追踪的ID,方便在Rocketbot中搜索
* ActiveSpan提供了三个方法进行信息打印:
* error:会将本次调用转为失败状态,同时可以打印对应的堆栈信息和错误提示
* info:打印info级别额信息
* debug:打印debug级别的信息
*
* @return
*/
@RequestMapping(value = "/exception")
public String exception() {
ActiveSpan.info("打印info信息");
ActiveSpan.debug("打印debug信息");
//使得当前的链路报错,并且提示报错信息
try {
int i = 10 / 0;
} catch (Exception e) {
ActiveSpan.error(new RuntimeException("报错了"));
//返回trace id
return TraceContext.traceId();
}
return "你怎么可以这个样子";
}
}
- 将项目打包,部署,并使用如下的命令启动:
java -javaagent:/usr/local/skywalking/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar -Dserver.port=8082 -Dskywalking.agent.service_name=skywalking_mysql -jar springboot2-1.0.jar &
- 访问接口,获取追踪的ID:
- 可以看到追踪的ID已经打印出来了,此时我们在Rocketbot上进行搜索。
- 可以搜索到对应的追踪记录,但是显示调用是失败的,这是因为使用了ActiveSpan.error方法,点开追踪的详细信息。
3.4 过滤指定的端点
3.4.1 概述
- 在开发的过程中,有一些端点(接口)并不需要去进行监控,比如Swagger相关的端点。这个时候我们使用Skywalking提供的过滤插件来进行过滤。
3.4.2 应用示例
- 在上面的SpringBoot项目中增加如下的控制器:
package com.example.springboot2.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 许大仙
* @version 1.0
* @since 2021-01-20 16:46
*/
@RestController
public class FilterController {
/**
* 此接口可以被追踪
*
* @return
*/
@GetMapping(value = "/include")
public String include() {
return "include";
}
/**
* 此接口不可以被追踪
*
* @return
*/
@GetMapping(value = "/exclude")
public String exclude() {
return "exclude";
}
}
- 将项目打包并上传到/usr/local/skywalking中。
- 将agent中的agent/optional-plugins/apm-trace-ignore-plugin-8.3.0.jar插件复制到plugins目录中。
cd /usr/local/skywalking/apache-skywalking-apm-bin-es7/agent
cp optional-plugins/apm-trace-ignore-plugin-8.3.0.jar plugins/
- 启动SpringBoot应用,并添加过滤参数。
java -javaagent:/usr/local/skywalking/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar -Dserver.port=8082 -Dskywalking.agent.service_name=skywalking_mysql -Dskywalking.trace.ignore_path=/exclude -jar springboot2-1.0.jar &
这里添加
-Dskywalking.trace.ignore_path
参数来标识需要过滤那些请求,支持Ant Path
表达式:
/path/*
,/path/**
,/path/?
?
匹配任何单字符*
匹配0或者任意数量的字符**
匹配0或者更多的目录
- 调用接口,接口的地址为:
- 会发现exclude接口已经被过滤了,只有include接口能被看到。
3.5 告警功能
3.5.1 概述
- Skywalking每隔一段时间根据收集到的链路追踪的数据和配置的告警规则(如服务响应时间、服务响应时间百分比)等,判断如果达到阈值则发送相应的告警信息。发送告警信息是通过调用webhook接口完成,具体的webhook接口可以由使用者自行定义,从而开发者可以在指定的webhook接口中编写各种告警方式,比如邮、短信等。告警的信息也可以在Rocketbot中查看到。
- 以下是默认的告警规则配置,位于Skywalking安装目录下的config文件下的
alarm-settings.yml
文件中。
# Sample alarm rules.
rules:
# Rule unique name, must be ended with `_rule`.
service_resp_time_rule:
metrics-name: service_resp_time
op: ">"
threshold: 1000
period: 10
count: 3
silence-period: 5
message: Response time of service {name} is more than 1000ms in 3 minutes of last 10 minutes.
service_sla_rule:
# Metrics value need to be long, double or int
metrics-name: service_sla
op: "<"
threshold: 8000
# The length of time to evaluate the metrics
period: 10
# How many times after the metrics match the condition, will trigger alarm
count: 2
# How many times of checks, the alarm keeps silence after alarm triggered, default as same as period.
silence-period: 3
message: Successful rate of service {name} is lower than 80% in 2 minutes of last 10 minutes
service_resp_time_percentile_rule:
# Metrics value need to be long, double or int
metrics-name: service_percentile
op: ">"
threshold: 1000,1000,1000,1000,1000
period: 10
count: 3
silence-period: 5
message: Percentile response time of service {name} alarm in 3 minutes of last 10 minutes, due to more than one condition of p50 > 1000, p75 > 1000, p90 > 1000, p95 > 1000, p99 > 1000
service_instance_resp_time_rule:
metrics-name: service_instance_resp_time
op: ">"
threshold: 1000
period: 10
count: 2
silence-period: 5
message: Response time of service instance {name} is more than 1000ms in 2 minutes of last 10 minutes
database_access_resp_time_rule:
metrics-name: database_access_resp_time
threshold: 1000
op: ">"
period: 10
count: 2
message: Response time of database access {name} is more than 1000ms in 2 minutes of last 10 minutes
endpoint_relation_resp_time_rule:
metrics-name: endpoint_relation_resp_time
threshold: 1000
op: ">"
period: 10
count: 2
message: Response time of endpoint relation {name} is more than 1000ms in 2 minutes of last 10 minutes
# Active endpoint related metrics alarm will cost more memory than service and service instance metrics alarm.
# Because the number of endpoint is much more than service and instance.
#
# endpoint_avg_rule:
# metrics-name: endpoint_avg
# op: ">"
# threshold: 1000
# period: 10
# count: 2
# silence-period: 5
# message: Response time of endpoint {name} is more than 1000ms in 2 minutes of last 10 minutes
webhooks:
# - http://127.0.0.1/notify/
# - http://127.0.0.1/go-wechat/
- 规则中的参数属性如下: | 属性 | 描述 | | —- | —- | | metrics-name | oal脚本中的度量名称 | | threshold | 阈值,与metrics-name和下面的比较符号相匹配,单位毫秒 | | op | 比较操作符,可以设定>,<,= | | period | 多久检查一次当前的指标数据是否符合告警规则,单位分钟 | | count | 达到多少次后,发送告警消息 | | silence-period | 在多久之内,忽略相同的告警消息,单位分钟 | | message | 告警消息内容 | | include-names | 本规则告警生效的服务列表 |
- webhooks可以配置告警产生时的调用地址。
3.5.2 告警功能测试
- 定义一个实体类用于封装接口告警信息:
package com.example.springboot2.domain;
/**
* @author 许大仙
* @version 1.0
* @since 2021-01-20 17:31
*/
public class AlarmMessage {
private Integer scopeId;
private String name;
private Integer id0;
private Integer id1;
private String alarmMessage;
private long startTime;
public Integer getScopeId() {
return scopeId;
}
public void setScopeId(Integer scopeId) {
this.scopeId = scopeId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getId0() {
return id0;
}
public void setId0(Integer id0) {
this.id0 = id0;
}
public Integer getId1() {
return id1;
}
public void setId1(Integer id1) {
this.id1 = id1;
}
public String getAlarmMessage() {
return alarmMessage;
}
public void setAlarmMessage(String alarmMessage) {
this.alarmMessage = alarmMessage;
}
public long getStartTime() {
return startTime;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
}
@Override
public String toString() {
return "AlarmMessage{" +
"scopeId=" + scopeId +
", name='" + name + '\'' +
", id0=" + id0 +
", id1=" + id1 +
", alarmMessage='" + alarmMessage + '\'' +
", startTime=" + startTime +
'}';
}
}
- 定义告警Controller:
package com.example.springboot2.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 该接口主要用于模拟超时,多次调用之后就可以生成告警信息
*
* @author 许大仙
* @version 1.0
* @since 2021-01-20 17:28
*/
@RestController
public class AlarmController {
/**
* 每次调用睡眠1.5秒,模拟超时的报警
*
* @return
*/
@GetMapping(value = "/timeout")
public String timeout() {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "timeout";
}
}
- 定义WebHooks:
package com.example.springboot2.web;
import com.example.springboot2.domain.AlarmMessage;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.ArrayList;
import java.util.List;
/**
* 产生告警的时候会调用接口
*
* @author 许大仙
* @version 1.0
* @since 2021-01-20 17:31
*/
@RestController
public class WebHooksController {
private List<AlarmMessage> alarmMessageList = new ArrayList<>();
/**
* 触发webhook
*
* @param alarmMessageList
*/
@PostMapping(value = "/webhook")
public void webhook(@RequestBody List<AlarmMessage> alarmMessageList) {
this.alarmMessageList = alarmMessageList;
}
/**
* 显示告警信息
*
* @return
*/
@GetMapping(value = "/show")
public List<AlarmMessage> show() {
return this.alarmMessageList;
}
}
3.5.3 部署测试
- 将项目打包并上传到/usr/local/skywalking中(略)。
- 修改告警规则配置文件,将webhook地址修改为:
# 修改部分
webhooks:
- http://127.0.0.1:8090/webhook
# - http://127.0.0.1/go-wechat/
- 重启Skywalking(略)。
- 启动SpringBoot应用:
java -javaagent:/usr/local/skywalking/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar -Dserver.port=8090 -Dskywalking.agent.service_name=skywalking_mysql -jar springboot2-1.0.jar &
- 不停的调用接口,接口地址为:http://192.168.159.103:8090/timeout。
- 直到出现告警。
- 查看告警信息:
- 从上图中可以看到,我们已经获取了告警相关的信息,在生产中使用可以在webhook接口中对接短信、邮件等平台,当告警出现的时候能迅速发送信息给对应的处理人员,提高故障处理的速度。
4 Skywalking的原理
4.1 java agent的原理
4.1.1 概述
- 我们知道,要使用Skywalking去监控服务,需要在其VM参数中添加“
-javaagent:/usr/local/skywalking/apache-skywalking-apm-bin-es7/agent/skywalking-agent.jar
”,这里就使用到了java agent技术。
4.1.2 java agent是什么?
- java agent是java命令的一个参数,参数
javaagent
可以用于指定一个jar包。- 这个jar包的MANIFEST.MF文件必须指定Premain-Class项。
- Premain-Class指定的那个类必须实现premain()方法。
- 当Java虚拟机启动时,在执行main函数之前,JVM会运行
-javaagent
所指定的jar包内Premain-Class这个类的premain方法。
4.1.3 如何使用java agent?
定义一个MANIFEST.MF文件,必须包含Premain-Class选项,通常也会加入Can-Redefine-Classes和Can-Retransform-Classes选项。
创建一个Premain-Class指定的类,类中包含premain方法,方法逻辑由用户自己确定。
将premain类和MANIFEST.MF文件打成jar包。
使用参数
-javaagent:jar包路径
启动要代理的方法。
4.1.4 搭建java agent工程
- 使用Maven搭建java_agent总工程,其中java_agent_demo子工程是真正的逻辑实现,而java_agent_user子工程是测试。
- 在java_agent_demo的工程中引入maven-assembly-plugin插件,用于将java_agent_demo工程打成符合java agent标准的jar包:
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>PreMainAgent</Premain-Class>
<Agent-Class>PreMainAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
- 在java_agent_demo工程创建PreMainAgent类。
import java.lang.instrument.Instrumentation;
public class PreMainAgent {
/**
* 在这个 premain 函数中,开发者可以进行对类的各种操作。
* 1、agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。与 main 函数不同的是,
* 这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。
* 2、Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。*
* java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,
* 集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=========premain方法执行1========");
System.out.println(agentArgs);
}
/**
* 如果不存在 premain(String agentArgs, Instrumentation inst)
* 则会执行 premain(String agentArgs)
*
* @param agentArgs
*/
public static void premain(String agentArgs) {
System.out.println("=========premain方法执行2========");
System.out.println(agentArgs);
}
}
- 通过IDEA,进行打包。
- java_agent_user工程新建一个测试类。
/**
* @author 许大仙
* @version 1.0
* @since 2021-01-21 08:46
*/
public class Main {
public static void main(String[] args) {
System.out.println("你好 世界");
}
}
- 先运行一次,然后点击编辑MAIN启动类:
- 在VM Options中添加代码:
-javaagent:D:/project/java_agent/java_agent_demo/target/java_agent_demo-1.0.jar
_
- 启动的时候加载javaagent,指向java_agent_demo工程编译出来的javaagent的jar包地址。
4.1.5 统计方法的调用时间
- Skywalking中对每个调用的时长都进行了统计,我们要使用ByteBuddy和java agent技术来统计方法的调用时长。
- Byte Buddy是开源的、基于Apache2.0许可证的库,它致力于解决字节码操作和instrumentation API的复杂性。Byte Buddy所声称的目标是将显示的字节码操作隐藏在一个类型安全的领域特定语言背后,通过Byte Buddy,任何熟悉Java编程语言的人都有望非常容易的进行字节码的操作。Byte Buddy提供了额外的依赖API来生成java agent,可以轻松的增加我们已有的代码。
- 需要在java_agent_demo工程中添加如下的依赖:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.9.2</version>
</dependency>
- 新建一个MyInterceptor的类,用来统计调用的时长。
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class MyInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method,
@SuperCall Callable<?> callable)
throws Exception {
long start = System.currentTimeMillis();
try {
//执行原方法
return callable.call();
} finally {
//打印调用时长
System.out.println(method.getName() + ":" + (System.currentTimeMillis() - start) + "ms");
}
}
}
- 修改PreMainAgent的代码:
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
import java.lang.instrument.Instrumentation;
public class PreMainAgent {
public static void premain(String agentArgs, Instrumentation inst) {
//创建一个转换器,转换器可以修改类的实现
//ByteBuddy对java agent提供了转换器的实现,直接使用即可
AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
return builder
// 拦截任意方法
.method(ElementMatchers.<MethodDescription>any())
// 拦截到的方法委托给TimeInterceptor
.intercept(MethodDelegation.to(MyInterceptor.class));
}
};
new AgentBuilder // Byte Buddy专门有个AgentBuilder来处理Java Agent的场景
.Default()
// 根据包名前缀拦截类
.type(ElementMatchers.nameStartsWith("com.agent"))
// 拦截到的类由transformer处理
.transform(transformer)
.installOn(inst);
}
}
- 对java_agent_demo工程重新打包。
- 将java_agent_user工程中的Main类方法com.agent包下,修改代码的内容为:
package com.agent;
/**
* @author 许大仙
* @version 1.0
* @since 2021-01-21 08:46
*/
public class Main {
public static void main(String[] args) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("你好 世界");
}
}
- 执行Main方法之后的显示结果为:
- 我们在没有修改代码的情况下,利用了java agent和Byte Buddy统计出了方法的时长,Skywalking的agent也是基于这些技术来实现统计时长的调用的。
4.2 Open Tracing介绍
4.2.1 概述
- Open Tracing通过提供平台无关、厂商无关的API,使得开发人员能够方便的添加(或更换)追踪系统的实现。Open Tracing最核心的概念就是Trace。
4.2.2 Trace
- 在广义上,一个trace代表了一个事务或者流程(在分布式)系统中的执行过程。在Open Tracing标准中,trace是多个span的一个有向无环图(DAG),每一个span代表trace中被命名并计时的连续性的执行片段。
- 例如客户端发起一次请求,就可以认为是一次Trace。将上面的图通过Open Tracing的语义修改完之后做可视化,得到下面的图:
- 图中的每一个色块其实就是一个span。
4.2.3 Span的概念
- 一个Span代表系统中具有开始时间和执行时长的逻辑运行单元。span之间通过嵌套或者顺序排列建立逻辑因果关系。
- Span里面的信息包含:操作的名字,开始时间和结束时间,可以携带多个
key:value
构成的Tags(key必须是String,value可以是String、Boolean或者数字等),还可以携带Logs信息(不一定所有的实现都支持),也必须是key:value
形式。 - 下面的例子是一个Trace,里面有8个Span:
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C 是 Span A 的孩子节点, ChildOf)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G 在 Span F 后被调用, FollowsFrom)
- 一个Span可以和一个或者多个Span间存在因果关系。Open Tracing定义了两种关系:ChildOf和FollowsFrom。这两种引用类型代表了子节点和父节点间的直接因果关系。未来,OpenTracing将支 持非因果关系的span引用关系。(例如:多个span被批量处理,span在同一个队列中,等等)
- ChildOf 很好理解,就是父亲 Span 依赖另一个孩子 Span。比如函数调用,被调者是调用者的孩子,比如说 RPC 调用,服务端那边的Span,就是 ChildOf 客户端的。很多并发的调用,然后将结果聚合起来的操作,就构成了 ChildOf 关系。
- 如果父亲 Span 并不依赖于孩子 Span 的返回结果,这时可以说它他构成 FollowsFrom 关系。
4.2.4 Log的概念
- 每个Span可以进行多次Logs操作,每一个Logs操作,都需要一个带时间戳的时间名称、以及可选的任意大小的存储结果。
- 如下图是一个异常的Log:
4.2.5 Tags的概念
- 每个Span可以有多个键值对(
key:value
)形式的Tags,Tags是没有时间戳的,支持简单的对Span进行注解和补充。