1.相关概念
如下在Cat中我们称为: logview消息树(或者MessageTree),即应用内部的调用链路。可参考:https://www.jianshu.com/p/855207522411
2.整体设计介绍
Cat整体处理流程如下图所示:
(cat-server对应开源的cat-consumer模块)
1. 业务接入cat-client,进行Cat埋点
2. cat-client将埋点信息,组织成logview消息树,上报到后端cat-sever服务器(通过tcp方式上报)
3. cat-server端接受到logview,将logview放到不同的线程里进行处理
3.1 Transaction线程,提取logview中transaction数据,构造transaction报表。
3.2 Event线程,提取logview中event数据,构造event报表
3.3 Problem线程,提取logview中problem数据,构造problem报表
3.4 Logview线程,将logview进行存储。
4. 查询端通过指定报表,查询相应数据。
3.logView树构造
logView树(也叫 消息树、messageTree):在同一个线程里,通过transaction容器,将Cat产生的各个埋点进行串联,形成的进程内部的调用链路。
埋点示例一:
public void test() throws InterruptedException {
Cat.initializeByDomain("cat.test");
Transaction t1 = Cat.newTransaction("method", "test");
try{
test2("dpg");
}catch (Exception e){
Cat.logError(e);
t1.setStatus(e); //设置transaction状态
}finally {
t1.complete(); //transaction一定要complete,一般放到finally来保证
}
//cat数据上报是异步的,需要sleep一段时间等待数据上报,在结束进程
Thread.sleep(3000);
}
private void test2(String param) throws Exception {
Cat.logEvent("test2param", param);
Transaction t2 = Cat.newTransaction("method2", "test2");
//中间执行业务逻辑
Thread.sleep(10);
t2.setSuccessStatus();
t2.complete();
throw new Exception("error");
}
生成的logView如下所示:
埋点示例二:
public void test() throws InterruptedException {
Cat.initializeByDomain("cat.test");
Transaction t1 = Cat.newTransaction("method", "test");
try{
test2("dpg");
}catch (Exception e){
Cat.logError(e);
t1.setStatus(e); //设置transaction状态
}finally {
t1.complete(); //transaction一定要complete,一般放到finally来保证
}
Transaction t3 = Cat.newTransaction("DbMethod", "DbTest");
Cat.logEvent("db", "mysql");
t3.complete();
//cat数据上报是异步的,需要sleep一段时间等待数据上报,在结束进程
Thread.sleep(3000);
}
private void test2(String param) throws Exception {
Cat.logEvent("test2param", param);
Transaction t2 = Cat.newTransaction("method2", "test2");
//中间执行业务逻辑
Thread.sleep(10);
t2.setSuccessStatus();
t2.complete();
throw new Exception("error");
}
会生成2个logView树,如下图所示:
4.原理解析
你的埋点会被cat-cliet 串街成一个logview消息树,具体实现原理如下:
(t:代表transaction, E:代表Event)(t:transaction开始,T:transaction结束)
1. 我们使用ThreadLocal变量,当new 一个Transaction1,会向stack push,并构造 Logview 消息树
2. Transaction2 new的时候,也会向stack push,并将Trasaction2 放到 当前logView 消息树中
3. Event new的时候,会将Event放到当前logView消息树中
4. Transaction2 complete,会从stack堆栈pop,并记录transaction2的耗时时间、状态。
5. Transactin1 complete,会从stack堆栈pop,并记录transaction1的耗时时间、状态。发现此时当前stack为空,就会将当前构建好的Logview消息树发送出去
6. 当代码流程在有Cat 埋点,就会走上面同样逻辑。 (最顶层通过Transaction树组织起来)
4.1 注意
1. Transaction一定要complete,并且不能跨线程comoplete(transaction的生成和complete需要在同一个线程里)
a. transaction只有complete我们才会发送整个logview消息树,如果你不complete,消息树就不会发送,挤压在内存里,造成监控数据不准。进而引发内存泄漏风险。
b. 由于cat-client内部使用ThreadLocal变量,所以需要在同一个线程进行new 和complete,否则complete会失效。
2. transaction complete顺序,和new的顺序相反。最先new的transaction,最后complete
a. transaction是一个堆栈结构,需要complete 和new 相互对应。
b. 如果,new的顺序是 a b c。 complete的顺序 也是 a b c。那么在complete a的时候,此时堆栈内容是 c、b、a,
b1. 我们会从stack pop一个对象,发现是c,不是a本身,就会把c complete。
b2. 继续stack pop一个对象,发现是 b,不是a本身,就会把 b complete。
b3. 继续stack pop 一个对象,发现是a本身,就会把a complete
b4. 之后b、c complete,就不会产生任何动作。
因为b、c被提前complete,所以会导致耗时统计不准。
5. 采样与聚合
1. 使用Cat埋点,cat会在客户端将埋点串成logview消息树上报到cat后端,进而解析成相应报表。
2. 当应用流量变得很大,埋点生成消息树也会成爆发行增长,为了不影响客户端机器性能,以及降低网络流量,我们会对cat埋点的消息树,进行采样聚合。
采样:只将部分logview 原样上报,减少上报量。比如采样率:1%, 100个logview消息树,只会上报一个。
聚合:将未被采样到的logview消息树,进行聚合、编码变成一个消息树 上报,保证报表统计的准确性。
如下图所示,cat-client会将近3s未被采样到的logview消息树,聚合成一个消息树。(被采样到的logview会被原样上报)
在生成的新消息树里,会统计每个transaction/event 次数、耗时。这样就可以把很多logview 合并成一个logview,极大减少了网络流量和消息量。(多个logview被聚合成一个logview上报,丢失了调用链路关系,但是指标数量信息没有丢失)
5.1 如何判断一个链路是否被聚合
比如在Cat上查看一个链路,如下所示:
最顶层的埋点是System TransactionAggregator 或者 System EventAggregator 或则System _CatMergeTree(低版本) 则代表这个链路是被聚合的,不是真实的链路。 聚合的链路只是将:埋点的Transaction和Event 次数信息统计上报上来,为了保证Transaction、Event报表统计的准确性,不代表真实的链路
5.2 如何查看被采样到的链路
problem报表都是采样到的链路。对于耗时长(比如耗时长的rpc、db、缓存调用)、失败的链路我们会在problem报表展示,所以在problem报表可以看到有问题的采样链
6.截断
对于采样命中的logview,我们会将整个logview发送到cat后端,进行存储。
对于有些业务,单个logview大小可能很多(比如:在一个循环里加了cat埋点),为了保证logview整个传输存储的成功率,我们会对logview大小进行限制。
如果发现logview埋点个数(Transaction+Event数量)超过2000,我们会将logview进行截断,以2000个大小作为限制进行截断。
如果点击logview如上图所示,有个 RootLogview、ParentLogview的超链接,那么这个logview就是被截断的。
比如原始的logview,因为过长被截成5段。
其中:RootLogview:链接到第一段logview
ParentLogview:连接到上一个被截断后的logview
6.1 截断原理
a. 如上图所示,当前我们处于1这个阶段,此时内存构造的logview树状态是A(其中,t:代表 开始一个Transaction,T:代表结束一个Transaction,E:代表一个Event或者Error)
b. 当我们继续new 一个Transctioin t7时,会检查当前内存中logview的长度,如果长度超过2000限制(即:Transaction + Event + Error 数量),开始对logview进行截断,进入阶段2
c. 在阶段2,我们会把已经complete的Transaction(t3、t4)打包在一起,构造一个新的完整logview A1 发送(当然发送是异步,不会阻塞进程)
d. 没有complete的Transaction(t1、t2、t5、t6)会再次组装在一起,变成A2,继续留在内存里 ,这样当new Transaction t7,t7被加入到A2中,进入到阶段3,继续等待新的埋点数据加入
e. 直到整个t1 这个transaction complete,整个logview才会发送走
6.2 截断问题
a. 统计次数不准:根据上面所述,在截断时, t1、t2、t5、t6 其实统计数据会不准。因为在A1中发送过一次,在A2中还会发送一次(当然截断次数越多,统计数值偏差越大)。不过为了修复统计次数不准,我们不会统计t1次数,知道最后一次阶段上报才会统计t1,其实这么做也只是解决了t1统计不准确问题。具体为什么不修复其他transaction统计不准问题,只能说太复杂,不好搞
b. 耗时统计不准:被阶段后,同一个transaction被放到2个logview里了(比如t2),这样耗时就被拆分成2部分,所以平均耗时,会偏低
6.3 截断时机:
1. transaction+event+error数量超过 2000
2. logview 跨小时超过10s