进行性能分析时,需要明确优化的目标,例如,是优化整体的性能,还是某个功能的性能。
明确优化目标后就需要能够知道当前的性能瓶颈,性能消耗在什么地方,以及如何去衡量,这样也能够在优化过程中以及优化完成后用相同的方式去衡量效果。
CPU
内存
磁盘:
错误
其他:
上面的命令默认情况下都是查看进程的情况,现在很多程序都是多线程,因此,需要能够查看线程的情况。
因此,通常的方式是,通过top查看到占用较高的某个线程,然后通过pidstat持续观察该线程的cpu占用情况,如果持续飙高,再通过日志或者代码定位该线程对应的逻辑模块。
每隔固定时间,CPU产生一个中断,看当前是哪个进程、哪个函数,就更新相应的进程和函数的计数器,通过这种定时采集的方式,就知道CPU有多少时间在某个进程或者某个函数上了。
生成火焰图,除了使用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
然后就可以用浏览器打开perf.svg进行分析。
火焰图的特点:
怎么用火焰图查找性能问题呢?
横轴的宽度表示占用时间,因此,方块越宽表示占用时间越多,所以,我们的目的就是找到比较宽的方块。
在最下面找比较宽的方块,当找到一个比较宽的方块时,再在这个方块的上面的方块中找比较宽的方块,这样一步一步往上找,根据代码分析出性能出现问题的地方,然后对这部分代码进行优化。
内核文档:ftrace - Function Tracer
ftrace提供tracefs的接口供用户使用,4.1内核以前的版本,tracing所有的控制文件都在debugfs中,tracefs通常会挂载到/sys/kernel/debug/tracing,为了向后兼容,新的内核在挂载debugfs时,会同时将tracefs挂载到/sys/kernel/debug/tracing和/sys/kernel/tracing。
从上面可以看到,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
可以打印有哪些进程在调用打开文件。
使用日志进行性能分析时,除了使用火焰图从整体上看时间的分布,经常会有需要看某个函数占用的耗时。
perf-tools中的funcgraph脚本可以监测到函数从进入到出来的耗时,但是该功能只能对内核函数使用,无法对用户态使用。
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系统调用的情况。
使用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);
}
通过上面的示例可以看出,整个逻辑不复杂,就是通过perf_event_open打开一个fd,并且需要告知需要获取的是什么数据,然后通过ioctl开启内核统计,再执行用例并关闭统计,最后调用read读取需要的数据。
因此,这里重要的是:我们需要获取什么数据,然后对应到perf_event_attr中的type和config。
上面的代码只是对某个事件进行了统计,得到某个事件发生了多少次,这种对应了perf-tools和bcc-tools中的*count
工具。但是,有时候我们除了想知道事件发生了多少次,还需要对事件的某个属性进行分类的统计,或者分析函数的调用栈,总之,希望可以得到更多详细的数据。
这被称为“采样”:设定相应的频率/定时时间,当定时时间到,就会收集一些数据,然后进行相应的分析。
在perf_event_attr中有几个跟采样相关的字段:
用C程序计算C代码执行了多少条机器指令
由于eBPF程序的编写门槛太高,于是就出现了一些高级语言,能够帮助快速编写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)
从上面的例子中,我们知道,要想编写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:");
}
上面提到了很多名词,例如,uprobe、kprobe、perf、perf_event_open等,下面对这些词汇的关系进行整理:
内核提供的能力:
perf list tracepoint
查看接口层:
应用层: