[转帖]《Linux性能优化实战》笔记(24)—— 动态追踪 DTrace

linux,性能,优化,实战,笔记,动态,追踪,dtrace · 浏览次数 : 0

小编点评

## 1. eBPF 追踪的工作原理 eBPF 是一个新的动态追踪技术,它可以用来替代传统的 ftrace 和 perf,提供更高性能的工具。 eBPF 使用了内核中的数据结构来进行数据操作,这使得它比 ftrace 和 perf 更高效。 eBPF 的工作原理如下: * 首先, eBPF 读取内核中的数据结构,这些数据结构包含了函数的参数和返回值。 * 然后, eBPF 使用这些数据结构来进行数据操作,例如调用函数、读取文件等。 * 最后, eBPF 发送数据给用户,用户可以查看数据。 ## 2. eBPF 的优点 * 更高效:eBPF 可以利用内核中的数据结构来进行数据操作,这使得它比 ftrace 和 perf 更高效。 * 更安全:eBPF 可以使用内核中的安全机制来保护用户数据,例如防止内存访问错误。 * 更灵活:eBPF 可以使用不同的数据结构来进行数据操作,例如可以使用内存结构来存储数据。 ## 3. eBPF 的缺点 * 仍然是一个相对较新的技术,与 ftrace 和 perf 相对于,它仍然存在一些问题。 * eBPF 的性能可能与 ftrace 和 perf 相对有所不同。 ## 4. eBPF 的应用场景 * 动态跟踪内核中的各种函数和事件。 * 动态跟踪用户之间的数据。 * 动态跟踪容器中的数据。 ## 5. 选择追踪工具的建议 * 如果您需要一个更高性能的工具,可以选择 eBPF。 * 如果您需要一个更安全的工具,可以选择 eBPF。 * 如果您需要一个更灵活的工具,可以选择 eBPF。 ## 6. 总结 eBPF 是一个新的动态追踪技术,它提供更高性能的工具,但是仍存在一些问题。 eBPF 的应用场景包括动态跟踪内核中的各种函数和事件,动态跟踪用户之间的数据,动态跟踪容器中的数据。 选择 eBPF 的工具,应该根据您的具体工作原理进行选择。

正文

使用 perf 对系统内核线程进行分析时,内核线程依然还在正常运行中,所以这种方法也被称为动态追踪技术。动态追踪技术通过探针机制来采集内核或者应用程序的运行信息,从而可以不用修改内核和应用程序的代码就获得丰富的信息,帮你分析、定位想要排查的问题。

以往,在排查和调试性能问题时,我们往往需要先为应用程序设置一系列的断点(比如使用GDB),然后以手动或者脚本(比如 GDB 的 Python 扩展)的方式,在这些断点处分析应用程序的状态。或者,增加一系列的日志,从日志中寻找线索。不过,断点往往会中断应用的正常运行;而增加新的日志,往往需要重新编译和部署。这些方法虽然在今天依然广泛使用,但在排查复杂的性能问题时,往往耗时耗力,更会对应用的正常运行造成巨大影响。此外,这类方式还有大量的性能问题。比如,出现的概率小,只有线上环境才能碰到。这种难以复现的问题,亦是一个巨大挑战。

动态追踪技术的出现,就为这些问题提供了完美的方案:它既不需要停止服务,也不需要修改应用程序的代码;所有一切还按照原来的方式正常运行时,就可以帮你分析出问题的根源。同时,相比以往的进程级跟踪方法(比如 ptrace),动态追踪往往只会带来很小的性能损耗(通常在 5% 或者更少)。

 

一、 动态追踪

说到动态追踪(Dynamic Tracing),就不得不提源于 Solaris 系统的 DTrace。DTrace 是动态追踪技术的鼻祖,它提供了一个通用的观测框架,并可以使用 D 语言进行自由扩展。

DTrace 的工作原理如下图所示。它的运行常驻在内核中,用户可以通过 dtrace 命令,把D 语言编写的追踪脚本,提交到内核中的运行时来执行。DTrace 可以跟踪用户态和内核态的所有事件,并通过一些列的优化措施,保证最小的性能开销。

虽然直到今天,DTrace 本身依然无法在 Linux 中运行,但它同样对 Linux 动态追踪产生了巨大的影响。很多工程师都尝试过把 DTrace 移植到 Linux 中,这其中,最著名的就是RedHat 主推的 SystemTap。同 DTrace 一样,SystemTap 也定义了一种类似的脚本语言,方便用户根据需要自由扩展。不过,不同于 DTrace,SystemTap 并没有常驻内核的运行时,它需要先把脚本编译为内核模块,然后再插入到内核中执行。这也导致 SystemTap 启动比较缓慢,并且依赖于完整的调试符号表。

总的来说,为了追踪内核或用户空间的事件,Dtrace 和 SystemTap 都会把用户传入的追踪处理函数(一般称为 Action),关联到被称为探针的检测点上。这些探针,实际上也就是各种动态追踪技术所依赖的事件源。

 

1. 动态追踪的事件源

根据事件类型的不同,动态追踪所使用的事件源可以分为静态探针、动态探针以及硬件事件等三类。它们的关系如下图所示

硬件事件通常由性能监控计数器 PMC(Performance Monitoring Counter)产生,包括了各种硬件的性能情况,比如 CPU 的缓存、指令周期、分支预测等等。

静态探针,是指事先在代码中定义好,并编译到应用程序或者内核中的探针。这些探针只有在开启探测功能时,才会被执行到;未开启时并不会执行。常见的静态探针包括内核中的跟踪点(tracepoints)和 USDT(Userland Statically Defined Tracing)探针。

  • 跟踪点(tracepoints),实际上就是在源码中插入的一些带有控制条件的探测点,这些探测点允许事后再添加处理函数。比如在内核中,最常见的静态跟踪方法就是 printk,即输出日志。Linux 内核定义了大量的跟踪点,可以通过内核编译选项,来开启或者关闭。
  • USDT 探针,全称是用户级静态定义跟踪,需要在源码中插入 DTRACE_PROBE() 代码,并编译到应用程序中。不过,也有很多应用程序内置了 USDT 探针,比如MySQL、PostgreSQL 等。

动态探针,则是指没有事先在代码中定义,但却可以在运行时动态添加的探针,比如函数的调用和返回等。动态探针支持按需在内核或者应用程序中添加探测点,具有更高的灵活性。常见的动态探针有两种,即用于内核态的 kprobes 和用于用户态的 uprobes。

  • kprobes 用来跟踪内核态的函数,包括用于函数调用的 kprobe 和用于函数返回的kretprobe。
  • uprobes 用来跟踪用户态的函数,包括用于函数调用的 uprobe 和用于函数返回的uretprobe。
  • kprobes 需要内核编译时开启 CONFIG_KPROBE_EVENTS;而uprobes 则需要内核编译时开启 CONFIG_UPROBE_EVENTS。

2. 动态追踪机制

在这些探针的基础上,Linux 也提供了一系列的动态追踪机制,比如 ftrace、perf、eBPF 等。

ftrace 最早用于函数跟踪,后来又扩展支持了各种事件跟踪功能。ftrace 的使用接口跟我们之前提到的 procfs 类似,它通过 debugfs(4.1 以后也支持 tracefs),以普通文件的形式,向用户空间提供访问接口。这样,不需要额外的工具,你就可以通过挂载点(通常为 /sys/kernel/debug/tracing 目录)内的文件读写,来跟 ftrace 交互,跟踪内核或者应用程序的运行事件。

perf 是我们的老朋友了,我们在前面的好多案例中,都使用了它的事件记录和分析功能,这实际上只是一种最简单的静态跟踪机制。你也可以通过 perf ,来自定义动态事件(perf probe),只关注真正感兴趣的事件。

eBPF 则在 BPF(Berkeley Packet Filter)的基础上扩展而来,不仅支持事件跟踪机制,还可以通过自定义的 BPF 代码(使用 C 语言)来自由扩展。所以,eBPF 实际上就是常驻于内核的运行时,可以说就是 Linux 版的 DTrace。

除此之外,还有很多内核外的工具,也提供了丰富的动态追踪功能。最常见的就是前面提到的 SystemTap,我们之前多次使用过的 BCC(BPF Compiler Collection),以及常用于容器性能分析的 sysdig 等。在分析大量事件时,使用我们上节课提到的火焰图,可以将大量数据可视化展示,让你更直观发现潜在的问题。

接下来,我就通过几个例子,带你来看看,要怎么使用这些机制,来动态追踪内核和应用程序的执行情况。
 

二、 ftrace

我们先来看 ftrace。刚刚提到过,ftrace 通过 debugfs(或者 tracefs),为用户空间提供接口。所以使用 ftrace,往往是从切换到 debugfs 的挂载点开始。

  1. $ cd /sys/kernel/debug/tracing
  2. $ ls
  3. README instances set_ftrace_notrace trace_marker_raw
  4. available_events kprobe_events set_ftrace_pid trace_options
  5. ...

如果这个目录不存在,则说明你的系统还没有挂载 debugfs,你可以执行下面的命令来挂载它:

mount -t debugfs nodev /sys/kernel/debug

ftrace 提供了多个跟踪器,用于跟踪不同类型的信息,比如函数调用、中断关闭、进程调度等。具体支持的跟踪器取决于系统配置,你可以执行下面的命令,来查询所有支持的跟踪器。

  1. $ cat available_tracers
  2. hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop

这其中,function 表示跟踪函数的执行,function_graph 则是跟踪函数的调用关系,也就是生成直观的调用关系图。这便是最常用的两种跟踪器。除了跟踪器外,使用 ftrace 前,还需要确认跟踪目标,包括内核函数和内核事件。其中,函数就是内核中的函数名。而事件,则是内核源码中预先定义的跟踪点。同样地,你可以执行下面的命令,来查询支持的函数和事件:

  1. $ cat available_filter_functions
  2. $ cat available_events

接下来,我就以 ls 命令为例,带你一起看看 ftrace 的使用方法。

为了列出文件,ls 命令会通过 open 系统调用打开目录文件,而 open 在内核中对应的函数名为 do_sys_open。 所以,我们要做的第一步,就是把要跟踪的函数设置为do_sys_open:

echo do_sys_open > set_graph_function

接下来,第二步,配置跟踪选项,开启函数调用跟踪,并跟踪调用进程:

  1. echo function_graph > current_tracer
  2. echo funcgraph-proc > trace_options

接着,第三步,也就是开启跟踪:

echo 1 > tracing_on

第四步,执行一个 ls 命令后,再关闭跟踪:

  1. ls
  2. echo 0 > tracing_on

第五步,也是最后一步,查看跟踪结果:

  1. cat trace
  2. # tracer: function_graph
  3. #
  4. # CPU TASK/PID DURATION FUNCTION CALLS
  5. # | | | | | | | | |
  6. 0) ls-12276 | | do_sys_open() {
  7. 0) ls-12276 | | getname() {
  8. 0) ls-12276 | | getname_flags() {
  9. 0) ls-12276 | | kmem_cache_alloc() {
  10. 0) ls-12276 | | _cond_resched() {
  11. 0) ls-12276 | 0.049 us | rcu_all_qs();
  12. 0) ls-12276 | 0.791 us | }
  13. 0) ls-12276 | 0.041 us | should_failslab();
  14. 0) ls-12276 | 0.040 us | prefetch_freepointer();
  15. 0) ls-12276 | 0.039 us | memcg_kmem_put_cache();
  16. 0) ls-12276 | 2.895 us | }
  17. 0) ls-12276 | | __check_object_size() {
  18. 0) ls-12276 | 0.067 us | __virt_addr_valid();
  19. 0) ls-12276 | 0.044 us | __check_heap_object();
  20. 0) ls-12276 | 0.039 us | check_stack_object();
  21. 0) ls-12276 | 1.570 us | }
  22. 0) ls-12276 | 5.790 us | }
  23. 0) ls-12276 | 6.325 us | }
  24. ...

在最后得到的输出中:

  • 第一列表示运行的 CPU;
  • 第二列是任务名称和进程 PID;
  • 第三列是函数执行延迟;
  • 最后一列,则是函数调用关系图,通过不同级别的缩进,直观展示了各函数间的调用关系。

当然,我想你应该也发现了 ftrace 的使用缺点——五个步骤实在是麻烦,用起来并不方便。不过,不用担心, trace-cmd 已经帮你把这些步骤给包装了起来。这样,你就可以在同一个命令行工具里,完成上述所有过程。

你可以执行下面的命令,来安装 trace-cmd :

  1. # Ubuntu
  2. $ apt-get install trace-cmd
  3. # CentOS
  4. $ yum install trace-cmd

安装好后,原本的五步跟踪过程,就可以简化为下面这两步:

  1. $ trace-cmd record -p function_graph -g do_sys_open -O funcgraph-proc ls
  2. $ trace-cmd report
  3. ...
  4. ls-12418 [000] 85558.075341: funcgraph_entry: | do_sys_open() {
  5. ls-12418 [000] 85558.075363: funcgraph_entry: | getname() {
  6. ls-12418 [000] 85558.075364: funcgraph_entry: | getname_flags() {
  7. ls-12418 [000] 85558.075364: funcgraph_entry: | kmem_cache_alloc() {
  8. ls-12418 [000] 85558.075365: funcgraph_entry: | _cond_resched() {
  9. ls-12418 [000] 85558.075365: funcgraph_entry: 0.074 us | rcu_all_qs();
  10. ls-12418 [000] 85558.075366: funcgraph_exit: 1.143 us | }
  11. ls-12418 [000] 85558.075366: funcgraph_entry: 0.064 us | should_failslab();
  12. ls-12418 [000] 85558.075367: funcgraph_entry: 0.075 us | prefetch_freepointer();
  13. ls-12418 [000] 85558.075368: funcgraph_entry: 0.085 us | memcg_kmem_put_cache();
  14. ls-12418 [000] 85558.075369: funcgraph_exit: 4.447 us | }
  15. ls-12418 [000] 85558.075369: funcgraph_entry: | __check_object_size() {
  16. ls-12418 [000] 85558.075370: funcgraph_entry: 0.132 us | __virt_addr_valid();
  17. ls-12418 [000] 85558.075370: funcgraph_entry: 0.093 us | __check_heap_object();
  18. ls-12418 [000] 85558.075371: funcgraph_entry: 0.059 us | check_stack_object();
  19. ls-12418 [000] 85558.075372: funcgraph_exit: 2.323 us | }
  20. ls-12418 [000] 85558.075372: funcgraph_exit: 8.411 us | }
  21. ls-12418 [000] 85558.075373: funcgraph_exit: 9.195 us | }
  22. ...

你会发现,trace-cmd 的输出,跟上述 cat trace 的输出是类似的。通过这个例子我们知道,当你想要了解某个内核函数的调用过程时,使用 ftrace ,就可以跟踪到它的执行过程。

 

三、 perf

我们前面使用 perf record/top 时,都是先对事件进行采样,然后再根据采样数,评估各个函数的调用频率。实际上,perf 的功能远不止于此。比如,perf 可以用来分析 CPU cache、CPU 迁移、分支预测、指令周期等各种硬件事件,也可以只对感兴趣的事件进行动态追踪。

案例1,跟踪内核函数 do_sys_open

接下来,我们还是以内核函数 do_sys_open,以及用户空间函数 readline 为例,看一看perf 动态追踪的使用方法。可以通过 perf list ,查询所有支持的事件:

perf list

然后,在 perf 的各个子命令中添加 --event 选项,设置追踪感兴趣的事件。如果这些预定义的事件不满足实际需要,你还可以使用 perf probe 来动态添加。而且,除了追踪内核事件外,perf 还可以用来跟踪用户空间的函数。

我们先来看第一个 perf 示例,内核函数 do_sys_open 的例子。你可以执行 perf probe命令,添加 do_sys_open 探针

  1. $ perf probe --add do_sys_open
  2. Added new event:
  3. probe:do_sys_open (on do_sys_open)
  4. You can now use it in all perf tools, such as:
  5. perf record -e probe:do_sys_open -aR sleep 1

探针添加成功后,就可以在所有的 perf 子命令中使用。比如,下面就是一个 perf record 的示例,执行它就可以对 10s 内的 do_sys_open 进行采样:

  1. $ perf record -e probe:do_sys_open -aR sleep 10
  2. [ perf record: Woken up 1 times to write data ]
  3. [ perf record: Captured and wrote 0.148 MB perf.data (19 samples) ]

而采样成功后,就可以执行 perf script ,来查看采样结果了:

  1. perf script
  2. perf 12886 [000] 89565.879875: probe:do_sys_open: (ffffffffa807b290)
  3. sleep 12889 [000] 89565.880362: probe:do_sys_open: (ffffffffa807b290)
  4. sleep 12889 [000] 89565.880382: probe:do_sys_open: (ffffffffa807b290)
  5. sleep 12889 [000] 89565.880635: probe:do_sys_open: (ffffffffa807b290)
  6. sleep 12889 [000] 89565.880669: probe:do_sys_open: (ffffffffa807b290)

输出中,同样也列出了调用 do_sys_open 的任务名称、进程 PID 以及运行的 CPU 等信息。不过,对于 open 系统调用来说,只知道它被调用了并不够,我们需要知道的是,进程到底在打开哪些文件。所以,实际应用中,我们还希望追踪时能显示这些函数的参数。

对于内核函数来说,你当然可以去查看内核源码,找出它的所有参数。不过还有更简单的方法,那就是直接从调试符号表中查询。执行下面的命令,你就可以知道 do_sys_open 的所有参数:

  1. $ perf probe -V do_sys_open
  2. Available variables at do_sys_open
  3. @<do_sys_open+0>
  4. char* filename
  5. int dfd
  6. int flags
  7. struct open_flags op
  8. umode_t mode

从这儿可以看出,我们关心的文件路径,就是第一个字符指针参数(也就是字符串),参数名称为 filename。如果这个命令执行失败,就说明调试符号表还没有安装。那么,你可以执行下面的命令,安装调试信息后重试:

  1. # Ubuntu
  2. $ apt-get install linux-image-`uname -r`-dbgsym
  3. # CentOS
  4. $ yum --enablerepo=base-debuginfo install -y kernel-debuginfo-$(uname -r)

找出参数名称和类型后,就可以把参数加到探针中了。不过由于我们已经添加过同名探针,所以在这次添加前,需要先把旧探针给删掉:

  1. # 先删除旧的探针
  2. perf probe --del probe:do_sys_open
  3. # 添加带参数的探针
  4. $ perf probe --add 'do_sys_open filename:string'
  5. Added new event:
  6. probe:do_sys_open (on do_sys_open with filename:string)
  7. You can now use it in all perf tools, such as:
  8. perf record -e probe:do_sys_open -aR sleep 1

新的探针添加后,重新执行 record 和 script 子命令,采样并查看记录:

  1. # 重新采样记录
  2. $ perf record -e probe:do_sys_open -aR ls
  3. # 查看结果
  4. $ perf script
  5. perf 13593 [000] 91846.053622: probe:do_sys_open: (ffffffffa807b290) filename_string="/proc/13596/status"
  6. ls 13596 [000] 91846.053995: probe:do_sys_open: (ffffffffa807b290) filename_string="/etc/ld.so.cache"
  7. ls 13596 [000] 91846.054011: probe:do_sys_open: (ffffffffa807b290) filename_string="/lib/x86_64-linux-gnu/libselinux.so.1"
  8. ls 13596 [000] 91846.054066: probe:do_sys_open: (ffffffffa807b290) filename_string="/lib/x86_64-linux-gnu/libc.so.6”
  9. ...
  10. # 使用完成后不要忘记删除探针
  11. $ perf probe --del probe:do_sys_open

现在,你就可以看到每次调用 open 时打开的文件了。不过,这个结果是不是看着很熟悉呢?其实,在我们使用 strace 跟踪进程的系统调用时,也经常会看到这些动态库的影子。比如,使用 strace 跟踪 ls 时,你可以得到下面的结果:

  1. $ strace ls
  2. ...
  3. access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
  4. access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
  5. openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
  6. ...
  7. access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
  8. openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
  9. ...

你估计在想,既然 strace 也能得到类似结果,本身又容易操作,为什么我们还要用 perf呢?实际上,很多人只看到了 strace 简单易用的好处,却忽略了它对进程性能带来的影响。从原理上来说,strace 基于系统调用 ptrace 实现,这就带来了两个问题:

  • 由于 ptrace 是系统调用,就需要在内核态和用户态切换。当事件数量比较多时,繁忙的切换必然会影响原有服务的性能;
  • ptrace 需要借助 SIGSTOP 信号挂起目标进程。这种信号控制和进程挂起,会影响目标进程的行为。

所以,在性能敏感的应用(比如数据库)中,我并不推荐你用 strace (或者其他基于ptrace 的性能工具)去排查和调试。在 strace 的启发下,结合内核中的 utrace 机制, perf 也提供了一个 trace 子命令,是取代 strace 的首选工具。相对于 ptrace 机制来说,perf trace 基于内核事件,自然要比进程跟踪的性能好很多。

perf trace 的使用方法如下所示,跟 strace 其实很像:

  1. $ perf trace ls
  2. ? ( ): ls/14234 ... [continued]: execve()) = 0
  3. 0.177 ( 0.013 ms): ls/14234 brk( ) = 0x555d96be7000
  4. 0.224 ( 0.014 ms): ls/14234 access(filename: 0xad98082 ) = -1 ENOENT No such file or directory
  5. 0.248 ( 0.009 ms): ls/14234 access(filename: 0xad9add0, mode: R ) = -1 ENOENT No such file or directory
  6. 0.267 ( 0.012 ms): ls/14234 openat(dfd: CWD, filename: 0xad98428, flags: CLOEXEC ) = 3
  7. 0.288 ( 0.009 ms): ls/14234 fstat(fd: 3</usr/lib/locale/C.UTF-8/LC_NAME>, statbuf: 0x7ffd2015f230 ) = 0
  8. 0.305 ( 0.011 ms): ls/14234 mmap(len: 45560, prot: READ, flags: PRIVATE, fd: 3 ) = 0x7efe0af92000
  9. 0.324 Dockerfile test.sh
  10. ( 0.008 ms): ls/14234 close(fd: 3</usr/lib/locale/C.UTF-8/LC_NAME> ) = 0
  11. ...

不过,perf trace 还可以进行系统级的系统调用跟踪(即跟踪所有进程),而 strace 只能跟踪特定的进程。

 

案例2,跟踪库函数 readline

第二个 perf 的例子是用户空间的库函数。以 bash 调用的库函数 readline 为例,使用类似的方法,可以跟踪库函数的调用(基于 uprobes)。readline 的作用,是从终端中读取用户输入,并把这些数据返回调用方。所以,跟 open系统调用不同的是,我们更关注 readline 的调用结果。

我们执行下面的命令,通过 -x 指定 bash 二进制文件的路径,就可以动态跟踪库函数。这其实就是跟踪了所有用户在 bash 中执行的命令

  1. # 为/bin/bash添加readline探针
  2. $ perf probe -x /bin/bash 'readline%return +0($retval):string’
  3. # 采样记录
  4. $ perf record -e probe_bash:readline__return -aR sleep 5
  5. # 查看结果
  6. $ perf script
  7. bash 13348 [000] 93939.142576: probe_bash:readline__return: (5626ffac1610 <- 5626ffa46739) arg1="ls"
  8. # 跟踪完成后删除探针
  9. $ perf probe --del probe_bash:readline__return

当然,如果你不确定探针格式,也可以通过下面的命令,查询所有支持的函数和函数参数:

  1. # 查询所有的函数
  2. $ perf probe -x /bin/bash —funcs
  3. # 查询函数的参数
  4. $ perf probe -x /bin/bash -V readline
  5. Available variables at readline
  6. @<readline+0>
  7. char* prompt

跟内核函数类似,如果你想要查看普通应用的函数名称和参数,那么在应用程序的二进制文件中,同样需要包含调试信息。

 

四、 eBPF 和 BCC

ftrace 和 perf 的功能已经比较丰富了,不过,它们有一个共同的缺陷,那就是不够灵活,没法像 DTrace 那样通过脚本自由扩展。eBPF 就是 Linux 版的 DTrace,可以通过 C 语言自由扩展(这些扩展通过 LLVM 转换为 BPF 字节码后,加载到内核中执行)。下面这张图,就表示了 eBPF 追踪的工作原理:

从图中你可以看到,eBPF 的执行需要三步:

  • 从用户跟踪程序生成 BPF 字节码;
  • 加载到内核中运行;
  • 向用户空间输出结果。

所以,从使用上来说,eBPF 要比我们前面看到的 ftrace 和 perf 都更复杂。实际上,在 eBPF 执行过程中,编译、加载还有 maps 等操作,对所有的跟踪程序来说都是通用的。把这些过程通过 Python 抽象起来,也就诞生了 BCC(BPF Compiler
Collection)。

BCC 把 eBPF 中的各种事件源(比如 kprobe、uprobe、tracepoint 等)和数据操作(称为 Maps),也都转换成了 Python 接口(也支持 lua)。这样,使用 BCC 进行动态追踪时,编写简单的脚本就可以了。不过要注意,因为需要跟内核中的数据结构交互,真正核心的事件处理逻辑,还是需要我们用 C 语言来编写。

BCC安装方法如下:

  1. # Ubuntu
  2. sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
  3. echo "deb https://repo.iovisor.org/apt/$(lsb_release -cs) $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/iovisor.list
  4. sudo apt-get update
  5. sudo apt-get install bcc-tools libbcc-examples linux-headers-$(uname -r)
  6. # REHL 7.6
  7. yum install bcc-tools

安装后,BCC 会把所有示例(包括 Python 和 lua),放到 /usr/share/bcc/examples 目录中:

  1. $ ls /usr/share/bcc/examples
  2. hello_world.py lua networking tracing

接下来,还是以 do_sys_open 为例,我们一起来看看,如何用 eBPF 和 BCC 实现同样的动态跟踪。通常,我们可以把 BCC 应用拆分为下面这四个步骤。

第一,跟所有的 Python 模块使用方法一样,在使用之前,先导入要用到的模块:

from bcc import BPF

第二,需要定义事件以及处理事件的函数。这个函数需要用 C 语言来编写,作用是初始化刚才导入的 BPF 对象。这些用 C 语言编写的处理函数,要以字符串的形式送到 BPF 模块中处理:

  1. # define BPF program (""" is used for multi-line string).
  2. # '#' indicates comments for python, while '//' indicates comments for C.
  3. prog = """
  4. #include <uapi/linux/ptrace.h>
  5. #include <uapi/linux/limits.h>
  6. #include <linux/sched.h>
  7. // define output data structure in C
  8. struct data_t {
  9. u32 pid;
  10. u64 ts;
  11. char comm[TASK_COMM_LEN];
  12. char fname[NAME_MAX];
  13. };
  14. BPF_PERF_OUTPUT(events);
  15. // define the handler for do_sys_open.
  16. // ctx is required, while other params depends on traced function.
  17. int hello(struct pt_regs *ctx, int dfd, const char __user *filename, int flags){
  18. struct data_t data = {};
  19. data.pid = bpf_get_current_pid_tgid();
  20. data.ts = bpf_ktime_get_ns();
  21. if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0) {
  22. bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename);
  23. }
  24. events.perf_submit(ctx, &data, sizeof(data));
  25. return 0;
  26. }
  27. """
  28. # load BPF program
  29. b = BPF(text=prog)
  30. # attach the kprobe for do_sys_open, and set handler to hello
  31. b.attach_kprobe(event="do_sys_open", fn_name="hello")

第三步,是定义一个输出函数,并把输出函数跟 BPF 事件绑定:

  1. # process event
  2. start = 0
  3. def print_event(cpu, data, size):
  4. global start
  5. # event’s type is data_t
  6. event = b["events"].event(data)
  7. if start == 0:
  8. start = event.ts
  9. time_s = (float(event.ts - start)) / 1000000000
  10. print("%-18.9f %-16s %-6d %-16s" % (time_s, event.comm, event.pid, event.fname))
  11. # loop with callback to print_event
  12. b["events"].open_perf_buffer(print_event)

最后一步,就是执行事件循环,开始追踪 do_sys_open 的调用:

  1. # print header
  2. print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE”))
  3. # start the event polling loop
  4. while 1:
  5. try:
  6. b.perf_buffer_poll()
  7. except KeyboardInterrupt:
  8. exit()

把上面几个步骤的代码保存到 trace-open.py 中,然后就可以用 Python 来运行了。如果一切正常,你可以看到如下输出:

  1. $ python trace-open.py
  2. TIME(s) COMM PID FILE
  3. 0.000000000 irqbalance 1073 /proc/interrupts
  4. 0.000175401 irqbalance 1073 /proc/stat
  5. 0.000258802 irqbalance 1073 /proc/irq/9/smp_affinity
  6. 0.000290102 irqbalance 1073 /proc/irq/0/smp_affinity

从输出中,你可以看到 irqbalance 进程(你的环境中可能还会有其他进程)正在打开很多文件,而 irqbalance 依赖这些文件中读取的内容,来执行中断负载均衡。

通过这个简单的示例,你也可以发现,eBPF 和 BCC 的使用,其实比 ftrace 和 perf 有更高的门槛。想用 BCC 开发自己的动态跟踪程序,至少要熟悉 C 语言、Python 语言、被跟踪事件或函数的特征(比如内核函数的参数和返回格式)以及 eBPF 提供的各种数据操作方法。不过,因为强大的灵活性,虽然 eBPF 在使用上有一定的门槛,却也无法阻止它成为目前最热门、最受关注的动态追踪技术。

当然,BCC 软件包也内置了很多已经开发好的实用工具,默认安装到/usr/share/bcc/tools/ 目录中,使用场景如下图所示

这些工具,一般都可以直接拿来用。而在编写其他的动态追踪脚本时,它们也是最好的参考资料。不过,有一点需要你特别注意,很多 eBPF 的新特性,都需要比较新的内核版本(如下图所示)。如果某些工具无法运行,很可能就是因为使用了当前内核不支持的特性。

 

五、 SystemTap 和 sysdig

SystemTap 也是一种可以通过脚本进行自由扩展的动态追踪技术。在 eBPF 出现之前,SystemTap 是 Linux 系统中功能最接近 DTrace 的动态追踪机制。不过要注意,SystemTap 在很长时间以来都游离于内核之外。所以,从稳定性上来说,SystemTap 只在 RHEL 系统中好用,在其他系统中则容易出现各种异常问题。当然,反过来说,支持 3.x 等旧版本的内核,也是 SystemTap 相对于 eBPF的一个巨大优势。

sysdig 则是随着容器技术的普及而诞生的,主要用于容器的动态追踪。sysdig 汇集了一些列性能工具的优势,可以说是集百家之所长。我习惯用这个公式来表示 sysdig 的特点:sysdig = strace + tcpdump + htop + iftop + lsof + docker inspect。而在最新的版本中(内核版本 >= 4.14),sysdig 还可以通过 eBPF 来进行扩展,所以,也可以用来追踪内核中的各种函数和事件。

 

六、 如何选择追踪工具

到这里,你可能又觉得头大了,这么多动态追踪工具,在实际场景中到底该怎么选择呢?还是那句话,具体性能工具的选择,就要从具体的工作原理来入手。在这里,我也总结了几个常见的动态追踪使用场景,并且分别推荐了适合的工具。

与[转帖]《Linux性能优化实战》笔记(24)—— 动态追踪 DTrace相似的内容:

[转帖]《Linux性能优化实战》笔记(24)—— 动态追踪 DTrace

使用 perf 对系统内核线程进行分析时,内核线程依然还在正常运行中,所以这种方法也被称为动态追踪技术。动态追踪技术通过探针机制来采集内核或者应用程序的运行信息,从而可以不用修改内核和应用程序的代码就获得丰富的信息,帮你分析、定位想要排查的问题。 以往,在排查和调试性能问题时,我们往往需要先为应用程

[转帖]《Linux性能优化实战》笔记(一)—— 平均负载

最近在看极客时间的《Linux性能优化实战》课程,记录下学习内容。 一、 平均负载(Load Average) 1. 概念 我们都知道uptime命令的最后三列分别是过去 1 分钟、5 分钟、15 分钟系统的平均负载,到底平均负载是什么? 简单来说,平均负载是指单位时间内,系统处于可运行状态和不可中

[转帖]《Linux性能优化实战》笔记(二)—— CPU 上下文切换(上)

上一篇的最后一个例子,在多个进程竞争CPU时,我们看到每个进程实际上%usr部分只有20%多,70%多是在wait,但是load远远高于单个进程使用CPU达到100%。 这让我想到之前看的RWP公开课,里面有一篇连接池管理。为什么相同的业务量,起6千个连接(进程)远远要慢于200个连接,因为绝大多数

[转帖]《Linux性能优化实战》笔记(八)—— 内存是怎么工作的

一、 内存映射 我们通常所说的内存容量,指的是物理内存。物理内存也称为主存,大多数计算机用的主存都是动态随机访问内存(DRAM)。只有内核才可以直接访问物理内存。那么,进程要访问内存时,该怎么办呢? Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样,进程就可以

[转帖]《Linux性能优化实战》笔记(22)—— 网络丢包问题分析

所谓丢包,是指在网络数据的收发过程中,由于种种原因,数据包还没传输到应用程序中,就被丢弃了。这些被丢弃包的数量,除以总的传输包数,也就是我们常说的丢包率。丢包率是网络性能中最核心的指标之一。丢包通常会带来严重的性能下降,特别是对 TCP 来说,丢包通常意味着网络拥塞和重传,进而还会导致网络延迟增大、

[转帖]《Linux性能优化实战》笔记(23)—— 内核线程 CPU 利用率过高,perf 与 火焰图

在排查网络问题时,我们还经常碰到的一个问题,就是内核线程的 CPU 使用率很高。比如,在高并发的场景中,内核线程 ksoftirqd 的 CPU 使用率通常就会比较高。回顾一下前面学过的 CPU 和网络模块,你应该知道,这是网络收发的软中断导致的。 要分析 ksoftirqd 这类 CPU 使用率比

[转帖]《Linux性能优化实战》笔记(25)—— 总结:Linux 性能工具速查

一、 性能工具速查 在梳理性能工具之前,首先给你提一个问题,那就是,在什么情况下,我们才需要去查找、挑选性能工具呢? 其实在我看来,只有当你想了解某个性能指标,却不知道该怎么办的时候,才会想到,“要是有一个性能工具速查表就好了”这个问题。如果已知一个性能工具可用,我们更多会去查看这个工具的手册,找出

[转帖]《Linux性能优化实战》笔记(21)—— 网络性能优化思路

一、 确定优化目标 优化前,我会先问问自己,网络性能优化的目标是什么?实际上,虽然网络性能优化的整体目标,是降低网络延迟(如 RTT)和提高吞吐量(如BPS 和 PPS),但具体到不同应用中,每个指标的优化标准可能会不同,优先级顺序也大相径庭。 拿NAT 网关来说,由于其直接影响整个数据中心的网络出

[转帖]《Linux性能优化实战》笔记(十九)—— DNS 解析原理与故障案例分析

一、 域名与 DNS 解析 域名主要是为了方便让人记住,而 IP 地址是机器间的通信的真正机制。以 time.geekbang.org 为例,最后面的 org 是顶级域名,中间的 geekbang 是二级域名,而最左边的 time 则是三级域名。点(.)是所有域名的根,所有域名都以点作为后缀。 把域

[转帖]《Linux性能优化实战》笔记(20)—— 使用 tcpdump 和 Wireshark 分析网络流量

tcpdump 和 Wireshark 是最常用的网络抓包和分析工具,更是分析网络性能必不可少的利器。 tcpdump 仅支持命令行格式使用,常用在服务器中抓取和分析网络包。Wireshark 除了可以抓包,还提供了强大的图形界面和汇总分析工具,在分析复杂的网络情景时,尤为简单和实用。因而,在实际分