业务时延检测利器-uftrace

  本文来自博客园,作者:T-BARBARIANS,博文严禁转载,转载必究!

  篇幅较长,阅读耗时告警!

一、前言

  作为后台程序的开发人员,应用程序的性能一直是我们的核心关注点。

  大到业务程序的架构设计、支撑业务的组件选型,小到具体某些功能相似方法的性能横向对比、编译优化、甚至抠某一行代码,目的都是为了给我们的程序插上翅膀。

  有了这些就够了么?显然不是的,因为做到了这些,实际的综合表现还是未知的。就像生产一款新型汽车,组装好了你得拉出来溜一溜,对照众多测试用例,核验各项设计指标。

  本文要介绍的就是借助uftrace工具,核查C/C++/Rust程序中每一个函数的执行耗时,在此基础上,优化耗时大的函数,真正为我们的程序起飞打下坚实基础。

二、抛砖引玉

  介绍uftrace之前,先简单聊一聊perf。

  perf是c/c++领域性能分析的一大利器。perf可以统计过去一段时间里,所有被执行函数和指令的CPU使用总时间,通过统计报表的形式,直观展示热点函数和指令的CPU占比。例如下图一所示,开发者可以通过对热点的针对性分析,达到程序性能优化的目的。

图 1

  perf完美了么,就没有它不能覆盖的性能分析场景了么?显然不是,perf是基于过去一段时间的所有累计之和,是从宏观的角度去分析。那从微观角度,比如我有一个需求:有没有这样的一种工具,它可以记录某个用户态函数在过去一段时间里,每一次执行的单独耗时,以及它的完整调用链?从而挖掘出宏观角度发现不了的深坑!

  例如一个“hello world”函数,大部分时间该函数的执行时长都比较一致,但是会偶发执行时长大幅增加的情况,那这背后的原因就值得我们去深入分析。

图 2

  perf tools的ftrace具备“记录函数每一次的执行耗时”能力,但是ftrace是基于内核的,只能跟踪分析linux内核中各种函数的执行耗时。这里我使用trace-cmd工具(ftrace的一个命令行工具,大大简化ftrace的使用)来记录“do_page_fault”缺页中断函数在一段时间范围的执行耗时。

  1、只查看函数的每一次执行耗时。

图 3

  由图3可知,do_page_fault在内核的每一次执行,耗时都集中在1us左右,短时间内未见异常。

  2、查看do_page_fault每次耗时,以及函数内部各子函数的执行耗时。

图 4

 图 5

  图5直接展示了do_page_fault函数内部各子函数的执行耗时。假设当某一次do_page_fault耗时异常,那就可以通过日志准确定位到某个异常的子函数,这样对我们定位问题是非常有用的。

  那用户态呢,有分析和记录用户态函数执行耗时的工具么?答案是当然有,优秀的工具凤毛麟角,uftrace百里挑一!

 三、uftrace简介

  uftrace是一个分析C/C++/Rust用户态程序性能,且支持多线程性能分析的开源工具。

(1)安装简介

  1、获取最新Release安装包,https://github.com/namhyung/uftrace

  2、解压安装包,执行相关配置,编译,安装命令  

  

  

  最后,在指定的安装目录:/usr/local/uftrace下,有如下目录,uftrace的可执行程序就位于bin目录下。

  

(2)uftrace初露锋芒

  如何使用uftrace呢?通过一个demo,我们先来实践一下uftrace的基本使用方法,以及初步介绍uftrace的时延分析方法。

  有如下malloc多线程程序,每一个线程执行10S,且在每一个线程里会不断的多次重复执行malloc()和free()。

  另外:

  1、编译选项里要添加参数 -pg(使编译器在函数入口插入对mcount()桩函数的调用,从而实现每个函数的耗时记录);

  2、去掉所有优化选项-O,否则对于庞大的程序,uftrace只能展示优化后代码的串行执行顺序,非常不利于我们对照代码进行问题排查。

 1 #include <iostream>
 2 #include <pthread.h>
 3 #include <unistd.h>
 4 #include <sys/syscall.h>  
 5 
 6 using namespace std;
 7 
 8 #define gettid() syscall(__NR_gettid)
 9 
10 // g++ -g -pg -o multi_thread_malloc -std=c++11 multi_thread_malloc.c -lpthread
11 
12 void *malloc_time_delay(void *para)
13 {
14     int last_time;
15     int current_time;
16     int malloc_times = 0;
17     char *ptr;
18     
19     last_time = time(NULL);
20     current_time = time(NULL);
21     
22     //cout << "thread id:" << gettid() << ", malloc test begin" << endl << endl;
23     
24     while(1) {
25     
26         if(current_time - last_time >= 10) {
27             break;
28         }
29 
30         ptr = (char *)malloc(1024);
31         if(ptr == NULL) {
32             cout << "malloc error" << endl;
33             break;
34         }
35 
36         free(ptr);
37 
38         malloc_times++;
39         current_time = time(NULL);
40         usleep(1000);
41     }
42 
43     cout << "thread id:" << gettid() << ", malloc test over, malloc total times:" << malloc_times << endl;
44     pthread_exit(NULL);
45 }
46 
47 int main(int argc, char *argv[])
48 {
49     int cnt;
50     int max_thread_num = 2;
51 
52     pthread_t thread_ptr[8];
53     
54     for(cnt = 0; cnt < max_thread_num; ++cnt) {
55         if(pthread_create(&thread_ptr[cnt], NULL, malloc_time_delay, NULL)) {
56             cout << "malloc_time_delay thread create failed" << endl;
57             return -1;
58         }
59     }
60 
61     for(cnt = 0; cnt < max_thread_num; ++cnt) {
62         pthread_join(thread_ptr[cnt], NULL);
63     }
64 
65     return 0;
66 }

  执行如图6所示的命令:

  /usr/local/uftrace/bin/uftrace --time record ./multi_thread_malloc

  --time表示记录时间信息,record表示使用分析模式。程序执行完毕后,会在当前目录生成uftrace.data目录,里面记录了程序运行过程中采集到的相关信息。当然,也可以在程序运行过程中通过:kill -15 $(pid)结束程序,uftrace会保存程序结束前的采集数据,方便我们分析长时间运行的程序。

 图 6

  通过replay模式查看multi_thread_malloc程序的时延信息。虽然记录了函数调用链和每个函数的执行时间,是不是总感觉乱乱的?那是因为uftrace把多线程的所有信息按时间顺序串行记录啦,导致记录信息有交叉。

  结果截取片段:

图 7

  升级一下命令,增加对线程id的过滤,一下子就清爽了很多,只展示了thread-8374随时间串行执行的执行链(结果只截取了片段)。

  /usr/local/uftrace/bin/uftrace replay -f time,delta,duration,tid --tid 8374

图 8

  但是问题又来了,我怎么知道众多的malloc函数里是否存在超长执行的情况呢?

  那就使用时间过滤条件和函数过滤条件试试,比如查看是否存在执行耗时大于1us的malloc函数。

  /usr/local/uftrace/bin/uftrace replay -f time,delta,duration,tid --tid 8374 -t 1us --filter=malloc

  结果截取片段:

图 9

   问题又来了,受屏幕限制,没法查看完所有的malloc函数执行耗时。那就把过滤后的结果进行重定向吧,通过文档查看所有的malloc执行耗时。

  /usr/local/uftrace/bin/uftrace replay -f time,delta,duration,tid --tid 8374 -t 1us > thread-8374.log


图 10

  multi_thread_malloc只是一个简单示例,通过uftrace分析,还真有波动较大的malloc执行耗时,不过都是us级,影响不大。但是uftrace提供的这种分析能力,可以让我们有机会发现大型,多线程用户态程序在运行过程中的明显耗时异常,从而挖掘出业务代码中隐藏的深坑,这与内核使用的ftrace异曲同工!

四、uftrace大显身手

  我的代码逻辑没有问题(高光时刻)!看似很OK,现实很打脸(尬得一匹)。其实逻辑没有问题,不代表就是一份好代码。

  接下来说说uftrace在我们业务系统中解决的问题吧。

(1)背景

  我们有一个大型的多线程应用程序,代码量大,框架逻辑非常复杂。基于历史原因,该进程里同时有一个超时检测线程,专门用于检测是否存在任务运行超时、超长的工作线程,如果有工作线程出现超时运行情况,就会产生相关告警信息。

(2)问题现象

  家里测试千百遍,运行很爽,很流畅,从未报线程运行超时。但是用户现场却不定时,不定期的出现任务线程超时告警,少的时候一两个,多的时候所有线程都在报任务超时,且超时都是秒级往上。好比监测中的心电图,突然出现一个突刺冲高,或者多个线程某段时间集体冲高,简直要了老命。

  线程超时少发时,还能扛住现场业务流量。线程超时多发时,完全cover不住,丢包严重,造成了非常大的负面影响。老板说:“解决不了就给我立马滚蛋!”

(3)尝试的解决手段

  问题很棘手,脑瓜嗡嗡嗡。

  1、尝试过perf,但是perf是基于长时间的综合统计,根本不知道偶发的超时突刺出现在哪;

  2、尝试过pstack,可是这个命令又卡又慢,等返回信息时,采集到的说不定是已经超时后的栈信息;

  3、尝试过超时出现时,主动抛异常,并打印堆栈信息。看似有希望,但是捕捉到的有可能只是其中一段信息,或者说是超时执行链中的某一小段,信息量太少,根本没法站在更宏观的角度去观察问题所在。

  确实,对于这种偶发性超时突刺问题,之前并没有发现比较好的定位手段。

  4、带着相关问题搜索了很多信息,gprof是第一个我觉得有希望解决问题的工具,它是一个程序运行时间监测工具,可以统计出各个函数的调用次数、时间、以及产生函数的调用图。但是使用后发现一个致命缺点:不支持多线程,只有放弃。

  5、继续带着相关关键词,github搜索一通,终于发现了uftrace,作用跟gprof类似,且完全支持多线程。写了一个前文第三部分,第(2)节“uftrace初露锋芒”的demo,经过验证后,得到了上面例举图片的结果。心中一阵莫名的惊喜,至少我有了可以尝试定位问题的手段。

(4)问题举例

  问题1:循环+树遍历

  使用uftrace运行我们的被监测程序,出现超时告警日志后,手动Kill掉程序,得到了过去一段时间该进程的所有运行日志。

  心情忐忑又焦急,一是担心抓不到问题点;二是日志量很大,需要使用前面的相关过滤手段产生日志文件,然后再把日志切割分片(split命令,推荐)为很多小份日志(日志大了电脑没法打开了。。。),最后才能详细的研究相关日志。

  功夫不负有心人,先上图(图比较宽,受排版影响,有些模糊)。

图 11

图 12

  抓取结果如上图所示,执行链实在太长,只截取了开始和结束的部分信息。

  日志说明:

  1、上帝角度。函数调用链非常清楚。线程ID:68963随时间递增的执行过程打印得明明白白。

  2、问题证据。记录了函数StreamTcpReassembleAppLayer的执行耗时为4.542秒,直接把线程68963卡死。

  3、耗时原因。在函数里有一个while循环,第一次while循环时,SBB_RB_NEXT函数只执行了1次;第二次while循环时,SBB_RB_NEXT执行了2次;依次递增,到最后一次while循环时,SBB_RB_NEXT执行了8800+次;整个while循环里,执行了300多万次SBB_RB_NEXT!

  站在上帝的视角,发现问题的同时还抓到了证据!这就是uftrace的价值所在!

  问题分析:

  1、为什么家里不会复现?家里的测试数据,while执行几十,上百次到顶。通常也是几百或者上千微秒,耗时很短,问题根本无法复现。

  2、耗时原因是什么?数据重组导致外层按数据总个数循环,每一次的外层循环再嵌套了一次里层按偏移遍历树的操作,导致出现N*M的操作。万万没想到居然还有这种流量存在,也进一步说明了当前使用的数据结构存在缺陷。

  解决方法:

  现场的快速解决方式:某个异常数据超过设定的阈值后直接砍断。业务先恢复了再说,哈哈哈!

 

  问题2:系统调用access

  自从解决了上面的重大问题,现场业务再也没有出现过超时告警了,老板最近也不叫我立马滚蛋了。可是好景不长,线程超时告警又出现了,不过这次是出现在家里,还用不着被老板威胁!

  再次秀出独门绝技,三下五除二就把问题给捉住了,老板扬起嘴角笑了笑并说道:“下午请你吃个饼”,但是闭口不提涨工资。

  结果如下图所示:

图 13

  日志说明:

  使用了条件“-t 2”,查看日志里是否存在大于2s以上的函数耗时。结果还真有,居然是access,真是想不到是它!

  问题分析:

  为什么access系统调用造成了长延时?后来才知道是有位兄弟会使劲的往某个目录下新增文件,一个目录下居然达到了上万个文件,然后某个时候又会去读取其中的某个文件,结果就是卡死!还是印证了前面提到过的一句话:代码没问题不代表就是份好代码。

  解决方法:

  写文件改为写数据库,把所有要保存的文件信息按id存到数据库。

 

  uftrace使用技巧tip:

  record模式

  1、使用-t,只记录大于时间参数t的执行日志,相当于检查业务运行过程中是否存在偶发时延,可以减少很多日志量。

  在摸石头过河的初期,先使用 -t 比较好,因为我们不知道时延发生在哪里,如果真的有大于等于 -t 的时延存在,时延被记录的同时,也会减少非常多的日志。

  2、使用-F,只记录上一步通过 -t 得到的,发生大时延的函数,可以过滤掉很多不相关,对分析没有帮助的日志。

  先使用 -t 捕捉到时延发生时的调用链,再使用-F跑一次,得到时延发生时的详细日志。

  

  replay模式

  1、可以使用-t,查看结果中是否存在大于指定时间参数以上的运行函数,帮助我们快速定位时延位置;

  2、可以使用-r,快速获取指定时间范围内的日志信息,也是一种很好的过滤能力。这个能力,我给社区提过一个BUG,且得到了修正。https://github.com/namhyung/uftrace/issues/1593

  其它技巧,uftrace安装包的 /doc/uftrace.html文档有很详细的介绍,真的写得非常好,朋友们可以多多参考。

五、结语

  通常,我们很难观察到业务程序的偶发时延,想挖掘出偶发时延的背后原因更是难上加难!万幸还有uftrace的存在,可以针对性的解决这一类问题。

  现实中总有很多意料之外的事情,不仅仅是技术,生活亦是如此,即使我们在这之前已经做了很充分的准备工作。遇到问题时只要我们不放弃,且经得住持久战,那解决问题后的成就感就会让自己觉得之前的所有付出都是值得的。

  技术是不断实践积累的,在此分享出来与大家一起共勉!

  如果文章对你有些许帮助,还请各位技术爱好者登录点赞呀,非常感谢!

 

   本文来自博客园,作者:T-BARBARIANS,博文严禁转载,转载必究!

posted @ 2023-03-06 09:45  T-BARBARIANS  阅读(1805)  评论(4编辑  收藏  举报