基于BCC进行性能追踪
BCC是一个工具包,用来创建高效的、在内核中运行的性能追踪程序。它首先是一个编译器,能够将你写的代码编译为eBPF程序,此外,它还提供了大量有用的工具和样例
BCC的基础是eBPF,因此其大部分功能需要内核版本在4.1+才能使用,其提供的某些根据甚至需要4.4或4.6版本的内核。要编译和运行BCC程序,你除了需要安装bcc之外,还需要llvm/clang,以及内核头文件。
BCC的支持内核中多种追踪机制,包括kprobe、uprobe、tracepoint、USDT(User-level statically defined tracing probes ),能够监控块设备IO、TCP函数、文件系统操作、任何syscall、Node.js/Java/MySQL/libc等多种常用软件/平台定义的USDT。
在网络监控方面,BCC提供了分布式网桥、HTTP过滤器、快速封包丢弃、隧道监控等样例代码。
BCC通过以下途径让eBPF开发变得简单:
- 在C中使用kernel instrumentation,提供了LLVM的一个C wrapper
- 提供Python和Lua的前端
要使用BCC,你通常需要4.1或者更新版本的内核,并且启用以下内核配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
CONFIG_BPF=y CONFIG_BPF_SYSCALL=y # [optional, for tc filters] CONFIG_NET_CLS_BPF=m # [optional, for tc actions] CONFIG_NET_ACT_BPF=m CONFIG_BPF_JIT=y # [for Linux kernel versions 4.1 through 4.6] CONFIG_HAVE_BPF_JIT=y # [for Linux kernel versions 4.7 and later] CONFIG_HAVE_EBPF_JIT=y # [optional, for kprobes] CONFIG_BPF_EVENTS=y # Need kernel headers through /sys/kernel/kheaders.tar.xz CONFIG_IKHEADERS=y |
1 2 3 4 |
# Ubuntu sudo apt-get install bpfcc-tools linux-headers-$(uname -r) # CentOS yum install bcc-tools |
从0.10.0开始,BCC依赖libbpf,后者提供对BPF系统调用的包装函数,提供bpf.h / btf.h的UAPI头文件。
安装构建BCC需要的依赖,以及相关工具:
1 2 3 4 5 |
apt install -y bison build-essential cmake flex git libedit-dev \ python3-distutils llvm clang llvm-dev libclang-dev python zlib1g-dev libelf-dev libfl-dev apt install arping netperf iperf3 ln -s `which python3` /usr/bin/python |
编译并安装BCC:
1 2 3 4 5 6 7 8 9 10 11 |
git clone https://github.com/iovisor/bcc.git mkdir bcc/build; cd bcc/build cmake .. make sudo make install cmake -DPYTHON_CMD=python3 .. # build python3 binding pushd src/python/ make sudo make install |
BCC自带的工具位于 /usr/share/bcc/tools下,需要将其加入到PATH环境变量。
我们可以先了解以下BCC提供的各种工具,这些工具可以直接用来解决性能追踪等问题,也是发明新轮子的重要参考。
下图显示了工作在系统各层次的工具列表:
在使用BCC之前,我们有必要回顾以下常用的Linux性能分析工具。
1 2 |
uptime 23:51:26 up 21:31, 1 user, load average: 30.02, 26.43, 19.02 |
这个命令可以查看系统启动了多久,还可以快速查看工作负载情况。最后的三个数字分别是最近1/5/15分钟的、基于指数衰减移动和平均(exponentially damped moving sum averages)算法的工作负载信息。如果你发现第一个数字比第二个小很多,可能提示你错过了高负载时刻。
工作负载提示了有多少进程想要运行(去获得CPU):包括积极等待被调度的进程,以及被不可中断睡眠(通常是块IO)阻塞的进程。
内核在重大事件发生后,都会输出信息到日志环:
1 2 3 4 5 |
dmesg | tail # [1880957.563150] perl invoked oom-killer: gfp_mask=0x280da, order=0, oom_score_adj=0 # [1880957.563400] Out of memory: Kill process 18694 (perl) score 246 or sacrifice child # [1880957.563408] Killed process 18694 (perl) total-vm:1972392kB, anon-rss:1953348kB, file-rss:0kB # [2320864.954447] TCP: Possible SYN flooding on port 7001. Dropping request. Check SNMP counters. |
系统工作不正常是,有必要观察这些日志。
1 2 3 4 5 6 |
vmstat 1 # procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- # r b swpd free buff cache si so bi bo in cs us sy id wa st # 1 0 0 35469380 1681776 43056984 0 0 673 552 174 706 17 8 74 1 0 # 4 0 0 35468148 1681776 43059452 0 0 12 3480 21357 44556 8 3 88 0 0 # 10 0 0 35467088 1681784 43059824 0 0 20 3212 23257 45228 14 5 81 0 0 |
这个工具给出系统的各种宏观统计信息:
- r等待获得运行机会的CPU数量,比起load average,更明确的显示了CPU饱和度,因为它没有包含I/O,如果数量大于CPU核心,说明饱和了
- free 空闲内存KB
- si/ so 换入/换出的页数量,如果非零提示内存不足
-
us,
sy,
id,
wa,
st 处理器时间的分解:用户空间耗时,内核空间耗时,空闲时间,等待IO时间,被偷走的时间(虚拟化环境下被其它客户机偷走使用的)
- 如果us+sy高提示CPU繁忙、wa高则提示IO瓶颈
- 系统需要消耗sy时间来处理IO,如果sy时间总是很高,例如占比20%+,可能提示内核处理IO效率较差
- CPU利用90%+不一定说明CPU饱和,查看r列更靠谱
这个命令可以分别查看每个CPU核心的使用情况:
1 2 3 4 5 6 |
mpstat -P ALL 1 # Linux 5.8.0-55-generic (zircon) 06/11/2021 _x86_64_ (16 CPU) # 09:14:03 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle # 09:14:04 PM all 2.95 0.00 10.39 0.80 0.00 0.00 0.00 0.68 0.00 85.18 # 09:14:04 PM 0 5.00 0.00 5.00 0.00 0.00 0.00 0.00 3.00 0.00 87.00 # 09:14:04 PM 1 6.06 0.00 5.05 0.00 0.00 0.00 0.00 0.00 0.00 88.89 |
能够用来发现CPU负载不均衡的情况。单个热点CPU,提示存在一个忙的单线程应用程序。
1 2 3 4 5 6 7 |
pidstat 1 # Linux 5.8.0-55-generic (zircon) 06/11/2021 _x86_64_ (16 CPU) # 09:17:58 PM UID PID %usr %system %guest %wait %CPU CPU Command # 09:17:59 PM 1000 2736 4.95 1.98 0.00 0.00 6.93 6 gnome-shell # 09:17:59 PM 1000 6916 0.99 0.00 0.00 0.00 0.99 11 gnome-terminal- # 09:17:59 PM 1000 7380 8.91 51.49 0.00 0.00 60.40 4 vmware-vmx |
类似于top,但是滚动更新,有助于录制和事后分析或者发现特定的pattern。
1 |
iostat -x 1 |
用于了解块设备的工作情况:
- r/s, w/s, rkB/s, wkB/s:读写的次数、读写的字节数
- await:等待IO完成的平均ms
- avgqu-sz:平均发送给目标设备的请求数量,数量大于1提示饱和(尽管设备通常能够并行处理请求,特别是由多个后端磁盘组成的虚拟设备)
- %util:提示设备有多少时间百分比是忙着的。尽管取决于设备,大于60%通常会导致低性能。接近100%通常提示设备饱和。但是,对于多个后端磁盘组成的虚拟设备,100%仅仅提示所有时间都有后端磁盘在工作,但是可能远远没有饱和。
需要注意,很多应用程序都使用异步IO技术,这样即使设备饱和,应用程序也不会出现直接的(可被预读、缓冲写缓和)延迟。
1 2 3 4 |
free -m # total used free shared buff/cache available # Mem: 128677 6601 77687 12528 44389 108334 # Swap: 62499 0 62499 |
需要安装下面的软件:
1 |
apt install sysstat |
修改配置文件:
1 |
ENABLED="true" |
并重启服务:
1 |
systemctl restart sysstat.service |
查看网络接口的吞吐情况:
1 2 3 4 5 |
sar -n DEV 1 # 接口 收包数 发包数 收KB 发KB 网卡利用率 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 |
查看TCP统计信息:
1 2 3 4 5 6 7 8 9 10 11 |
sar -n TCP,ETCP 1 # 本地主动发起连接数量 # 本地被动accept连接数量 # 09:38:04 PM active/s passive/s iseg/s oseg/s 09:38:05 PM 0.00 0.00 5.00 5.00 # 重传次数 09:39:41 PM atmptf/s estres/s retrans/s isegerr/s orsts/s 09:39:42 PM 0.00 0.00 0.00 0.00 0.00 |
这个工具可以探测新 exec()的进程,注意它不会追踪 fork()因而不能看到所有的进程。
该工具可用于发现短暂存在的、消耗CPU的进程,这些进程可能难以在周期性的、快照式的监控工具(例如top)中出现。
这个工具可以探测 open()系统调用,从而推测应用程序的工作方式(会使用哪些数据文件、配置文件、日志文件)。某些时候,应用程序因为反复尝试打开不存在的文件,或者反复写入大量数据到文件,而性能低下,该工具也可以发现这类场景。
类似的工具还包括 btrfsslower, xfsslower, zfsslower。
这些工具可以探测单个的、缓慢的文件系统操作。由于磁盘是异步处理IO的,难以将块设备层的延迟和应用程序的延迟关联起来。在VFS/文件系统接口层进行探测,更加接近应用程序,延迟相关的可能性也更大。
跟踪此片IO延迟(从向设备发起请求到处理完毕),退出此工具时会绘制直方图。
iostat之类工具能给出平均延迟,此工具则通过直方图显示延迟的分布情况。
按进程和延迟探测块IO操作。
按进程显示块IO的汇总信息。
每行打印统计周期内(默认1s)文件系统缓存命中、丢失、读/写操作缓存命中率。
追踪IPv4/IPv6的 bind()系统调用。
该工具监控每一个新发起的主动TCP连接(通过 connect()系统调用)。
该工具监控每一个新发起的被动TCP连接(通过 accept()系统调用)。
每当出现一个TCP重传包时,该工具打印一行内容。
TCP重传会导致延迟和吞吐量问题。
对于ESTABLISHED状态连接的重传,需要检查网络patterns;对于SYN_SENT状态连接的重传,可能提示目标内核CPU饱和以及内核丢包。
显示线程在CPU运行队列上等待的时间的直方图。
在CPU饱和的情况下,该工具可以度量等待CPU消耗的时间。
以特定的周期,获取栈追踪采样,并且在结束时,打印每个独特的栈追踪的数量。
该工具可以辅助理解哪些代码路径消耗CPU资源,因为消耗资源越多的代码路径,被采样到的概率越大。
能够探测你指定的函数,并且在满足条件的时候打印指定格式的追踪消息。
命令格式:
1 2 3 4 5 |
trace [-h] [-b BUFFER_PAGES] [-p PID] [-L TID] [-v] [-Z STRING_SIZE] [-S] [-M MAX_EVENTS] [-t] [-u] [-T] [-C] [-c CGROUP_PATH] [-n NAME] [-f MSG_FILTER] [-B] [-s SYM_FILE_LIST] [-K] [-U] [-a] [-I header] # 探针规格定义 probe [probe ...] |
选项:
perf_events 环缓冲占用的页数量,默认64
-p PID 追踪特定的进程
-L TID 追踪特定的线程
-v 执行前打印生成的BPF代码
-S 包含trace程序自身的栈追踪,默认排除
-M 退出前允许的最大事件数量
-t 打印时间戳信息
-t -u 打印UNIX时间戳
-T 打印时间
-c cgroup路径
-n 仅仅打印进程名包含该选项指定字符串的进程
-f 仅仅打印消息包含该选项指定字符串的事件
-B 运行对二进制值调用STRCMP
-s 逗号分割的符号文件,用于栈的符号解析
-K 输出内核栈追踪
-U 输出用户栈追踪
-a 在栈中打印虚拟地址
-I 额外包含在BPF程序中的头文件地址
示例:
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# 追踪内核的发生的do_sys_open函数调用 trace do_sys_open # 追踪open系统调用,并打印打开的文件名 # ::do_sys_open (没有前缀)表示你需要一个entry probe # 格式化为字符串 # 打印第二个参数 trace 'do_sys_open "%s", arg2' # 追踪进程名包含main的进程的do_sys_open调用 trace 'do_sys_open "%s", arg2' -n main # 追踪打开文件名包含config的进程的do_sys_open调用 trace 'do_sys_open "%s", arg2' -f config # 追踪do_sys_open调用,并打印返回值 # 前缀 r 用来定义一个return probe,关键字retval代表函数返回值 trace 'r::do_sys_open "%llx", retval' # 追踪内核函数kfree_skb的0x12偏移量的指令 trace kfree_skb+0x12 # 追踪读取超过20000bytes的sys_read系统调用 # 注意:在4.17+,系统调用函数发生了重命名,例如 sys_read被重命名为 __x64_sys_read trace 'sys_read (arg3 > 20000) "read %d bytes", arg3' # 中间这个bash表示包含目标函数的库或者二进制文件的名字。如果不指定二进制 # 文件的绝对路径,则它必须位于PATH中 trace 'r:bash:readline "%s", retval' # 追踪共享库的导出函数,或者二进制文件导入的函数 trace'r:/usr/lib64/libtinfo.so:curses_version "Version=%s", retval' # 追踪标记参数为42的、由libc发起的open调用 # 指定库名时不需要前缀lib trace 'c:open (arg2 == 42) "%s %d", arg1, arg2' # 追踪malloc调用,打印分配的内存大小 trace 'c:malloc "size = %d", arg1' # 追踪通过write调用写到stdout的字节数 trace 'p:c:write (arg1 == 1) "writing %d bytes to STDOUT", arg3' # 追踪失败的__kmalloc调用,也就是返回值为0的 trace 'r::__kmalloc (retval == 0) "kmalloc failed!"' # 多个探针可以在单个命令中执行 # 返回值必须cast为int然后再和0比较,因为argN和retval的默认类型是64bit整数 # 可能溢出为负数了 trace 'r:c:read ((int)retval < 0) "read failed: %d", retval' \ 'r:c:write ((int)retval < 0) "write failed: %d", retval' -T # 该工具也对内核tracepoint提供了基本的支持 # 前缀 t 用来定义一个tracepoint trace 't:block:block_rq_complete "sectors=%d", args->nr_sector' -T # 要想知道tracepoint的结构的格式,也就是 args 指针的格式,可以利用工具tplist tplist -v block:block_rq_complete # block:block_rq_complete # dev_t dev; # sector_t sector; # unsigned int nr_sector; # int errors; # char rwbs[8]; # 越来越多的高层次的库,启用了基于USDT探针的内省支持。这些探针的用法就像内核的tracepoint # 前缀 u 用来定义一个USDT # %U表示将arg3解析为用户空间符号,类似的%K则解析为内核空间符号 trace 'u:pthread:pthread_create "%U", arg3' -T -C # Ruby OpenJDK Node等平台,也都支持USDT # 下面的例子追踪Ruby方法调用,需要Ruby构建时启用配置--enable-dtrace # 类名 方法名 trace 'u:ruby:method__entry "%s.%s", arg1, arg2' -p $(pidof irb) -T # 根据字符串来过滤 # 打开文件名中包含test.txt trace 'p:c:open (STRCMP("test.txt", arg1)) "opening %s", arg1' -T # 通过提供函数签名,来提升可读性 trace 'p:c:open(char *filename) "opening %s", filename' |
追踪一个函数,并且打印它的参数汇总信息。
对函数、追踪点、USDT探针进行计数。
显示使用了BPF程序和Map的进程。
打印全局范围内,在bash中执行的所有命令。
追踪对内核函数 cap_capable()的调用,此函数负责进行security capability的检查。
完整的开发文档参考:https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md
要基于BCC开发BPF程序,你可以使用多种编程语言,例如Python、Lua或者CPP。不过,你通常都要在这些编程语言代码中,用字符串嵌入C语言的BPF程序片段。
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 |
#include <unistd.h> #include <fstream> #include <iostream> #include <string> #include "bcc_version.h" #include "BPF.h" const std::string BPF_PROGRAM = R"( int on_sys_clone(void *ctx) { bpf_trace_printk("Hello, World! Here I did a sys_clone call!\n"); return 0; } )"; int main() { // 此类提供BPF各种操作接口 ebpf::BPF bpf; // 初始化C编写的BPF程序片段 auto init_res = bpf.init(BPF_PROGRAM); if (init_res.code() != 0) { std::cerr << init_res.msg() << std::endl; return 1; } // 打开此输入流用于查看BPF程序打印的日志 std::ifstream pipe("/sys/kernel/debug/tracing/trace_pipe"); std::string line; // 获取系统调用的函数名,4.17+给系统调用函数名加了前缀__x64_,这个函数会自动添加前缀 std::string clone_fnname = bpf.get_syscall_fnname("clone"); // 挂钩探针 到此函数 探针的BPF函数名 auto attach_res = bpf.attach_kprobe(clone_fnname, "on_sys_clone"); if (attach_res.code() != 0) { std::cerr << attach_res.msg() << std::endl; return 1; } std::cout << "Starting HelloWorld with BCC " << LIBBCC_VERSION << std::endl; while (true) { if (std::getline(pipe, line)) { std::cout << line << std::endl; // 当pipe中没有更多数据可读,卸载探针 auto detach_res = bpf.detach_kprobe(clone_fnname); if (detach_res.code() != 0) { std::cerr << detach_res.msg() << std::endl; return 1; } break; } else { std::cout << "Waiting for a sys_clone event" << std::endl; sleep(1); } } return 0; } |
1 2 3 4 |
from bcc import BPF # This may not work for 4.17 on x64, you need replace kprobe__sys_clone with kprobe____x64_sys_clone BPF(text='int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\\n"); return 0; }').trace_print() |
类似C++的ebpf::BPF类,Python中的BPF类提供了BPF操作的各种接口。
text参数是需要执行的C语言编写的BPF程序。
kprobe__sys_clone()这种命名约定,用于快捷的定义Kprobe,前缀 kprobe__表示这是一个Kprobe,后面的则是需要instrument的内核函数的名字。
Kprobe接收一个 ctx参数,这个例子中访问该参数,因此将其转型为 void*。
函数bpf_trace_printk()打开公共的内核调试追踪管道(kernel debug trace pipe)并写入消息,也就是/sys/kernel/debug/tracing/trace_pipe。此管道是公共的,也就是所有BPF程序都可能写入消息,导致输出混乱。此外此管道性能较差,因此可以考虑BPF_PERF_OUTPUT()。
trace_print()函数读取trace_pipe中的消息并打印。
1 2 3 4 |
b = BPF(text=prog) # ... # 获得字段元组,而非字符串 (task, pid, cpu, flags, ts, msg) = b.trace_fields() |
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 |
// b = BPF(text=""" #include <uapi/linux/ptrace.h> // 定义一个哈希表,使用默认的键值类型u64 BPF_HASH(last); int do_trace(struct pt_regs *ctx) { u64 ts, *tsp, delta, key = 0; // 查找键值 tsp = last.lookup(&key); if (tsp != NULL) { // 此助手函数返回当前纳秒数 delta = bpf_ktime_get_ns() - *tsp; if (delta < 1000000000) { // output if time is less than 1 second bpf_trace_printk("%d\\n", delta / 1000000); } // 删除键值 last.delete(&key); } ts = bpf_ktime_get_ns(); // 更新键值 last.update(&key, &ts); return 0; } // """) // b.attach_kprobe(event=b.get_syscall_fnname("sync"), fn_name="do_trace") |
由于bpf_trace_printk()性能较差,不适合在生产环境中使用。perfbuf可以作为其代替品。perfbuf本质上是BPF_MAP_TYPE_PERF_EVENT_ARRAY类型的Map。
使用BCC时,可以直接调用 BPF_PERF_OUTPUT()以使用perfbuf。
1 2 |
from bcc import BPF # prog = """ |
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 |
#include <linux/sched.h> // 定义一个结构,作为输出的“事件” struct data_t { u32 pid; u64 ts; char comm[TASK_COMM_LEN]; }; // 定义名为events的perfbuf BPF_PERF_OUTPUT(events); int hello(struct pt_regs *ctx) { struct data_t data = {}; // 当前进程ID(内核角度的PID,对应用户空间的Thread ID),此ID存放在低32bit // 当前线程组ID(内核角度的Thread Group ID,对应用户空间的PID),此ID存放在高32bit // 这里直接赋值给u32,导致高32bit丢弃,存储线程ID // 对于多线程程序,它的所有线程共享TGID,如果需要区分不同线程,应当使用PID data.pid = bpf_get_current_pid_tgid(); // 时间戳 data.ts = bpf_ktime_get_ns(); // 当前进程名 bpf_get_current_comm(&data.comm, sizeof(data.comm)); // 提交事件到用户空间 events.perf_submit(ctx, &data, sizeof(data)); return 0; } |
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 |
# """ # 加载并挂钩BPF程序 b = BPF(text=prog) b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello") # header print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE")) start = 0 # 处理事件的回调函数 def print_event(cpu, data, size): global start # 访问perfbuf 转换事件(data)为Python对象 event = b["events"].event(data) if start == 0: start = event.ts time_s = (float(event.ts - start)) / 1000000000 print("%-18.9f %-16s %-6d %s" % (time_s, event.comm, event.pid, "Hello, perf_output!")) # 打开perfbuf,设置回调函数 b["events"].open_perf_buffer(print_event) while 1: # 阻塞式的拉取事件 b.perf_buffer_poll() |
1 2 3 4 5 |
from __future__ import print_function from bcc import BPF from time import sleep # b = BPF(text=""" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <uapi/linux/ptrace.h> #include <linux/blkdev.h> // 声明一个直方图Map BPF_HISTOGRAM(dist); // kprobe__开头的,表示是Kprobe,后面的blk_account_io_done就是目标函数 int kprobe__blk_account_io_done(struct pt_regs *ctx, struct request *req) { // 让直方图的指定索引上的计数增加 // 将块IO数据长度取2为底的对数,作为索引值。这表示进行2^的桶分布 // 打个比方,当前操作字节数为64KB,取对数得到6,因此增加下面的第6个桶的计数 // kbytes : count distribution // 0 -> 1 : 3 | | // 2 -> 3 : 0 | | // 4 -> 7 : 211 |********** | // 8 -> 15 : 0 | | // 16 -> 31 : 0 | | // 32 -> 63 : 0 | | // 64 -> 127 : 1 | | // 128 -> 255 : 800 |**************************************| dist.increment(bpf_log2l(req->__data_len / 1024)); return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# """) # header print("Tracing... Hit Ctrl-C to end.") # trace until Ctrl-C try: sleep(99999999) except KeyboardInterrupt: print() # output b["dist"].print_log2_hist("kbytes") |
1 |
b = BPF(src_file = "vfsreadlat.c") |
要运行tracepoint BPF程序,你需要Linux内核4.7+版本。
1 2 3 4 5 6 7 |
// instrument内核中追踪带年random:urandom_read。追踪点具有稳定的API,应当尽可能使用追踪点 // 而不是kprobes TRACEPOINT_PROBE(random, urandom_read) { // args is from /sys/kernel/debug/tracing/events/random/urandom_read/format bpf_trace_printk("%d\\n", args->got_bits); return 0; } |
所有可用的追踪点可以通过下面的命令获得:
1 2 3 |
tplist # 或者 perf list |
追踪点可用参数可以通过下面的命令获得:
1 2 3 4 5 |
tplist -v random:urandom_read # random:urandom_read # int got_bits; # int pool_left; # int input_left; |
下面的例子,追踪内核函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <uapi/linux/ptrace.h> #include <linux/sched.h> struct key_t { u32 prev_pid; u32 curr_pid; }; BPF_HASH(stats, struct key_t, u64, 1024); // 第二个参数开始,对应被探测的内核函数的参数 int count_sched(struct pt_regs *ctx, struct task_struct *prev) { struct key_t key = {}; u64 zero = 0, *val; key.curr_pid = bpf_get_current_pid_tgid(); key.prev_pid = prev->pid; // 等价于 stats.increment(key) val = stats.lookup_or_try_init(&key, &zero); if (val) { (*val)++; } return 0; } |
注册探针:
1 |
b.attach_kprobe(event="finish_task_switch", fn_name="count_sched") |
需要注意的一点是,你可以在探针的参数列表中,将被探测函数的参数列出来,并访问。 finish_task_switch的签名:
1 2 |
static struct rq *finish_task_switch(struct task_struct *prev) __releases(rq->lock); |
因此在探针中我们可以访问prev参数,代表被切换出去的进程。
下面的例子追踪用户空间函数strlen()。
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 |
#include <uapi/linux/ptrace.h> struct key_t { char c[80]; }; BPF_HASH(counts, struct key_t); int count(struct pt_regs *ctx) { // 从上下文中读取strlen()的第一个参数 if (!PT_REGS_PARM1(ctx)) return 0; struct key_t key = {}; u64 zero = 0, *val; // 将参数存入结构 bpf_probe_read_user(&key.c, sizeof(key.c), (void *)PT_REGS_PARM1(ctx)); // 将结构存入Map,如果没有对应的key则初始化 val = counts.lookup_or_try_init(&key, &zero); // 增加计数 if (val) { (*val)++; } return 0; }; |
1 2 |
# 挂钩到 库c 的strlen函数 钩子函数为count b.attach_uprobe(name="c", sym="strlen", fn_name="count") |
USDT即用户静态定义的追踪点(user statically-defined tracing point),相当于tracepoint的用户态版本。很多流行的软件、框架都预置了USDT。
1 2 3 4 5 6 7 8 9 10 11 |
#include <uapi/linux/ptrace.h> int do_trace(struct pt_regs *ctx) { uint64_t addr; char path[128]={0}; // 读取USDT探针的第6个参数,到addr变量 bpf_usdt_readarg(6, ctx, &addr); // 将addr作为指针看待,并且从它所在位置读取最多128字节的字符串 bpf_probe_read_user(&path, sizeof(path), (void *)addr); bpf_trace_printk("path:%s\\n", path); return 0; }; |
1 2 3 4 5 6 7 |
# 为指定的PID初始化USDT追踪 u = USDT(pid=int(pid)) # 挂钩do_trace到追踪点http__server__request u.enable_probe(probe="http__server__request", fn_name="do_trace") # 初始化BPF b = BPF(text=bpf_text, usdt_contexts=[u]) |
1 2 3 4 5 |
# 在Python程序中定义一个内核常量,不需要给出头文件 REQ_WRITE = 1 # from include/linux/blk_types.h # 加载BPF程序 # b = BPF(text=""" |
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 |
#include <uapi/linux/ptrace.h> #include <linux/blkdev.h> // 哈希表 键的类型(值类型是默认的u64) BPF_HASH(start, struct request *); // KProbe的参数pt_regs,包含registers、BPF上下文 // struct request *req是目标函数blk_start_request的第一个参数 void trace_start(struct pt_regs *ctx, struct request *req) { u64 ts = bpf_ktime_get_ns(); // 直接使用请求参数的指针作为键,在追踪程序中这很常见。因为任何两个结构都不会有相同的地址 // 可以安全的作为唯一键(注意内存被释放并重用的情况) start.update(&req, &ts); } void trace_completion(struct pt_regs *ctx, struct request *req) { u64 *tsp, delta; // 查找先前记录的时间戳 tsp = start.lookup(&req); if (tsp != 0) { // 获取消耗的时间 delta = bpf_ktime_get_ns() - *tsp; // 打印本次块IO操作的信息 // 直接访问结构字段,BCC会自动将其转换为bpf_probe_read_kernel()调用 // 某些情况下BCC无法处理复杂的解引用操作,你可以手工调用该助手函数 bpf_trace_printk("%d %x %d\\n", req->__data_len, req->cmd_flags, delta / 1000); start.delete(&req); } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
# """) # 函数 void blk_start_request ( struct request * req); 开始请求驱动来处理块IO请求 b.attach_kprobe(event="blk_start_request", fn_name="trace_start") # 函数 void blk_mq_start_request(struct request *rq); 高IOPS下Linux的块设备层形成了瓶颈 # 因此多核心系统下,3.13+添加的Multi queue特性可以减少块设备层的性能影响。传统块设备层有能力 # 处理万级别的IOPS,但是随着SSD这样的高性能设备的出现,其出现能力瓶颈。Multi queue的主要思想 # 是,为每个CPU核心设置一个请求队列,均衡核之间的负载,减少对请求队列的锁竞争 b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_start") # 函数 void blk_account_io_done(struct request *req, u64 now); 在块设备IO完成后调用 # 此函数优于blk_account_io_completion(),后者可能在请求部分完成的情况下调用,调用可能发生多次, # 这种现象会发生在loop设备/老的SCSI上 b.attach_kprobe(event="blk_account_io_done", fn_name="trace_completion") |
可以使用具有稳定接口的追踪点block:block_rq_issue、block:block_rq_complete代替上述Kprobe。
所有统计延迟的监控程序,都是选择适当的键,然后在entry/return钩子中进行计时:
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 |
#include <uapi/linux/ptrace.h> BPF_HASH(start, u32); BPF_HISTOGRAM(dist); int do_entry(struct pt_regs *ctx) { u32 pid; u64 ts, *val; pid = bpf_get_current_pid_tgid(); ts = bpf_ktime_get_ns(); // 记录开始时间 start.update(&pid, &ts); return 0; } int do_return(struct pt_regs *ctx) { u32 pid; u64 *tsp, delta; pid = bpf_get_current_pid_tgid(); tsp = start.lookup(&pid); if (tsp != 0) { // 计算耗时 delta = bpf_ktime_get_ns() - *tsp; // 更新桶计数 dist.increment(bpf_log2l(delta / 1000)); start.delete(&pid); } return 0; } |
1 2 3 4 5 |
# 从文件中加载BPF程序 b = BPF(src_file = "vfsreadlat.c") # 挂钩的内核函数是vfs_read b.attach_kprobe(event="vfs_read", fn_name="do_entry") b.attach_kretprobe(event="vfs_read", fn_name="do_return") |
Leave a Reply