Dog 简介
下面是 DOG 的架构图,客户端将消息投递给 Kafka,由 dog-server 来消费消息,存储用到了 Cassandra 和 ClickHouse,后面再介绍具体存哪些数据。
1、也有 APM 系统是不通过消息中间件的,比如 Cat 就是客户端通过 Netty 连接到服务端来发送消息的。
2、Server 端使用了 Lambda 架构模式,Dog UI 上查询的数据,由每一个 Dog-server 的内存数据和下游储存的数据聚合而来。
下面,我们简单介绍下 Dog UI 上一些比较重要的功能,我们之后再去分析怎么实现相应的功能。
注意:下面的图都是我自己画的,不是真的页面截图,数值上可能不太准确
下图示例 transaction 报表:
点击上图中 type 中的某一项,我们有这个 type 下面每个 name 的报表。比如点击 URL,我们可以得到每个接口的数据统计:
当然,上图中点击具体的 name,还有下一个层级 status 的统计数据,这里就不再贴图了。Dog 总共设计了 type、name、status 三级属性。上面两个图中的最后一列是 sample,它可以指引到 sample 视图:
Sample 就是取样的意思,当我们看到有个接口失败率很高,或者 P90 很高的时候,你知道出了问题,但因为它只有统计数据,所以你不知道到底哪里出了问题,这个时候,就需要有一些样本数据了。我们每分钟对 type、name、status 的不同组合分别保存最多 5 个成功、5 个失败、5 个慢处理的样本数据。
点击上面的 sample 表中的某个 T、F、L 其实就会进入到我们的 trace 视图,展示出这个请求的整个链路:
通过上面这个 trace 视图,可以非常快速地知道是哪个环节出了问题。当然,我们之前也说过,我们的 trace 依赖于我们的埋点丰富度,但是 Dog 是一个 Metrics 为主的系统,所以它的 Traces 能力是不够的,不过大部分情况下,对于排查问题应该是足够用的。
对于应用开发者来说,下面这个 Problem 视图应该是非常有用的:
它展示了各种错误的数据统计,并且提供了 sample 让开发者去排查问题。
最后,我们再简单介绍下 Heartbeat 视图,它和前面的功能没什么关系,就是大量的图,我们有 gc、heap、os、thread 等各种数据,让我们可以观察到系统的健康情况。
这节主要介绍了一个 APM 系统通常包含哪些功能,其实也很简单对不对,接下来我们从开发者的角度,来聊聊具体的实现细节问题。
客户端数据模型
大家都是开发者,我就直接一些了,下图介绍了客户端的数据模型:
对于一条 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,我们稍后再介绍。
下面表格示例一下打点数据,这样比较直观一些:
简单介绍几点内容:
type 为 URL、SQL、Redis、FeignClient、HttpClient 等这些数据,属于自动埋点的范畴。通常做 APM 系统的,都要完成一些自动埋点的工作,这样应用开发者不需要做任何的埋点工作,就能看到很多有用的数据。像最后两行的 type=Order 属于手动埋点的数据。
打点需要特别注意 type、name、status 的维度“爆炸”,它们的组合太多会非常消耗资源,它可能会直接拖垮我们的 Dog 系统。type 的维度可能不会太多,但是我们可能需要注意开发者可能会滥用 name 和 status,所以我们一定要做 normalize(如 url 可能是带动态参数的,需要格式化处理一下)。
表格中的最后两条是开发者手动埋点 的数据,通常用来统计特定的场景,比如我想知道某个方法被调用的情况,调用次数、耗时、是否抛异常、入参、返回值等。因为自动埋点是业务不想关的,冷冰冰的数据,开发者可能想要埋一些自己想要统计的数据。
开发者在手动埋点的时候,还可以上报更多的业务相关的数据上来,参考表格最后一列,这些数据可以做业务分析来用。比如我是做支付系统的,通常一笔支付订单会涉及到非常多的步骤(国外的支付和大家平时使用的微信、支付宝稍微有点不一样),通过上报每一个节点的数据,最后我就可以在 Dog 上使用 bizId 来将整个链路串起来,在排查问题的时候是非常有用的(我们在做支付业务的时候,支付的成功率并没有大家想象的那么高,很多节点可能出问题)。
客户端设计
上一节我们介绍了单条 message 的数据,这节我们覆盖一下其他内容。
首先,我们介绍客户端的 API 使用:
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 的属性很好理解,它持有 root transaction 的引用,用来遍历整颗树。另外就是需要携带机器信息 messageEnv。
treeId 应该有个算法能保证全局唯一,简单介绍下 Dog 的实现:{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 的内容,这部分内容其实最简单,但是能做出很多花花绿绿的图表出来,可以实现面向老板编程。
前面我们介绍了 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 怎么处理等等。