APM分享:如何实现无侵入APM

1. APM概述

APM 通常认为是 Application Performance Management 的简写,它主要有三个方面的内容,分别是 Logs(日志)、Traces(链路追踪) 和 Metrics(报表统计)。

1.1. Logs

Logs 系统的重要性不言而喻,通常我们在排查特定的请求的时候,是非常依赖于上下文的日志的。

以前我们都是通过 terminal 登录到机器里面去查 log(我好几年都是这样过来的),但是由于集群化和微服务化的原因,继续使用这种方式工作效率会比较低,因为你可能需要登录好几台机器搜索日志才能找到需要的信息,所以需要有一个地方中心化存储日志,并且提供日志查询。

Logs 的典型实现是 ELK (ElasticSearch、Logstash、Kibana),三个项目都是由 Elastic 开源,其中最核心的就是 ES 的储存和查询的性能得到了大家的认可,经受了非常多公司的业务考验。

Logstash 负责收集日志,然后解析并存储到 ES。通常有两种比较主流的日志采集方式,一种是通过一个客户端程序 FileBeat,收集每个应用打印到本地磁盘的日志,发送给 Logstash;另一种则是每个应用不需要将日志存储到磁盘,而是直接发送到 Kafka 集群中,由 Logstash 来消费。

Kibana 是一个非常好用的工具,用于对 ES 的数据进行可视化,简单来说,它就是 ES 的客户端。

我们回过头来分析 Logs 系统,Logs 系统的数据来自于应用中打印的日志,它的特点是数据量可能很大,取决于应用开发者怎么打日志,Logs 系统需要存储全量数据,通常都要支持至少 1 周的储存。

每条日志包含 ip、thread、class、timestamp、traceId、message 等信息,它涉及到的技术点非常容易理解,就是日志的存储和查询。

使用也非常简单,排查问题时,通常先通过关键字搜到一条日志,然后通过它的 traceId 来搜索整个链路的日志。

题外话,Elastic 其实除了 Logs 以外,也提供了 Metrics 和 Traces 的解决方案,不过目前国内用户主要是使用它的 Logs 功能。

1.2. Trace

前面介绍的 Logs 系统使用的是开发者打印的日志,所以它是最贴近业务的。而 Traces 系统就离业务更远一些了,它关注的是一个请求进来以后,经过了哪些应用、哪些方法,分别在各个节点耗费了多少时间,在哪个地方抛出的异常等,用来快速定位问题。

经过多年的发展,Traces 系统虽然在服务端的设计很多样,但是客户端的设计慢慢地趋于统一,所以有了 OpenTracing 项目,我们可以简单理解为它是一个规范,它定义了一套 API,把客户端的模型固化下来。当前比较主流的 Traces 系统中,Jaeger、SkyWalking 是使用这个规范的,而 Zipkin、Pinpoint 没有使用该规范。限于篇幅,本文不对 OpenTracing 展开介绍。

从上面这个图中,可以非常方便地看出,这个请求经过了 3 个应用,通过线的长短可以非常容易看出各个节点的耗时情况。通常点击某个节点,我们可以有更多的信息展示,比如点击 HttpClient 节点我们可能有 request 和 response 的数据。

另一个比较好的开源 Traces 系统是由韩国人开源的 Pinpoint,它的打点数据非常丰富,这里有官方提供的 Live Demo,大家可以去玩一玩。

最近比较火的是由 CNCF(Cloud Native Computing Foundation) 基金会管理的 Jeager:

当然也有很多人使用的是 Zipkin,算是 Traces 系统中开源项目的老前辈了:

上面介绍的是目前比较主流的 Traces 系统,在排查具体问题的时候它们非常有用,通过链路分析,很容易就可以看出来这个请求经过了哪些节点、在每个节点的耗时、是否在某个节点执行异常等。

1.3. Metrics

在 Metrics 方面做得比较好的开源系统,是大众点评开源的 Cat,下面这个图是 Cat 中的 transaction 视图,它展示了很多的我们经常需要关心的统计数据:

下图是 Cat 的 problem 视图,对我们开发者来说就太有用了,应用开发者的目标就是让这个视图中的数据越少越好。

1.4. Trace 和 Metrics的区别

Metrics 做的是数据统计,比如某个 URL 或 DB 访问被请求多少次,P90 是多少毫秒,错误数是多少等这种问题。而 Traces 是用来分析某次请求,它经过了哪些链路,比如进入 A 应用后,调用了哪些方法,之后可能又请求了 B 应用,在 B 应用里面又调用了哪些方法,或者整个链路在哪个地方出错等这些问题。

Metrics 和 Traces 之间的联系是非常紧密的,它们的数据结构都是一颗调用树,区别在于这颗树的枝干和叶子多不多。在 Traces 系统中,一个请求所经过的链路数据是非常全的,这样对排查问题的时候非常有用,但是如果要对 Traces 中的所有节点的数据做报表统计,将会非常地耗费资源,性价比太低。而 Metrics 系统就是面向数据统计而生的,所以树上的每个节点我们都会进行统计,所以这棵树不能太“茂盛”。

我们关心的其实是,哪些数据值得统计?首先是入口,其次是耗时比较大的地方,比如 db 访问、http 请求、redis 请求、跨服务调用等。当我们有了这些关键节点的统计数据以后,对于系统的健康监控就非常容易了。

  • http 入口
  • MySQL调用
  • Redis
  • 跨应用调用
  • http 调用
  • Log 打点

2. 如何实现无侵入APM

2.1. 数据结构

  • Metrics

对于一条 Message 来说,用于统计的字段是 type, name, status,所以我们能基于 type、type+name、type+name+status 三种维度的数据进行统计。

Message 中其他的字段:timestamp 表示事件发生的时间;success 如果是 false,那么该事件会在 problem 报表中进行统计;data 不具有统计意义,它只在链路追踪排查问题的时候有用;businessData 用来给业务系统上报业务数据,需要手动打点,之后用来做业务数据分析。

Message 有两个子类 Event 和 Transaction,区别在于 Transaction 带有 duration 属性,用来标识该 transaction 耗时多久,可以用来做 max time, min time, avg time, p90, p95 等,而 event 指的是发生了某件事,只能用来统计发生了多少次,并没有时间长短的概念。

Transaction 有个属性 children,可以嵌套 Transaction 或者 Event,最后形成一颗树状结构,用来做 trace,我们稍后再介绍。

  • Trace

Tree 的属性很好理解,它持有 root transaction 的引用,用来遍历整颗树。另外就是需要携带机器信息 messageEnv。

treeId 应该有个算法能保证全局唯一。

关于treeId的一些说明如下:
下面简单介绍几个 tree id 相关的内容,假设一个请求从 A->B->C->D 经过 4 个应用,A 是入口应用,那么会有:

1、总共会有 4 个 Tree 对象实例从 4 个应用投递到 Kafka,跨应用调用的时候需要传递 treeId, parentTreeId, rootTreeId 三个参数;
2、A 应用的 treeId 是所有节点的 rootTreeId;
3、B 应用的 parentTreeId 是 A 的 treeId,同理 C 的 parentTreeId 是 B 应用的 treeId;
4、在跨应用调用的时候,比如从 A 调用 B 的时候,为了知道 A 的下一个节点是什么,所以在 A 中提前为 B 生成 treeId,B 收到请求后,如果发现 A 已经为它生成了 treeId,直接使用该 treeId。

大家应该也很容易知道,通过这几个 tree id,我们是想要实现 trace 的功能。

  • Heartbeat

Message 有两个子类 Event 和 Transaction,这里我们再加一个子类 Heartbeat,用来上报心跳数据。

我们主要收集了 thread、os、gc、heap、client 运行情况(产生多少个 tree,数据大小,发送失败数)等,同时也提供了 api 让开发者自定义数据进行上报。Dog client 会开启一个后台线程,每分钟运行一次 Heartbeat 收集程序,上报数据。

再介绍细一些。核心结构是一个 Map<String, Double>,key 类似于 “os.systemLoadAverage”,“thread.count” 等,前缀 os,thread,gc 等其实是用来在页面上的分类,后缀是显示的折线图的名称。

2.2. 架构

2.3. 系统集成

  • 方案1:
    • 通过 javaagent 的方式,在启动脚本中,加上相应的 agent,
    • 开发者如果想要手动做一些埋点,可能需要再提供一个简单的 client jar 包给开发者,用来桥接到 agent 里。
  • 方案2:
    • 就是提供一个 jar 包,由开发者来引入这个依赖。
  • 方案3:
    • 搞一个应用容器,通过classloader替换掉对应的class实现。

三种方案各有优缺点,
Pinpoint 和 Skywalking 使用的是 第一种方案,
Zipkin、Jaeger、Cat 使用的是第二种方案
阿里的Pandora是第三种方案。

2.3.1. Instruction

JVMTI是一套Native接口,在Java SE 5之前,要实现一个Agent只能通过编写Native代码来实现。从Java SE 5开始,可以使用Java的Instrumentation接口(java.lang.instrument)来编写Agent。无论是通过Native的方式还是通过Java Instrumentation接口的方式来编写Agent,它们的工作都是借助JVMTI来进行完成,下面介绍通过Java Instrumentation接口编写Agent的方法。

public static void premain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs, Instrumentation inst);

2.3.2. 字节码增强

2.3.3. 探针切点

  • http:
    • @Controller的类和有@RequestMapping的方法
    • 通过实现一个 Filter 来拦截所有的请求
  • mysql:
    • prepareStatement.execute() prepareStatement.executeUpdate() 和 prepareStatement.executeQuery() 做增强
    • Mybatis Interceptor 的方式
  • spring service: @Service的类做增强
  • redis: 增强 RedisTemplate 的方式
  • http 调用: 为 HttpClient 和 OkHttp 增加 interceptor 的方式
  • Log 打点: 通过 plugin 的方式,将 log 中打印的 error 上报上来
  • 跨应用调用: 通过代理 feign client 的方式,dubbo、grpc 等方式可能需要通过拦截器

2.4. 无侵入

我们的agent的可能会用到一些第三方的类库,这就会产生一个问题。

这些第三方的类库可能和业务应用中的类会产生冲突,导致各种奇怪的报错。

解决方案方案有几个:

  1. 把类库代码copy一份,并且采用独立的类类路径。
  2. 采用单独的classloader进行类隔离。

2.5. Demo效果

3. 总结

本文概述了 apm的分类、核心数据结构、以及若干实现的关键知识点。

通过这些知识点的介绍,
一方面,在我们使用apm系统的时候,能更加的得心应手。做到知其然,知其所以然。
另一方面,基于这些涉及到的知识点,也可以方便实现一些其他有用的工具和中间件,提高业务效能。

4. 参考

SkyWalking 极简入门
APM 介绍与实现
APM系统的设计和实现
Dapper,大规模分布式系统的跟踪系统
Java 动态调试技术原理及实践
jvm-sandbox
Java字节码技术(二)字节码增强之ASM、JavaAssist、Agent、Instrumentation
阿里巴巴微服务架构演进

/** * RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS. * LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables*/ /* var disqus_config = function () { this.page.url = PAGE_URL; // Replace PAGE_URL with your page's canonical URL variable this.page.identifier = PAGE_IDENTIFIER; // Replace PAGE_IDENTIFIER with your page's unique identifier variable }; */ (function() { // DON'T EDIT BELOW THIS LINE var d = document, s = d.createElement('script'); s.src = 'https://chenzz.disqus.com/embed.js'; s.setAttribute('data-timestamp', +new Date()); (d.head || d.body).appendChild(s); })();