Cat原理简析

发布时间:2025-12-09 22:11:45 浏览次数:4

Cat原理简析

  • 链路追踪系统设计思路
    • 如何高效组织业务日志
    • 如何动态串联业务日志
    • 通用解决方案
      • 链路定义
      • 链路染色
      • 链路上报
      • 链路存储
  • Cat原理
    • 客户端原理
      • API设计
      • 序列化和通信
      • 客户端埋点
      • 核心类分析
      • 流程分析
        • 启动流程:
        • 消息生产
        • Context 线程本地变量
        • Transaction事务的开启
        • 其他类型消息组合
        • 关闭Transaction:
        • 发送数据
        • 消息序列化
        • MessageID
  • 服务端原理
  • 存储数据设计
  • 小结

本文为Cat链路追踪监控工具原理简析篇,主要参考官方文档和其他资料整理而来:

  • 官方文档: Cat设计方案
  • 美团技术团队: CAT 3.0 开源发布,支持多语言客户端及多项性能提升-
  • 美团技术团队: 深度剖析开源分布式监控CAT
  • 美团技术团队: 可视化全链路日志追踪
  • 美团技术团队: 日志篇文章汇总
  • 美团技术团队: 分布式会话跟踪系统架构设计与实践
  • 凤凰架构: 可观测性
  • 美团点评CAT源码分析

链路追踪系统设计思路

链路追踪系统设计需要考虑三个方面:

  • 事件日志
    • 输出
    • 收集与缓冲
    • 加工与聚合
    • 存储与查询
  • 链路追踪
    • 追踪与跨度
    • 数据收集方式
  • 聚合指标
    • 指标收集
    • 存储查询
    • 监控告警

本节主要针对链路追踪这个环节进行讨论,链路追踪的难点在于:

  • 如何从大量离散日志中快速收集并筛选出需要的日志,并按照链路执行流程串联起来进行可视化展示,即可视化的全链路日志追踪

可视化的全链路日志追踪需要解决两个问题:

  • 如何高效组织业务日志
    • 为了实现高效的业务追踪,首先需要准确完整地描述出业务逻辑,形成业务逻辑的全景图,而业务追踪其实就是通过执行时的日志数据,在全景图中还原出业务执行的现场。
  • 如何动态串联业务日志
    • 业务逻辑执行时的日志数据原本是离散存储的,而此时需要实现的是,随着业务逻辑的执行动态串联各个逻辑节点的日志,进而还原出完整的业务逻辑执行现场。

如何高效组织业务日志

通过对业务逻辑进行抽象,定义出业务逻辑链路:

  • 逻辑节点:业务系统的众多逻辑可以按照业务功能进行拆分,形成一个个相互独立的业务逻辑单元,即逻辑节点,可以是本地方法也可以是RPC等远程调用方法。
  • 逻辑链路:业务系统对外支撑着众多的业务场景,每个业务场景对应一个完整的业务流程,可以抽象为由逻辑节点组合而成的逻辑链路。

一次业务追踪就是逻辑链路的某一次执行情况的还原,逻辑链路完整准确地描述了业务逻辑全景,同时作为载体可以实现业务日志的高效组织。


如何动态串联业务日志

由于逻辑节点之间、逻辑节点内部往往通过MQ或者RPC等进行交互,所以可以采用分布式会话跟踪提供的分布式参数透传能力实现业务日志的动态串联:

  • 通过在执行线程和网络通信中持续地透传参数,实现在业务逻辑执行的同时,不中断地传递链路和节点的标识,实现离散日志的染色。
  • 基于标识,染色的离散日志会被动态串联至正在执行的节点,逐渐汇聚出完整的逻辑链路,最终实现业务执行现场的高效组织和可视化展示。

与分布式会话跟踪方案不同的是,当同时串联多次分布式调用时,需要结合业务逻辑选取一个公共id作为标识

例如上面的审核场景涉及2次RPC调用,为了保证2次执行被串联至同一条逻辑链路,此时结合审核业务场景,选择初审和复审相同的“任务id”作为标识,完整地实现审核场景的逻辑链路串联和执行现场还原。


通用解决方案

明确日志的高效组织和动态串联这两个基本问题后,通用解决方案可以拆解为以下步骤:


链路定义

“链路定义”的含义为:使用特定语言,静态描述完整的逻辑链路,链路通常由多个逻辑节点,按照一定的业务规则组合而成,业务规则即各个逻辑节点之间存在的执行关系,包括串行、并行、条件分支。

DSL(Domain Specific Language)是为了解决某一类任务而专门设计的计算机语言,可以通过JSON或XML定义出一系列节点(逻辑节点)的组合关系(业务规则)。因此,本方案选择使用DSL描述逻辑链路,实现逻辑链路从抽象定义到具体实现。

  • 逻辑链路1-DSL:
[{"nodeName": "A","nodeType": "rpc"},{"nodeName": "Fork","nodeType": "fork","forkNodes": [[{"nodeName": "B","nodeType": "rpc"}],[{"nodeName": "C","nodeType": "local"}]]},{"nodeName": "Join","nodeType": "join","joinOnList": ["B","C"]},{"nodeName": "D","nodeType": "decision","decisionCases": {"true": [{"nodeName": "E","nodeType": "rpc"}]},"defaultCase": [{"nodeName": "F","nodeType": "rpc"}]}]

链路染色

“链路染色”的含义为:在链路执行过程中,通过透传串联标识,明确具体是哪条链路在执行,执行到了哪个节点。

链路染色包括两个步骤:

  • 步骤一:确定串联标识,当逻辑链路开启时,确定唯一标识,能够明确后续待执行的链路和节点。
    • 链路唯一标识 = 业务标识 + 场景标识 + 执行标识 (三个标识共同决定“某个业务场景下的某次执行”)
    • 业务标识:赋予链路业务含义,例如“用户id”、“活动id”等等。
    • 场景标识:赋予链路场景含义,例如当前场景是“逻辑链路1”。
    • 执行标识:赋予链路执行含义,例如只涉及单次调用时,可以直接选择“traceId”;涉及多次调用时则,根据业务逻辑选取多次调用相同的“公共id”。
    • 节点唯一标识 = 链路唯一标识 + 节点名称 (两个标识共同决定“某个业务场景下的某次执行中的某个逻辑节点”)
    • 节点名称:DSL中预设的节点唯一名称,如“A”。
  • 步骤二:传递串联标识,当逻辑链路执行时,在分布式的完整链路中透传串联标识,动态串联链路中已执行的节点,实现链路的染色。例如在“逻辑链路1”中:
    • 当“A”节点触发执行,则开始在后续链路和节点中传递串联标识,随着业务流程的执行,逐步完成整个链路的染色。
    • 当标识传递至“E”节点时,则表示“D”条件分支的判断结果是“true”,同时动态地将“E”节点串联至已执行的链路中。

链路上报

“链路上报”的含义为:在链路执行过程中,将日志以链路的组织形式进行上报,实现业务现场的准确保存。

Transaction之间是有引用的,因此在end方法中只需要将第一个Transaction(封装在MessageTree中)通过MessageManager来flush,在拼接消息时可以根据这个引用关系来找到所有的Transaction 。所以来看代码:


发送数据

MessageManager 会通过 flush 将消息树上报到服务器,我们来通过下面源码分析一下flush方法,函数首先判断是否分配MessageID,没有则分配, 然后调用TcpSocketSender的send函数来发送消息。

send函数也不是立即发送, 仅仅只是插入内存队列。读者可以去看看 TcpSocketSender 的 initialize() 方法, 有行代码 Threads.forGroup(“cat”).start(this) ,这行代码使得客户端在初始化的时候, 就开启一个上报线程,上报线程一直读取内存队列,获取要发送的消息树,调用 sendInternal(MessageTree tree) 方法将消息树发送到服务器。

这样子,客户端就实现了消息的多线程、异步化、队列化,从而保证日志的记录不会因为CAT系统异常而影响主业务线程。


1.首先获取到发送类的对象,调用其方法进行发送:


2.发送时是经典的生产者-消费者模型,生产者只需要向队列中放入数据,消费者监听队列,获取数据并发送:

3.消费者线程拉取消息:


消息序列化

上报线程通过 sendInternal(MessageTree tree) 将消息发送到服务器,在 sendInternal 方法内, TcpSocketSender 在发送报文之前,会先调用m_codec.encode(tree, buf) 对消息树进行序列化,序列化就是将对象编码为一组字节,使得对象能够通过 tcp/ip 协议发送到服务器端的技术, 服务器再通过反序列化, 将字节解码为对象。

在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。但是通过公共接口编码的字节会有很多冗余信息来保证不同对象与字节之间的正确编解码,在CAT中,需要传输的只有MessageTree这么一个对象。通过自定义的序列化方案可以节省许多不必要的字节信息,保证网络传输的高效性。

public class PlainTextMessageCodec implements MessageCodec, LogEnabled {@Overridepublic void encode(MessageTree tree, ByteBuf buf) throws UnsupportedEncodingException {int count = 0;int index = buf.writerIndex();buf.writeInt(0); // place-holdercount += encodeHeader(tree, buf);if (tree.getMessage() != null) {count += encodeMessage(tree.getMessage(), buf);}buf.setInt(index, count);}}

被序列化的字节码包含3个部分:

1、 前4个字节包含整组字节串的长度,首先通过buf.writeInt(0)占位,编码完通过buf.setInt(index, count)将字节码长度写入buf头4个字节。

2、编码消息树的头部,依次将tree的version, domain, hostName, ipAdress, treadGroupName, treadId, threadName, MessageId, parentMessageId, rootMessageId, sessionToken写入头部,字段之间以"\t"分隔,并以"\n"结尾。空用null表示。

3、编码消息体,每个消息都是以一个表示消息类型的字符开头。

a."A"表示没有嵌套其他类型消息的事务,b.有嵌套其他消息的事务,以一个 "t" 开头,然后递归去遍历并编码子消息, 最后以一个"T"结束,c."E"/"L"/"M"/"H"分别表示Event/Trace/Metric/Heartbeat类型消息;
  • 然后依次记录时间、type、name
  • 然后根据条件依次写入status、duration+us、data
  • 字段之间依然以"\t"分割,以"\n"结尾,空用null表示

比如上面其它消息组合章节的案例中,MessageTree通过编码之后:

口PT1CatWin7-caoh.kingsoft.cn192.168.37.41main1mainCat-c0a82529-423686-40028nullnullnullt2018-05-02 22:59:05.347URLWebPageH2018-05-02 22:59:05.353Heartbeat1hearbeat0cpu=90&mem=70M2018-05-02 22:59:05.353metric10total_feeL2018-05-02 22:59:05.354Trace1debug0user_debug_dataE2018-05-02 22:59:05.354Event1Name10data1E2018-05-02 22:59:05.354Event2Name20data2E2018-05-02 22:59:05.354RemoteCallService10Cat-c0a82529-423686-40026T2018-05-02 22:59:07.507URLWebPage02160695usk1=v1&k2=v2&k3=v3

上面一串字符串,是通过字节码转换成string的结果, 最前面的乱码,实际上表示的是4个字节的int类型转为string类型表现形式。字节码转int后是541,是整个字节码的长度。

最终TcpSocketSender 通过ChannelManager 将编码后的字节码发送到服务器。这里采用的是netty客户端。


MessageID

CAT每个消息都有一个唯一的ID,这个ID在客户端生成,后续都通过这个ID在进行消息内容的查找。典型的RPC消息串起来的问题,比如A调用B的时候,在A这端生成一个Message-ID,在A调用B的过程中,将Message-ID作为调用传递到B端,在B执行过程中,B用context传递的Message-ID作为当前监控消息的Message-ID。

CAT消息的Message-ID格式ShopWeb-0a010680-375030-2,CAT消息一共分为四段:

  • 第一段是应用名shop-web。
  • 第二段是当前这台机器的IP的16进制格式,01010680表示10.1.6.108。
  • 第三段的375030,是系统当前时间除以小时得到的整点数。
  • 第四段的2,是表示当前这个客户端在当前小时的顺序递增号。

一定得注意的是,同一台客户端机器产生的Message-ID的第四段,即当前小时的顺序递增号,在当前小时内一定不能重复,因为在服务端,CAT会为每个客户端IP、每个小时的原始消息存储都创建一个索引文件,每条消息的索引记录在索引文件内的偏移位置是由顺序递增号决定的,一旦顺序号重复生成,那么该小时的重复索引数据将会被覆盖,导致我们无法通过索引找到原始消息数据。


服务端原理

单机的consumer架构设计如下:

如上图,CAT服务端在整个实时处理中,基本上实现了全异步化处理。

  • 消息接受是基于Netty的NIO实现。
  • 消息接受到服务端就存放内存队列,然后程序开启一个线程会消费这个消息做消息分发。
  • 每个消息都会有一批线程并发消费各自队列的数据,以做到消息处理的隔离。
  • 消息存储是先存入本地磁盘,然后异步上传到HDFS文件,这也避免了强依赖HDFS。

当某个报表处理器处理来不及时候,比如Transaction报表处理比较慢,可以通过配置支持开启多个Transaction处理线程,并发消费消息。


存储数据设计

消息存储是CAT最有挑战的部分。关键问题是消息数量多且大,目前美团每天处理消息1000亿左右,大小大约100TB,单物理机高峰期每秒要处理100MB左右的流量。CAT服务端基于此流量做实时计算,还需要将这些数据压缩后写入磁盘。

整体存储结构如下图:


CAT在写数据一份是Index文件,一份是Data文件.

  • Data文件是分段GZIP压缩,每个分段大小小于64K,这样可以用16bits可以表示一个最大分段地址。
  • 一个Message-ID都用需要48bits的大小来存索引,索引根据Message-ID的第四段来确定索引的位置,比如消息Message-ID为ShopWeb-0a010680-375030-2,这条消息ID对应的索引位置为2*48bits的位置。
  • 48bits前面32bits存数据文件的块偏移地址,后面16bits存数据文件解压之后的块内地址偏移。
  • CAT读取消息的时候,首先根据Message-ID的前面三段确定唯一的索引文件,在根据Message-ID第四段确定此Message-ID索引位置,根据索引文件的48bits读取数据文件的内容,然后将数据文件进行GZIP解压,在根据块内偏移地址读取出真正的消息内容。

小结

大家在学习Cat客户端原理时,可以对照一开始给出的通用解决四部曲来看,看看理论与实践落地的差异与联系。

更多Cat源码可以参考该系列: Cat源码系列,大佬分析的很透彻,本文在客户端源码分析部分,也是大量借鉴了该系列中客户端源码篇,对于服务端原理篇和其他部分,本文只是一笔带过,更多详情,大家可以参考该源码系列。

需要做网站?需要网络推广?欢迎咨询客户经理 13272073477