背景
对于算法和数据结构应该大家都不陌生,在这门学科的语境里我们用 O(xxx) 来衡量算法的复杂度。但是实际的工作中性能工程师要回答的常常不是时间复杂度问题,而是 1、程序的哪个部分慢? 2、慢的部分,单次执行的耗时是多少?
对于第 1 个问题我以前的文章 完全掌握火焰图的作图原理&看图技巧 有回答过一些。这里我准备回答一下第 2 个问题,执行慢的函数,它到底有多慢呢?
读完这篇文档,你就能在不改一行代码的情况下,知道给定函数单次执行的耗时,并且是纳秒级别的精度。
原理
进程运行在操作系统(kernel)之上,也就是说进程干了什么,现在在干什么操作系统是清清楚楚的。
这样的话!问题 2 的解决方案就变成了告诉操作系统,让它帮忙盯一下给定函数在什么时候开始执行,并且什么时候执行完成。我们只要拿到这两个时间点就能算出特定函数单次执行的耗时。那我们怎么才能告诉操作系统呢?
Linux 内核里面有一个叫 eBPF 的框架,它是我们与内核对话的接口人。我们使用一门叫 bpftrace 的语言就可以和它对话,把我们想要做的事情告诉它。eBPF 的整体的架构如下。
ebpf 官方图片地址:https://ebpf.io/static/c53dfcbff6ea67a8f00896bd76e4c07c/c5f83/bpftrace.png
用 C++ 写一个 hello world
我以前的职位是 MySQL-DBA ,估计关注我这个号的读者多半也接触过 MySQL,考虑一大家都有 trace MySQL 的需求,我这里就用 C++ 写一段 hello world 程序抛砖引玉一下(MySQL是 C++ 写的)。
#include <unistd.h>
#include <iostream>
using namespace std;
int hello(int i)
{
cout<<"hello world " << i << " ." << endl;
// sleep(0);
return i;
}
int main(int argc, char *argv[])
{
for(int i = {0}; i < 3; i++)
{
hello(i);
}
return 0;
}
编译并执行
# 编译
mkdir build
cd build
cmake ..
cmake --build .
# 执行
./cpps
hello world 0 .
hello world 1 .
hello world 2 .
假设我们就是要确认一下 hello 这个函数,每执行一次耗时是多久。以前难于上青天,现在用 eBPF 就是分分钟的事。
eBPF 框架实践
bpftrace 的语法与其它的编程语言有着比较大的差异,但是用多了就习惯了,总的来讲它的模式是这样的。
probe /condition/ { action }
probe 就是我们追踪项, condition 指的过滤条件(只追踪特定 pid 也可以不指定),action 对应捕捉到之后要执行的动作。我默认在座的各位已经懂了(没有懂也没有事,后面会给语言的官方文档),那我们直接上 bpftrace 的代码,我尽可能多写点注释。
#!/usr/bin/env bpftrace
/*
追踪 /data/repos/cpp-20/build/cpps 程序 的 hello 函数的执行耗时
作者: 蒋乐兴
时间: 2022-02-17
*/
// BEGIN 和 END 是两个特别的 probe , 由于不需要过滤条件,所以 condition 部分就省略了
// 追踪启动时执行的 action
BEGIN
{
printf("**** start trace cpps.hello function time cost ****\n");
}
// 函数开始执行时对应的 action
uprobe:/data/repos/cpp-20/build/cpps:hello
{ // 记录一下开始执行的时候到 @start[cmmm] 变量中
print("enter function hello .");
@start[comm] = nsecs;
}
// 函数执行完成对应的 action
uretprobe:/data/repos/cpp-20/build/cpps:hello / @start[comm] /
{ // 取得函数返回时的时间,并计算耗时
// 清理 @start[comm] 变量为下一次执行做准备
print("exit function hello .");
printf("time-cost %d (ns) \n\n", (nsecs - @start[comm]));
delete(@start[comm]);
}
// 追踪退出时要执行 action
END
{
printf("**** end trace cpps.hello function time cost ****\n");
clear(@start);
}
现在 bpftrace 程序已经写好了,执行一下它就能把我们意图传递给 kernel ,追踪现在是如此简单。
1. 执行 bpftrace 追踪程序
bpftrace trace-hello-func-time-cost.bt
2. 执行我们刚才的 C++ 程序
/data/repos/cpp-20/build/cpps
3. 追踪程序会打印 hello 函数的执行耗时
Attaching 4 probes...
**** start trace cpps.hello function time cost ****
enter function hello .
exit function hello .
time-cost 30979 (ns)
enter function hello .
exit function hello .
time-cost 5490 (ns)
enter function hello .
exit function hello .
time-cost 4880 (ns)
^C**** end trace cpps.hello function time cost ****
最后
1.私信 "cpp-20" 我看到之后会给你发上面示例的源码。
2都到这里了,是时候图穷匕见了!我这人比较 real 就直说了,我想涨粉帮忙点下关注!我的技术文章质量还可以,关注应该不亏。
“在看” + “分享” + “点赞” + “收藏” 也是我继续写下去的动力;再次感谢!!!