eBPF 性能之颠 -- 函数执行耗时追踪

2023-02-20 10:52:52 浏览数 (2)

背景

对于算法和数据结构应该大家都不陌生,在这门学科的语境里我们用 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 写的)。

代码语言:javascript复制
#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;
}

编译并执行

代码语言:javascript复制
# 编译
mkdir build
cd build
cmake ..
cmake --build .

# 执行
./cpps 
hello world 0 .
hello world 1 .
hello world 2 .

假设我们就是要确认一下 hello 这个函数,每执行一次耗时是多久。以前难于上青天,现在用 eBPF 就是分分钟的事。



eBPF 框架实践

bpftrace 的语法与其它的编程语言有着比较大的差异,但是用多了就习惯了,总的来讲它的模式是这样的。

代码语言:javascript复制
probe /condition/ { action }

probe 就是我们追踪项, condition 指的过滤条件(只追踪特定 pid 也可以不指定),action 对应捕捉到之后要执行的动作。我默认在座的各位已经懂了(没有懂也没有事,后面会给语言的官方文档),那我们直接上 bpftrace 的代码,我尽可能多写点注释。

代码语言:javascript复制
#!/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) nn", (nsecs - @start[comm]));
    delete(@start[comm]);
}

// 追踪退出时要执行 action 
END 
{
    printf("**** end trace cpps.hello function time cost   ****n");
    clear(@start);
}

现在 bpftrace 程序已经写好了,执行一下它就能把我们意图传递给 kernel ,追踪现在是如此简单。

1. 执行 bpftrace 追踪程序

代码语言:javascript复制
bpftrace trace-hello-func-time-cost.bt

2. 执行我们刚才的 C 程序

代码语言:javascript复制
/data/repos/cpp-20/build/cpps

3. 追踪程序会打印 hello 函数的执行耗时

代码语言:javascript复制
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 就直说了,我想涨粉帮忙点下关注!我的技术文章质量还可以,关注应该不亏。

“在看” “分享” “点赞” “收藏” 也是我继续写下去的动力;再次感谢!!!

0 人点赞