---
name: apm
title: APM 介绍与实现
date: 2020-12
tags: 
categories: 
---

2020 年我离开了众安，在新的公司里面，其实有了更多自己的时间，不过大家也可以看到，我已经半年多没有写新的博客了。大家在文章下面的评论以及在个人中心给我的留言，我都有看。感谢大家的肯定、关心以及催更...

最近几个月，我们在公司开发了 APM 系统，借这个契机，给大家分享一下。当然，我不是来介绍项目的，而是借我们的项目给大家介绍 APM 系统相关内容。

---

本文来说说什么是 APM 系统，也就是大家平时说的监控系统，以及怎么实现一个 APM 系统。因为一些特殊的原因，我在文中会使用 **Dog** 作为我们的系统名称进行介绍。

我们为 **Dog** 规划的目标是接入公司的大部分应用，预计每秒处理 500MB-1000MB 的数据，单机每秒 100MB 左右，使用多台普通的 AWS EC2。

因为本文的很多读者供职的公司不一定有比较全面的 APM 系统，所以我尽量照顾更多读者的阅读感受，会在有些内容上啰嗦一些，希望大家可以理解。我会在文中提到 prometheus、grafana、cat、pinpoint、skywalking、zipkin 等一系列工具，如果你没有用过也不要紧，我会充分考虑到这一点。

本文预设的一些背景：Java 语言、web 服务、每个应用有多个实例、以微服务方式部署。另外，从文章的可阅读性上考虑，我假设每个应用的不同实例分布在不同的 IP 上，可能你的应用场景不一定是这样的。

## APM 简介

APM 通常认为是 Application Performance Management 的简写，它主要有三个方面的内容，分别是 **Logs(日志)**、**Traces(链路追踪)** 和 **Metrics(报表统计)**。以后大家接触任何一个 APM 系统的时候，都可以从这三个方面去分析它到底是什么样的一个系统。

> 有些场景中，APM 特指上面三个中的 Metrics，我们这里不去讨论这个概念

这节我们先对这 3 个方面进行介绍，同时介绍一下这 3 个领域里面一些常用的工具。

1、首先 Logs 最好理解，就是对各个应用中打印的 log 进行收集和提供查询能力。

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

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

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

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

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

![kibana-discover](https://assets.javadoop.com/imgs/20510079/apm/kibana-discover.png)

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

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

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

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

2、我们再来看看 Traces 系统，它用于记录整个调用链路。

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

经过多年的发展，Traces 系统虽然在服务端的设计很多样，但是客户端的设计慢慢地趋于统一，所以有了 [OpenTracing](https://opentracing.io/) 项目，我们可以简单理解为它是一个规范，它定义了一套 API，把客户端的模型固化下来。当前比较主流的 Traces 系统中，[Jaeger](https://github.com/jaegertracing/jaeger)、[SkyWalking](https://skywalking.apache.org) 是使用这个规范的，而 [Zipkin](https://zipkin.io)、[Pinpoint](https://pinpoint-apm.github.io/pinpoint/index.html) 没有使用该规范。限于篇幅，本文不对 OpenTracing 展开介绍。

下面这张图是我画的一个请求的时序图：

![trace](https://assets.javadoop.com/imgs/20510079/apm/trace.png)

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

下面这张图是 Skywalking 的图，它的 UI 也是蛮好的：

![skywalking-trace](https://assets.javadoop.com/imgs/20510079/apm/skywalking-trace.png)

SkyWalking 在国内应该比较多公司使用，是一个比较优秀的由国人发起的开源项目，已进入 Apache 基金会。

另一个比较好的开源 Traces 系统是由韩国人开源的 Pinpoint，它的打点数据非常丰富，这里有官方提供的 [Live Demo](http://125.209.240.10:10123/)，大家可以去玩一玩。

![pinpoint](https://assets.javadoop.com/imgs/20510079/apm/pinpoint.png)

最近比较火的是由 [CNCF](https://www.cncf.io/)(Cloud Native Computing Foundation) 基金会管理的 Jeager：

![jaeger](https://assets.javadoop.com/imgs/20510079/apm/jaeger.png) 

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

![zipkin](https://assets.javadoop.com/imgs/20510079/apm/zipkin.png)

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

虽然这里介绍的几个 Traces 系统的 UI 不一样，大家可能有所偏好，但是具体说起来，表达的都是一个东西，那就是一颗调用树，所以我们要来说说每个项目除了 UI 以外不一样的地方。

首先肯定是数据的丰富度，你往上拉看 Pinpoint 的树，你会发现它的埋点非常丰富，真的实现了一个请求经过哪些方法一目了然。

> 但是这真的是一个好事吗？值得大家去思考一下。两个方面，一个是对客户端的性能影响，另一个是服务端的压力。

其次，Traces 系统因为有系统间调用的数据，所以很多 Traces 系统会使用这个数据做系统间的调用统计，比如下面这个图其实也蛮有用的：

![trace-statistics](https://assets.javadoop.com/imgs/20510079/apm/trace-statistics.png)

另外，前面说的是某个请求的完整链路分析，那么就引出另一个问题，我们怎么获取这个“某个请求”，这也是每个 Traces 系统的不同之处。

比如上图，它是 Pinpoint 的图，我们看到前面两个节点的圆圈是不完美的，点击前面这个圆圈，就可以看出来原因了：

![pinpoint-dashboard](https://assets.javadoop.com/imgs/20510079/apm/pinpoint-dashboard.png)

图中右边的两个红圈是我加的。我们可以看到在 Shopping-api 调用 Shopping-order 的请求中，有 1 个失败的请求，我们用鼠标在散点图中把这个红点框出来，就可以进入到 trace 视图，查看具体的调用链路了。限于篇幅，我这里就不去演示其他 Traces 系统的入口了。

还是看上面这个图，我们看右下角的两个统计图，我们可以看出来在最近 5 分钟内 Shopping-api 调用 Shopping-order 的所有请求的耗时情况，以及时间分布。在发生异常的情况，比如流量突发，这些图的作用就出来了。

对于 Traces 系统来说，最有用的就是这些东西了，当然大家在使用过程中，可能也发现了 Traces 系统有很多的统计功能或者机器健康情况的监控，这些是每个 Traces 系统的差异化功能，我们就不去具体分析了。

3、最后，我们再来讨论 Metrics，它侧重于各种报表数据的收集和展示。

在 Metrics 方面做得比较好的开源系统，是大众点评开源的 [Cat](https://github.com/dianping/cat)，下面这个图是 Cat 中的 transaction 视图，它展示了很多的我们经常需要关心的统计数据：

![cat-transaction](https://assets.javadoop.com/imgs/20510079/apm/cat-transaction.png)

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

![cat-problem](https://assets.javadoop.com/imgs/20510079/apm/cat-problem.png)

本文之后的内容主要都是围绕着 Metrics 展开的，所以这里就不再展开更多的内容了。

另外，说到 APM 或系统监控，就不得不提 Prometheus+Grafana 这对组合，它们对机器健康情况、URL 访问统计、QPS、P90、P99 等等这些需求，支持得非常好，它们用来做监控大屏是非常合适的，非常酷炫。但是通常不能帮助我们排查问题，它看到的是系统压力高了、系统不行了，但不能一下子看出来为啥高了、为啥不行了。

## Metrics 和 Traces

因为本文之后要介绍的我们开发的 Dog 系统从分类来说，侧重于 Metrics，同时我们也提供 tracing 功能，所以这里单独写一小节，分析一下 Metrics 和 Traces 系统之间的联系和区别。

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

不过在前面介绍 Traces 的时候，我们也发现这类系统也会做很多的统计工作，它也覆盖了很多的 Metrics 的内容。

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

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

我这里不再具体去介绍他们的区别，大家看完本文介绍的 Metrics 系统实现以后，再回来思考这个问题会比较好。

Dog 在设计上，主要是做一个 Metrics 系统，统计关键节点的数据，另外也提供 trace 的能力，不过因为我们的树不是很”茂盛“，所以链路上可能是断断续续的，中间会有很多缺失的地带，当然应用开发者也可以加入手动埋点来弥补。

Dog 因为是公司内部的监控系统，所以对于公司内部大家会使用到的中间件相对是比较确定的，不需要像开源的 APM 一样需要打很多点，我们主要实现了以下节点的自动打点：

- http 入口：通过实现一个 Filter 来拦截所有的请求
- MySQL: 通过 Mybatis Interceptor 的方式
- Redis: 通过 javassist 增强 RedisTemplate 的方式
- 跨应用调用: 通过代理 feign client 的方式，dubbo、grpc 等方式可能需要通过拦截器
- http 调用: 通过 javassist 为 HttpClient 和 OkHttp 增加 interceptor 的方式
- Log 打点: 通过 plugin 的方式，将 log 中打印的 error 上报上来

打点的技术细节，就不在这里展开了，主要还是用了各个框架提供的一些接口，另外就是用到了 javassist 做字节码增强。

这些打点数据就是我们需要做统计的，当然因为打点有限，我们的 tracing 功能相对于专业的 Traces 系统来说单薄了很多。

## Dog 简介

下面是 DOG 的架构图，客户端将消息投递给 Kafka，由 dog-server 来消费消息，存储用到了 Cassandra 和 ClickHouse，后面再介绍具体存哪些数据。

![architecture](https://assets.javadoop.com/imgs/20510079/apm/architecture.png)

1、也有 APM 系统是不通过消息中间件的，比如 Cat 就是客户端通过 Netty 连接到服务端来发送消息的。

2、Server 端使用了 Lambda 架构模式，Dog UI 上查询的数据，由每一个 Dog-server 的内存数据和下游储存的数据聚合而来。

下面，我们简单介绍下 Dog UI 上一些比较重要的功能，我们之后再去分析怎么实现相应的功能。

> 注意：下面的图都是我自己画的，不是真的页面截图，数值上可能不太准确

下图示例 transaction 报表：

![transaction-type](https://assets.javadoop.com/imgs/20510079/apm/transaction-type.png)

点击上图中 type 中的某一项，我们有这个 type 下面每个 name 的报表。比如点击 URL，我们可以得到每个接口的数据统计：

![transaction-name](https://assets.javadoop.com/imgs/20510079/apm/transaction-name.png)

当然，上图中点击具体的 name，还有下一个层级 status 的统计数据，这里就不再贴图了。Dog 总共设计了 type、name、status 三级属性。上面两个图中的最后一列是 sample，它可以指引到 sample 视图：

![sample](https://assets.javadoop.com/imgs/20510079/apm/sample.png)

Sample 就是取样的意思，当我们看到有个接口失败率很高，或者 P90 很高的时候，你知道出了问题，但因为它只有统计数据，所以你不知道到底哪里出了问题，这个时候，就需要有一些样本数据了。我们每分钟对 type、name、status 的不同组合分别保存最多 5 个成功、5 个失败、5 个慢处理的样本数据。

点击上面的 sample 表中的某个 T、F、L 其实就会进入到我们的 trace 视图，展示出这个请求的整个链路：

![trace](https://assets.javadoop.com/imgs/20510079/apm/trace.png)

通过上面这个 trace 视图，可以非常快速地知道是哪个环节出了问题。当然，我们之前也说过，我们的 trace 依赖于我们的埋点丰富度，但是 Dog 是一个 Metrics 为主的系统，所以它的 Traces 能力是不够的，不过大部分情况下，对于排查问题应该是足够用的。

对于应用开发者来说，下面这个 Problem 视图应该是非常有用的：

![problem](https://assets.javadoop.com/imgs/20510079/apm/problem.png)

它展示了各种错误的数据统计，并且提供了 sample 让开发者去排查问题。

最后，我们再简单介绍下 Heartbeat 视图，它和前面的功能没什么关系，就是大量的图，我们有 gc、heap、os、thread 等各种数据，让我们可以观察到系统的健康情况。

![heartbeat-heap](https://assets.javadoop.com/imgs/20510079/apm/heartbeat-heap.png)

这节主要介绍了一个 APM 系统通常包含哪些功能，其实也很简单对不对，接下来我们从开发者的角度，来聊聊具体的实现细节问题。

## 客户端数据模型

大家都是开发者，我就直接一些了，下图介绍了客户端的数据模型：

![data-model](https://assets.javadoop.com/imgs/20510079/apm/data-model.png)

对于一条 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，我们稍后再介绍。

下面表格示例一下打点数据，这样比较直观一些：

![client](https://assets.javadoop.com/imgs/20510079/apm/client.png)

简单介绍几点内容：

- type 为 URL、SQL、Redis、FeignClient、HttpClient 等这些数据，属于自动埋点的范畴。通常做 APM 系统的，都要完成一些自动埋点的工作，这样应用开发者不需要做任何的埋点工作，就能看到很多有用的数据。像最后两行的 type=Order 属于手动埋点的数据。
- 打点需要特别注意 type、name、status 的维度“爆炸”，它们的组合太多会非常消耗资源，它可能会直接拖垮我们的 Dog 系统。type 的维度可能不会太多，但是我们可能需要注意开发者可能会滥用 name 和 status，所以我们一定要做 normalize（如 url 可能是带动态参数的，需要格式化处理一下）。
- 表格中的最后两条是开发者**手动埋点**的数据，通常用来统计特定的场景，比如我想知道某个方法被调用的情况，调用次数、耗时、是否抛异常、入参、返回值等。因为自动埋点是业务不想关的，冷冰冰的数据，开发者可能想要埋一些自己想要统计的数据。
- 开发者在手动埋点的时候，还可以上报更多的业务相关的数据上来，参考表格最后一列，这些数据可以做业务分析来用。比如我是做支付系统的，通常一笔支付订单会涉及到非常多的步骤（国外的支付和大家平时使用的微信、支付宝稍微有点不一样），通过上报每一个节点的数据，最后我就可以在 Dog 上使用 bizId 来将整个链路串起来，在排查问题的时候是非常有用的（我们在做支付业务的时候，支付的成功率并没有大家想象的那么高，很多节点可能出问题）。

## 客户端设计

上一节我们介绍了单条 message 的数据，这节我们覆盖一下其他内容。

首先，我们介绍客户端的 API 使用:

```java
public void test() {
  Transaction transaction = Dog.newTransaction("URL", "/test/user");
  try {
    Dog.logEvent("User", "name-xxx", "status-yyy");
    
    // do something
    
    Transaction sql = Dog.newTransaction("SQL", "UserMapper.insert");
    // try-catch-finally
    
    transaction.setStatus("xxxx");    
    transaction.setSuccess(true/false);
  } catch (Throwable throwable) {
    transaction.setSuccess(false);
    transaction.setData(Throwables.getStackTraceAsString(throwable));
    throw throwable;
  } finally {
    transaction.finish();
  }
}
```

上面的代码示例了如何嵌套使用 Transaction 和 Event，当最外层的 Transaction 在 finally 代码块调用 finish() 的时候，完成了一棵树的创建，进行消息投递。

我们往 Kafka 中投递的并不是一个 Message 实例，因为一次请求会产生很多的 Message 实例，而是应该组织成 一个 Tree 实例以后进行投递。下图描述 Tree 的各个属性：

![tree](https://assets.javadoop.com/imgs/20510079/apm/tree.png)

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

treeId 应该有个算法能保证全局唯一，简单介绍下 Dog 的实现：${appName}-${encode(ip)}-${当前分钟}-${自增id}。

> 下面简单介绍几个 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 的功能。

介绍完了 tree 的内容，我们再简单讨论下应用集成方案。

集成无外乎两种技术，一种是通过 javaagent 的方式，在启动脚本中，加上相应的 agent，这种方式的优点是开发人员无感知，运维层面就可以做掉，当然开发者如果想要手动做一些埋点，可能需要再提供一个简单的 client jar 包给开发者，用来桥接到 agent 里。另一种就是提供一个 jar 包，由开发者来引入这个依赖。

两种方案各有优缺点，Pinpoint 和 Skywalking 使用的是 javaagent 方案，Zipkin、Jaeger、Cat 使用的是第二种方案，Dog 也使用第二种手动添加依赖的方案。

> 通常来说，做 Traces 的系统选择使用 javaagent 方案比较省心，因为这类系统 agent 做完了所有需要的埋点，无需应用开发者感知。

最后，我再简单介绍一下 Heartbeat 的内容，这部分内容其实最简单，但是能做出很多花花绿绿的图表出来，可以实现面向老板编程。

![heartbeat-sample](https://assets.javadoop.com/imgs/20510079/apm/heartbeat-sample.png)

前面我们介绍了 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 等其实是用来在页面上的分类，后缀是显示的折线图的名称。

关于客户端，这里就介绍这么多了，其实实际编码过程中，还有一些细节需要处理，比如如果一棵树太大了要怎么处理，比如没有 rootTransaction 的情况怎么处理（开发者只调用了 Dog.logEvent(...)），比如内层嵌套的 transaction 没有调用 finish 怎么处理等等。

## Dog server 设计

下图示例了 server 的整体设计，值得注意的是，我们这里对线程的使用非常地克制，图中只有 3 个工作线程。

![server-design](https://assets.javadoop.com/imgs/20510079/apm/server-design.png)

首先是 Kafka Consumer 线程，它负责批量消费消息，从 kafka 集群中消费到的是一个个 Tree 的实例，接下来考虑怎么处理它。

在这里，我们需要将树状结构的 message 铺平，我们把这一步叫做 deflate，并且做一些预处理，形成下面的结构：

![deflate](https://assets.javadoop.com/imgs/20510079/apm/deflate.png)

接下来，我们就将 DeflateTree 分别投递到两个 Disruptor 实例中，我们把 Disruptor 设计成单线程生产和单线程消费，主要是性能上的考虑。消费线程根据 DeflateTree 的属性使用绑定好的 Processor 进行处理，比如 DeflateTree 中 `List<Message> problmes` 不为空，同时自己绑定了 ProblemProcessor，那么就需要调用 ProblemProcessor 来处理。

> 科普时间：[Disruptor](https://lmax-exchange.github.io/disruptor/) 是一个高性能的队列，性能比 JDK 中的 BlockingQueue 要好

这里我们使用了 2 个 Disruptor 实例，当然也可以考虑使用更多的实例，这样每个消费线程绑定的 processor 就更少。我们这里把 Processor 绑定到了 Disruptor 实例上，其实原因也很简单，为了性能考虑，我们想让每个 processor 只有单线程使用它，单线程操作可以减少线程切换带来的开销，可以充分利用到系统缓存，以及在设计 processor 的时候，不用考虑并发读写的问题。

这里要考虑负载均衡的情况，有些 processor 是比较耗费 CPU 和内存资源的，一定要合理分配，不能把压力最大的几个任务分到同一个线程中去了。

核心的处理逻辑都在各个 processor 中，它们负责数据计算。接下来，我把各个 processor 需要做的主要内容介绍一下，毕竟能看到这里的开发者，应该真的是对 APM 的数据处理比较感兴趣的。

### Transaction processor

transaction processor 是系统压力最大的地方，它负责报表统计，虽然 Message 有 Transaction 和 Event 两个主要的子类，但是在实际的一颗树中，绝大部分的节点都是 transaction 类型的数据。

![transaction-type](https://assets.javadoop.com/imgs/20510079/apm/transaction-type.png)

下图是 transaction processor 内部的一个主要的数据结构，最外层是一个时间，我们用分钟时间来组织，我们最后在持久化的时候，也是按照分钟来存的。第二层的 HostKey 代表哪个应用以及哪个 ip 来的数据，第三层是 type、name、status 的组合。最内层的 Statistics 是我们的数据统计模块。

![transaction-statistics](https://assets.javadoop.com/imgs/20510079/apm/transaction-statistics.png)

另外我们也可以看到，这个结构到底会消耗多少内存，其实主要取决于我们的 type、name、status 的组合也就是 ReportKey 会不会很多，也就是我们前面在说客户端打点的时候，要避免维度爆炸。

最外层结构代表的是时间的分钟表示，我们的报表是基于每分钟来进行统计的，之后持久化到 ClickHouse 中，但是我们的使用者在看数据的时候，可不是一分钟一分钟看的，所以需要做数据聚合，下面展示两条数据是如何做聚合的，在很多数据的时候，都是按照同样的方法进行合并。

![transaction-cal](https://assets.javadoop.com/imgs/20510079/apm/transaction-cal.png)

> 你仔细想想就会发现，前面几个数据的计算都没毛病，但是 P90, P95 和 P99 的计算是不是有点欺骗人啊？其实这个问题是真的无解的，我们只能想一个合适的数据计算规则，然后我们再想想这种计算规则，可能算出来的值也是差不多可用的就好了。

另外有一个细节问题，我们需要让内存中的数据提供最近 30 分钟的统计信息，30 分钟以上的才从 DB 读取。然后做上面介绍的 merge 操作。

> 讨论：我们是否可以丢弃一部分实时性，我们每分钟持久化一次，我们读取的数据都是从 DB 来的，这样可行吗？
>
> 不行，因为我们的数据是从 kafka 消费来的，本身就有一定的滞后性，我们如果在开始一分钟的时候就持久化上一分钟的数据，可能之后还会收到前面时间的消息，这种情况处理不了。

比如我们要统计最近一小时的情况，那么就会有 30 分钟的数据从各个机器中获得，有 30 分钟的数据从 DB 获得，然后做合并。

这里值得一提的是，在 transaction 报表中，count、failCount、min、max、avg 是比较好算的，但是 P90、P95、P99 其实不太好算，我们需要一个数组结构，来记录这一分钟内所有的事件的时间，然后进行计算，我们这里讨巧使用了 [Apache DataSketches](https://datasketches.apache.org/)，它非常好用，这里我就不展开了，感兴趣的同学可以自己去看一下。

到这里，大家可以去想一想储存到 ClickHouse 的数据量的问题。app_name、ip、type、name、status 的不同组合，每分钟一条数据。

### Sample Processor

sample processor 消费 deflate tree 中的 `List<Transaction> transactions` 和 `List<Event> events` 的数据。

我们也是按照分钟来采样，最终每分钟，对每个 type、name、status 的不同组合，采集最多 5 个成功、5 个失败、5 个慢处理。

相对来说，这个还是非常简单的，它的核心结构如下图：

![sample-structure](https://assets.javadoop.com/imgs/20510079/apm/sample-structure.png)

结合 Sample 的功能来看比较容易理解：

![sample](https://assets.javadoop.com/imgs/20510079/apm/sample.png)

### Problem Processor

在做 deflate 的时候，所有 success=false 的 Message，都会被放入 `List<Message> problmes` 中，用来做错误统计。

Problem 内部的数据结构如下图：

![problem-structure](https://assets.javadoop.com/imgs/20510079/apm/problem-structure.png)

大家看下这个图，其实也就知道要做什么了，我就不啰嗦了。其中 samples 我们每分钟保存 5 个 treeId。

顺便也再展示下 Problem 的视图：

![problem](https://assets.javadoop.com/imgs/20510079/apm/problem.png)

关于持久化，我们是存到了 ClickHouse 中，其中 sample 用逗号连接成一个字符串，problem_data 的列如下：

```sql
event_date, event_time, app_name, ip, type, name, status, count, sample
```

### Heartbeat processor

Heartbeat 处理 `List<Heartbeat> heartbeats` 的数据，题外话，正常情况下，一颗树里面只有一个 Heartbeat 实例。

前面我也简单提到了一下，我们 Heartbeat 中用来展示图表的核心数据结构是一个 `Map<String, Double>` 。

收集到的 key-value 数据如下所示：

```json
{
  "os.systemLoadAverage": 1.5,
  "os.committedVirtualMemory": 1234562342,
  "os.openFileDescriptorCount": 800,
  "thread.count": 600,
  "thread.httpThreadsCount": 250,
  "gc.ZGC Count": 234,
  "gc.ZGC Time(ms)": 123435,
  "heap.ZHeap": 4051233219,
  "heap.Metaspace": 280123212
}
```

前缀是分类，后缀是图的名称。客户端每分钟收集一次数据进行上报，然后就可以做很多的图了，比如下图展示了在 heap 分类下的各种图：

![heartbeat-heap](https://assets.javadoop.com/imgs/20510079/apm/heartbeat-heap.png)

Heartbeat processor 要做的事情很简单，就是数据存储，Dog UI 上的数据是直接从 ClickHouse 中读取的。

heartbeat_data 的列如下：

```sql
event_date, event_time, timestamp, app_name, ip, name, value
```

### MessageTree Processor

前面我们多次提到了 Sample 的功能，这些采样的数据帮助我们恢复现场，这样我们可以通过 trace 视图来跟踪调用链。

![trace](https://assets.javadoop.com/imgs/20510079/apm/trace.png)

要做上面的这个 trace 视图，我们需要上下游的所有的 tree 的数据，比如上图是 3 个 tree 实例的数据。

> 之前我们在客户端介绍的时候说过，这几个 tree 通过 parent treeId 和 root treeId 来组织。

要做这个视图，给我们提出的挑战就是，我们需要保存全量的数据。

> 大家可以想一想这个问题，为啥要保存全量数据，我们直接保存被 sample 到的数据不就好了吗？

这里我们用到了 Cassandra 的能力，Cassandra 在这种 kv 的场景中，有非常不错的性能，而且它的运维成本很低。

我们以 treeId 作为主键，另外再加 data 一个列即可，它是整个 tree 的实例数据，数据类型是 blob，我们会先做一次 gzip 压缩，然后再扔给 Cassandra。

### Business Processor

我们在介绍客户端的时候说过，每个 Message 都可以携带 Business Data，不过只有应用开发者自己手动埋点的时候才会有，当我们发现有业务数据的时候，我们会做另一个事情，就是把这个数据存储到 ClickHouse 中，用来做业务分析。

我们其实不知道应用开发者到底会把它用在什么场景中，因为每个人负责的项目都不一样，所以我们只能做一个通用的数据模型。

![data-model](https://assets.javadoop.com/imgs/20510079/apm/data-model.png)

回过头来看这个图，BusinessData 中我们定义了比较通用的 userId 和 bizId，我们认为它们可能是每个业务场景会用到的东西。userId 就不用说了，bizId 大家可以做来记录订单 id，支付单 id 等。

然后我们提供了 3 个 String 类型的列 ext1、ext2、ext3 和两个数值类型的列 extVal1 和 extVal2，它们可以用来表达你的业务相关的参数。

我们的处理当然也非常简单，将这些数据存到 ClickHouse 中就可以了，表中主要有这些列：

```sql
event_data, event_time, user, biz_id, timestamp, type, name, status, app_name、ip、success、ext1、ext2、ext3、ext_val1、ext_val2
```

这些数据对我们 Dog 系统来说肯定不认识，因为我们也不知道你表达的是什么业务，type、name、status 是开发者自己定义的，ext1, ext2, ext3 分别代表什么意思，我们都不知道，我们只负责存储和查询。

这些业务数据非常有用，基于这些数据，我们可以做很多的数据报表出来。因为本文是讨论 APM 的，所以该部分内容就不再赘述了。

## 其他

ClickHouse 需要批量写入，不然肯定是撑不住的，一般一个 batch 至少 10000 行数据。

我们在 Kafka 这层控制了，一个 app_name + ip 的数据，只会被同一个 dog-server 消费，当然也不是说被多个 dog-server 消费会有问题，但是这样写入 ClickHouse 的数据就会更多。

还有个关键的点，前面我们说了每个 processor 是由单线程进行访问的，但是有一个问题，那就是来自 Dog UI 上的请求可怎么办？这里我想了个办法，那就是将请求放到一个 Queue 中，由 Kafka Consumer 那个线程来消费，它会将任务扔到两个 Disruptor 中。比如这个请求是 transaction 报表请求，其中一个 Disruptor 的消费者会发现这个是自己要干的，就会去执行这个任务。

## 小结

如果你了解 Cat 的话，可以看到 Dog 在很多地方和 Cat 有相似之处，或者直接说”抄“也行，之前我们也考虑过直接使用 Cat 或者在 Cat 的基础上做二次开发。但是我看完 Cat 的源码后，就放弃了这个想法，仔细想想，只是借鉴 Cat 的数据模型，然后我们自己写一套 APM 其实不是很难，所以有了我们这个项目。

行文需要，很多地方我都避重就轻，因为这不是什么源码分析的文章，没必要处处谈细节，主要是给读者一个全貌，读者能通过我的描述大致想到需要处理哪些事情，需要写哪些代码，那就当我表述清楚了。

欢迎大家提出自己的疑问或者想法，有不懂或者我有错漏的地方，欢迎指正~

## references

Pinpoint: [[官网](https://pinpoint-apm.github.io/pinpoint/index.html)] [[Live Demo](http://125.209.240.10:10123/)] [[Github](https://github.com/pinpoint-apm/pinpoint)]

Skywalking: [[官网](https://skywalking.apache.org/)] [[Live Demo](http://demo.skywalking.apache.org/)] [[Github](https://github.com/apache/skywalking)]

Jaeger: [[官网](https://www.jaegertracing.io/)] [[Github](https://github.com/jaegertracing/jaeger)]

Zipkin: [[官网](https://zipkin.io/)] [[Github](https://github.com/openzipkin/zipkin)]

Cat: [[Github](https://github.com/dianping/cat)] 

OpenTracing: [[官网](https://opentracing.io/)]
