原文地址:
1、https://zhuanlan.zhihu.com/p/361579294
2、https://www.cnblogs.com/itxiaoshen/p/16513711.html
github: https://skywalking.apache.org/
一、SkyWalking 是什么?
一个开源的可观测平台,用于从服务和云原生基础设施收集,分析, 聚合及可视化数据。
SkyWalking 提供了一种简便的方式来清晰地观测分布式系统,甚至横跨多个云平台。SkyWalking 更是一个现代化的应用程序性能监控(Application Performance Monitoring)系统,尤其专为云原生、基于容器的分布式系统设计。
二、为什么使用SkyWalking
SkyWalking为服务(service),服务实例(service instance),以及端点(endpoint)提供了可观测能力。
使用 SkyWalking 时,用户可以看到服务与端点之间的拓扑结构,每个服务/服务实例/端点的性能指标,还可以设置报警规则。
除此之外, 你还可以通过以下方式集成
- 其他分布式追踪使用 Skywalking 原生代理和Zipkin , Jaeger 和 OpenCensus 的 SDK;
- 其他度量指标系统,例如 Prometheus , Sleuth(Micrometer)。
概念讲解:
服务(service):表示对请求提供相同行为的一组工作负载。在使用打点代理或 SDK 的时候,你可以定义服务的名字。SkyWalking 还可以使用在 Istio 等平台中定义的名称。
服务实例(Service Instance):上述的一组工作负载中的每一个工作负载称为一个实例。就像 Kubernetes 中的 pods
一样,服务实例未必就是操作系统上的一个进程。 但当你在使用打点代理的时候,一个服务实例实际就是操作系统上的一个真实进程。
端点(Endpoint):对于特定服务所接收的请求路径,如 HTTP 的 URI 路径和 gRPC 服务的类名 + 方法签名。
三、SkyWalking架构
SkyWalking 逻辑上分为四部分:探针,平台后端,存储和用户界面。
- 探针 基于不同的来源可能是不一样的,但作用都是收集数据,将数据格式化为 SkyWalking 适用的格式。
- 平台后端 支持数据聚合,数据分析以及驱动数据流从探针到用户界面的流程。分析包括 Skywalking 原生追踪和性能指标以及第三方来源,包括 Istio 及 Envoy telemetry , Zipkin 追踪格式化等。你甚至可以使用 Observability Analysis Language 对原生度量指标 和 用于扩展度量的计量系统 自定义聚合分析。
- 存储 通过开放的插件化的接口存放 SkyWalking 数据。你可以选择一个既有的存储系统,如 ElasticSearch,也可以选择自己实现一个存储系统。
- UI 一个基于接口高度定制化的Web系统,用户可以可视化查看和管理 SkyWalking 数据。
四、探针
在 SkyWalking中,探针表示集成到目标系统中的代理或SDK库,它负责收集遥测数据,包括链路追踪和性能指标。根据目标系统的技术栈,探针可能有差异巨大的方式来达到以上功能。但从根本上来说都是一样的,即收集并格式化数据,并发送到后端。
从高层次上来讲,SkyWalking 探针可分为以下三组:
- 基于语言的原生代理 这种类型的代理运行在目标服务的用户空间中,就像用户代码的一部分一样。如SkyWalking Java 代理,使用
-javaagent
命令行参数在运行期间对代码进行操作,操作
一词表示修改并注入用户代码。另一种代理是使用目标库提供的钩子函数或拦截机制。如你所见,这些探针是基于特定的语言和库。 - 服务网格探针 服务网格探针从服务网格的 Sidecar 和控制面板收集数据。在以前,代理只用作整个集群的入口,但是有了服务网格和 Sidecar 之后,我们可以基于此进行观测了。
- 第三方打点类库 SkyWalking 也能够接收其他流行的打点库产生的数据格式。SkyWalking 通过分析数据,将数据格式化成自身的链路和度量数据格式。该功能最初只能接收 Zipkin 的 span 数据。
你不必同时使用 基于语言的原生代理 和 服务网格探针 ,因为两者都收集指标数据,否则你的系统就要承受双倍负载,且分析数量会翻倍。
有如下几种推荐的方式来使用探针:
- 只使用 基于语言的原生代理
- 只使用 第三方打点库,如Zipkin 打点系统
- 只使用 服务网格探针
- 使用 服务网格探针,配合 语言原生代理 或 第三方打点库,来 追踪状态 。(高级用法)
服务自动打点代理:
服务自动打点代理是基于语言的原生代理的一部分,这种代理需要依靠某些语言特定的特性,通常是一种基于虚拟机的语言。
1、自动打点代理是什么意思?
对于最终用户来说他们不需要修改代码(至少在绝大多数情况下),只是被代理给修改了,这种做法通常叫做"在运行时操作代码"。底层原理就是自动打点代理利用了虚拟机提供的用于修改代码的接口来动态加入打点的代码,如通过 javaagent premain 来修改 Java 类。
此外, 我们说大部分自动打点代理是基于虚拟机的,但实际上你也可以在编译期构建这样的工具。
2、有什么限制?
- 进程内传播在大多数情况下成为可能 许多高级编程语言(如 Java, .NET)都是用于构建业务系统。大部分业务逻辑代码对于每一个请求来说都运行在同一个线程内,这使得传播是基于线程 ID 的,以确保上下文是安全的。
- 仅仅对某些框架和库奏效. 因为是代理来在运行时修改代码的,这也意味着代理插件开发者事先就要知道所要修改的代码是怎么样的。因此,在这种探针下通常会有一个已支持的列表清单。如 SkyWalking Java 代理支持列表。
- 跨线程可能并非总是奏效 如上所述,每个请求的代码大都运行在一个线程之内, 对于业务代码来说尤其如此。但是在其他一些场景下,它们也会在不同线程下工作,比如指派任务到其他线程,任务池,以及批处理。对于一些语言,可能还提供了协程或类似的概念如
Goroutine
使得开发者可以低开销地来执行异步操作,在这些场景下,自动打点可能会遇到一些问题。
所以说自动打点没有什么神秘的,总而言之就是,自动打点代理开发者写了一个激活程序,使得打点的代码自动运行,仅此而已
服务网格探针:
1、什么是服务网格?
服务网格通常用于描述组成此类应用程序的微服务网络以及它们之间的交互。随着服务网格的大小和复杂性的增长,它会变得更难理解和管理。它需要包括发现、负载平衡、故障恢复、度量和监视以及更复杂的操作需求A/B测试、金丝雀发布、限流、访问控制和端到端身份验证。
2、探针从哪里采集数据
Istio 是一个非常典型的服务网格的设计和实现。它定义了 控制平面 和 数据平面,被广泛使用。下面是 Istio 的架构 :
服务网格探针可以选择从 控制平面 和 数据平面 采集数据。在 Istio 中,指的是从 Mixer(Control Panel) 或者 Envoy sidecar(Data Panel) 中采集遥测数据。探针从客户端和服务器端收集每个请求的两个遥测实体,它们其实是相同的数据。
3、服务网格如何使后端工作
从探针中,您可以看到在这种探针中一定没有相关的跟踪,那么为什么 SkyWalking 平台仍然可以工作?
服务网格探针从每个请求收集遥测数据,因此它知道源、目标、端点、延迟和状态。通过这些,后端可以通过将这些调用合并为行来描述整个拓扑图,以及每个节点通过传入请求的度量。后端解析跟踪数据,请求相同的度量数据。因此,正确的表述是:
服务网格度量就是跟踪解析器生成的度量。他们是相同的。
SkyWalking分布式系统应用程序性能监控工具-上
概述
微服务系统监控三要素
现在系统基本都是微服务架构,对于复杂微服务链路调用如下问题如何解决?
- 一个请求经过了这些服务后其中出现了一个调用失败的问题,如何定位问题发生的地方?
- 如何计算每个节点访问流量?
- 流量波动的时候,增加哪些节点集群服务?
为了解决分布式应用、微服务系统面临的这些挑战,APM系统(Application Performance Management,即应用性能管理,简单来说就是应用监控)为之诞生,核心满足微服务系统监控的三要素如下:
- Logging : 就是记录系统行为的离散事件,例如,服务在处理某个请求时打印的错误日志,我们可以将这些日志信息记录到 ElasticSearch 或是其他存储中,然后通过 Kibana 或是其他工具来分析这些日志了解服务的行为和状态。大多数情况下,日志记录的数据很分散,并且相互独立,比如错误日志、请求处理过程中关键步骤的日志等等
- Metrics :是系统在一段时间内某一方面的某个度量,例如,电商系统在一分钟内的请求次数。我们常见的监控系统中记录的数据都属于这个范畴,例如 Promethus、Open-Falcon 等,这些监控系统最终给运维人员展示的是一张张二维的折线图。Metrics 是可以聚合的,例如,为电商系统中每个 HTTP 接口添加一个计数器,计算每个接口的 QPS,之后我们就可以通过简单的加和计算得到系统的总负载情况。
- Tracing :即我们常说的分布式链路追踪。在微服务架构系统中一个请求会经过很多服务处理,调用链路会非常长,要确定中间哪个服务出现异常是非常麻烦的一件事。通过分布式链路追踪,运维人员就可以构建一个请求的视图,这个视图上展示了一个请求从进入系统开始到返回响应的整个流程。这样,就可以从中了解到所有服务的异常情况、网络调用,以及系统的性能瓶颈等。
OpenTracing
早在在 2010 年 4 月谷歌发表了一篇论文《Dapper, a Large-Scale Distributed Systems TracingInfrastructure》阐述分布式追踪的概念,OpenTracing用于分布式跟踪和上下文传播的一致的、表达的、提供了一个标准的与供应商无关的api框架,这意味着如果开发者想要尝试一种不同的分布式追踪系统,开发者只需要简单地修改Tracer配置即可,而不需要替换整个分布式追踪系统;OpenTracing API目前也支持众多语言。了解OpenTracing API可以有利于更好学习本篇的主角SkyWalking。
开源APM系统
目前市面上开源的APM系统主要有CAT、Zipkin、Pinpoint,大都是参考Google的Dapper实现的
- CAT: 是由国内美团点评开源的,基于Java语言开发,目前提供Java、C/C++、Node.js、Python、Go等语言的客户端,监控数据会全量统计,国内很多公司在用,例如美团点评、携程、拼多多等,CAT跟下边要介绍的Zipkin都需要在应用程序中埋点,对代码侵入性强,我们倾向于选择对代码无侵入的产品,所以淘汰了CAT
- Zipkin: 由Twitter公司开发并开源,Java语言实现,侵入性相对于CAT要低一点,需要对web.xml之类的配置文件做修改,但依然对代码有侵入,也没有选择
- Pinpoint: 一个韩国团队开源的产品,运用了字节码增强技术,只需要在启动时添加启动参数即可,对代码无侵入,目前支持Java和PHP语言,底层采用HBase来存储数据,探针收集的数据粒度非常细,但性能损耗大,因其出现的时间较长,完成度也很高,应用的公司较多。
SkyWalking介绍
Apache SkyWalking 官网地址 https://skywalking.apache.org/ 最新版本9.1.0
Apache SkyWalking 文档地址 https://skywalking.apache.org/docs/
Apache SkyWalking v9.1.0文档地址 https://skywalking.apache.org/docs/main/v9.1.0/readme/
Apache SkyWalking GitHub源码地址 https://github.com/apache/skywalking
Apache SkyWalking用于分布式系统的应用程序性能监控工具,特别为微服务、云本地和基于容器(Kubernetes)架构设计。Service Mesh和FaaS已就绪,内置服务网格和FaaS可观察性,收集和分析Istio + Envoy Service Mesh和OpenFunction作为FaaS平台的数据。
SkyWalking基本可以满足对于分布式系统APM的所有需要的功能,功能非常强大、性能表现优秀、对业务代码无侵入, 增长势头强劲,社区活跃,中文文档齐全,支持多语言探针, SkyWalking 支持Dubbo、gRPC、SOFARPC 等很多框架,包含了云原生架构下的分布式系统的监控、跟踪、诊断、日志记录功能,可以在浏览器上观察分布式系统应用程序发生的一切。
-
核心功能
- 服务、服务实例、端点(URI)指标分析
- 根本原因分析。在运行时上分析由进程内代理和ebpf分析器支持的代码。
- 业务拓扑图分析
- 服务实例和端点(URI)依赖关系分析
- 服务和端点检测速度慢
- 性能优化
- 分布式跟踪和上下文传播
- 数据库访问指标。检测慢速数据库访问语句(包括SQL语句)
- 消息队列性能和消耗延迟监视
- 浏览器性能监控
- 基础设施(虚拟机、网络、磁盘等)监控
- 跨指标、跟踪和日志的协作
- 告警
-
特点
- 多语言支持,符合技术栈的Agent包括net Core、PHP、NodeJS、Golang、LUA、Rust和c++代理,积极开发和维护。用于C、c++、Golang和Rust的eBPF分析器作为附加。
- 为多种开源项目提供了插件,为 Tomcat、 HttpClient、Spring、RabbitMQ、MySQL 等常见基础设施和组件提供了自动探针。
- 微内核 + 插件的架构,存储、集群管理、使用插件集合都可以进行自由选择。
- 优秀的可视化效果。
基本概念
SkyWalking是一个开源的可观测平台的APM系统,用于收集、分析、聚合和可视化来自服务和云原生基础设施的数据。SkyWalking提供了一种简单的方式来维护分布式系统的清晰视图,甚至跨云。它是一个现代化的APM,专门为本地云、基于容器的分布式系统设计。
- 服务:表示一组/一组工作负载,它们为传入请求提供相同的行为,如一个微服务。在使用仪器代理或sdk时自定义服务名称。SkyWalking也可以使用你在Istio等平台中定义的名称。
- 服务实例:Service组中的每个工作负载都被称为一个实例,如运行某个微服务实例。
- 端点:服务中用于传入请求的路径,例如HTTP URI路径或gRPC服务类+方法签名。
- 进程:操作系统进程,在某些场景中,某个服务实例运行多个进程。
SkyWalking涵盖了所有3个可观察性领域,包括跟踪、指标和日志:
- 跟踪:SkyWalking原生数据格式,包括Zipkin v1和v2,以及Jaeger。
- 指标:SkyWalking集成了服务网格平台,如Istio、Envoy和Linkerd,将可观测性构建到数据平面或控制平面。此外,SkyWalking本机代理可以在度量模式下运行,这大大提高了性能。
- 日志记录:包括从磁盘或网络收集的日志。本机代理可以自动将跟踪上下文与日志绑定,或者使用skywalk通过文本内容绑定跟踪和日志。
架构
使用skywalk,用户可以了解服务和端点之间的拓扑关系,查看每个服务/服务实例/端点的指标,设置告警规则。SkyWalking逻辑上分为四个部分:探针、平台后端、存储和UI。
- Agent(探针):探针收集数据并根据SkyWalking的要求对数据进行重新格式化(不同的探测器支持不同的来源);Agent 运行在各个服务实例中,负责采集服务实例的 Trace 、Metrics 等数据,然后通过 gRPC 方式上报给 SkyWalking 后端。
- OAP:SkyWalking 的后端服务,支持数据聚合、分析和流处理,包括跟踪、指标和日志。
- 接收 Agent 上报上来的 Trace、Metrics 等数据,交给 Analysis Core (涉及SkyWalking OAP 中的多个模块)进行流式分析,最终将分析得到的结果写入持久化存储中。
- 响应 SkyWalking UI 界面发送来的查询请求,将前面持久化的数据查询出来,组成正确的响应结果返回给 UI 界面进行展示。
- 存储:SkyWalking数据可以选择存储在已实现的ElasticSearch, H2, MySQL, TiDB, InfluxDB的持久化系统,一般线上使用ElasticSearch 集群作为其后端存储。
- UI:可视化和管理SkyWalking 数据,前后端分离,该 UI 界面负责将用户的查询操作封装为 GraphQL 请求提交给 OAP 后端触发后续的查询操作,待拿到查询结果之后会在前端负责展示。
部署
下载
# 官网下载最新版本9.1.0
wget https://dlcdn.apache.org/skywalking/9.1.0/apache-skywalking-apm-9.1.0.tar.gz
# 接下文件
tar -xvf apache-skywalking-apm-9.1.0.tar.gz
# 进入目录
cd apache-skywalking-apm-bin/
部署ES
持久化存储我们选择ES,由于前面有两篇文章都讲过ES部署,一篇使用docker部署,一篇使用二进制文件部署,有兴趣可以前往去看。这里我们就直接使用
部署SkyWalking
SkyWalk部署很简单,部署方式有很多,官方提供二进制、Docker、K8S的部署指引,Docker和K8s部署详细可以查阅官网
# oap-server
docker run --name oap --restart always -d -e SW_STORAGE=elasticsearch -e SW_STORAGE_ES_CLUSTER_NODES=elasticsearch:9200 apache/skywalking-oap-server:9.1.0
# UI
docker run --name oap --restart always -d -e SW_OAP_ADDRESS=http://oap:12800 apache/skywalking-ui:9.1.0
学习则选择二进制部署方式,默认配置可以直接运行,但使用的存储是H2内存数据库,重启后数据丢失,修改oap-server配置文件中存储模式即可, vi config/application.yml
SkyWalk的webapp也即是UI默认的8080端口,这里就不修改
# 启动
sh ./bin/startup.sh
# 启动后skywalking-oap-server会先判断是否有相应的表,没有则创建,创建的表也比较多,需要稍等一会,下面日志已经使用elasticsearch做持久化存储
2022-07-23 10:46:17,966 - org.apache.skywalking.oap.server.starter.config.ApplicationConfigLoader - 118 [main] INFO [] - Provider=elasticsearch config=clusterNodes has been set as 192.168.5.52:9200
访问http://192.168.5.52:8080/ ,出现UI界面,当然9.x比8.x多出很多功能,包括服务网格、函数,可视化能力也是越来越强
Agent应用
下载
# 选择目前最新版本8.11.0
wget https://dlcdn.apache.org/skywalking/java-agent/8.11.0/apache-skywalking-java-agent-8.11.0.tgz
# 解压后skywalk-agent.jar在根目录下,agent.config在config目录下
说明
- Agent可用于JDK 8 - 17。
- 在config/agent.config service_name。可以是英文中的任何字符串。
- agent在config/agent.config backend_service。默认点为127.0.0.1:11800,仅适用于本地后端,如果是远程配置远程地址。
- 将-javaagent:/path/to/skywalk -package/agent/skywalk-agent.jar添加到JVM参数。并确保将它添加到-jar参数之前。
Agent包目录如下:
配置
使用前面的库存微服务和订单微服务模块,将agent.config 拷贝到两个项目的resources目录下
库存微服务修改agent.config下面两个配置
agent.service_name=${SW_AGENT_NAME:ecom-storage-service}
# Backend service addresses.
collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:192.168.5.52:11800}
启动jvm参数增加
-javaagent:F:\commoms\skywalking-agent\skywalking-agent.jar
-Dskywalking_config=F:\dev\simple-ecommerce\ecom-storage-service\src\main\resources\agent.config
订单微服务修改agent.config下面两个配置
agent.service_name=${SW_AGENT_NAME:ecom-order-service}
# Backend service addresses.
collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:192.168.5.52:11800}
启动jvm参数增加
-javaagent:F:\commoms\skywalking-agent\skywalking-agent.jar
-Dskywalking_config=F:\dev\simple-ecommerce\ecom-order-service\src\main\resources\agent.config
启动库存微服务和订单微服务,启动日志中加载agent.config和使用skywalk-agent.jar
访问订单接口,http://localhost:4070/order/create/1000/1001/2 ,访问库存接口http://localhost:4080/list ,在普通服务的Service 页面中查看到两个微服务
在Topology查看微服务之间拓扑关系,可以设置查询的深度
在跟Trace查看跟踪列表信息,选择查询条件,可以切换树结构、表格、统计
点进列表的记录后还可以查看到详细信息,包括使用那个组件都有,但识别不一定很准确
普通服务的Service 页面选择服务如订单服务,显示订单服务的各项概览信息,可切换到实例、断点、拓扑、追踪、性能分析、日志等功能页面
自定义SkyWalking链路追踪
如果想要对项目里的业务方法实现链路追踪,方便排查问题,做法如下,先引入依赖
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>8.11.0</version>
</dependency>
然后在业务方法加上@Trace+@Tags注解,如果只需要一个Tag也可以直接@Trace+@Tag
@Override
@GlobalTransactional
@Trace
@Tags({@Tag(key = "order",value = "returnedObj"),
@Tag(key = "param1", value = "arg[0]"),
@Tag(key = "param2", value = "arg[1]")})
public Order create(String userId, String commodityCode, int orderCount) {
String xid = RootContext.getXID();
log.info("order xid:{}",xid);
storageFeignService.deduct(commodityCode,orderCount);
// int i = 1/0;
Order order = new Order();
order.setUserId(userId);
order.setCommodityCode(commodityCode);
order.setCount(orderCount);
order.setMoney(orderCount*10);
orderMapper.insert(order);
return order;
}
重新启动库存和订单微服务,再次访问订单接口,http://localhost:4070/order/create/1000/1001/2
选择服务后再选择相应的功能页面
点击查询详细,这时候就可以查看业务方法的参数值和返回值信息
其他功能
性能剖析
在系统性能监控方法上,Skywalking 提出了代码级性能剖析这种在线诊断方法。这种方法基于一个高级语言编程模型共性,即使再复杂的系统,再复杂的业务逻辑,都是基于线程去进行执行的,而且多数逻辑是在单个线程状态下执行的;代码级性能剖析就是利用方法栈快照,并对方法执行情况进行分析和汇总;并结合有限的分布式追踪 span 上下文,对代码执行速度进行估算。有如下优势:
- 精确的问题定位,直接到代码方法和代码行
- 无需反复的增删埋点,大大减少了人力开发成本
- 不用承担过多埋点对目标系统和监控系统的压力和性能风险
- 按需使用,平时对系统无消耗,使用时的消耗稳定可能
SkyWalking的跟踪或者说性能剖析,选择某个服务
根据选择端点的名称及相应的规则建立任务,后续再调用任务列表的端口会自动记录剖析剖析当前端口数据并生成剖析结果
为了更好演示在库存微服务的创建订单方法中增加一个睡眠3秒,然后重新启动订单微服务
再次多次访问创建订单接口 http://localhost:4070/order/create/1000/1001/2 ,需要连续执行多次请求,因为存在采样设置。如果执行次数少,可能不会出现采样数据,每个服务,相同时间只能添加一个任务,添加的任务不能更改,也不能删除,只能等待过期后自动删除。
日志
在库存和订单微服务中引入依赖
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>8.11.0</version>
</dependency>
在库存和订单微服务中,增加分布式链路追踪ID在logback.xml加入如下配置,[%tid]
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%tid] [%thread] %-5level %logger{36} -%msg%n</Pattern>
</layout>
</encoder>
</appender>
gRPC reporter上报日志在logback.xml加入如下配置:
<appender name="grpc-log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{tid}] [%thread] %-5level %logger{36} -%msg%n</Pattern>
</layout>
</encoder>
</appender>
访问订单接口 http://localhost:4070/order/create/1000/1001/2,查看订单和库存微服务的日志中已带有 TID
也通过GRPC上传到SkyWalking后端,通过Log页面可以查看日志信息
可以通过TID查询对应日志详细信息
告警
在config/alarm-settings.yml ,已经默认若干项告警,我们简单修改告警信息内容,增加一串标识"Itxs Alarm"例如,配置webhooks
rules:
# Rule unique name, must be ended with `_rule`.
service_instance_resp_time_rule:
metrics-name: service_instance_resp_time
op: ">"
threshold: 1000
period: 10
count: 2
silence-period: 5
message: Itxs Alarm esponse time of service instance {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: Itxs Alarm esponse time of endpoint relation {name} is more than 1000ms in 2 minutes of last 10 minutes
webhooks:
- http://192.168.4.210:8080/alarm/
新建一个webhooks接口服务端,创建AlarmMessage实体类
package com.aotain.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AlarmMessage implements Serializable {
private String scopeId;
private String scope;
private String name;
private String id0;
private String id1;
private String ruleName;
//告警的消息
private String alarmMessage;
//告警的产生时间
private Long startTime;
}
创建一个控制器AlarmController,提供/alarm接口,这里简单就显示信息,后续可以根据实际调用微信、钉钉告警之类。
package com.aotain.controller;
import com.aotain.entity.AlarmMessage;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class AlarmController {
@PostMapping("/alarm")
public String alarm(@RequestBody List<AlarmMessage> alarmMessageList) throws Exception {
System.out.println(alarmMessageList);
return "ok";
}
}
这里使用ApiFox多线程访问http://localhost:4070/order/create/1000/1001/2
查看告警,已经为我们修改后告警信息,含有Itxs Alarm的前缀字符串
查看事件
查看webhooks调用接口,已经收到SkyWalking调用过来的数据,这里后续可扩展为实际的告警方式处理。
SkyWalking原理
SkyWalking Agent原理
无侵入实现原理
上面使用Skywalking并没有修改程序中任何一行 Java 代码,这里便是使用到了 Java Agent 技术,如果平常基于增删改查业务逻辑那就基本不会使用到Java Agent,但我们平时用过的不少工具如热部署工具JRebel,SpringBoot的热部署插件,各种线上诊断工具(btrace, greys),阿里开源的arthas都是基于java Agent来实现的。在JDK1.5以后就有java Agent,使用agent技术构建一个独立于应用程序的代理程序(即Agent),用来协助监测、运行甚至替换其他JVM上的程序。使用它可以实现虚拟机级别的AOP功能,典型的优势就是无代码侵入。Agent大体可分为两种:
- 在主程序之前运行的Agent。
- 在主程序之后运行的Agent(前者的升级版,1.6以后提供)。
主程序之前运行的Agent
premain为主程序之前运行的Agent,在实际使用过程中,javaagent是java命令的一个参数。通过java 命令启动我们的应用程序的时候,可通过参数 -javaagent 指定一个 jar 包(也就是我们的代理agent),能够实现在我们应用程序的主程序运行之前来执行我们指定jar包中的特定方法,在该方法中我们能够实现动态增强Class等相关功能,并且该 jar包有2个要求:
- 这个 jar 包的 META-INF/MANIFEST.MF 文件必须指定 Premain-Class 项,该选项指定的是一个类的全路径。
- Premain-Class 指定的那个类必须实现 premain() 方法。
从字面上理解,Premain-Class 就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行-javaagent
所指定 jar 包内 Premain-Class 这个类的 premain 方法 。
-javaagent
所在包java.lang.instrument
,是rt.jar
中定义的一个包,有两个重要的类:
java.lang.instrument包提供了一些工具帮助开发人员在 Java 程序运行时动态修改系统中的 Class 类型。其中使用该软件包的一个关键组件就是 Javaagent,从本质上来讲,Java Agent 是一个遵循一组严格约定的常规 Java 类,就如上面说到 javaagent命令要求指定的类中必须要有premain()方法,并且对premain方法的签名也有要求,签名必须满足以下两种格式:
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
JVM 会优先加载 带 Instrumentation
签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。
创建PreAgentDemo的maven项目,编写一个agent程序com.itxs.agent.PreAgentDemo,完成premain
方法的签名,这里先做一个简单的日志输出。
package com.itxs.agent;
import java.lang.instrument.Instrumentation;
public class PreAgentDemo {
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("PreAgentDemo run");
System.out.println("PreAgentDemo receive params agentArgs=" + agentArgs);
}
}
maven项目pom文件增加如下坐标
<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.itxs.agent.PreAgentDemo</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>
对PreAgentDemo
项目进行打包,得到 PreAgentDemo-1.0.jar
,放在G:\other下,查看jar包中的MANIFEST.MF文件
Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)
Premain-Class :包含 premain 方法的类(类的全路径名)
接着创建一个test-demo项目,编写一个简单测试类App,运行JVM参数添加
-javaagent:G:\other\PreAgentDemo-1.0.jar=param1=value1,param2=value2,param3=value3
上运行结果可以看到在测试程序main函数启动前先输出premain方法打印的日志。实际开发中大部分类加载都会通过该方法。当然,遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如ASM,bytebuddy,javassist,cglib等等来改写实现类。
Instrumentation 中的核心 API 方法:
- addTransformer()/removeTransformer() 方法:注册/注销一个 ClassFileTransformer 类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义(修改类的字节码)。
- redefineClasses() 方法:该方法针对的是已经加载的类,它会对传入的类进行重新定义。
- getAllLoadedClasses()方法:返回当前 JVM 已加载的所有类。
- getInitiatedClasses() 方法:返回当前 JVM 已经初始化的类。
- getObjectSize()方法:获取参数指定的对象的大小。
主程序之后运行的Agent
agentmain,可以在 main 函数开始运行之后再运行。跟premain
函数一样, 开发者可以编写一个含有agentmain
函数的 Java 类。
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
同样需要在MANIFEST.MF文件里面设置“Agent-Class”来指定包含 agentmain 函数的类的全路径。在前面工程基础上增加com.itxs.agent.AgentDemo文件,也是简单打印日志。
package com.itxs.agent;
import java.lang.instrument.Instrumentation;
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("AgentDemo run");
}
}
在pom.xml中添加配置如下
<Agent-Class>com.itxs.agent.AgentDemo</Agent-Class>
重新打包 PreAgentDemo-1.0.jar
并覆盖到G:\other下,在测试类App修改如下代码
package com.itxs;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class App
{
public static void main( String[] args ) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
System.out.println( "itxs app main run!" );
//获取当前系统中所有 运行中的 虚拟机
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vm : list) {
if (vm.displayName().endsWith("com.itxs.App")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vm.id());
virtualMachine.loadAgent("G:/other/PreAgentDemo-1.0.jar");
virtualMachine.detach();
}
}
}
}
list()方法会去寻找当前系统中所有运行着的JVM进程,你可以打印vmd.displayName()看到当前系统都有哪些JVM进程在运行。因为main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。在windows中安装的jdk无法找到,如遇到这种情况手动将你jdk安装目录下:lib目录中的tools.jar添加进当前工程的Libraries中。
agent要在主程序运行后加载,我们不可能在主程序中编写加载的代码,只能另写程序,那么另写程序如何与主程序进行通信?这里用到的机制就是attach机制,它可以将JVM A连接至JVM B,并发送指令给JVM B执行。
字节码操作(增强)
Byte Buddy概述
Byte Buddy是一个代码生成和操作库,用于在Java应用程序运行时创建和修改Java类,并且不需要编译器的帮助。与Java类库附带的代码生成实用程序不同,Byte Buddy允许创建任意类,并且不局限于为创建运行时代理实现接口。此外,Byte Buddy提供了一个方便的API,可以手动更改类,可以使用Java代理,也可以在构建期间更改类。
- 无需理解字节码指令,即可使用较为简单的API就能很容易操作字节码,控制类和方法。
- 已支持Java 11,轻量,仅取决于Java字节代码解析器库ASM的访问者API,它本身不需要任何其他依赖项。
- 比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有一定的优势。
反射机制可以知道调用的方法或字段,但反射性能很差,反射能绕开类型安全检查,不安全,比如权限暴力破解;java编程语言代码生成库也有多种:
- Java Proxy:是 JDK 自带的一个代理工具,它允许为实现了一系列接口的类生成代理类。Java Proxy 要求目标类必须实现接口是一个非常大限制,例如在某些场景中目标类没有实现任何接口且无法修改目标类的代码实现,Java Proxy 就无法对其进行扩展和增强了。
- CGLIB 诞生于 Java 初期,但不幸的是没有跟上 Java 平台的发展。虽然 CGLIB 本身是一个相当强大的库,但也变得越来越复杂,导致许多用户放弃了CGLIB 。
- Javassist:其使用对 Java 开发者来说是非常友好的,它使用Java 源代码字符串和 Javassist 提供的一些简单 API ,共同拼凑出用户想要的 Java 类,Javassist 自带一个编译器,拼凑好的 Java 类在程序运行时会被编译成为字节码并加载到 JVM 中。Javassist 库简单易用,而且使用 Java 语法构建类与平时写 Java 代码类似,但是 Javassist 编译器在性能上比不了 Javac 编译器,而且在动态组合字符串以实现比较复杂的逻辑时容易出错。
- Byte Buddy:提供了一种非常灵活且强大的领域特定语言,通过编写简单的 Java 代码即可创建自定义的运行时类。与此同时,Byte Buddy 还具有非常开放的定制性,能够应付不同复杂度的需求。
上面所有代码生成技术中推荐使用Byte Buddy,因为Byte Buddy代码生成可的性能最高;Byte Buddy 的主要侧重点在于生成更快速的代码,如下图
ByteBuddy API
Class<?> dynamicType = new ByteBuddy()
// 生成 Object的子类
.subclass(Object.class)
// 生成类的名称
.name("com.itxs.type")
// 拦截其中的toString()方法
.method(ElementMatchers.named("toString"))
// 让toString()方法返回固定值
.intercept(FixedValue.value("Hello World!"))
.make()
// 加载新类型,默认WRAPPER策略,也即是ClassLoadingStrategy.Default.WRAPPER可以不写
.load(getClass().getClassLoader(),ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
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 中。
method() 方法可以通过传入的 ElementMatchers 参数匹配多个需要修改的方法,这里的ElementMatchers.named("toString") 即为按照方法名匹配 toString() 方法。如果同时存在多个重载方法,则可以使用 ElementMatchers 其他 API 描述方法的签名,如下所示
// 指定方法名称
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 普通类代理示例
在test-demo项目中添加ByteBuddy的依赖
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.12</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.12.12</version>
<scope>test</scope>
</dependency>
创建普通类OrderService
package com.itxs.service;
public class OrderService {
public String addOrder(){
System.out.println("=====do addOrder==========");
return "1000000001";
}
public String getOrder(String orderId){
System.out.println("=====do getOrder==========");
return orderId;
}
public String getOrder(String orderId,String status){
System.out.println("=====do getOrder two params==========");
return orderId+status;
}
}
创建拦截器类TestInterceptor
package com.itxs.interceptor;
import net.bytebuddy.implementation.bind.annotation.*;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class TestInterceptor {
@RuntimeType //将返回值转换成具体的方法返回值类型,加了这个注解 intercept 方法才会被执行
public Object intercept(
// 被拦截的目标对象 (动态生成的目标对象)
@This Object target,
// 正在执行的方法Method 对象(目标对象父类的Method)
@Origin Method method,
// 正在执行的方法的全部参数
@AllArguments Object[] argumengts,
// 目标对象的一个代理
@Super Object delegate,
// 方法的调用者对象 对原始方法的调用依靠它
@SuperCall Callable<?> callable) throws Exception {
//目标方法执行前执行日志记录
System.out.println("prepare do method="+method.getName());
// 调用目标方法
Object result = callable.call();
//目标方法执行后执行日志记录
System.out.println("have down method="+method.getName());
return result;
}
}
创建普通类代理测试类ByteBuddyTest
package com.itxs;
import com.itxs.interceptor.TestInterceptor;
import com.itxs.service.OrderService;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
public class ByteBuddyTest {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Class<? extends OrderService> generateClass = new ByteBuddy()
// 创建一个UserService 的子类
.subclass(OrderService.class)
//指定类的名称
.name("com.itxs.service.OrderServiceImpl")
// 指定要拦截的方法
.method(ElementMatchers.named("getOrder").and(ElementMatchers.returns(String.class).and(ElementMatchers.takesArguments(2))))
// 为方法添加拦截器 如果拦截器方法是静态的 这里可以传 LogInterceptor.class
.intercept(MethodDelegation.to(new TestInterceptor()))
// 动态创建对象,但还未加载
.make()
// 设置类加载器 并指定加载策略(默认WRAPPER)
.load(ByteBuddy.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
// 开始加载得到 Class
.getLoaded();
OrderService orderService = generateClass.newInstance();
System.out.println(orderService.addOrder());
System.out.println(orderService.getOrder("2000000000"));
System.out.println(orderService.getOrder("3000000000","支付中"));
}
}
在程序中用到ByteBuddy的MethodDelegation对象,它可以将拦截的目标方法委托给其他对象处理,注解使用说明如下:
- @RuntimeType:不进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。
- @This:注入被拦截的目标对象(动态生成的目标对象)。
- @Origin:注入正在执行的方法Method 对象(目标对象父类的Method)。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。
- @AllArguments:注入正在执行的方法的全部参数。
- @Super:注入目标对象的一个代理。
- @SuperCall:这个注解比较特殊,我们要在 intercept() 方法中调用 被代理/增强 的方法的话,需要通过这种方式注入,与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。另外,@SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。
运行ByteBuddyTest,增强的方法输出就是上面代码中方法匹配名称为getOrder且返回值为String且有两个入参的结果。
自定义Agent案例
Java Agent十分强大,使用Transformer等高级功能进行类替换,方法修改等,要使用Instrumentation的相关API则需要对字节码等技术有较深的认识。接下来ByteBuddy结合Java Agent技术实现一个统计方法耗时的示例。
在上面的PreAgentDemo项目中加入依赖byte-buddy和byte-buddy-agent的依赖,上面测试工程Pom文件有
创建耗时统计拦截器类
package com.itxs.agent.interceptor;
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 TimeConsumingInterceptor {
/***
* 拦截方法
* @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() + ":time consuming total" + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
创建JavaAgentCase的premain实现
package com.itxs.agent;
import com.itxs.agent.interceptor.TimeConsumingInterceptor;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.instrument.Instrumentation;
public class JavaAgentCase {
/***
* 执行方法拦截
* @param agentArgs:-javaagent 命令携带的参数。在前面介绍 SkyWalking Agent 接入时提到
* agent.service_name 这个配置项的默认值有三种覆盖方式,
* 其中,使用探针配置进行覆盖,探针配置的值就是通过该参数传入的。
* @param instrumentation:java.lang.instrumen.Instrumentation 是 Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法。
*/
public static void premain(String agentArgs, Instrumentation instrumentation) {
// 动态构建操作,根据transformer规则执行拦截操作,匹配上的具体的类型描述
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
// 构建拦截规则
return builder
// method()指定哪些方法需要被拦截,ElementMatchers.any()表示拦截所有方法
.method(ElementMatchers.any())
// intercept()指定拦截上述方法的拦截器
.intercept(MethodDelegation.to(TimeConsumingInterceptor.class));
};
// 采用Byte Buddy的AgentBuilder结合Java Agent处理程序
new AgentBuilder
// 采用ByteBuddy作为默认的Agent实例
.Default()
// 拦截匹配方式:类以com.itxs.service开始,也即是com.itxs.service包下的所有类
.type(ElementMatchers.nameStartsWith("com.itxs.service"))
// 拦截到的类由transformer处理
.transform(transformer)
// 安装到 Instrumentation
.installOn(instrumentation);
}
}
重新打包好PreAgentDemo-1.0.jar,准备测试类UserService.java
package com.itxs.service;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class UserService {
private static Random random = new Random();
public void getUser(){
System.out.println("=====do getUser==========");
try {
TimeUnit.SECONDS.sleep(random.nextInt(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void updateUser(){
System.out.println("=====do updateUser==========");
try {
TimeUnit.SECONDS.sleep(random.nextInt(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
创建启动测试类
package com.itxs;
import com.itxs.service.UserService;
public class Application
{
public static void main( String[] args ) {
System.out.println("Application main start run-----------");
UserService service = new UserService();
service.getUser();
service.updateUser();
}
}
启动参数中jvm参数添加javaagent,可参考上面示例,执行Application的main后从日志可以看到UserService的方法被增强了