Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

基于BCC进行性能追踪

7
Jun
2019

基于BCC进行性能追踪

By Alex
/ in Linux
/ tags eBPF
0 Comments
简介

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开发变得简单:

  1. 在C中使用kernel instrumentation,提供了LLVM的一个C wrapper
  2. 提供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
安装软件包
Shell
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需要的依赖,以及相关工具:

Shell
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:

Shell
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_tracing_tools_2019

非BCC工具

在使用BCC之前,我们有必要回顾以下常用的Linux性能分析工具。

uptime
Shell
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)阻塞的进程。

dmesg

内核在重大事件发生后,都会输出信息到日志环:

Shell
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.

系统工作不正常是,有必要观察这些日志。

vmstat
Shell
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

这个工具给出系统的各种宏观统计信息:

  1. r等待获得运行机会的CPU数量,比起load average,更明确的显示了CPU饱和度,因为它没有包含I/O,如果数量大于CPU核心,说明饱和了
  2. free 空闲内存KB
  3. si/ so 换入/换出的页数量,如果非零提示内存不足
  4. us, sy, id, wa, st 处理器时间的分解:用户空间耗时,内核空间耗时,空闲时间,等待IO时间,被偷走的时间(虚拟化环境下被其它客户机偷走使用的)
    1. 如果us+sy高提示CPU繁忙、wa高则提示IO瓶颈
    2. 系统需要消耗sy时间来处理IO,如果sy时间总是很高,例如占比20%+,可能提示内核处理IO效率较差
    3. CPU利用90%+不一定说明CPU饱和,查看r列更靠谱
mpstat

这个命令可以分别查看每个CPU核心的使用情况:

Shell
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,提示存在一个忙的单线程应用程序。

pidstat
Shell
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。

iostat
Shell
1
iostat -x 1

用于了解块设备的工作情况:

  1.   r/s, w/s, rkB/s, wkB/s:读写的次数、读写的字节数
  2. await:等待IO完成的平均ms
  3. avgqu-sz:平均发送给目标设备的请求数量,数量大于1提示饱和(尽管设备通常能够并行处理请求,特别是由多个后端磁盘组成的虚拟设备)
  4. %util:提示设备有多少时间百分比是忙着的。尽管取决于设备,大于60%通常会导致低性能。接近100%通常提示设备饱和。但是,对于多个后端磁盘组成的虚拟设备,100%仅仅提示所有时间都有后端磁盘在工作,但是可能远远没有饱和。

需要注意,很多应用程序都使用异步IO技术,这样即使设备饱和,应用程序也不会出现直接的(可被预读、缓冲写缓和)延迟。

free
Shell
1
2
3
4
free -m
#               total        used        free      shared  buff/cache   available
# Mem:         128677        6601       77687       12528       44389      108334
# Swap:         62499           0       62499
sar

需要安装下面的软件:

Shell
1
apt install sysstat

修改配置文件:

/etc/default/sysstat,
Conf
1
ENABLED="true"

并重启服务:

Shell
1
systemctl restart sysstat.service

查看网络接口的吞吐情况:

Shell
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统计信息:

Shell
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
可观察性工具
execsnoop

这个工具可以探测新 exec()的进程,注意它不会追踪 fork()因而不能看到所有的进程。

该工具可用于发现短暂存在的、消耗CPU的进程,这些进程可能难以在周期性的、快照式的监控工具(例如top)中出现。

opensnoop

这个工具可以探测 open()系统调用,从而推测应用程序的工作方式(会使用哪些数据文件、配置文件、日志文件)。某些时候,应用程序因为反复尝试打开不存在的文件,或者反复写入大量数据到文件,而性能低下,该工具也可以发现这类场景。

ext4slower

类似的工具还包括 btrfsslower, xfsslower, zfsslower。

这些工具可以探测单个的、缓慢的文件系统操作。由于磁盘是异步处理IO的,难以将块设备层的延迟和应用程序的延迟关联起来。在VFS/文件系统接口层进行探测,更加接近应用程序,延迟相关的可能性也更大。

biolatency

跟踪此片IO延迟(从向设备发起请求到处理完毕),退出此工具时会绘制直方图。

iostat之类工具能给出平均延迟,此工具则通过直方图显示延迟的分布情况。

biosnoop

按进程和延迟探测块IO操作。

biotop

按进程显示块IO的汇总信息。

cachestat

每行打印统计周期内(默认1s)文件系统缓存命中、丢失、读/写操作缓存命中率。

bindsnoop

追踪IPv4/IPv6的 bind()系统调用。

tcpconnect

该工具监控每一个新发起的主动TCP连接(通过 connect()系统调用)。

tcpaccept

该工具监控每一个新发起的被动TCP连接(通过 accept()系统调用)。

tcpretrans

每当出现一个TCP重传包时,该工具打印一行内容。

TCP重传会导致延迟和吞吐量问题。

对于ESTABLISHED状态连接的重传,需要检查网络patterns;对于SYN_SENT状态连接的重传,可能提示目标内核CPU饱和以及内核丢包。

runqlat

显示线程在CPU运行队列上等待的时间的直方图。

在CPU饱和的情况下,该工具可以度量等待CPU消耗的时间。

profile

以特定的周期,获取栈追踪采样,并且在结束时,打印每个独特的栈追踪的数量。

该工具可以辅助理解哪些代码路径消耗CPU资源,因为消耗资源越多的代码路径,被采样到的概率越大。

trace

能够探测你指定的函数,并且在满足条件的时候打印指定格式的追踪消息。

命令格式:

Shell
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程序中的头文件地址

示例:

Shell
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'
argdist

追踪一个函数,并且打印它的参数汇总信息。

funccount

对函数、追踪点、USDT探针进行计数。

bpflist

显示使用了BPF程序和Map的进程。

bashreadline

打印全局范围内,在bash中执行的所有命令。

capable

追踪对内核函数 cap_capable()的调用,此函数负责进行security capability的检查。

开发

完整的开发文档参考:https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md

HelloWorld

要基于BCC开发BPF程序,你可以使用多种编程语言,例如Python、Lua或者CPP。不过,你通常都要在这些编程语言代码中,用字符串嵌入C语言的BPF程序片段。

C++
C++
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;
}
python
Python
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中的消息并打印。

常用编程接口
读取追踪管道字段
Python
1
2
3
4
b = BPF(text=prog)
# ...
# 获得字段元组,而非字符串
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
使用Map
C
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")
使用perfbuf

由于bpf_trace_printk()性能较差,不适合在生产环境中使用。perfbuf可以作为其代替品。perfbuf本质上是BPF_MAP_TYPE_PERF_EVENT_ARRAY类型的Map。

使用BCC时,可以直接调用 BPF_PERF_OUTPUT()以使用perfbuf。

Python
1
2
from bcc import BPF
# prog = """

C
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;
} 

Python
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()
生成直方图
Python
1
2
3
4
5
from __future__ import print_function
from bcc import BPF
from time import sleep
 
# b = BPF(text="""

C
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;
}

Python
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")
从文件加载BPF程序
Python
1
b = BPF(src_file = "vfsreadlat.c")
tracepoint

要运行tracepoint BPF程序,你需要Linux内核4.7+版本。

C
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;
}

所有可用的追踪点可以通过下面的命令获得:

Shell
1
2
3
tplist
# 或者
perf list

追踪点可用参数可以通过下面的命令获得:

Shell
1
2
3
4
5
tplist -v random:urandom_read
# random:urandom_read
#     int got_bits;
#     int pool_left;
#     int input_left;
kprobe

下面的例子,追踪内核函数:

C
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;
}

注册探针:

Python
1
b.attach_kprobe(event="finish_task_switch", fn_name="count_sched")

需要注意的一点是,你可以在探针的参数列表中,将被探测函数的参数列出来,并访问。 finish_task_switch的签名:

C
1
2
static struct rq *finish_task_switch(struct task_struct *prev)
    __releases(rq->lock);

因此在探针中我们可以访问prev参数,代表被切换出去的进程。

uprobe

下面的例子追踪用户空间函数strlen()。

C
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;
};

Python
1
2
# 挂钩到         库c      的strlen函数   钩子函数为count
b.attach_uprobe(name="c", sym="strlen", fn_name="count")
USDT

USDT即用户静态定义的追踪点(user statically-defined tracing point),相当于tracepoint的用户态版本。很多流行的软件、框架都预置了USDT。

C
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;
};

Python
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])
工具源码分析
disksnoop
Python
1
2
3
4
5
# 在Python程序中定义一个内核常量,不需要给出头文件
REQ_WRITE = 1        # from include/linux/blk_types.h
 
# 加载BPF程序
# b = BPF(text=""" 

C
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);
    }
}
 

Python
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。

vfsreadlat

所有统计延迟的监控程序,都是选择适当的键,然后在entry/return钩子中进行计时:

vfsreadlat.c
C
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;
}

Python
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")
← Byte Buddy学习笔记
Kubernetes的Service Catalog机制 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • eBPF学习笔记
  • Cilium学习笔记
  • 操作系统知识集锦
  • 利用perf剖析Linux应用程序
  • Linux目录层次和配置文件

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • Bazel学习笔记 38 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
  • Three.js学习笔记 24 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2