• Linux中常用的性能分析工具


    1 明确目标

    进行性能分析时,需要明确优化的目标,例如,是优化整体的性能,还是某个功能的性能。

    明确优化目标后就需要能够知道当前的性能瓶颈,性能消耗在什么地方,以及如何去衡量,这样也能够在优化过程中以及优化完成后用相同的方式去衡量效果。

    2 基础命令

    2.1 基本指标查看

    CPU

    • mpstat -P ALL 1:查看CPU的各种状态下的时间,可以用于查找CPU时间分布不均衡的问题
    • pidstat 1:展示进程的CPU使用情况
    • top:查看当前的负载以及占用CPU和内存较高的进程

    内存

    • free -m
    • vmstat 1:查看内存、swap、cpu等使用情况

    磁盘:

    • df -h:查看磁盘使用情况
    • iostat -xz 1:查看磁盘的读写情况

    错误

    • dmesg |tail:查看内核的相关日志
    • grep ERROR /var/log/messages:查看系统和服务的错误信息

    其他:

    • sar:用于输出系统行为的信息,例如,sar -w 1可以用于查看创建进程的速度
    2.2 线程

    上面的命令默认情况下都是查看进程的情况,现在很多程序都是多线程,因此,需要能够查看线程的情况。

    • top -H -p 6666:查看6666进程下面所有线程的cpu和内存情况
    • pidstat -p:后面既可以指定进程ID,也可以指定线程ID

    因此,通常的方式是,通过top查看到占用较高的某个线程,然后通过pidstat持续观察该线程的cpu占用情况,如果持续飙高,再通过日志或者代码定位该线程对应的逻辑模块。

    3 perf命令

    3.1 perf的基本原理

    每隔固定时间,CPU产生一个中断,看当前是哪个进程、哪个函数,就更新相应的进程和函数的计数器,通过这种定时采集的方式,就知道CPU有多少时间在某个进程或者某个函数上了。

    3.2 基本使用方法
    • perf list:查看可用的采样点
    • perf record:采集程序运行的特征,配合perf script生成trace的数据,然后就可以用于生成火焰图
    • perf top:可以查看函数级别的占用情况
    • perf probe:在可执行文件或者so种插入监测点,然后再通过perf record进行数据采集
    3.3 火焰图分析

    生成火焰图,除了使用perf工具,还需要生成火焰图的脚本:FlameGraph

    然后用下面的方式生成火焰图:

    # -F 100表示采样频率,每秒100次
    # -p 31955表示对31599进程进行采样
    # -g表示记录调用栈
    # sleep 180表示总共采样180s
    perf record -F 100 -p 31955 -g -- sleep 180
    perf script -i perf.data &> perf.unfold
    ./FlameGraph/stackcollapse-perf.pl perf.unfold &> perf.folded
    ./FlameGraph/flamegraph.pl perf.folded > perf.svg
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后就可以用浏览器打开perf.svg进行分析。

    火焰图的特点:

    • 每个小方块表示一个函数
    • 方块的颜色深浅没有关系,是随机取的颜色
    • 纵轴表示调用栈的深度,调用的方向是从下往上,也就是下面的函数调用了上面的函数,因此,函数越往上则越底层
    • 横轴的宽度表示占用时间,但是从左到右并不表示时间顺序,而只是按照字母表顺序排列

    怎么用火焰图查找性能问题呢?

    横轴的宽度表示占用时间,因此,方块越宽表示占用时间越多,所以,我们的目的就是找到比较宽的方块。

    在最下面找比较宽的方块,当找到一个比较宽的方块时,再在这个方块的上面的方块中找比较宽的方块,这样一步一步往上找,根据代码分析出性能出现问题的地方,然后对这部分代码进行优化。

    4 ftrace

    内核文档:ftrace - Function Tracer

    ftrace提供tracefs的接口供用户使用,4.1内核以前的版本,tracing所有的控制文件都在debugfs中,tracefs通常会挂载到/sys/kernel/debug/tracing,为了向后兼容,新的内核在挂载debugfs时,会同时将tracefs挂载到/sys/kernel/debug/tracing和/sys/kernel/tracing。

    • current_tracer:当前使用的tracer
    • available_tracers:可用的tracer类型
    • tracing_on:打开(1)和关闭(0)tracer
    • trace & trace_pipe:两个文件都可以查看tracer采集的数据,但是trace是个静态的文件,每次读取该文件展示的都是当前缓冲区中的数据,而trace_pipe是个动态的文件,每次读取只能读取到新的数据,已经读取的数据不会被输出
    • trace_options:控制数据采集的选项,例如,数据的展示和tracer的工作方式
    • options/:可用的选项,可以通过修改下面的文件控制,也可以将文件名写入trace_options控制
    • kprobe_events:开启kprobe_events
    • uprobe_events:uprobe tracers
    • uprobe_profile:
    • events/:所有的监测点,按照分组进行组织
    • available_events:可用的events,将上面events目录中的所有文件都列在这里
    • trace_marker:
    4.1 Tracers
    • function:可以用于监测所有的内核函数
    • function_graph:function类型的tracer只是在函数的入口添加监测点,而function_graph类型的tracer会在函数的入口和出口添加监测点,可以用于绘制函数调用关系图
    • blk:块设备的检测点,被blktrace命令使用
    • hwlat:用于检测硬件是否有延迟
    4.2 perf-tools

    从上面可以看到,ftrace基本就是通过tracefs提供操作的接口,但是里面的目录和文件又很多,而某个功能其实一般也只用到了几个文件而已,为了方便对某个功能的使用,perf-tools用shell脚本的方式对这些功能进行了封装,可以通过命令的方式使用。

    用例1:uprobe

    uprobe脚本可以对用户态的程序添加监测点:./uprobe 'p:/lib64/libdl-2.28.so:dlopen +0(%di):string'对libdl-2.28.so里面的dlopen添加监测点,当系统中有程序使用dlopen调用so时,就会打印一个事件,表明某个程序调用了dlopen,并且调用的so的路径。

    用例2:kprobe

    kprobe脚本可以对内核的函数添加监测点,可以添加监测点的函数列表位于/sys/kernel/debug/tracing/available_filter_functions中。使用./kprobe 'p:do_sys_open filename=+0(%si):string'可以监测open系统调用,并且打印出文件名。

    用例3:tracepoint

    tpoint脚本可以列出内核现在的tracepoint,然后进行监测。使用./tpoint syscalls:sys_enter_openat可以打印有哪些进程在调用打开文件。

    4.3 日常使用

    使用日志进行性能分析时,除了使用火焰图从整体上看时间的分布,经常会有需要看某个函数占用的耗时。

    perf-tools中的funcgraph脚本可以监测到函数从进入到出来的耗时,但是该功能只能对内核函数使用,无法对用户态使用。

    5 再看perf probe

    perf probe可以操作监测点,执行perf probe -x /lib64/libdl-2.28.so 'dlopen\@\@GLIBC_2.2.5'可以增加监测点probe_libdl:dlopen,然后通过perf record -e probe_libdl:dlopen -a -- sleep 5可以采集dlopen的调用。

    然后执行perf script可以直接查看采集到的数据:

    请添加图片描述

    通过对perf probe进行strace发现,它也是通过操作ftrace中的tracefs实现的。

    请添加图片描述

    perf也对ftrace进行了简单的封装,提供了perf ftrace命令,可以通过perf ftrace进行内核函数监测,使用命令perf ftrace -p 53901 -t function -T do_sys_open监测某个进程对open系统调用的情况。

    6 perf_event_open

    使用perf probe只是增加监测点,但是要获取实际的数据,就需要通过perf record命令,而perf record就是使用perf_event_open进行数据的获取。

    下面是perf_event_open中的示例程序,用于统计printf的指令的数量:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    // hw_event:perf_event_attr的指针
    // pid:要进行trace的进程
    // cpu:要进行trace的cpu
    static long perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
                                int cpu, int group_fd, unsigned long flags) {
        int ret;
    
        ret = syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags);
        return ret;
    }
    
    int main(int argc, char **argv) {
        struct perf_event_attr pe;
        long long count;
        int fd;
    
        memset(&pe, 0, sizeof(struct perf_event_attr));
        pe.type = PERF_TYPE_HARDWARE; // 指定事件类型
        pe.size = sizeof(struct perf_event_attr);
        pe.config = PERF_COUNT_HW_INSTRUCTIONS; // 失效指令?
        pe.disabled = 1; // 默认关闭,后续可以通过ioctl或者prctl进行开启
        pe.exclude_kernel = 1; // 忽略内核空间的事件
        pe.exclude_hv = 1; // 忽略hypervisor的事件
    
        // pid=0,表示对当前进程进行trace
        // cpu=-1,表示对所有cpu进行trace
        fd = perf_event_open(&pe, 0, -1, -1, 0);
        if (fd == -1) {
            fprintf(stderr, "Error opening leader %llx\n", pe.config);
            exit(EXIT_FAILURE);
        }
    
        // 通过ioctl控制perf_event的运行
        // PERF_EVENT_IOC_RESET:重置事件的统计值
        // PERF_EVENT_IOC_ENABLE:启用事件
        ioctl(fd, PERF_EVENT_IOC_RESET, 0);
        ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
    
        // 执行printf,这次统计的就是该printf的指令数量
        printf("Measuring instruction count for this printf\n");
    
        // PERF_EVENT_IOC_DISABLE:停用事件
        ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
    
        // 读取统计值
        read(fd, &count, sizeof(long long));
    
        printf("Used %lld instructions\n", count);
    
        close(fd);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    通过上面的示例可以看出,整个逻辑不复杂,就是通过perf_event_open打开一个fd,并且需要告知需要获取的是什么数据,然后通过ioctl开启内核统计,再执行用例并关闭统计,最后调用read读取需要的数据。

    因此,这里重要的是:我们需要获取什么数据,然后对应到perf_event_attr中的type和config。

    上面的代码只是对某个事件进行了统计,得到某个事件发生了多少次,这种对应了perf-tools和bcc-tools中的*count工具。但是,有时候我们除了想知道事件发生了多少次,还需要对事件的某个属性进行分类的统计,或者分析函数的调用栈,总之,希望可以得到更多详细的数据。

    这被称为“采样”:设定相应的频率/定时时间,当定时时间到,就会收集一些数据,然后进行相应的分析。

    在perf_event_attr中有几个跟采样相关的字段:

    • sample_period、sample_freq:采样周期是指多少个事件溢出一次,当溢出时,采集的数据就会放到mmap的buffer中;采样频率则通过时钟中断进行定时采样
    • sample_type:指定要采样的数据,PERF_SAMPLE_TIME(记录时间)、PERF_SAMPLE_READ(记录所有事件的计数)、PERF_SAMPLE_CPU(记录CPU编号)、PERF_SAMPLE_REGS_USER(记录用户态的寄存器的状态)、PERF_SAMPLE_STACK_USER(记录用户态的栈)
    • sample_regs_user:设置要采样的寄存器
    • sample_stack_user:设置要采样的栈大小

    用C程序计算C代码执行了多少条机器指令

    7 bcc & bpftrace

    由于eBPF程序的编写门槛太高,于是就出现了一些高级语言,能够帮助快速编写eBPF程序:

    • bcc:一套工具集,用户可以直接用高级语言(python、C++、lua)编写用户态程序
    • bpftrace:用户可以使用类似awk的方式编写eBPF程序

    总之,上述两种方式都是基于eBPF的跟踪工具,bcc可以用于编写复杂逻辑,而bpftrace可以用于快速编写单行程序。

    下面的bcc程序用于统计块IO的分布直方图:

    #!/usr/bin/env python
    
    from bcc import BPF
    from time import sleep
    
    # 定义加载到eBPF虚拟机的内核代码
    bpf_text = """
    #include 
    #include 
    
    struct proc_key_t {
        char name[TASK_COMM_LEN];
        u64 slot;
    };
    
    // 定义一个histogram类型的BPF map对象,它的名字为dist
    // key的类型为proc_key_t,里面包含进程的名称和计算范围
    BPF_HISTOGRAM(dist, struct proc_key_t);
    
    // 定义tracepoint的跟踪点
    // block:跟踪块这个类别
    // block_rq_issue:要跟踪的函数,这个函数的含义是发起IO请求
    // block/block_rq_issue位于/sys/kernel/tracing/events子目录中
    // 也可以通过perf list|grep block_rq_issue得到
    TRACEPOINT_PROBE(block, block_rq_issue)
    {
        // 从bytes参数中得到要读取的数据,然后求log
        struct proc_key_t key = {.slot = bpf_log2l(args->bytes / 1024)};
    
        // 从内核空间中读取进程名,然后保存到key中
        bpf_probe_read_kernel(&key.name, sizeof(key.name), args->comm);
    
        // 将刚才计算的值追加到直方图中
        dist.increment(key);
        return 0;
    }
    """
    
    # 将上述的内核代码载入,相当于执行bpf_prog_load
    b = BPF(text=bpf_text)
    
    print("Tracing block I/O... Hit Ctrl-C to end.")
    
    # trace until Ctrl-C
    dist = b.get_table("dist")
    
    try:
        sleep(99999999)
    except KeyboardInterrupt:
        # 打印直方图
        dist.print_log2_hist("Kbytes", "Process Name", section_print_fn=bytes.decode)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    从上面的例子中,我们知道,要想编写bcc程序,最关键是要知道要跟踪的点以及对获取的数据如何处理。例如,这里的内核代码是跟踪block_rq_issue这个tracepoint,然后根据参数得到对应的数据,再将数据计算后更新map,而在用户代码中只有map的获取和打印逻辑。

    下面的代码实现了跟上述代码一样的目的:获取块IO的调用直方图

    #!/usr/bin/bpftrace
    
    BEGIN
    {
        printf("Tracing block device I/O... Hit Ctrl-C to end.\n");
    }
    
    tracepoint:block:block_rq_issue
    {
        @[args->comm] = hist(args->bytes);
    }
    
    END
    {
        printf("\nI/O size (bytes) histograms by process name:");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • BEGIN表示加载完eBPF程序后执行的动作,通常用于打印表头或者提示信息。
    • tracepoint:block:block_rq_issue表示跟踪block:block_rq_issue的tracepoint
    • 当跟踪某个函数时,一个关键的点是,该函数有哪些参数,例如,这里使用的是args->bytes,那么,block_rq_issue是否有其他参数呢?通过bpftrace -vl tracepoint:block:block_rq_issue可以查看该跟踪点的所有参数
    • hist()是一个map函数,输出行以2次方的间隔开始
    • END在跟踪结束后执行

    8 关系

    上面提到了很多名词,例如,uprobe、kprobe、perf、perf_event_open等,下面对这些词汇的关系进行整理:

    请添加图片描述

    内核提供的能力:

    • kprobe:动态探针,用于跟踪内核函数
    • uprobe:动态探针,用于跟踪用户态函数
    • tracepoint:静态探针,内核已经预先定义了一些跟踪点,可以通过perf list tracepoint查看
    • usdt:
    • eBPF:扩展的BPF机制

    接口层:

    • ftrace:一套trace系统,对用户提供debugfs的接口
    • perf_event_open:系统调用,用于读取内核的统计和采样数据

    应用层:

    • perf/perf_events:用于性能分析,FlameGraph
    • trace-cmdtrace-cmd: A front-end for Ftrace
    • perf-tools:基于ftrace的分析工具
    • BCC:基于eBPF的用于开发内核跟踪程序的工具集,能够简化eBPF程序的编写,用户态的语言可以使用python、C++、lua
    • bcc-tools:使用bcc编写的工具
    • bpftrace:基于bcc进行更高层的抽象,能够使用类似于awk的语言快速编写eBPF工具,特别是单行程序

    参考文档

  • 相关阅读:
    HTTP协议初识·中篇
    优思学院:想成为质量工程师?了解质量工程师职责和能力是关键!
    B50 - 基于51单片机的儿童成长管理系统
    五、Maven-单一架构案例(业务功能:批复奏折,业务功能:登录检查,打包部署)
    【项目方案】DUBBO 负载均衡策之略哈希一致的应用
    解码自然语言处理之 Transformers
    Spring依赖注入源码解析(下)
    相机摄影入门技巧,数码摄影技巧大全
    [设计模式] 静态代理居然能解决这种问题,我惊讶了!
    2022年下半年软件设计师上午真题及答案解析
  • 原文地址:https://blog.csdn.net/ILOVEYOUXIAOWANGZI/article/details/133995814