Skywalking应用

sleuth+zipkin还是不够强大,skywalking更强大

Skywalking概述

image-20220210212236905

随着互联网架构的扩张,分布式系统变得日趋复杂,越来越多的组件开始走向分布式化,如微服务、消息收发、分布式数据库、分布式缓存、分布式对象存储、跨域调用,这些组件共同构成了繁杂的分布式网络。

我们思考下这些问题:

  • 1:一个请求经过了这些服务后其中出现了一个调用失败的问题,如何定位问题发生的地方?
  • 2:如何计算每个节点访问流量?
  • 3:流量波动的时候,增加哪些节点集群服务?

这些问题要想得到解决,一定是有数据支撑,绝不是靠开发人员或者运维人员的直觉。为了解决分布式应用、微服务系统面临的这些挑战,==APM系统==营运而生

微服务系统监控三要素

image-20220210212337482

  • Logging 就是记录系统行为的离散事件,例如,服务在处理某个请求时打印的错误日志,我们可以将这些日志信息记录到 ElasticSearch 或是其他存储中,然后通过 Kibana 或是其他工具来分析这些日志了解服务的行为和状态。大多数情况下,日志记录的数据很分散,并且相互独立,比如错误日志、请求处理过程中关键步骤的日志等等。
  • Metrics系统在一段时间内某一方面的某个度量,可聚合的数据,且通常是固定类型的时序数据,例如,电商系统在一分钟内的请求次数。我们常见的监控系统中记录的数据都属于这个范畴,例如Promethus、Open-Falcon 等,这些监控系统最终给运维人员展示的是一张张二维的折线图。Metrics 是可以聚合的,例如,为电商系统中每个 HTTP 接口添加一个计数器,计算每个接口的 QPS,之后我们就可以通过简单的加和计算得到系统的总负载情况
  • Tracing 即我们常说的分布式链路追踪,记录单个请求的处理流程,其中包括服务调用和处理时长等信息。在微服务架构系统中一个请求会经过很多服务处理,调用链路会非常长,要确定中间哪个服务出现异常是非常麻烦的一件事。通过分布式链路追踪,运维人员就可以构建一个请求的视图,这个视图上展示了一个请求从进入系统开始到返回响应的整个流程。这样,就可以从中了解到所有服务的异常情况、网络调用,以及系统的性能瓶颈等。

什么是链路追踪

谷歌在 2010 年 4 月发表了一篇论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》介绍了分布式追踪的概念,之后很多互联网公司都开始根据这篇论文打造自己的分布式链路追踪系统。APM 系统的核心技术就是分布式链路追踪。

在此文中阐述了Google在生产环境下对于分布式链路追踪系统Drapper的设计思路与使用经验。随后各大厂商基于这篇论文都开始自研自家的分布式链路追踪产品。如阿里的Eagle eye(鹰眼)、zipkin,京东的“Hydra”、大众点评的“CAT”、新浪的“Watchman”、唯品会的“Microscope”、窝窝网的“Tracing”都是基于这片文章的设计思路而实现的。所以要学习分布式链路追踪,对于Dapper论文的理解至关重要。

但是也带来了新的问题:各家的分布式追踪方案是互不兼容的,这才诞生了 OpenTracing。

OpenTracing 是一个 Library,定义了一套通用的数据上报接口,要求各个分布式追踪系统都来实现这套接口。这样一来,应用程序只需要对接OpenTracing,而无需关心后端采用的到底什么分布式追踪系统,因此开发者可以无缝切换分布式追踪系统,也使得在通用代码库增加对分布式追踪的支持成为可能。

OpenTracing 于 2016 年 10 月加入 CNCF 基金会,是继 Kubernetes 和Prometheus 之后,第三个加入CNCF 的开源项目。它是一个中立的(厂商无关、平台无关)分布式追踪的 API 规范,提供统一接口,可方便开发者在自己的服务中集成一种或多种分布式追踪的实现。

image-20220210212714808

OpenTracing API 目前支持的语言众多:

image-20220210212724985

目前,主流的分布式追踪实现基本都已经支持 OpenTracing

下面通过官方的一个示例简单介绍说明什么是 Tracing,把Tracing学完后,更有助于大家运用Skywalking UI进行数据分析。

在一个分布式系统中,追踪一个事务或者调用流程,可以用下图方式描绘出来。这类流程图可以看清各组件的组合关系,但它并不能看出一次调用触发了哪个组件调用、什么时间调用、是串行调用还是并行调用。

image-20220210212816727

一种更有效的展现方式就是下图这样,这是一个典型的 trace 视图,这种展现方式增加显示了执行时间的上下文,相关服务间的层次关系,进程或者任务的串行或并行调用关系。这样的视图有助于发现系统调用的关键路径。通过关注关键路径的执行过程,开发团队就可以专注于优化路径中的关键服务,最大幅度的提升系统性能。例如下图中,我们可以看到请求串行的调用了授权服务、订单服务以及资源服务,在资源服务中又并行的执行了三个子任务。我们还可以看到,在这整个请求的生命周期中,资源服务耗时是最长的。

image-20220210212834679

分布式追踪系统的原理:

分布式追踪系统大体分为三个部分,数据采集、数据持久化、数据展示。数据采集是指在代码中埋点,设置请求中要上报的阶段,以及设置当前记录的阶段隶属于哪个上级阶段。数据持久化则是指将上报的数据落盘存储,数据展示则是前端查询与之关联的请求阶段,并在界面上呈现。

上图是一个请求的流程例子,请求从客户端发出,到达负载均衡,再依次进行认证、计费,最后取到目标资源。请求过程被采集之后,会以上图的形式呈现,横坐标是时间,圆角矩形是请求的执行的各个阶段

OpenTracing

学好OpenTracing,更有助于我们运用Skywalking 。

数据模型

这部分在 OpenTracing 的规范中写的非常清楚,下面只大概翻译一下其中的关键部分,细节可参考原始文档 《The OpenTracing Semantic Specification》。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
单个Trace中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)
  • Trace:一个 Trace 代表一个事务、请求或是流程在分布式系统中的执行过程。OpenTracing 中的一条 Trace调用链,由多个 Span 组成,一个 Span 代表系统中具有开始时间和执行时长的逻辑单元,Span 一般会有一个名称,一条 Trace中 Span 是首尾连接的。

  • Span:Span 代表系统中具有开始时间和执行时长的逻辑单元,Span 之间通过嵌套或者顺序排列建立逻辑因果关系。

1
2
3
4
5
6
7
8
9
10
单个Trace中Span间的时间关系


––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time

[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]

每个span中可以包含的信息

  • 操作名称:例如访问的具体 RPC 服务,访问的 URL 地址等;

  • 起始时间;2021-1-25 22:00:00

  • 结束时间;2021-1-30 22:00:00

  • Span Tag:一组键值对(k-v)构成的Span标签集合,其中键必须为字符串类型,值可以是字符串、bool 值或者数字;

  • Span Log:一组 Span 的日志集合;

  • SpanContext:Trace 的全局上下文信息;

  • References:Span 之间的引用关系,下面详细说明 Span 之间的引用关系;在一个 Trace 中,一个 Span 可以和一个或者多个 Span 间存在因果关系。目前,OpenTracing 定义了 ChildOf 和 FollowsFrom 两种 Span 之间的引用关系。这两种引用类型代表了子节点和父节点间的直接因果关系。

  • ChildOf 关系:一个 Span 可能是一个父级 Span 的孩子,即为ChildOf 关系。下面这些情况会构成 ChildOf 关系:

  • 一个 HTTP 请求之中,被调用的服务端产生的 Span,与发起调用的客户端产生的 Span,就构成了 ChildOf 关系;

  • 一个 SQL Insert 操作的 Span,和 ORM 的 save 方法的 Span构成 ChildOf 关系。

很明显,上述 ChildOf 关系中的父级 Span 都要等待子 Span 的返回,子Span 的执行时间影响了其所在父级 Span 的执行时间,父级 Span 依赖子Span 的执行结果。除了串行的任务之外,我们的逻辑中还有很多并行的任务,它们对应的 Span 也是并行的,这种情况下一个父级 Span 可以合并所有子Span 的执行结果并等待所有并行子 Span 结束。

  • FollowsFrom 关系:表示跟随关系,意为在某个阶段之后发生了另一个阶段,用来描述顺序执行关系
  • Logs:每个 Span 可以进行多次 Logs 操作,每一次 Logs 操作,都需要带一个时间戳,以及一个可选的附加信息。
  • Tags:每个 Span 可以有多个键值对形式的 Tags,Tags 是没有时间戳的,只是为Span 添加一些简单解释和补充信息。
  • SpanContext Baggage:SpanContext 表示进程边界,在跨进调用时需要将一些全局信息,例如,TraceId、当前 SpanId 等信息封装到 Baggage 中传递到另一个进程(下游系统)中。

​ Baggage 是存储在 SpanContext 中的一个键值对集合。它会在一条 Trace中全局传输,该 Trace 中的所有 Span 都可以获取到其中的信息。

​ 需要注意的是,由于 Baggage 需要跨进程全局传输,就会涉及相关数据的序列化和反序列化操作,如果在 Baggage 中存放过多的数据,就会导致序列化和反序列化操作耗时变长,使整个系统的 RPC 的延迟增加、吞吐量下降。

​ 虽然 Baggage 与 Span Tags 一样,都是键值对集合,但两者最大区别在于Span Tags 中的信息不会跨进程传输,而 Baggage 需要全局传输。因此,OpenTracing 要求实现提供 Inject 和 Extract 两种操作,SpanContext 可以通过 Inject 操作向 Baggage 中添加键值对数据,通过 Extract 从 Baggage 中获取键值对数据。

核心接口语义

OpenTracing 希望各个实现平台能够根据上述的核心概念来建模实现,不仅如此,OpenTracing 还提供了核心接口的描述,帮助开发人员更好的实现OpenTracing 规范。

Span 接口: Span接口必须实现以下的功能:

  • 获取关联的 SpanContext:通过 Span 获取关联的SpanContext 对象。

  • 关闭(Finish)Span:完成已经开始的 Span。

  • 添加 Span Tag:为 Span 添加 Tag 键值对。

  • 添加 Log:为 Span 增加一个 Log 事件。

  • 添加 Baggage Item:向 Baggage 中添加一组键值对。

  • 获取 Baggage Item:根据 Key 获取 Baggage 中的元素。

SpanContext 接口: SpanContext 接口必须实现以下功能,用户可以通过 Span 实例或者Tracer 的 Extract 能力获取 SpanContext 接口实例。

Tracer 接口: Tracer 接口必须实现以下功能:

  • 创建 Span:创建新的 Span。
  • 注入 SpanContext:主要是将跨进程调用携带的 Baggage 数据记录到当前 SpanContext 中。
  • 提取 SpanContext ,主要是将当前 SpanContext 中的全局信息提取出来,封装成 Baggage 用于后续的跨进程调用。

常见APM系统

我们前面提到了APM系统,APM 系统(Application Performance Management,即应用性能管理)是对企业的应用系统进行实时监控,实现对应用性能管理和故障定位的系统化解决方案,在运维中常用。

  • CAT(开源): 由国内美团点评开源的,基于 Java 语言开发,目前提供 Java、C/C++、Node.js、Python、Go 等语言的客户端,监控数据会全量统计。国内很多公司在用,例如美团点评、携程、拼多多等。CAT 需要开发人员手动在应用程序中埋点,对代码侵入性比较强。

  • Zipkin(开源): 由 Twitter 公司开发并开源,Java 语言实现。侵入性相对于 CAT 要低一点,需要对web.xml 等相关配置文件进行修改,但依然对系统有一定的侵入性。Zipkin 可以轻松与 Spring Cloud 进行集成,也是 Spring Cloud 推荐的 APM 系统。

  • Pinpoint(开源): 韩国团队开源的 APM 产品,运用了字节码增强技术,只需要在启动时添加启动参数即可实现 APM 功能,对代码无侵入。目前支持 Java 和 PHP 语言,底层采用 HBase 来存储数据,探针收集的数据粒度非常细,但性能损耗较大,因其出现的时间较长,完成度也很高,文档也较为丰富,应用的公司较多。

  • SkyWalking(开源): 国人开源的产品,2019 年 4 月 17 日SkyWalking 从 Apache 基金会的孵化器毕业成为顶级项目。目前SkyWalking 支持 Java、.Net、Node.js 等探针,数据存储支持MySQL、ElasticSearch等。还有很多不开源的 APM 系统,例如,淘宝鹰眼、Google Dapper 等等

我们将学习Skywalking,Skywalking有很多优秀特性。SkyWalking 对业务代码无侵入,性能表现优秀,SkyWalking 增长势头强劲,社区活跃,中文文档齐全,支持多语言探针, SkyWalking 支持Dubbo、gRPC、SOFARPC 等很多框架。

Skywalking介绍

2015年由个人吴晟(华为开发者)主导开源,作者是华为开发云监控产品经理,主导监控产品的规划、技术路线及相关研发工作,也是OpenTracing分布式追踪标准组织成员 ,该项目 2017年加入Apache孵化器,是一个分布式系统的应用程序性能监控工具(APM),专为微服务、云原生架构和基于容器(Docker、K8s、Mesos)架构而设计。

image-20220210214410120

Skywalking是一个可观测性分析平台和应用性能管理系统,它也是基于OpenTracing规范、开源的AMP系统。Skywalking提供分布式跟踪、服务网格遥测分析、度量聚合和可视化一体化解决方案。支持Java, .Net Core, PHP,NodeJS, Golang, LUA, c++代理。支持Istio +特使服务网格我们在学习Skywalking之前,可以先访问官方提供的控制台演示

演示地址:http://demo.skywalking.apache.org/ 账号:skywalking 密码:skywalking

image-20220210214519479

核心功能

  • 指标分析:服务,实例,端点指标分析
  • 问题分析:在运行时分析代码,找到问题的根本原因
  • 服务拓扑:提供服务的拓扑图分析
  • 依赖分析:服务实例和端点依赖性分析
  • 服务检测:检测慢速的服务和端点
  • 性能优化:根据服务监控的结果提供性能优化的思路
  • 链路追踪:分布式跟踪和上下文传播
  • 数据库监控:数据库访问指标监控统计,检测慢速数据库访问语句(包括SQL语句)
  • 服务告警:服务告警功能

名词解释:

  • 服务(service):业务资源应用系统
  • 端点(endpoint):应用系统对外暴露的功能接口
  • 实例(instance):物理机

image-20220210214600123

特点

  • 多语言自动探针,支持 Java、.NET Code 等多种语言。
  • 为多种开源项目提供了插件,为 Tomcat、 HttpClient、Spring、
  • RabbitMQ、MySQL 等常见基础设施和组件提供了自动探针。
  • 微内核 + 插件的架构,存储、集群管理、使用插件集合都可以进行自由选择。
  • 支持告警。
  • 优秀的可视化效果。

架构图

image-20220210214629717

skyWalking整体可分为:客户端,服务端

客户端:agent组件

  • 基于探针技术采集服务相关信息(包括跟踪数据和统计数据),然后将采集到的数据上报给skywalking的数据收集器

服务端:又分为OAP,Storage,WebUI

  • OAP:observability analysis platform可观测性分析平台,负责接收客户端上报的数据,对数据进行分析,聚合,计算后将数据进行存储,并且还会提供一些查询API进行数据的查询,这个模块其实就是我们所说的链路追踪系统的Collector收集器
  • Storage:skyWalking的存储介质,默认是采用H2,同时支持许多其他的存储介质,比如:ElastaticSearch,mysql等
  • WebUI:提供一些图形化界面展示对应的跟踪数据,指标数据等等

Skywalking安装

Skywalking数据存储方式有2种,分别为H2(内存)和elasticsearch,如果数据量比较大,建议使用后者,工作中也建议使用后者。

Skywalking自身提供了UI管理控制台,我们安装的组件:

  • 1:elasticsearch,建议使用elasticsearch7.x
  • 2:elasticsearch-hq,elasticsearch的管理工具,更方便管理 elasticsearch
  • 3:Skywalking
  • 4:Skywalking-UI

elasticsearch安装

1)系统资源配置修改

elasticsearch占用系统资源比较大,我们需要修改下系统资源配置,这样才能很好的运行elasticsearch,修改虚拟机配置, vi /etc/security/limits.conf ,追加内容:

1
2
* soft nofile 65536 
* hard nofile 65536

修改 vi /etc/sysctl.conf ,追加内容

1
vm.max_map_count=655360

让配置立即生效:

1
/sbin/sysctl -p

2)安装elasticsearch

建议安装:elasticsearch7.x,我们这里选择7.6.2,并且采用容器的安装方式,安装如下:

1
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 --restart=always -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms84m -Xmx512m" -d elasticsearch:7.6.2

3)elasticsearch跨域配置

elasticsearch默认是没有开启跨域,我们需要配置跨域,并配置集群节点名字

1
2
#进入容器 
docker exec -it elasticsearch /bin/bash

修改容器中 /usr/share/elasticsearch/config/elasticsearch.yml文件,添加配置如下

1
2
3
4
5
6
7
8
9
10
11
12
cluster.name: "elasticsearch" 
http.cors.enabled: true
http.cors.allow-origin: "*"
network.host: 0.0.0.0
discovery.zen.minimum_master_nodes: 1

参数说明:
cluster.name:集群服务名字
http.cors.enabled:开启跨域
http.cors.allow-origin: 允许跨域域名,*代表所有域名
network.host: 外部访问的IP
discovery.zen.minimum_master_nodes: 最小主节点个数

安装完成后,重启容器 docker restart elasticsearch ,再访问http://192.168.200.129:9200/ 效果如下

image-20220210215117678

安装 ElasticSearch管理界面elasticsearch-hq

1
docker run -d --name elastic-hq -p 5000:5000 --restart always elastichq/elasticsearch-hq

安装完成后,访问控制台地址http://192.168.200.129:5000/#!/clusters/elasticsearch

image-20220210215200022

Skywalking安装

Skywalking的安装我们也采用Docker安装方式,同时我们需要为Skywalking指定存储服务:

1
2
3
4
5
6
7
8
#安装Skywalking 
docker run --name skywalking -d -p 1234:1234 -p 11800:11800 -p 12800:12800 --restart always --link elasticsearch:elasticsearch -e SW_STORAGE=elasticsearch7 -e SW_STORAGE_ES_CLUSTER_NODES=elasticsearch:9200 apache/skywalking-oap-server

参数说明:
--link elasticsearch:elasticsearch:存储服务使用elasticsearch
-e SW_STORAGE=elasticsearch7:存储服务elasticsearch的版本
-e SW_STORAGE_ES_CLUSTER_NODES=elasticsearch:9200:存储服务
elasticsearch的链接地址

接下来安装Skywalking-UI,需要指定Skywalking服务名字:

1
docker run --name skywalking-ui -d -p 8080:8080 --link skywalking:skywalking -e SW_OAP_ADDRESS=skywalking:12800 -- restart always apache/skywalking-ui

安装完成后,我们接下来访问Skywalking控制台:http://192.168.200.129:8080

image-20220210215321884

关于SkywalkingUI的使用,我们在下一节知识点详细讲解。

Skywalking生产环境问题

1)ES分片数量上限

如果此时访问http://192.168.200.129:8080/ 出现500错误,错误如下,此时其实是Elasticsearch出了问题:

1
"com.netflix.zuul.exception.ZuulException","message":"GENER AL"

我们可以查看 skywalking 的日志 docker logs –since 30m skywalking ,会报如下错误:

1
Failed: 1: this action would add [2] total shards, but this cluster currently has [1000]/[1000] maximum shards open;]

这种错误一般是生产环境中Elasticsearch分片数量达到了峰值,es集群的默认最大分片数是1000,我们需要调整Elasticsearch的默认分片数量,修改方式有多种,最常用的是直接修改 elasticsearch.yml 配置文件:

1
2
3
4
5
6
#进入elasticsearch容器 
docker exec -it elasticsearch /bin/bash
#编辑
vi /usr/share/elasticsearch/config/elasticsearch.yml
#添加如下配置
cluster.max_shards_per_node: 10000000

保存配置后,记得删除 data/nodes 数据包,再重启elasticsearch,此时就可以正常访问了。

2)磁盘清理

如果此时能打开 skywalking-ui 界面,但是没有数据,则需要清理磁盘空间,可能是磁盘空间满了,如果是docker容器,可以使用 docker system prune 命令实现清理, docker system prune 命令可以用于清理磁盘,删除关闭的容器、无用的数据卷和网络,以及dangling镜像(即无tag的镜像)。docker system prune -a命令清理得更加彻底,可以将没有容器使用Docker镜像都删掉。

上面单个部署还是比较麻烦的,直接用compose一键部署吧

docker-compose部署

创建 docker-compose.yml 并配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
version: '3.3'
services:
elasticsearch:
image: elasticsearch:7.6.2
container_name: elasticsearch
restart: always
privileged: true
hostname: elasticsearch
ports:
- 9200:9200
- 9300:9300
environment:
- discovery.type=single-node
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- TZ=Asia/Shanghai
networks:
- skywalking
ulimits:
memlock:
soft: -1
hard: -1
elasticsearch-hq:
image: elastichq/elasticsearch-hq
container_name: elasticsearch-hq
restart: always
privileged: true
hostname: elasticsearch-hq
ports:
- 5000:5000
environment:
- TZ=Asia/Shanghai
networks:
- skywalking
oap:
image: apache/skywalking-oap-server:8.3.0-es7
container_name: oap
hostname: oap
privileged: true
depends_on:
- elasticsearch
links:
- elasticsearch
restart: always
ports:
- 11800:11800
- 12800:12800
environment:
SW_STORAGE: elasticsearch7
SW_STORAGE_ES_CLUSTER_NODES: elasticsearch:9200
TZ: Asia/Shanghai
volumes:
- ./config/alarm-settings.yml:/skywalking/config/alarm-settings.yml
networks:
- skywalking
ui:
image: apache/skywalking-ui:8.3.0
container_name: ui
privileged: true
depends_on:
- oap
links:
- oap
restart: always
ports:
- 8080:8080
environment:
SW_OAP_ADDRESS: oap:12800
TZ: Asia/Shanghai
networks:
- skywalking

networks:
skywalking:
driver: bridge

通过命令一键启动:docker-compose up -d

启动成功后访问skywalking的webui页面:http://192.168.200.129:8080/

Skywalking应用

相关术语:

  • skywalking-collector:链路数据归集器,数据可以落地 ElasticSearch/H2
  • skywalking-ui:web可视化平台,用来展示落地的数据
  • skywalking-agent:探针,用来收集和发送数据到归集器

agent下载

Skywalking-agent,它简称探针,用来收集和发送数据到归集器,我们先来学习下探针使用,探针对应的jar包在Skywalking源码中,我们需要先下载源码。

Skywalking源码下载地址: https://archive.apache.org/dist/skywalking/,我们当前使用的版本是 8.3.0 ,选择下载对应版本。

image-20220210221022181

agent目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
agent
├── activations
│ ├── apm-toolkit-kafka-activation-8.3.0.jar
│ ├── ...
│ └── apm-toolkit-trace-activation-8.3.0.jar
├── config # Agent 配置文件
│ └── agent.config
├── logs # 日志文件
├── optional-plugins # 可选插件
│ ├── apm-customize-enhance-plugin-8.3.0.jar
│ ├── apm-gson-2.x-plugin-8.3.0.jar
│ └── ... ...
├── bootstrap-plugins # jdk插件
│ ├── apm-jdk-http-plugin-8.3.0.jar
│ └── apm-jdk-threading-plugin-8.3.0.jar
├── plugins # 当前生效插件
│ ├── apm-activemq-5.x-plugin-8.3.0.jar
│ ├── apm-armeria-0.84.x-plugin-8.3.0.jar
│ ├── apm-armeria-0.85.x-plugin-8.3.0.jar
│ └── ... ...
├── optional-reporter-plugins
│ └── kafka-reporter-plugin-8.3.0.jar
└── skywalking-agent.jar【应用的jar包】

目录结构说明:

  • activations 当前skywalking正在使用的功能组件。
  • agent.config 文件是 SkyWalking Agent 的唯一配置文件。
  • plugins 目录存储了当前 Agent 生效的插件。
  • optional-plugins 目录存储了一些可选的插件(这些插件可能会影响整个 系统的性能或是有版权问题),如果需要使用这些插件,需将相应 jar 包移 动到 plugins 目录下。

比如要采集gateway的,把optional-plugins下的

apm-spring-cloud-gateway-2.1.x-plugin-8.3.0.jar和

apm-spring-webflux-5.x-plugin-8.3.0.jar

拷贝到plugins下即可

  • skywalking-agent.jar 是 Agent 的核心 jar 包,由它负责读取 agent.config 配置文件,加载上述插件 jar 包,运行时收集到 的 Trace 和 Metrics 数据也是由它发送到 OAP 集群的。

我们在使用Skywalking的时候,整个过程中都会用到 skywalking-agent.jar ,而无论是RPC还是HTTP开发的项目,用法都一样,因此我们讲解当前主流的SpringBoot项目对agent的使用即可。

agent应用

项目使用agent,如果是开发环境,可以使用IDEA集成,如果是生产环境,需要将项目打包上传到服务器。为了使用agent,我们同时需要将下载的apache-skywalking-apm-bin 文件包上传到服务器上去。不过无论是开发环境还是生产环境使用agent,对项目都是无侵入式的。

应用名配置

我们需要用到 agent ,此时需要将 agent/config/agent.config 配置文件拷贝到每个需要集成Skywalking工程的resource目录下,我们将agent.config 拷贝到 工程\hailtaxi-parent 的每个子工程目录下,并修改其中的 agent.service_name,修改如下

1
2
3
4
5
6
hailtaxi-gateway: 
agent.service_name=${SW_AGENT_NAME:hailtaxi-gateway}
hailtaxi-driver:
agent.service_name=${SW_AGENT_NAME:hailtaxi-driver}
hailtaxi-order:
agent.service_name=${SW_AGENT_NAME:hailtaxi-order}

agent.config 是一个 KV 结构的配置文件,类似于 properties 文件,value 部分使用 “${}” 包裹,其中使用冒号 (”:”) 分为两部分,前半部分是可以覆盖该配置项的系统环境变量名称,后半部分为默认值。例如这里的agent.service_name 配置项,如果系统环境变量中指定了 SW_AGENT_NAME值(注意,全是大写),则优先使用环境变量中指定的值,如果环境变量未指定,则使用 hailtaxi-driver 这个默认值。直接把配置修改好后放到项目的resource目录下(或者其他路径)是最不容易才出错的一种方式,同时我们可以采用其他方式覆盖默认值:

1)JVM覆盖配置

例如这里的 agent.service_name 配置项,如果在 JVM 启动之前,明确中指定了下面的 JVM 配置:

1
2
# "skywalking."是 Skywalking环境变量的默认前缀 
-Dskywalking.agent.service_name = hailtaxi-driver

2)探针配置覆盖

将 Java Agent 配置为如下:

1
2
# 默认格式是 -javaagent:agent.jar=[option1]=[value1], [option2]=[value2] 
-javaagent:/path/skywalking-agent.jar=agent.service_name=hailtaxi-driver

此时会使用该 Java Agent 配置值覆盖 agent.config 配置文件中agent.service_name 默认值。

但是这些配置都有不同优先级,优先级如下:

1
探针配置 > JVM配置 > 系统环境变量配置 > agent.config文件默认值

IDEA集成使用agent

1、修改agent中数据收集服务的地址: agent/config/agent.confg

1
collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERV ICES:192.168.213.130:11800}

当然也可以同构JVM参数配置

2、使用探针配置为3个项目分别配置agent:

1)hailtaxi-driver:

1
2
3
4
-javaagent:C:\developer\skywalking\apache-skywalking-apm-bin\agent\skywalking-agent.jar -Dskywalking.agent.service_name=hailtaxi-driver

我的:
-javaagent:D:\tools\skywalking\apache-skywalking-apm-bin\agent\skywalking-agent.jar -Dskywalking.agent.service_name=hailtaxi-driver

将上面配置赋值到IDEA中:

image-20220210221458731

2)hailtaxi-order

1
2
3
4
-javaagent:C:\developer\skywalking\apache-skywalking-apm-bin\agent\skywalking-agent.jar -Dskywalking.agent.service_name=hailtaxi-order 

我的:
-javaagent:D:\tools\skywalking\apache-skywalking-apm-bin\agent\skywalking-agent.jar -Dskywalking.agent.service_name=hailtaxi-order

将上面配置赋值到IDEA中:

image-20220210221519158

3)hailtaxi-gateway

1
2
3
4
-javaagent:C:\developer\skywalking\apache-skywalking-apm-bin\agent\skywalking-agent.jar -Dskywalking.agent.service_name=hailtaxi-gateway

我的:
-javaagent:D:\tools\skywalking\apache-skywalking-apm-bin\agent\skywalking-agent.jar -Dskywalking.agent.service_name=hailtaxi-order

将上面配置赋值到IDEA中:

image-20220210221546026

此时启动IDEA,并访问:http://192.168.200.129:8080 效果如下:

image-20220210221552477

如果你要追踪Gateway的话,你会发现:无法通过gateway发现路由的服务链路?

原因: Spring Cloud Gateway 是基于 WebFlux 实现,必须搭配上apm-spring-cloud-gateway-2.1.x-plugin 和 apm-spring-webflux-x.x-plugin 两个插件

方案:将agent/optional-plugins下的两个插件 复制到 agent/plugins目录下

生产环境使用agent

原理很清晰,通知agent探针使用,

所以我们生产环境只要指定agent的jar包的位置就行了,和windows下idea中配置一样用

生产环境使用,因此我们需要将agent和每个项目的jar包上传到服务器上,上传 apache-skywalking-apm-bin 至 /usr/local/server/skywalking ,再将工程\hailtaxi-parent 中的项目打包,并分别上传到服务器上,如下三个工程:

  • hailtaxi-order-1.0-SNAPSHOT.jar
  • hailtaxi-gateway-1.0-SNAPSHOT.jar
  • hailtaxi-driver-1.0-SNAPSHOT.jar

1)启动hailtaxi-gateway

1
java -javaagent:/usr/local/server/skywalking/apache-skywalking-apm-bin/agent/skywalking-agent.jar -Dskywalking.agent.service_name=hailtaxi-gateway -jar hailtaxi-gateway-1.0-SNAPSHOT.jar &

2)启动hailtaxi-driver

1
java -javaagent:/usr/local/server/skywalking/apache-skywalking-apm-bin/agent/skywalking-agent.jar -Dskywalking.agent.service_name=hailtaxi-driver -jar hailtaxi-driver-1.0-SNAPSHOT.jar &

3)启动hailtaxi-order

1
java -javaagent:/usr/local/server/skywalking/apache-skywalking-apm-bin/agent/skywalking-agent.jar -Dskywalking.agent.service_name=hailtaxi-order -jar hailtaxi-order-1.0-SNAPSHOT.jar &

Rocketbot

前面我们已经完成了SkyWalking环境搭建和项目应用agent使用,我们来看如何使用 SkyWalking 提供的 UI 界面—— Skywalking Rocketbot。OAP服务和Rocket(其实就是个web项目)均已启动

image-20220210221741729

Rocketbot-仪表盘

具体细则可参考资料: Skywalking仪表盘使用

image-20220210221753791

Rocketbot从多个方面展示了服务信息,我们分别从多个方面进行讲解。

上图中的【仪表盘】、【拓扑图】、【追踪】、【性能剖析】、【日志】、【警告】属于功能菜单。

仪表盘属于数据统计功能,分别从服务热度、响应水平、服务个数、节点信息等展示统计数据。

  • Global Heatmap 面板:热力图,从全局展示了某段时间请求的热度。
  • Global Percent Response 面板 :展示了全局请求响应时间的 P99、P95、P75 等分位数。
  • Global Brief 面板:展示了 SkyWalking 能感知到的 Service、Endpoint 的个数。
  • Global Top Troughput 面板:展示了吞吐量前几名的服务。
  • Global Top Slow Endpoint 面板:展示了耗时前几名的 Endpoint。
  • Service (Avg) ResponseTime 面板:展示了指定服务的(平均)耗时。
  • Service (Avg) Throughput 面板:展示了指定服务的(平均)吞吐量。
  • Service (Avg) SLA 面板:展示了指定服务的(平均)SLA(ServiceLevel Agreement,服务等级协议)。
  • Service Percent Response 面板:展示了指定服务响应时间的分位数。
  • Service Slow Endpoint 面板:展示了指定服务中耗时比较长的Endpoint 信息。
  • Running ServiceInstance 面板:展示了指定服务下的实例信息。

除了 SkyWalking Rocketbot 默认提供的这些面板,我们还可以点击锁型按钮,自定义 Global 面板。在 ServiceInstance 面板中展示了很多ServiceInstance 相关的监控信息,例如,JVM 内存使用情况、GC 次数、GC 耗时、CPU 使用率、ServiceInstance SLA 等等信息。

Rocketbot-拓扑图

image-20220210221901653

【拓扑图】展示当前整个业务服务的拓扑图。点击拓扑图中的任意节点,可以看到服务相应的状态信息,其中包括响应的平均耗时、SLA 等监控信息。点击拓扑图中任意一条边,还可以看到一条调用链路的监控信息,其中会分别从客户端(上游调用方)和服务端(下游接收方)来观测这条调用链路的状态,其中展示了该条链路的耗时、吞吐量、SLA 等信息。

追踪

image-20220210221916134

【追踪】主要用来查询 Trace 信息,如下图所示。在①处可以选择 Trace的查询条件,其中可以指定 Trace 涉及到的 Service、ServiceInstance、Endpoint 以及Trace 的状态继续模糊查询,还可以指定 TraceId 和时间范围进行精确查询。在②处可以直接根据请求连接查找调用链路信息。在③处展示了Trace 的简略信息。在④处可以选择不同的方式展示追踪信息。

在这里,我们不仅能看到调用链路信息,还能看到MySQL操作监控,如下图

image-20220210221929371

错误异常信息也能追踪,如下图:

image-20220210221936001

性能分析

在传统的监控系统中,我们如果想要得知系统中的业务是否正常,会采用进程监控、日志收集分析等方式来对系统进行监控。当机器或者服务出现问题时,则会触发告警及时通知负责人。通过这种方式,我们可以得知具体哪些服务出现了问题。但是这时我们并不能得知具体的错误原因出在了哪里,开发人员或者运维人员需要到日志系统里面查看错误日志,甚至需要到真实的业务服务器上查看执行情况来解决问题。

如此一来,仅仅是发现问题的阶段,可能就会耗费相当长的时间;另外,发现问题但是并不能追溯到问题产生具体原因的情况,也常有发生。这样反反复复极其耗费时间和精力,为此我们便有了基于分布式追踪的APM系统。

通过将业务系统接入分布式追踪中,我们就像是给程序增加了一个放大镜功能,可以清晰看到真实业务请求的整体链路,包括请求时间、请求路径,甚至是操作数据库的语句都可以看得一清二楚。通过这种方式,我们结合告警便可以快速追踪到真实用户请求的完整链路信息,并且这些数据信息完全是持久化的,可以随时进行查询,复盘错误的原因。

然而随着我们对服务监控理解的加深,我们发现事情并没有那么简单。在分布式链路追踪中我们有这样的两个流派:代码埋点和字节码增强。无论使用哪种方式,底层逻辑一定都逃不过面向切面这个基础逻辑。因为只有这样才可以做到大面积的使用。这也就决定了它只能做到框架级别和RPC粒度的监控。这时我们可能依旧会遇到程序执行缓慢或者响应时间不稳定等情况,但无法具体查询到原因。这时候,大家很自然的会考虑到增加埋点粒度,比如对所有的Spring Bean方法、甚至主要的业务层方法都加上埋点。但是这种思路会遇到不小的挑战:

第一,增加埋点时系统开销大,埋点覆盖不够全面。通过这种方式我们确实可以做到具体业务场景具体分析。但随着业务不断迭代上线,弊端也很明显:大量的埋点无疑会加大系统资源的开销,造成CPU、内存使用率增加,更有可能拖慢整个链路的执行效率。虽然每个埋点消耗的性能很小,在微秒级别,但是因为数量的增加,甚至因为业务代码重用造成重复埋点或者循环使用,此时的性能开销已经无法忽略。

第二,动态埋点作为一项埋点技术,和手动埋点的性能消耗上十分类似,只是减少的代码修改量,但是因为通用技术的特别,上一个挑战中提到的循环埋点和重复使用的场景甚至更为严重。比如选择所有方法或者特定包下的所有方法埋点,很可能造成系统性能彻底崩溃。

第三,即使我们通过合理设计和埋点,解决了上述问题,但是JDK函数是广泛使用的,我们很难限制对JDK API的使用场景。对JDK过多方法、特别是非RPC方法的监控会造成系统的巨大延迟风险。而且有一些基础类型和底层工具类,是很难通过字节码进行增强的。当我们的SDK使用不当或者出现bug时,我们无法具体得知真实的错误原因。

Skywalking中可以使用性能剖析分析特定端点的性能,我们需要先创建一个监控任务:

image-20220210222038407

新建任务后,在右侧可以查看任务性能分析报表,还可以点击分析线程栈信息,如下图:

image-20220210222044998

告警

SkyWalking 告警功能是在6.x版本新增的,其核心由一组规则驱动,这些规则定义在 config/alarm-settings.yml 文件中。 告警的定义分为两部分:

  1. 告警规则:它们定义了应该如何触发度量警报,应该考虑什么条件。

  2. Webhook(网络钩子):定义当警告触发时,哪些服务终端需要被告知

警告规则详解

Skywalking每隔一段时间根据收集到的链路追踪的数据和配置的告警规则(如服务响应时间、服务响应时间百分比)等,判断如果达到阈值则发送相应的告警信息。发送告警信息是通过调用webhook接口完成,具体的webhook接口可以使用者自行定义,从而开发者可以在指定的webhook接口中编写各种告警方式,比如邮件、短信等。告警的信息也可以在RocketBot中查看到。

我们可以进入到Skywalking容器中,再进入到config文件夹下就可以看到alarm-settings.yml,如下图:

image-20220210222131197

SkyWalking 的发行版都会默认提供 config/alarm-settings.yml 文件,里面预先定义了一些常用的告警规则。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# 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/


告警规则配置项的说明:

  • Rule name:规则名称,也是在告警信息中显示的唯一名称。必须以_rule 结尾,前缀可自定义

  • Metrics name:度量名称,取值为 oal 脚本中的度量名,目前只支持long 、 double 和 int 类型。

  • Include names:该规则作用于哪些实体名称,比如服务名,终端名(可选,默认为全部)

  • Exclude names:该规则作不用于哪些实体名称,比如服务名,终端名(可选,默认为空)

  • Threshold:阈值

  • OP: 操作符,目前支持 > 、 < 、 =

  • Period:多久告警规则需要被核实一下。这是一个时间窗口,与后端部署环境时间相匹配

  • Count:在一个Period窗口中,如果values超过Threshold值(按op),达到Count值,需要发送警报

  • Silence period:在时间N中触发报警后,在TN -> TN + period这个阶段不告警。 默认情况下,它和Period一样,这意味着相同的告警(在同一个Metrics name拥有相同的Id)在同一个Period内只会触发一次

  • message:告警消息

在配置文件中预先定义的告警规则总结如下:

  1. 在过去10分钟内服务平均响应时间超过1秒达3次

  2. 在过去10分钟内服务成功率低于80%达2次

  3. 在过去10分钟内服务90%响应时间低于1秒达3次

  4. 在过去10分钟内服务的响应时间超过1秒达2次

  5. 在过去10分钟内端点的响应时间超过1秒达2次

这些警告信息最终会在Skywalking-UI上展示,效果如下:

image-20220210222304421

Webhook规则

Webhook配置其实是警告消息接收回调处理,我们可以在程序中写一个方法接收警告信息,Skywalking会以 application/json 格式通过http请求发送,消息格式声明为:List<org.apache.skywalking.oap.server.core.alarm.AlarmMessage 。

字段如下:

  • scopeId, scope: 所有的scope实体在
  • org.apache.skywalking.oap.server.core.source.DefaultScopeDefine里面声明。
  • name. 目标scope实体名称。
  • id0: scope实体ID,匹配名称。
  • id1: 不使用。
  • ruleName: 配置在 alarm-settings.yml 里面的规则名称.
  • alarmMessage: 告警信息.
  • startTime:触发告警的时间 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[ 
{
"scopeId": 2,
"scope": "SERVICE_INSTANCE",
"name":
"c00158f28efc45cd813e21b6b8848a3a@192.168.1.104 of
hailtaxi-driver",
"id0":
"aGFpbHpdmVy.1_YzAwMAMTkyLjE2OC4xLjEwNA\u003d\u003d",
"id1": "",
"ruleName": "service_instance_resp_time_rule",
"alarmMessage": "Response time of service instance
c00158f28efc45cd813e21b6b8848a3a@192.168.1.104 of
hailtaxi-driver is more than 1000ms in 2 minutes of last
10 minutes",
"startTime": 1611612258056
}
]

自定义Webhook消息接收

我们按照如下步骤,可以在自己程序中接收警告信息:

1)定义消息接收对象

在 hailtaxi-api 中创建com.itheima.skywalking.model.AlarmMessage ,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data 
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class AlarmMessage {

private int scopeId;
private String name;
private String id0;
private String id1;
private String alarmMessage;
private long startTime;
String ruleName;

}

2)接收警告方法创建

在 hailtaxi-driver 中创建

com.itheima.driver.controller.AlarmMessageController 用于接收警告消息,代码如下:

一般情况下,这种接收告警的api会被放置在比较清闲的后台服务中!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController 
@RequestMapping(value = "/skywalking")
public class AlarmMessageController {
/**
* 接收警告信息
* @param alarmMessageList
*/
@PostMapping("/webhook")
public void webhook(@RequestBody List<AlarmMessage> alarmMessageList) {
for (AlarmMessage alarmMessage : alarmMessageList) {
System.out.println("webhook:"+alarmMessage);
}
}
}

3)修改Webhook地址

修改 alarm-settings.yml 中的webhook地址:

1
2
3
4
5
webhooks:
# - http://127.0.0.1/notify/
# - http://127.0.0.1/go-wechat/
# 这里通知直接点对点通知,不用走gateway
- http://26.26.26.1:18081/skywalking/webhook

因为skywalking默认有一个告警规则:10分钟内服务成功率低于80%超过2次

所以为了能演示出告警效果,我们在 hailtaxi-driver 项目中的driver/info 接口中添加一个一句话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
*司机信息
*/
//@GetMapping(value="/info/{id}")
@RequestMapping(value="/info/{id}")
public Driver info(@PathVariable(value="id")Stringid,HttpServletRequestrequest){
int i=1/0;//产生异常
Enumeration<String> headerNames=request.getHeaderNames();
while(headerNames.hasMoreElements()){
String name=headerNames.nextElement();      
String value=request.getHeader(name);
System.out.println(name+":"+value);
System.out.println("--------------------------");
}
returndriverService.findById(id);
}

测试时将网关的条件断言给注释一下!!!

此时我们程序中就能接收警告信息了

Skywalking Agent原理剖析

  • 能够基于Java Agent编写出普通类的代理
  • 理解Byte Buddy的作用
  • 能够基于Byte Buddy编写动态代理
  • 可以实现Skywalking8.3.0的源码导入
  • 掌握Skywalking Agent工作原理
  • 掌握Skywalking Agent插件加载源码流程

Byte Buddy

Byte Buddy 是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。除了 Java 类库附带的代码生成实用程序外, Byte Buddy 还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外, Byte Buddy 提供了一种方便的 API,可以使用 Java 代理或在构建过程中手动更改类。无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。

已支持Java 11,库轻量,仅取决于Java字节代码解析器库ASM的访问者API,它本身不需要任何其他依赖项。

比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有一定的优势。

官网: https://bytebuddy.net

Byte Buddy应用场景

Java 是一种强类型的编程语言,即要求所有变量和对象都有一个确定的类型,如果在赋值操作中出现类型不兼容的情况,就会抛出异常。强类型检查在大多数情况下是可行的,然而在某些特殊场景下,强类型检查则成了巨大的障碍。

我们在做一些通用工具封装的时候,类型检查就成了很大障碍。比如我们编写一个通用的Dao实现数据操作,我们根本不知道用户要调用的方法会传几个参数、每个参数是什么类型、需求变更又会出现什么类型,几乎没法在方法中引用用户方法中定义的任何类型。我们绝大多数通用工具封装都采用了反射机制,通过反射可以知道用户调用的方法或字段,但是Java反射有很多缺陷:

1:反射性能很差

2:反射能绕开类型安全检查,不安全,比如权限暴力破解

java编程语言代码生成库不止 Byte Buddy 一个,以下代码生成库在 Java中也很流行:

  • Java Proxy

Java Proxy 是 JDK 自带的一个代理工具,它允许为实现了一系列接口的类生成代理类。Java Proxy 要求目标类必须实现接口是一个非常大限制,例如,在某些场景中,目标类没有实现任何接口且无法修改目标类的代码实现,JavaProxy 就无法对其进行扩展和增强了。

  • CGLIB

CGLIB 诞生于 Java 初期,但不幸的是没有跟上 Java 平台的发展。虽然CGLIB 本身是一个相当强大的库,但也变得越来越复杂。鉴于此,导致许多用户放弃了 CGLIB 。

  • Javassist

Javassist 的使用对 Java 开发者来说是非常友好的,它使用Java 源代码字符串和 Javassist 提供的一些简单 API ,共同拼凑出用户想要的 Java 类,Javassist自带一个编译器,拼凑好的 Java 类在程序运行时会被编译成为字节码并加载到JVM 中。Javassist 库简单易用,而且使用 Java 语法构建类与平时写 Java 代码类似,但是 Javassist 编译器在性能上比不了 Javac 编译器,而且在动态组合字符串以实现比较复杂的逻辑时容易出错。

  • Byte Buddy

Byte Buddy 提供了一种非常灵活且强大的领域特定语言,通过编写简单的Java 代码即可创建自定义的运行时类。与此同时,Byte Buddy 还具有非常开放的定制性,能够应付不同复杂度的需求。

上面所有代码生成技术中,我们推荐使用Byte Buddy,因为Byte Buddy代码生成可的性能最高,Byte Buddy 的主要侧重点在于生成更快速的代码,如下图:

image-20220424073320832

Byte Buddy学习

我们接下来详细讲解一下Byte Buddy Api,对重要的方法和类进行深度剖析。

ByteBuddy语法

任何一个由 Byte Buddy 创建/增强的类型都是通过 ByteBuddy 类的实例来完成的,我们先来学习一下ByteBuddy类,如下代码:

1
2
3
4
5
6
DynamicType.Unloaded<?> dynamicType = new ByteBuddy() 
// 生成 Object的子类
.subclass(Object.class)
// 生成类的名称为"com.itheima.Type"
.name("com.itheima.Type")
.make();

Byte Buddy 动态增强代码总共有三种方式:

  • subclass:对应 ByteBuddy.subclass() 方法。这种方式比较好理解,就 是为目标类(即被增强的类)生成一个子类,在子类方法中插入动态代码。
  • rebasing:对应 ByteBuddy.rebasing() 方法。当使用 rebasing 方式 增强一个类时,Byte Buddy 保存目标类中所有方法的实现,也就是说,当 Byte Buddy 遇到冲突的字段或方法时,会将原来的字段或方法实现复制到 具有兼容签名的重新命名的私有方法中,而不会抛弃这些字段和方法实现。从 而达到不丢失实现的目的。这些重命名的方法可以继续通过重命名后的名称进 行调用。
  • redefinition:对应 ByteBuddy.redefine() 方法。当重定义一个类 时,Byte Buddy 可以对一个已有的类添加属性和方法,或者删除已经存在 的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原来存在 的方法实现就会消失。

通过上述三种方式完成类的增强之后,我们得到的是DynamicType.Unloaded 对象,表示的是一个未加载的类型,我们可以使用ClassLoadingStrategy 加载此类型。 Byte Buddy 提供了几种类加载策略,这些加载策略定义在 ClassLoadingStrategy.Default 中,其中:

  • WRAPPER 策略:创建一个新的 ClassLoader 来加载动态生成的类型。
  • CHILD_FIRST 策略:创建一个子类优先加载的 ClassLoader,即打破了双亲委派模型。
  • INJECTION 策略:使用反射将动态生成的类型直接注入到当前ClassLoader 中。实现如下:
1
2
3
4
5
6
7
8
9
10
Class<?> dynamicClazz = new ByteBuddy() 
// 生成 Object的子类
.subclass(Object.class)
// 生成类的名称为"com.itheima.Type"
.name("com.itheima.Type")
.make()
.load(Demo.class.getClassLoader(),
//使用WRAPPER 策略加载生成的动态类型
ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();

前面动态生成的 com.itheima.Type 类型只是简单的继承了 Object 类,在实际应用中动态生成新类型的一般目的就是为了增强原始的方法,下面通过一个示例展示 Byte Buddy 如何增强 toString() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建ByteBuddy对象 
String str = new ByteBuddy()
// subclass增强方式
.subclass(Object.class)
// 新类型的类名
.name("com.itheima.Type")
// 拦截其中的toString()方法
.method(ElementMatchers.named("toString"))
// 让toString()方法返回固定值
.intercept(FixedValue.value("Hello World!"))
.make()
// 加载新类型,默认WRAPPER策略
.load(ByteBuddy.class.getClassLoader())
.getLoaded()
// 通过 Java反射创建 com.xxx.Type实例
.newInstance()
// 调用 toString()方法
.toString();

首先需要关注这里的 method() 方法,method() 方法可以通过传入的ElementMatchers 参数匹配多个需要修改的方法,这里的ElementMatchers.named(“toString”) 即为按照方法名匹配 toString() 方法。如果同时存在多个重载方法,则可以使用 ElementMatchers 其他 API 描述方法的签名,如下所示:

1
2
3
4
5
6
// 指定方法名称 
ElementMatchers.named("toString")
// 指定方法的返回值
.and(ElementMatchers.returns(String.class))
// 指定方法参数
.and(ElementMatchers.takesArguments(0));

接下来需要关注的是 intercept() 方法,通过 method() 方法拦截到的所有方法会由 Intercept() 方法指定的 Implementation 对象决定如何增强。这里的 FixValue.value() 会将方法的实现修改为固定值,上例中就是固定返回 “Hello World!” 字符串。

Byte Buddy 中可以设置多个 method() 和 Intercept() 方法进行拦截和修改, Byte Buddy 会按照栈的顺序来进行拦截。

ByteBuddy 案例

创建一个项目 agent-demo ,添加如下坐标

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies> 
<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>
</dependencies>

我们先创建一个普通类,再为该类创建代理类,创建代理对方法进行拦截做处理

1)普通类

创建 com.itheima.service.UserService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UserService {

//方法1
public String username(){
System.out.println("username().....");
return "张三";
}

//方法2
public String address(String username){
System.out.println("address(String username).....");
return username+"来自 【湖北省-仙居-恩施土家族苗族自治州】";
}

//方法3
public String address(String username,String city){
System.out.println("address(String username,String city).....");
return username+"来自 【湖北省"+city+"】";
}
}

2)代理测试 com.itheima.service.UserServiceTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ByteBuddyTest {

public static void main(String[] args) throws IllegalAccessException, InstantiationException {
/**
* 运行期动态创建一个类
*/
Class<? extends UserService> aClass = new ByteBuddy()
// 要增强的某个类
.subclass(UserService.class)
// 生成类的名称
.name("com.itheima.service.UserServiceImpl")
// 对什么方法增强
// .method(ElementMatchers.named("username"))
// .method(ElementMatchers.returns(String.class).and(ElementMatchers.takesArguments(0)))
// .intercept(FixedValue.value("唐僧"))
.method(ElementMatchers.isDeclaredBy(UserService.class))
// 为方法添加拦截器 如果拦截器方法是静态的 这里 可以传 LogInterceptor.class
.intercept(MethodDelegation.to(new LogInterceptor()))
//生成class 但并未加载
.make()
// 设置类加载器 并指定加载策略(默认WRAPPER)
.load(ByteBuddy.class.getClassLoader())
// 开始加载得到 Class
.getLoaded();
//创建实例
UserService userServiceImpl = aClass.newInstance();
//调用方法
System.out.println(userServiceImpl.username());
System.out.println(userServiceImpl.address("唐僧老师"));
System.out.println(userServiceImpl.address("唐僧老师", "仙居-恩施"));


/*Proxy.newProxyInstance(ByteBuddyTest.class().getClassLoader(),new Class<>()[]{} ,new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

Oject result = method.invoke()
return result;
}
});*/
}
}

3创建拦截器,编写拦截器方法:

com.itheima.service.LogInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.itheima.service;

import net.bytebuddy.implementation.bind.annotation.*;

import java.lang.reflect.Method;
import java.util.concurrent.Callable;

public class LogInterceptor {


@RuntimeType
public Object intercept(// 被拦截的目标对象 (动态生成的目标对象)
@This Object target,
// 正在执行的方法Method 对象(目标对象父类的Method)
@Origin Method method,
// 正在执行的方法的全部参数
@AllArguments Object[] argumengts,
// 目标对象的一个代理
@Super Object delegate,
// 方法的调用者对象 对目标对象方法的调用依靠它
@SuperCall Callable<?> callable) {
//具体的增强逻辑的地方
Object result = null;
try {
System.out.println("method before-----");
result = callable.call();
System.out.println("method after-----");
} catch (Exception e) {
System.out.println("method exception-----");
} finally {
System.out.println("method final-----");
}
// 这里还可以对返回结果进行增强的,比如result+"123"
return result;
}

}

在程序中我们 用到 ByteBuddy 的 MethodDelegation 对象,它可以将拦截的目标方法委托给其他对象处理,这里有几个注解我们先进行说明:

  • @RuntimeType:不进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。
  • @This:注入被拦截的目标对象(动态生成的目标对象)。
  • @Origin:注入正在执行的方法Method 对象(目标对象父类的Method)。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。
  • @AllArguments:注入正在执行的方法的全部参数。
  • @Super:注入目标对象的一个代理
  • @SuperCall:这个注解比较特殊,我们要在 intercept() 方法中调用被代理/增强 的方法的话,需要通过这种方式注入,与 Spring AOP 中 的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。另外,@SuperCall 注解还可以修饰Runnable 类型的参数,只不过目标方法的返回值就拿不到了。

运行测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
准备执行Method=username 

username().....

方法执行完成Method=username

张三

准备执行Method=address

address(String username).....

方法执行完成Method=address

唐僧老师来自 【湖北省-仙居-恩施土家族苗族自治州】

准备执行Method=address

address(String username,String city).....

方法执行完成Method=address

唐僧老师来自 【湖北省仙居恩施】

探针技术-javaAgent

使用Skywalking的时候,并没有修改程序中任何一行 Java 代码,这里便使用到了 Java Agent 技术,我们接下来展开对Java Agent 技术的学习。

javaAgent概述

Java Agent这个技术对大多数人来说都比较陌生,但是大家都都多多少少接触过一些,实际上我们平时用过的很多工具都是基于java Agent来实现的,例如:热部署工具JRebel,springboot的热部署插件,各种线上诊断工具(btrace, greys),阿里开源的arthas等等。其实java Agent在JDK1.5以后,我们可以使用agent技术构建一个独立于应用程序的代理程序(即Agent),用来协助监测、运行甚至替换其他JVM上的程序。使用它可以实现虚拟机级别的AOP功能,并且这种方式一个典型的优势就是无代码侵入。

Agent分为两种:

  • 1、在主程序之前运行的Agent,
  • 2、在主程序之后运行的Agent(前者的升级版,1.6以后提供)。

javaAgent入门

premain

premain:主程序之前运行的Agent

在实际使用过程中,javaagent是java命令的一个参数。通过java 命令启动我们的应用程序的时候,可通过参数 -javaagent 指定一个 jar 包(也就是我们的代理agent),能够实现在我们应用程序的主程序运行之前来执行我们指定jar

包中的特定方法,在该方法中我们能够实现动态增强Class等相关功能,并且该jar包有2个要求:

  1. 这个 jar 包的 META-INF/MANIFEST.MF 文件必须指定 Premain Class 项,该选项指定的是一个类的全路径

  2. Premain-Class 指定的那个类必须实现 premain() 方法。

META-INF/MANIFEST.MF

1
2
3
4
5
Manifest-Version: 1.0 
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.itheima.PreMainAgent

注意:最后需要多一行空行

  • Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)
  • Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)
  • Premain-Class :包含 premain 方法的类(类的全路径名)

从字面上理解,Premain-Class 就是运行在 main 函数之前的的类。当Java虚拟机启动时,在执行 main 函数之前,JVM 会先运行 -javaagent 所指定 jar包内 Premain-Class 这个类的 premain 方法 。

我们可以通过在命令行输入 java 看到相应的参数,其中就有和java agent相关的

image-20220424074109812

在上面 -javaagent 参数中提到了参阅 java.lang.instrument ,这是在rt.jar 中定义的一个包,该路径下有两个重要的类:

image-20220424074121102

该包提供了一些工具帮助开发人员在 Java 程序运行时,动态修改系统中的Class 类型。其中,使用该软件包的一个关键组件就是 Javaagent,如果从本质上来讲,Java Agent 是一个遵循一组严格约定的常规 Java 类。 上面说到javaagent命令要求指定的类中必须要有premain()方法,并且对premain方法的签名也有要求,签名必须满足以下两种格式

1
2
3
public static void premain(String agentArgs, Instrumentation inst) 

public static void premain(String agentArgs)

JVM 会优先加载 带 Instrumentation 签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法

demo:

1、在 agent-demo 中添加如下坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<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>
<!-- 添加 mplementation-*和Specification-*配置项-->
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<!--指定premain方法所在的类-->
<Premain-Class>com.itheima.agent.PreMainAgent</Premain-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>

2、编写一个agent程序: com.itheima.agent.PreMainAgent ,完成premain 方法的签名,先做一个简单的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.itheima.agent;

import com.itheima.service.TimerInterceptor;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
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) {
System.out.println("我的agent程序运行了,agentArgs="+agentArgs);
/**
* 基于 bytebuddy agent 来进行方法执行耗时的统计操作
*/
AgentBuilder.Transformer transformer = new MyTransfomer();
new AgentBuilder.Default()
.type(ElementMatchers.nameStartsWith("com.itheima.driver"))
.transform(transformer)
.installOn(inst);
}

public static class MyTransfomer implements AgentBuilder.Transformer {

@Override
public DynamicType.Builder<?> transform(
DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader,
JavaModule module) {

return builder.method(ElementMatchers.any()).intercept(MethodDelegation.to(TimerInterceptor.class));
}
}
}

下面先来简单介绍一下 Instrumentation 中的核心 API 方法:

  • addTransformer()/removeTransformer() 方法:注册/注销一个ClassFileTransformer 类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义(修改类的字节码)。
  • redefineClasses() 方法:该方法针对的是已经加载的类,它会对传入的类进行重新定义。
  • getAllLoadedClasses()方法:返回当前 JVM 已加载的所有类。
  • getInitiatedClasses() 方法:返回当前 JVM 已经初始化的类。
  • getObjectSize()方法:获取参数指定的对象的大小。

3、对 agent-demo 项目进行打包,得到 agent-demo-1.0-SNAPSHOT.jar

4、创建 agent-test 项目,编写一个类: com.itheima.Application

1
2
3
4
5
6
7
8
9
10
11
package com.itheima;

import com.itheima.driver.DriverService;

public class Application {

public static void main(String[] args) {
System.out.println("main 函数 运行了 ");
}
}

5、启动运行,添加 -javaagent 参数

1
-javaagent:/xxx.jar=option1=value1,option2=value2

image-20220424081155281

运行结果为:

1
2
3
4
5
我的agent程序跑起来啦! 

收到的agent参数是:k1=v1,k2=v2

main 函数 运行了

总结:

这种agent JVM会先执行premain方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。当然,遗漏的主要是系统类,因为很多系统类先于agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如ASM,bytebuddy,javassist,cglib等等来改写实现类。

agentmain

agentmain:主程序之后运行的Agent

上面介绍的是在 JDK 1.5中提供的,开发者只能在main加载之前添加手脚,在 Java SE 6 中提供了一个新的代理操作方法:agentmain,可以在 main函数开始运行之后再运行。跟 premain 函数一样, 开发者可以编写一个含有 agentmain 函数的 Java类,具备以下之一的方法即可

1
2
3
public static void agentmain (String agentArgs, Instrumentation inst) 

public static void agentmain (String agentArgs)

同样需要在MANIFEST.MF文件里面设置“Agent-Class”来指定包含agentmain 函数的类的全路径。

1:在agentdemo中创建一个新的类:

com.itheima.agent.AgentClass ,并编写方法agenmain

1
2
3
4
5
public class AgentClass { 
public static void agentmain (String agentArgs, Instrumentation inst){
System.out.println("agentmain runing");
}
}

2:在pom.xml中添加配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<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>
<!-- 添加 mplementation-*和Specification-*配置项-->
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<!--指定premain方法所在的类-->
<Premain-Class>com.itheima.agent.PreMainAgent</Premain-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>

3:对 agent-demo 重新打包

4:找到 agent-test 中的Application,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Application { 
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
System.out.println("main 函数 运行了 ");
//获取当前系统中所有 运行中的 虚拟机
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vm : list) {
if (vm.displayName().endsWith("com.itheima.Application")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vm.id());
virtualMachine.loadAgent("D:/agentdemo.jar");
virtualMachine.detach();
}
}
}
}

list()方法会去寻找当前系统中所有运行着的JVM进程,你可以打印vmd.displayName() 看到当前系统都有哪些JVM进程在运行。因为main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。

注意:在mac上安装了的jdk是能直接找到 VirtualMachine 类的,但是在windows中安装的jdk无法找到,如果你遇到这种情况,请手动将你jdk安装目录下:lib目录中的tools.jar添加进当前工程的Libraries中。

之所以要这样写是因为:agent要在主程序运行后加载,我们不可能在主程序中编写加载的代码,只能另写程序,那么另写程序如何与主程序进行通信?这里用到的机制就是attach机制,它可以将JVM A连接至JVM B,并发送指令给JVM B执行。

总结:
以上就是Java Agent的俩个简单小栗子了,Java Agent十分强大,它能做到的不仅仅是打印几个监控数值而已,还包括使用Transformer等高级功能进行类替换,方法修改等,要使用Instrumentation的相关API则需要对字节码等技术有较深的认识

agent 案例

需求:通过 java agent 技术实现一个统计方法耗时的案例

1、在 agent-test 项目中添加方法:

com.itheima.driver.DriverService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.itheima.driver;

import java.util.concurrent.TimeUnit;

public class DriverService {


public void didi() {
System.out.println("di di di ------------");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void dada() {
System.out.println("da da da ------------");
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

并在 com.itheima.Application 进行方法的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.itheima;

import com.itheima.driver.DriverService;

public class Application {

public static void main(String[] args) {
System.out.println("main 函数 运行了 ");
DriverService driverService = new DriverService();
driverService.didi();
driverService.dada();
}
}

2、在 agent-demo 中改造 com.itheima.agent.PreMainAgent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.itheima.driver;

import java.lang.instrument.Instrumentation;

public class PreMainAgent {

/***
* 执行方法拦截
* @param agentArgs:-javaagent 命令携带的参数。在前面介绍

SkyWalking Agent 接入时提到

* agent.service_name 这个配置项的默认值

有三种覆盖方式,

* 其中,使用探针配置进行覆盖,探针配置的值

就是通过该参数传入的。

* @param inst:java.lang.instrumen.Instrumentation 是

Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法。

*/
public static void premain(String agentArgs, Instrumentation inst) {
//动态构建操作,根据transformer规则执行拦截操作
AgentBuilder.Transformer transformer = new
AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?>
transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader,
JavaModule javaModule) {
//构建拦截规则
return builder
//method()指定哪些方法需要被拦截,ElementMatchers.any() 表示拦截所有方法
.method(ElementMatchers.<MethodDescription>any())
//intercept()指定拦截上述方法的拦截器
.intercept(MethodDelegation.to(TimeInterceptor.class));
}
};
//采用Byte Buddy的AgentBuilder结合Java Agent处理程序
new AgentBuilder
//采用ByteBuddy作为默认的Agent实例
.Default()
//拦截匹配方式:类以com.itheima.driver开始 (其实就是com.itheima.driver包下的所有类)
.type(ElementMatchers.nameStartsWith("com.itheima.driver")
)
//拦截到的类由transformer处理
.transform(transformer)
//安装到 Instrumentation
.installOn(inst);
}
}

在 agent-demo 项目中,创建 com.itheima.service.TimeInterceptor 实现统计拦截,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TimeInterceptor { 
/**
* 拦截方法
* @param method:拦截的方法
* @param callable:调用对象的代理对象
* @return
* @throws Exception
*/
@RuntimeType // 声明为static
public static Object intercept(@Origin Method method,@SuperCall Callable<?> callable) throws Exception {
//时间统计开始
long start = System.currentTimeMillis();
// 执行原函数
Object result = callable.call();
//执行时间统计
System.out.println(method.getName() + ":执行耗时" + (System.currentTimeMillis() - start) + "ms");
return result;
}
}

3、重新打包 agent-demo ,然后再次测试运行 agent-test 中的主类Application

试效果如下:

1
2
3
4
5
6
7
8
9
10
11
premain 执行了 

main 函数 运行了

di di di ------------

didi:执行耗时5009ms

da da da ------------

dada:执行耗时6002ms

Skywalking源码导入

我们已经学习了Skywalking的应用,接下来我们将剖析Skywalking源码,深度学习Skywalking agent。

源码环境搭建

当前最新版本是8.3.0,我们首先找到8.3.0的版本,然后下载并导入到IDEA,下载地址

https://github.com/apache/skywalking.git,我们直接用git克隆到本地。

  • 1、推荐大家将github仓库拷贝到码云上,以提升下载速度
  • 2、为了避免clone过程出错,可以设置git的全局参数: git config --global core.longpaths true 避免出现Filename too long的报错信息

1)下载工程

image-20220424084149064

image-20220424084153227

这个过程比较耗时间,需要大家耐心等待!

2)切换版本

将Skywalking工程加入到Maven工程中,我们用的是当前最新版本8.3.0,因此需要切换版本:

image-20220424084204110

项目导入IDEA后,会从指定路径加载项目,我们需要在skywalking的pom.xml中配置项目的工路径,添加如下properties配置即可:

1
<maven.multiModuleProjectDirectory>C:\developer\WorkSpace\skywalking</maven.multiModuleProjectDirectory>

pom中有一个插件 maven-enforcer-plugin 要求的maven的版本是3.6以上,需要注意!!!

我们接下来获取skywalking子模块的源码,需要在工程中执行如下命令:

1
2
git submodule init 
git submodule update

image-20220424084243859

该步骤非常重要,不完整执行成功,后续的编译会失败。git submodule update执行很慢,还可能中途中断

编译项目,此时会生成一些类 skywalking\apm-protocol\apm-network\target\generated- sources\protobuf\java\org\apache\skywalking\apm\network\common\v3 目录下的类如下图:

image-20220424084310711

接下来把生成的文件添加到类路径下,如下图:

image-20220424084320180

除了上面这里,还有很多个地方都需要这么操作,我们执行OAPServerStartUp 的main方法启动Skywalking,只要执行找不到类,就找下有没有任何编译后生成的类没在类路径下,都把他们设置为类路径即可。安装项目,Skywalking依赖的插件特别多,因此依赖的包也特别多,我们把Skywalking安装到本地,会耗费很长时间,但不要担心,因为迟早会安装完成,如下图:

image-20220424084332806

如果通过以上方式实在构建不了源码,也可尝试通过如下方式来,

1、通过命令拉取源码

1
git clone -b v8.3.0 --recurse-submodules https://gitee.com/giteets/skywalking.git

构建过程中遇到的最大问题是:git submodule 子模块的源码构建不出来,整体项目拉取下来后也可通过如下命令再次拉取子模块源码

1
2
git submodule init 
git submodule update

如果实在不行:在项目下有个 .gitmodules 文件,定义了子模块的仓库地址和应该安装到什么目录下

1
2
3
4
5
6
7
8
9
10
11
12
[submodule "apm-protocol/apm-network/src/main/proto"] 
path = apm-protocol/apm-network/src/main/proto
url = https://github.com/apache/skywalking-data-collect-protocol.git
[submodule "oap-server/server-query-plugin/query-graphql-plugin/src/main/resources/query-protocol"]
path = oap-server/server-query-plugin/query-graphql-plugin/src/main/resources/query-protocol
url = https://github.com/apache/skywalking-query-protocol.git
[submodule "skywalking-ui"]
path = skywalking-ui
url = https://github.com/apache/skywalking-rocketbot-ui.git
[submodule "test/e2e/e2e-protocol/src/main/proto"]
path = test/e2e/e2e-protocol/src/main/proto
url = https://github.com/apache/skywalking-data-collect-protocol.git

实在不行,就手动将这四个子模块分别手动下载到指定的path目录下,注意版本

2、将项目导入到idea,要求jkd8,maven3.6

3、在项目的 pom.xml 中添加 properties

1
<maven.multiModuleProjectDirectory>C:\developer\WorkSpace\skywalking</maven.multiModuleProjectDirectory>

4、 clean , package , install ,注意跳过测试

5、参考社区文档,设置idea,将生成的源代标记成 Sources Root

设置 生成的源代码(Generated Source Code)目录.

  • apm-protocol/apm-network/target/generated-sources/protobuf 目录下的 grpc-java 和 java 目录
  • oap-server/server-core/target/generated-sources/protobuf 目录下的 grpc-java 和 java 目录
  • oap-server/server-receiver-plugin/receiver-proto/target/generated-sources/protobuf 目录下的 grpc-java 和 java
  • oap-server/exporter/target/generated-sources/protobuf目录下的 grpc-java 和 java
  • oap-server/server-configuration/grpc-configuration-sync/target/generated-sources/protobuf 目录下的 grpc-java 和 java

image-20220424104505752

模块分析

apm-application-toolkit:常用的工具工程,例如:log4j、log4j2、logback 等常见日志框架的接入接口,Kafka轮询调用注解,apm-applicationtoolkit 模块类似于暴露 API 定义,对应的处理逻辑在 apm-sniffer/apmtoolkit-activation 模块中实现,如下图

image-20220424084639801

apm-commons:SkyWalking 的公共组件和工具类。如下图所示,其中包含两个子模块,apm-datacarrier 模块提供了一个生产者-消费者模式的缓存组件(DataCarrier),无论是在 Agent 端还是 OAP 端都依赖该组件。apmutil 模块则提供了一些常用的工具类,例如,字符串处理工具类(StringUtil)、占位符处理的工具类(PropertyPlaceholderHelper、PlaceholderConfigurerSupport)等等。

apache-skywalking-apm:SkyWalking 打包后使用的命令文件都在此目录中,例如,前文启动 OAP 和 SkyWalking Rocketbot 使用的 startup.sh 文件。

apm-protocol:该模块中只有一个 apm-network 模块,我们需要关注的是其中定义的 .proto 文件,定义 Agent 与后端 OAP 使用 gRPC 交互时的协议。

apm-sniffer:agent核心功能以及agent依赖插件,模块比较多:

  • apm-agent:只有一个类SkyWalkingAgent,是Skywalking的agent入 口。

  • apm-agent-core:看名字我们就知道它是Skywalking agent核心模块。

  • apm-sdk-plugin:该模块下包含了 SkyWalking Agent 的全部插件。

  • apm-toolkit-activation:apm-application-toolkit 模块的具体实 现。

  • apm-test-tools:Skywalking的测试功能。

  • bootstrap-plugins:该插件主要提供了Http和多线程相关的功能支持, 它里面有2个子工程。

  • optional-plugins:可选插件,例如对spring支持、对kotlin支持等, 它下面有多个插件工程实现。

  • optional-reporter-plugins:该工程插件主要提供一些数据报告,集成 了Kafka功能。

apm-webapp:SkyWalking Rocketbot 对应的后端。

oap-server:oap 主程序,该工程中有多个模块,我们对核心模块进行说明:

  • analyzer:数据分析工程,例如对内存分析、仪表盘分析报告等,它下面有 2个子工程。

  • exporter:导出数据功能。

  • oal-grammar:操作适配语法,例如SQL语法。

  • oal-rt:操作解析器,上面提供了语法,该工程提供对操作解析功能。

  • server-alarm-plugin:负责实现 SkyWalking 的告警功能。

  • server-cluster-plugin:OAP集群管理功能,提供了很多第三方介入的组 件。

  • server-configuration:负责管理 OAP 的配置信息,也提供了接入多种配置管理组件的相关插件。

  • server-core:SkyWalking OAP的核心实现都在该模块中。

  • server-library:OAP 以及 OAP 各个插件依赖的公共模块,其中提供了双队列 Buffer、请求远端的 Client 等工具类,这些模块都是对立于

  • SkyWalking OAP 体系之外的类库,我们可以直接拿着使用。

  • server-query-plugin:SkyWalking Rocketbot 发送的请求首先由该模块接收处理,目前该模块只支持 GraphQL 查询。

  • server-receiver-plugin:SkyWalking Agent 发送来的 Metrics、Trace 以及 Register 等写入请求都是首先由该模块接收处理的,不仅如此,该模块还提供了多种接收其他格式写入请求的插件。

  • server-starter:OAP 服务启动的入口。

  • server-storage-plugin:OAP 服务底层可以使用多种存储来保存 Metrics 数据以及Trace 数据,该模块中包含了接入相关存储的插件。

  • skywalking-agent:SkyWalking Agent 编译后生成的 jar 包都会放到 该目录中。

  • skywalking-ui:SkyWalking Rocketbot 的前端。

Skywalking Agent 启动流程剖析

我们已经学习了Skywalking常用操作,并且讲解了Java Agent,而且Skywalking Agent就是基于Java Agent研发而来,我们接下来深入学习Skywalking Agent架构、原理、常用组件。

Skywalking Agent架构

我们在学习Skywalking之前,先了解一下微内核架构,如下图:

image-20220424084856478

微内核架构(Microkernel Architecture),也被成为插件化架构(Plug-inArchitecture),是一种面向功能进行拆分的可扩展性架构,通常用于实现基于产品(原文为product-based,指存在多个版本,需要下载安装才能使用,与web-based想对应)的应用。

微内核架构的好处:

  • 1:测试成本下降。从软件工程的角度看,微内核架构将变化的部分和不变的 部分拆分,降低了测试的成本,符合设计模式中的开放封闭原则。
  • 2:稳定性。由于每个插件模块相对独立,即使其中一个插件有问题,也可以 保证内核系统以及其他插件的稳定性。
  • 3:可扩展性。在增加新功能或接入新业务的时候,只需要新增相应插件模块 即可;在进行历史功能下线时,也只需删除相应插件模块即可。

微内核的核心系统设计的关键技术有:插件管理,插件连接 和 插件通信。

SkyWalking Agent 采用了微内核架构(Microkernel Architecture),是一种面向功能进行拆分的可扩展性架构。

1
2
3
apm-agent-core:是Skywalking Agent的核心模块 

apm-sdk-plugin:是Skywalking需要的各个插件模块

image-20220424110930229

Skywalking Agent启动流程

1)启动OAP

我们接下来启动Skywalking oap,我们在 oap-server\server-starter 或 者 oap-server\server-starter-es7 中找到 OAPServerStartUp 类,执行该类的main方法即可启动,但默认用的是H2存储,如果希望用elasticsearch存储,需要修改被调用的服务 server-bootstrap 的配置文件 application.yml 配置elasticsearch位置:

1
2
3
4
5
6
7
8
9
10
storage: 
#selector: ${SW_STORAGE:h2}
selector: ${SW_STORAGE:elasticsearch7}
elasticsearch7:
nameSpace: ${SW_NAMESPACE:""}
#clusterNodes:
${SW_STORAGE_ES_CLUSTER_NODES:localhost:9200}
clusterNodes:
${SW_STORAGE_ES_CLUSTER_NODES:192.168.211.145:9200}
........略

存储直接使用上一次课准备好的es7的存储即可。

执行 OAPServerStartUp 的main方法不报错就没问题。

2)启动SkyWalking Rocketbot

apm-webapp 是 Spring Boot 的 Web项目,执行 ApplicationStartUp 中 的 main() 方法。正常启动之后,访问 http://localhost:8080,看到 SkyWalking Rocketbot 的 UI 界面即为启动成功。

如果修改启动端口,可以直接修改application.yml即可。

3)直接使用源码中的Agent

项目打包会生成 skywalking-agent 目录,里面有 skywalking-agent.jar ,如下图:

image-20220424111059913

我们来使用一下前面源码工程中打包生成的 skywalking-agent.jar ,复制该jar包的路径

找到 hailtaxi-parent 项目,修改 -javaagent 参数如下hailtaxi-gateway

image-20220424111112129

1
-javaagent:C:\developer\WorkSpace\sources\skywalking\skywalking-agent\skywalking-agent.jar -Dskywalking_config=C:\developer\WorkSpace\sources\skywalking\skywalking-agent\config\agent.config -Dskywalking.agent.service_name=hailtaxi-gateway

hailtaxi-driver 和 hailtaxi-order 进行相同配置即可!

全都启动后,查看 Skywalking Rocketbot :本地启动,需要等待一定的时间

Skywalking Agent源码剖析

1、创建 sw-agent-debugger 项目:一个普通的springboot项目即可

image-20220424111156400

2、添加启动 -javaagent 参数

1
-javaagent:D:\coder\creazySA_space\demo_skywalking\skywalking\skywalking-agent\skywalking-agent.jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Dskywalking_config=D:\coder\creazySA_space\demo_skywalking\skywalking\skywalking-agent\config\agent.config -Dskywalking.agent.service_name=sw-agent-debugger

image-20220424111203511

image-20220424111208532

启动的整个方法执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
* Main entrance. Use byte-buddy transform to enhance all classes, which define in plugins.
*/
public static void premain(String agentArgs, Instrumentation instrumentation) throws PluginException {
final PluginFinder pluginFinder;
try {
//初始化加载 agent.config 配置文件,其中会检测 Java Agent 参数以及环境变量是否覆盖了相应配置项
SnifferConfigInitializer.initializeCoreConfig(agentArgs);
} catch (Exception e) {
// try to resolve a new logger, and use the new logger to write the error log here
LogManager.getLogger(SkyWalkingAgent.class)
.error(e, "SkyWalking agent initialized failure. Shutting down.");
return;
} finally {
// refresh logger again after initialization finishes
LOGGER = LogManager.getLogger(SkyWalkingAgent.class);
}

try {
//插件管理 两步:1,加载所有插件 2,构建 PluginFinder
pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());
} catch (AgentPackageNotFoundException ape) {
LOGGER.error(ape, "Locate agent.jar failure. Shutting down.");
return;
} catch (Exception e) {
LOGGER.error(e, "SkyWalking agent initialized failure. Shutting down.");
return;
}
// 利用 ByteBuddy 来 进行字节码 增强
final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));
// 构建 AgentBuilder 忽略要增强的类
AgentBuilder agentBuilder = new AgentBuilder.Default(byteBuddy).ignore(
nameStartsWith("net.bytebuddy.")
.or(nameStartsWith("org.slf4j."))
.or(nameStartsWith("org.groovy."))
.or(nameContains("javassist"))
.or(nameContains(".asm."))
.or(nameContains(".reflectasm."))
.or(nameStartsWith("sun.reflect"))
.or(allSkyWalkingAgentExcludeToolkit())
.or(ElementMatchers.isSynthetic()));

JDK9ModuleExporter.EdgeClasses edgeClasses = new JDK9ModuleExporter.EdgeClasses();
try {
agentBuilder = BootstrapInstrumentBoost.inject(pluginFinder, instrumentation, agentBuilder, edgeClasses);
} catch (Exception e) {
LOGGER.error(e, "SkyWalking agent inject bootstrap instrumentation failure. Shutting down.");
return;
}

try {
agentBuilder = JDK9ModuleExporter.openReadEdge(instrumentation, agentBuilder, edgeClasses);
} catch (Exception e) {
LOGGER.error(e, "SkyWalking agent open read edge in JDK 9+ failure. Shutting down.");
return;
}

if (Config.Agent.IS_CACHE_ENHANCED_CLASS) {
try {
agentBuilder = agentBuilder.with(new CacheableTransformerDecorator(Config.Agent.CLASS_CACHE_MODE));
LOGGER.info("SkyWalking agent class cache [{}] activated.", Config.Agent.CLASS_CACHE_MODE);
} catch (Exception e) {
LOGGER.error(e, "SkyWalking agent can't active class cache.");
}
}
// 配置 agentBuilder *****重要*****
agentBuilder.type(pluginFinder.buildMatch())// 在类加载时根据传入的 ElementMatcher 进行拦截,拦截到的目标类将会被 transform() 方法中指定的 Transformer 进行增强
.transform(new Transformer(pluginFinder)) // 指定的 Transformer 会对前面拦截到的类进行增强
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(new Listener())// 设置监听
.installOn(instrumentation); // 织入到 instrumentation 中

try {
ServiceManager.INSTANCE.boot(); //使用 JDK SPI加载的方式并启动 BootService 服务。
} catch (Exception e) {
LOGGER.error(e, "Skywalking agent boot failure.");
}

Runtime.getRuntime()
.addShutdownHook(new Thread(ServiceManager.INSTANCE::shutdown, "skywalking service shutdown thread"));
}

我们总结一下Skywalking Agent启动流程

1:初始化配置信息。该步骤中会加载 agent.config 配置文件,其中会检 测 Java Agent 参数以及环境变量是否覆盖了相应配置项。

2:查找并解析 skywalking-plugin.def 插件文件。

3:AgentClassLoader 加载插件。

4:PluginFinder 对插件进行分类管理。

5:使用 Byte Buddy 库创建 AgentBuilder。这里会根据已加载的插件动 态增强目标类,插入埋点逻辑。

6:使用 JDK SPI 加载并启动 BootService 服务。

7:添加一个 JVM 钩子,在 JVM 退出时关闭所有 BootService 服务。

这是 org.apache.skywalking.apm.agent.SkyWalkingAgent#premain的主体工作流程

配置初始化

1
2
-javaagent:D:/project/skywalking/skywalking/apm-sniffer/apm-agent/target/skywalking-agent.jar 
-Dskywalking_config=D:/project/skywalking/hailtaxi-parent/hailtaxi-driver/src/main/resources/agent.config -Dskywalking.collector.backend_service=127.0.0.1:11800

启动driver服务的时候,会指定skywalking-agent.jar路径,同时会指定agent.config 配置文件路径,如上配置,此时需要初始化加载该文件,加载流程可以从启动类 SkyWalkingAgent.premain() 方法找答案。

image-20220424111328138

加载解析文件的时候,permain()方法会调用initializeCoreConfig(StringagentOptions)方法,并解析agent.config文件,并将文件内容存入到Properties中,此时加载是按照${配置项名称:默认值}的格式解析各个配置,如下图:

image-20220424111340266

loadConfig() 方法会优先根据环境变量(skywalking_config)指定的agent.config 文件路径加载。若环境变量未指定 skywalking_config 配置,则到 skywalking-agent.jar 同级的 config 目录下查找 agent.confg 配置文件

image-20220424111350594

解析前后的数据也是不一致的,如下图:

image-20220424111358246

overrideConfigBySystemProp() 方法中会遍历环境变量(即System.getProperties() 集合),如果环境变 是以 “skywalking.” 开头的,则认为是 SkyWalking 的配置,同样会填充到 Config 类中,以覆盖 agent.config 中的默认值。如下图:

image-20220424111407738

ConfigInitializer 工具类,将配置信息填充到 Config 中的静态字段中,SkyWalking Agent 启动所需的全部配置都已经填充到 Config 中,后续使用配置信息时直接访问 Config 中的相应静态字段即可。

image-20220424111420704

Config结构:

image-20220424111428437

Config中Agent类的 SERVICE_NAME 对应agent.config中的agent.service_name=${xxx}

Config中Collector类的 BACKEND_SERVICE 对应agent.config中的agent.backend_service=${xxx}

插件加载

加载插件执行流程:

1:new PluginBootstrap()

2:PluginBootstrap().loadPlugins()

3:AgentClassLoader.initDefaultLoader(); 没有指定类加载器的时候 使用PluginBootstrap.ClassLoader

4:创建PluginResourcesResolver插件加载解析器

5:将解析的插件存到List<PluginDefine> pluginClassList,此时只存 储了插件的名字和类路径

6:创建插件实例

7:将所有插件添加到Skywalking内核中

插件加载流程如下:

在 SkyWalkingAgent.premain() 方法中会执行插件加载,如下代码:

1
pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());

加载插件的全部详细代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package org.apache.skywalking.apm.agent.core.plugin;

import org.apache.skywalking.apm.agent.core.boot.AgentPackageNotFoundException;
import org.apache.skywalking.apm.agent.core.logging.api.ILog;
import org.apache.skywalking.apm.agent.core.logging.api.LogManager;
import org.apache.skywalking.apm.agent.core.plugin.loader.AgentClassLoader;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
* Plugins finder. Use {@link PluginResourcesResolver} to find all plugins, and ask {@link PluginCfg} to load all plugin
* definitions.
*/
public class PluginBootstrap {
private static final ILog LOGGER = LogManager.getLogger(PluginBootstrap.class);

/**
* load all plugins.
* 加载所有插件
* @return plugin definition list.
*/
public List<AbstractClassEnhancePluginDefine> loadPlugins() throws AgentPackageNotFoundException {
AgentClassLoader.initDefaultLoader(); // AgentClassLoader extends ClassLoader agent类加载器
// 插件资源的解析器,
PluginResourcesResolver resolver = new PluginResourcesResolver();
List<URL> resources = resolver.getResources();// 加载所有插件资源: 从 classpath:skywalking-plugin.def 文件中获取所有插件资源路径

if (resources == null || resources.size() == 0) {
LOGGER.info("no plugin files (skywalking-plugin.def) found, continue to start application.");
return new ArrayList<AbstractClassEnhancePluginDefine>();
}

for (URL pluginUrl : resources) {
try {
PluginCfg.INSTANCE.load(pluginUrl.openStream());// 加载获取所有插件信息,得到 List<PluginDefine>
} catch (Throwable t) {
LOGGER.error(t, "plugin file [{}] init failure.", pluginUrl);
}
}
// 拿到 List<PluginDefine> PluginDefine中有插件名称和 Class Name
List<PluginDefine> pluginClassList = PluginCfg.INSTANCE.getPluginClassList();
// 将 PluginDefine 转换成 AbstractClassEnhancePluginDefine
List<AbstractClassEnhancePluginDefine> plugins = new ArrayList<AbstractClassEnhancePluginDefine>();
for (PluginDefine pluginDefine : pluginClassList) {
try {
LOGGER.debug("loading plugin class {}.", pluginDefine.getDefineClass());
AbstractClassEnhancePluginDefine plugin = (AbstractClassEnhancePluginDefine) Class.forName(pluginDefine.getDefineClass(), true, AgentClassLoader
.getDefault()).newInstance();// 创建插件对象
plugins.add(plugin);
} catch (Throwable t) {
LOGGER.error(t, "load plugin [{}] failure.", pluginDefine.getDefineClass());
}
}

plugins.addAll(DynamicPluginLoader.INSTANCE.load(AgentClassLoader.getDefault()));

return plugins;

}

}

SkyWalking Agent 加载插件时使用到一个自定义的 ClassLoader ——AgentClassLoader,之所以自定义类加载器,目的是不在应用的 Classpath 中引入 SkyWalking 的插件 jar 包,这样就可以让应用无依赖、无感知的插件。

AgentClassLoader 作为一个类加载器,主要工作还是从其 Classpath 下加载类(或资源文件),对应的就是其 findClass() 方法和 findResource() 方法:我们来看一下findClass,主要根据类名获取它的Class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//扫描classpath所有的jar包
List<Jar> allJars = getAllJars();
//把包替换成路径,最后加上.class
String path = name.replace('.', '/').concat(".class");
//循环查找所有的jar包
for (Jar jar : allJars) {
//加载jar包的信息
JarEntry entry = jar.jarFile.getJarEntry(path);
if (entry == null) {
continue;
}
try {
//定位当前jar包位置
URL classFileUrl = new URL("jar:file:" + jar.sourceFile.getAbsolutePath() + "!/" + path);
//加载jar包
byte[] data;
try (final BufferedInputStream is = new BufferedInputStream(
classFileUrl.openStream()); final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int ch;
while ((ch = is.read()) != -1) {
baos.write(ch);
}
data = baos.toByteArray();
}
//返回当前对象的Class
return processLoadedClass(defineClass(name, data, 0, data.length));
} catch (IOException e) {
LOGGER.error(e, "find class fail.");
}
}
throw new ClassNotFoundException("Can't find " + name);
}

findResource()方法主要获取文件路径,换句话理解,就是获取插件路径,我们来看下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
protected URL findResource(String name) {
//扫描classpath所有的jar包
List<Jar> allJars = getAllJars();
//循环查找所有的jar包
for (Jar jar : allJars) {
//加载jar包的信息
JarEntry entry = jar.jarFile.getJarEntry(name);
if (entry != null) {
try {
//获取jar包的路径
return new URL("jar:file:" + jar.sourceFile.getAbsolutePath() + "!/" + name);
} catch (MalformedURLException ignored) {
}
}
}
return null;
}

解析插件

我们在学习插件解析之前,先看看插件是如何定义的。我们可以打开 apm-sniffer/apm-sdk-plugin ,它里面都是要用到的插件集合:

image-20220424111556705

我们看看 mysql-5.x-plugin ,在resources(也就是classpath)中定义skywalking-plugin.def文件,在该文件中定义加载插件需要解析的类,而插件类以key=value形式定义,如下图:

image-20220424111608479

PluginResourcesResolver

在 loadPlugins() 方法中使用了PluginResourcesResolver , PluginResourcesResolver 是 Agent 插件的资源解析器,会通过 AgentClassLoader 中的 findResource() 方法读取所有Agent 插件中的 skywalking-plugin.def 文件

image-20220424111630749

拿到全部插件的 skywalking-plugin.def 文件之后,PluginCfg 会逐行进行解析,转换成 PluginDefine 对象。PluginDefine 中有两个字段,分别对应skywalking-plugin.def 中的key和value,解析流程如下:

image-20220424111655813

接下来会遍历全部 PluginDefine 对象,通过反射将其中 defineClass 字段中记录的插件类实例化,核心逻辑如下:

image-20220424111708222

AbstractClassEnhancePluginDefine 抽象类是所有 Agent 插件类的顶级父类,其中定义了四个核心方法,决定了一个插件类应该增强哪些目标类、应该如何增强、具体插入哪些逻辑,如下所示:

image-20220424111722349

  • enhanceClass() 方法:返回的 ClassMatch,用于匹配当前插件要增强的目标类。
  • define() 方法:插件类增强逻辑的入口,底层会调用下面的 enhance()方法和 witnessClass() 方法。
  • enhance() 方法:真正执行增强逻辑的地方。
  • witnessClass() 方法:一个开源组件可能有多个版本,插件会通过该方法识别组件的不同版本,防止对不兼容的版本进行增强。

ClassMatch

enhanceClass() 方法决定了一个插件类要增强的目标类,返回值为ClassMatch 类型对象。ClassMatch 类似于一个过滤器,可以通过多种方式匹配到目标类,ClassMatch 接口的实现如下:

image-20220424111744125

  • NameMatch:根据其 className 字段(String 类型)匹配目标类的名称。
  • IndirectMatch:子接口中定义了两个方法
1
2
3
4
5
6
public interface IndirectMatch extends ClassMatch { 
//Junction是Byte Buddy中的类,可以通过and、or等操作 串联多个ElementMatcher,进行匹配
ElementMatcher.Junction buildJunction();
//用于检测传入的类型是否匹配该Match
boolean isMatch(TypeDescription typeDescription);
}
  • MultiClassNameMatch:其中会指定一个 matchClassNames 集合,该集合内的类即为目标类

  • ClassAnnotationMatch:根据标注在类上的注解匹配目标类。

  • MethodAnnotationMatch:根据标注在方法上的注解匹配目标类。

  • HierarchyMatch:根据父类或是接口匹配目标类。

我们来分析一下ClassAnnotationMatch的buildJunction()方法和isMatch()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public ElementMatcher.Junction buildJunction() {
ElementMatcher.Junction junction = null;
//annotations:指定了该 ClassAnnotationMatch 对象需要检查 的注解
//遍历该对象需要检查的所有注解
for (String annotation : annotations) {
if (junction == null) {
//检测类是否标注了指定注解
junction = buildEachAnnotation(annotation);
} else {
//使用 and 方式将所有Junction对象连接起来
junction = junction.and(buildEachAnnotation(annotation));
}
}
// 排除接口
junction = junction.and(not(isInterface()));
return junction;
}

isMatch()方法如下

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public boolean isMatch(TypeDescription typeDescription) {
List<String> annotationList = new ArrayList<String>(Arrays.asList(annotations));
// 获取该类上的注解
AnnotationList declaredAnnotations = typeDescription.getDeclaredAnnotations();
for (AnnotationDescription annotation : declaredAnnotations) {
// 匹配一个删除一个
annotationList.remove(annotation.getAnnotationType().getActualName());
}
// 如果全部删除,则匹配成功
return annotationList.isEmpty();
}

PluginFinder

PluginFinder 是 AbstractClassEnhancePluginDefine 查找器,可以根据给定的类查找用于增强的 AbstractClassEnhancePluginDefine 集合。

在 PluginFinder 的构造函数中会遍历前面课程已经实例化的AbstractClassEnhancePluginDefine ,并根据 enhanceClass() 方法返回的ClassMatcher 类型进行分类,得到如下两个集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class PluginFinder {
//定义了集合
//pluginFinder将插件分类保存在两个集合中,分别是:按名字分类和按 其他辅助信息分类
private final Map<String, LinkedList<AbstractClassEnhancePluginDefine>> nameMatchDefine = new HashMap<String, LinkedList<AbstractClassEnhancePluginDefine>>();//存储所有要按名字匹配增强的插件
private final List<AbstractClassEnhancePluginDefine> signatureMatchDefine = new ArrayList<AbstractClassEnhancePluginDefine>();
private final List<AbstractClassEnhancePluginDefine> bootstrapClassMatchDefine = new ArrayList<AbstractClassEnhancePluginDefine>();
//构造方法
public PluginFinder(List<AbstractClassEnhancePluginDefine> plugins) {
for (AbstractClassEnhancePluginDefine plugin : plugins) { // plugins 所有插件信息集合
//抽象方法enhanceClass方法定义在插件的抽象基类 AbstractClassEnhancePluginDefine中,每一个插件必须去实现这个类 中的方法
ClassMatch match = plugin.enhanceClass();// 获取插件按什么方式进行匹配增强。
// 故enhanceClass是每个插件都会自己去实现的方法,指定需要增强的类

if (match == null) {
continue;
}

if (match instanceof NameMatch) { //找到所有按名称进行匹配增强的插件 保存到 nameMatchDefine
NameMatch nameMatch = (NameMatch) match;
LinkedList<AbstractClassEnhancePluginDefine> pluginDefines = nameMatchDefine.get(nameMatch.getClassName());
if (pluginDefines == null) {
pluginDefines = new LinkedList<AbstractClassEnhancePluginDefine>();
nameMatchDefine.put(nameMatch.getClassName(), pluginDefines);
}
pluginDefines.add(plugin);
} else {
signatureMatchDefine.add(plugin);
}

if (plugin.isBootstrapInstrumentation()) {
bootstrapClassMatchDefine.add(plugin);
}
}
}

//typeDescription是bytebuddy的内置接口,是对类的完整描述,包含 了类的全类名
//传入typeDescription,返回可以运用于typeDescription的类的插件
public List<AbstractClassEnhancePluginDefine> find(TypeDescription typeDescription) {
List<AbstractClassEnhancePluginDefine> matchedPlugins = new LinkedList<AbstractClassEnhancePluginDefine>();
//根据名字信息匹配查找
String typeName = typeDescription.getTypeName();
if (nameMatchDefine.containsKey(typeName)) {
matchedPlugins.addAll(nameMatchDefine.get(typeName));
}

//通过除了名字之外的辅助信息,在signatureMatchDefine集 合中查找
for (AbstractClassEnhancePluginDefine pluginDefine : signatureMatchDefine) {
IndirectMatch match = (IndirectMatch) pluginDefine.enhanceClass();
if (match.isMatch(typeDescription)) {
matchedPlugins.add(pluginDefine);
}
}

return matchedPlugins;
}

public ElementMatcher<? super TypeDescription> buildMatch() {
//设置匹配的规则,名字是否相同,通过名字直接匹配
ElementMatcher.Junction judge = new AbstractJunction<NamedElement>() {
@Override
public boolean matches(NamedElement target) {
return nameMatchDefine.containsKey(target.getActualName());
}
};
judge = judge.and(not(isInterface()));//接口不增 强,排除掉
//如果无法确定类的全限定名,则通过注解、回调信息等辅助方 法间接匹配
for (AbstractClassEnhancePluginDefine define : signatureMatchDefine) {
ClassMatch match = define.enhanceClass();
if (match instanceof IndirectMatch) {
judge = judge.or(((IndirectMatch) match).buildJunction());
}
}
return new ProtectiveShieldMatcher(judge);
}

public List<AbstractClassEnhancePluginDefine> getBootstrapClassMatchDefine() {
return bootstrapClassMatchDefine;
}
}

AgentBuilder

利用bytebuddy的API生成一个代理,并执行transform方法和监听器Listener(主要是日志相关)。

在premain中,通过链式调用,被builderMatch()匹配到的类都会执行transform方法,transform定义了字节码增强的逻辑:

1
2
//使用ByteBuddy创建AgentBuilder 
final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEB UGGING_CLASS));

Config.Agent.IS_OPEN_DEBUGGING_CLASS 在 agent.config 中对应配置agent.is_open_debugging_class

如果将其配置为 true,则会将动态生成的类输出到 debugging 目录中。AgentBuilder 是 Byte Buddy 库专门用来支持 Java Agent 的一个 API,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 // 设置使用的ByteBuddy 对象 
new AgentBuilder.Default(byteBuddy).ignore(
nameStartsWith("net.bytebuddy.") // 不会拦截下列包中 的类
.or(nameStartsWith("org.slf4j."))
.or(nameStartsWith("org.groovy."))
.or(nameContains("javassist"))
.or(nameContains(".asm."))
.or(nameContains(".reflectasm."))
.or(nameStartsWith("sun.reflect"))
.or(allSkyWalkingAgentExcludeToolkit())
.or(ElementMatchers.isSynthetic()));

// 配置 agentBuilder *****重要*****
agentBuilder.type(pluginFinder.buildMatch())// 在类加载时根据传入的 ElementMatcher 进行拦截,拦截到的目标类将会被 transform() 方法中指定的 Transformer 进行增强
.transform(new Transformer(pluginFinder)) // 指定的 Transformer 会对前面拦截到的类进行增强
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(new Listener())// 设置监听
.installOn(instrumentation); // 织入到 instrumentation 中

上面代码中有些方法我们需要理解一下:

  • ignore() 方法:忽略指定包中的类,对这些类不会进行拦截增强。
  • type() 方法:在类加载时根据传入的 ElementMatcher 进行拦截,拦截到的目标类将会被 transform() 方法中指定的 Transformer 进行增强。
  • transform() 方法:这里指定的 Transformer 会对前面拦截到的类进行增强。
  • with() 方法:添加一个 Listener 用来监听 AgentBuilder 触发的事件。

首先, PluginFInder.buildMatch() 方法返回的 ElementMatcher 对象会将全部插件的匹配规则(即插件的 enhanceClass() 方法返回的 ClassMatch)用OR 的方式连接起来,这样,所有插件能匹配到的所有类都会交给 Transformer处理。

再来看 with() 方法中添加的监听器 —— SkywalkingAgent.Listener,它继承了 AgentBuilder.Listener 接口,当监听到 Transformation 事件时,会根据IS_OPEN_DEBUGGING_CLASS 配置决定是否将增强之后的类持久化成 class 文件保存到指定的 log 目录中。注意,该操作是需要加锁的,会影响系统的性能,一般只在测试环境中开启,在生产环境中不会开启。

Skywalking.Transformer实现了 AgentBuilder.Transformer 接口,其transform() 方法是插件增强目标类的入口。Skywalking.Transformer 会通过PluginFinder 查找目标类匹配的插件(即 AbstractClassEnhancePluginDefine对象),然后交由 AbstractClassEnhancePluginDefine 完成增强,核心实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
public DynamicType.Builder<?> transform(final DynamicType.Builder<?> builder,
final TypeDescription typeDescription,// 被拦截的目标类
final ClassLoader classLoader,// 加载目标类的ClassLoader
final JavaModule module) {
// 从PluginFinder中查找匹配该目标类的插件,PluginFinder的 查找逻辑不再重复
List<AbstractClassEnhancePluginDefine> pluginDefines = pluginFinder.find(typeDescription);// 从 PluginFinder 中查找匹配该目标类的插件
if (pluginDefines.size() > 0) {
DynamicType.Builder<?> newBuilder = builder;
EnhanceContext context = new EnhanceContext();
for (AbstractClassEnhancePluginDefine define : pluginDefines) {
// AbstractClassEnhancePluginDefine.define()方法是插件入口,在其中完成了对目标类的增强 ****重点*****
DynamicType.Builder<?> possibleNewBuilder = define.define(typeDescription, newBuilder, classLoader, context);
if (possibleNewBuilder != null) {
// 注意这里,如果匹配了多个插件,会被增强多次
newBuilder = possibleNewBuilder;
}
}
if (context.isEnhanced()) {
LOGGER.debug("Finish the prepare stage for {}.", typeDescription.getName());
}

return newBuilder;
}

LOGGER.debug("Matched class {}, but ignore by finding mechanism.", typeDescription.getTypeName());
return builder;
}

作业

1:如何自定义Skywalking插件

2:如何使用插件