概述

业务负载画像

  1. 负载是谁产生的(如:进程ID、用户ID、进程名、IP地址)?
  2. 负载为什么会产生(代码路径、调用栈、火焰图)?
  3. 负载的组成层是什么(IOPS、吞吐量、负载类型)?
  4. 负载怎样随着时间发生变化(比较每个周期的摘要信息)?

可以测量的指标:

  • 延迟: 多久可以完整一次请求或者操作,毫秒为单位
  • 速率: 每秒操作或请求的速率
  • 利用率:以百分比形式表示的某资源在一段时间内的繁忙程度
  • 成本: 开销/性能的 比例

下钻分析

  1. 从业务最高层就开始分析
  2. 检查下一个层级的细节
  3. 挑出最感兴趣的部分或者线索
  4. 如果问题还没有解决,跳转至第(2)步

USE 方法

  • 使用率
  • 饱和度
  • 错误

60 秒系统检查清单

  1. uptime
  2. dmesg | tail
  3. vmstat 1
  4. mpstat -P ALL 1
  5. pidstat 1
  6. iostat -xz 1
  7. free -m
  8. sar -n DEV 1
  9. sar -n TCP, ETCP 1
  10. top

BCC 工具检查清单

  1. execsnoop
  2. opensnoop
  3. ext4slower(或 brtfs*, xfs*, zfs*
  4. biolatency
  5. biosnoop
  6. cachestat
  7. tcpconnect
  8. tcpaccept
  9. tcpretrans
  10. runqlat
  11. profile

BCC 工具

常用 BCC 工具在内核中的位置

工具 来源 目标 描述
funccount BCC 软件 对事件进行计数,包括函数调用
stackcount BCC 软件 对引发某事件的函数调用栈进行计数
trace BCC 软件 定制化打印每个事件的细节信息
argdist BCC 软件 对事件的参数分布进行统计

funccount 的例子

 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
# 内核函数 tcp_drop() 是否被调用   
funccount-bpfcc tcp_drop

# 内核中最频繁的 VFS 是哪个
funccount-bpfcc 'vfs*'

# 用户态函数 pthread_mutext_lock 每秒被调用的次数
funccount-bpfcc -i 1 'c:pthread_mutex_lock'

# 全系统内, libc ku 中调用最频繁的与字符串相关的函数是哪个
funccount-bpfcc 'c:str*'

# 执行最频繁的系统调用是哪个
funccount-bpfcc 't:syscalls:sys_enter_*'


# 对某个进程 统计 VFS 执行次数
funccount-bpfcc -p 46649 'vfs_*'

# 对全局统计 VFS内核进行计数
funccount-bpfcc 'vfs_*'

# 对 TCP 内核进行计数
funccount-bpfcc 'tcp_*'

# 统计每秒 TCP 发送函数的调用次数
funccount-bpfcc -i 1 'tcp_send*'

# 每秒 I/O 事件的数量
funccount-bpfcc -i 1 't:block:*'

# 每秒新创建的进程数量
funccount-bpfcc -i 1 't:sched:sched_process_fork'

# 每秒 libc 中 getaddrinfo() 域名解析 函数的调用次数
funccount-bpfcc -i 1 'c:getaddrinfo'

# 对 libgo 中全部的 os.* 调用进行计数   
funccount-bpfcc -i 1 'go:os.*'


stackcount 例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 统计 ktime_get() 调用的代码路径
stackcount-bpfcc ktime_get   

# 对创建块 I/O 的函数调用栈进行计数:
stackcount-bpfcc t:block:block_rq_insert

# 对发送 IP 数据包的调用栈进行计数:
stackcount-bpfcc ip_output

# 对发送 IP 数据包的调用栈进行计数,同时显示对应的 PID:
stackcount-bpfcc -P ip_output

# 对导致线程阻塞并且导致脱离 CPU 的调用栈进行计数:
stackcount-bpfcc t:sched:sched_switch

# 对导致系统调用 read () 的调用栈进行计数:
stackcount-bpfcc t:syscalls:sys_enter_read

stackcount 生成火焰图

1
2
3
4
5
6
stackcount-bpfcc -f -P -D 10 ktime_get > out.stackcount01.txt

git clone https://github.com/brendangregg/FlameGraph.git
cd FlameGraph
./flamegraph.pl --hash --bgcolors=grey < /data/out.stackcount01.txt > \
out.stackcount01.svg

2025\06\bpf\20.jpg



trace 例子

 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
# 跟踪 open 系统调用,并打印 打开的 filename
trace-bpfcc 'do_sys_open "%s", arg2'


# 跟踪内核函数 do_sys_open (),并打印文件名:
trace-bpfcc 'do_sys_open "% s", arg2'

# 跟踪内核函数 do_sys_open (),并打印返回值:
trace-bpfcc 'r::do_sys_open "ret: % d", retval'

# 跟踪 do_nanosleep (),并且打印用户态的调用栈:
trace-bpfcc -U 'do_nanosleep "mode: % d", arg2'

# 跟踪通过 pam 库进行身份鉴别的请求:
trace-bpfcc 'pam:pam_start "% s: % s", arg1, arg2'




# 跟踪 udpv6 系统调用,这里需要结构体,所以有一个 -I 'net/sock.h' 
# 需要系统有完整的内核源码才能工作,未来会通过 BPF类型格式解决此问题
# 即将结构体信息嵌入到 二进制文件中,这样就不需要结构体的头文件了		
trace -I 'net/sock.h' \
      'udpv6_sendmsg(struct sock *sk) (sk->sk_dport == 13568)'
	  
	  
	  
# 打印时间戳,并打印内核堆栈、用户态堆栈,跟踪 tcp_send_fin 函数
trace-bpfcc -tKU 'tcp_send_fin'

argdist

 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
# 打印 kmalloc 分配的柱状图size
argdist-bpfcc  -H 'p::__kmalloc(u64 size):u64:size'


# 从 kmalloc 分配中打印每字节纳秒的直方图
argdist-bpfcc  -H 'r::__kmalloc(size_t size):u64:$latency/$entry(size)#ns per byte'

# 打印读取 size 满足条件,延迟 > 1秒的
argdist-bpfcc  -H 'r::__vfs_read(void *file, void *buf, size_t count):size_t:
            $entry(count):$latency > 1000000'


# 打印完整的 block I/O 请求的 sector 数量柱状图
argdist-bpfcc -H 't:block:block_rq_complete():u32:args->nr_sector'

# IRQ 中断请求的统计聚合
argdist-bpfcc -C 't:irq:irq_handler_entry():int:args->irq'

# usdt 用户态静态跟踪,1337 进程 的 pthread_start 频率
argdist-bpfcc -C 'u:pthread:pthread_start():u64:arg2' -p 1337


# 跟踪 1234 进程,打印 libc 的 malloc 调用的频繁(分配的内存 >1024字节) 
argdist-bpfcc -p 1234 -C 'p:c:malloc(size_t size):size_t:size:size>1024'

# 这里需要一个结构体,统计 cfs scheduling 队列剩余的运行时间
argdist-bpfcc -I 'kernel/sched/sched.h' \
  -C 'p::__account_cfs_rq_runtime(struct cfs_rq *cfs_rq):s64:cfs_rq->runtime_remaining'


# 统计 tcp 零窗口问题,看其返回的窗口大小
argdist-bpfcc -H 'r::__tcp_select_window():int:$retval'		



# 对 vfs_read() 返回值统计,用直方图打印   
argdist-bpfcc -H 'r::vfs_read()'




根据调用号(syscall ID)对系统调用进行计数,这里使用了 raw_syscalls:sysenter 这个跟踪点:
argdist-bpfcc -C 't:raw_syscalls:sys_enter():int:args->id'

# 对 tcp_sendmsg () 的参数 size 进行统计:
argdist-bpfcc -C 'p::tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size):u32:size'

#将 tcp_sendmsg () 的 size 作为以 2 的幂为区间的直方图打印出来:
argdist-bpfcc -H 'p::tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size):u32:size'

#将 PID 为 181 的进程按照文件描述符对 write () 调用进行计数:
argdist-bpfcc -p 181 -C 'p:c:write(int fd):int:fd'

# 打印出延迟大于 0.1 毫秒的进程读操
argdist-bpfcc -C 'r::vfs_read():u32:$PID:$latency > 100000'

调试

BCC 装载一个 BPF 程序并开始对某个事件进行插桩的步骤如下:

  1. 创建 Python BPF 对象,将 BPF C 程序传递给该 BPF 对象。
  2. 使用 BCC 改写器对 BPF C 程序进行预处理,将内存访问替换为 bpf_probe_read () 调用。
  3. 使用 Clang 将 BPF C 程序编译为 LLVM IR。
  4. 使用 BCC codegen 根据需要增加额外的 LLVM IR。
  5. LLVM 将 IR 编译为 BPF 字节码。
  6. 如果用到了映射表,就创建这些映射表。
  7. 字节码被传送到内核,并经过 BPF 验证器的检查。
  8. 事件被启用,BPF 程序被挂载到事件上。
  9. BCC 程序通过映射表或者 perf_event 缓冲区读取数据。

21.jpg

bpf_trace_printfk做调试
之后对

1
2
3
4
5
6
# 读取调试信息
# 会阻塞更多的消息,当读取后消息会被清除
cat /sys/kernel/debug/tracing/trace_pipe

# 打印头文件,不会阻塞
cat /sys/kernel/debug/tracing/trace

ebpf 参数,可以把这个程序的源码打印出来

1
execsnoop-bpfcc --ebpf

调试标志位

1
b = BPF(tex = bpf_text, debug = 0x2)   
标志位 名称 调试
0x1 DEBUG_LLVM_IR 打印编译好的 LLVM 中间表示形式
0x2 DEBUG_BPF 在分支处打印 BPF 字节码和寄存器状态
0x4 DEBUG_PREPROCESSOR 打印预处理结果(与 –ebpf 类似)
0x8 DEBUG_SOURCE 打印出源代码中内嵌的汇编指令
0x10 DEBUG_BPF_REGISTER_STATE 打印所有指令中的寄存器状态
0x20 DEBUG_BTF 打印出 BTF 调试信息(否则 BTF 错误会被忽略)

其他

1
2
bpflist-bpfcc
bpflist-bpfcc -vv

bpftrace

bpftrace 程序结构

  • 前端使用了 lex、yacc 做词法、语法分析
  • clang 解析 结构体
  • 后端将 bpftrace 程序编译成 LLVM 中间表示形式
  • 再通过 LLVM 库将其编译为 BPF 代码 22.jpg

bpftrace 内置的工具

bpf官方手册

主题 特色工具
CPU 相关 execsnoop.bt、runqlat.bt、runqlen.bt、cpuwalk.bt、offcputime.bt
内存相关 oomkill.bt、faults.bt、vmscan.bt、swapin.bt
文件系统相关 vfsstat.bt、filelife.bt、xfsdist.bt
存储 I/O 相关 biosnoop.bt、biolatency.bt、bitesize.bt、biostacks.bt、scsilatency.bt、nvmelatency.bt
网络相关 tcpaccept.bt、tcpconnect.bt、tcpdrop.bt、tcpretrans.bt、gethostlatency.bt
安全相关 ttysnoop.bt、elfsnoop.bt、setuids.bt
编程语言相关 jnistacks.bt、javacalls.bt
应用程序相关 threadsnoop.bt、pmheld.bt、naptime.bt、mysqld_qslower.bt
内核相关 mlock.bt、mheld.bt、kmem.bt、kpages.bt、workq.bt
容器相关 pidnss.bt、blkthrot.bt
虚拟机管理器相关 xenhyper.bt、cpustolen.bt、kvmexits.bt
调试器/多用途工具相关 execsnoop.bt、threadsnoop.bt、opensnoop.bt、killsnoop.bt、signals.bt

以直方图行驶统计 vfs_read() 返回值

1
bpftrace -e 'kretprobe:vfs_read { @bytes = hist(retval); } '

bpftrace 的单行程序

 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
# 展示系统中谁在执行什么命令:
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(args->filename)); }'



# 展示新进程的创建,以及参数信息
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { join(args->argv); }'


# 通过 openat () 查看打开文件动作,按进程统计
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'


# 按照不同程序统计系统调用
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'



# 按系统调用探针的名字对系统调用进行计数
bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'


# 按进程统计系统调用数量
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[pid, comm] = count(); }'


# 按进程展示总的读取字节数
bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret/ { @[comm] = sum(args->ret); }'


# 按进程展示 read 返回结果大小的分布
bpftrace -e 'tracepoint:syscalls:sys_exit_read { @[comm] = hist(args->ret); }'


# 展示进程的磁盘 I/O 尺寸
bpftrace -e 'tracepoint:block:block_rq_issue { printf("%d %s %d\n", pid, comm, args->bytes); }'


# 按进程展示页换入的数量
bpftrace -e 'software:major-faults:1 { @[comm] = count(); }'


# 按进程展示缺页中断的数量
bpftrace -e 'software:faults:1 { @[comm] = count(); }'


# 对 PID 为 189 的进程,以 49Hz 的频率抓取其用户态的调用栈信息:
bpftrace -e 'profile:hz:49 /pid == 189/ {@[ustack] = count (); }'

bpftrace 的探针格式

1
2
3
4
5
6
# 内核需要一个参数
kprobe:vfs_read


# 用户态需要路径和函数名
uprobe:/bin/bash:readline

查询可用的探针

1
bpftrace -l 'kprobe:vfs_*'

查看内核函数参数

1
2
3
4
5
6
7
8
bpftrace -lv 'tracepoint:syscalls:sys_enter_clone'

    int __syscall_nr
    unsigned long clone_flags
    unsigned long newsp
    int * parent_tidptr
    int * child_tidptr
    unsigned long tls

其他语法

1
2
3
4
5
# 条件判断
/pid > 100 & pid < 1000/

# 执行
{ action one; action two; action three }

变量

  • 内置变量
  • 临时变量: $x = 1; $y = “hello”; $z = (struct task_struct *)curtask;
  • 映射哈希表,可以在不同 action 之间传递(全局变量) @a = 1;

对内核函数 vfs_read() 进行计时,并用直方图打印

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/bpftrace

// this program times vfs_read()

kprobe:vfs_read
{
        @start[tid] = nsecs;
}

kretprobe:vfs_read
/@start[tid]/
{
        $duration_us = (nsecs - @start[tid]) / 1000;
        @us = hist($duration_us);
        delete(@start[tid]);
}

一个例子

1
2
3
4
# bpftrace -e 'tracepoint:syscalls:sys_enter_clone {
    printf("-> clone() by %s PID %d\n", comm, pid); }
  tracepoint:syscalls:sys_exit_clone {
    printf("<- clone() return %d, %s PID %d\n", args->ret, comm, pid); }'

bpftrace 探针类型

类型 缩写 描述
tracepoint t 内核静态插桩点
usdt U 用户态静态定义插桩点
kprobe k 内核动态函数插桩
kretprobe kr 内核动态函数返回值插桩
uprobe u 用户态动态函数插桩
uretprobe ur 用户态动态函数返回值插桩
software s 内核软件事件
hardware h 硬件基于计数器的插桩
profile p 对全部 CPU 进行时间采样
interval i 周期性报告(从一个 CPU 上)
BEGIN bpftrace 启动
END bpftrace 退出

硬件探针,预先定义好的硬件事件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
bpftrace -l 'hardware:*:*'
hardware:backend-stalls:
hardware:branch-instructions:
hardware:branch-misses:
hardware:bus-cycles:
hardware:cache-misses:
hardware:cache-references:
hardware:cpu-cycles:
hardware:frontend-stalls:
hardware:instructions:
hardware:ref-cycles:
硬件事件名称 缩写 默认采样间隔 描述
cpu-cycles cycles 1000000 CPU 运行时钟周期
instructions 1000000 CPU 运行指令数
cache-references 1000000 CPU 末级缓存引用
cache-misses 1000000 CPU 末级缓存未命中
branch-instructions branches 100000 跳转指令
bus-cycles 100000 总线周期
frontend-stalls 1000000 处理器前端阻塞(例如,取指令)
backend-stalls 1000000 处理器后端阻塞(例如,数据加载/存储)
ref-cycles 1000000 CPU 参考时钟周期(未使用 turbo)


软件探针,预先定义好的软件事件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
bpftrace -l 'software:*:*'
software:alignment-faults:
software:bpf-output:
software:context-switches:
software:cpu-clock:
software:cpu-migrations:
software:dummy:
software:emulation-faults:
software:major-faults:
software:minor-faults:
software:page-faults:
software:task-clock:
软件事件名称 缩写 默认采样间隔 描述
cpu-clock cpu 1000000 CPU 真实时间
task-clock 1000000 CPU 任务时间(只在任务运行在 CPU 上时增长)
page-faults faults 100 缺页中断
context-switches cs 1000 上下文切换
cpu-migrations 1 CPU 线程迁移
minor-faults 100 次要缺页中断:由内存满足
major-faults 1 主要缺页中断:由存储 I/O 满足
alignment-faults 1 对齐中断
emulation-faults 1 当指令模拟执行时触发中断
dummy 1 用于测试的假事件
bpf-output 1 BPF 输出通道

其他探针

  • profile,会在全部 cpu 上激活
  • interval,会在单个 cpu 上激活

bpftrace 内置的变量

官方文档
内置变量
内置函数
内置的map 函数

更多的例子

1
2
3
4
5
6
7
8

bpftrace -e 't:syscalls:sys_enter_setuid { printf("setuid by PID %d (%s), UID %d\n", pid, comm, uid); }'

bpftrace -e 'tracepoint:syscalls:sys_exit_setuid { printf("setuid by %s returned %d\n", comm, args->ret); }'

bpftrace -e 't:block:block_rq_insert { printf("Block I/O by %s\n", kstack); }'

bpftrace -e 't:block:block_rq_insert { @[kstack] = count(); }'

映射表变量

1
2
3
4
5
6
7
@start = nsecs;  // 整数类型

@last[tid] = nsecs;

@bytes = hist(retval);

@who[pid, comm] = count();  // 包含两个键

函数

1
2
3
bpftrace -e 'tracepoint:syscalls:sys_enter_execve {join(args->argv); }'   

bpftrace -e 'ur:/bin/bash:readline {printf("%s\n", str(retval)); }'

打印 堆栈信息

 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
bpftrace -e 't:block:block_rq_insert {@[kstack(3), comm] = count(); }'
# 结果
@[
    blk_mq_insert_requests+115
    blk_mq_insert_requests+115
    blk_mq_sched_insert_requests+187
, kworker/u64:5]: 20
@[
    blk_mq_insert_requests+115
    blk_mq_insert_requests+115
    blk_mq_sched_insert_requests+187
, kworker/u64:1]: 84
@[
    blk_mq_insert_requests+115
    blk_mq_insert_requests+115
    blk_mq_sched_insert_requests+187
, kworker/u64:4]: 144


bpftrace -e 'k:do_nanosleep {printf("%s", ustack(perf)); }'
# 结果
        503f4bd runtime.usleep.abi0+61 (/usr/share/grafana/bin/grafana)
        50145c5 runtime.sysmon+165 (/usr/share/grafana/bin/grafana)
        500bd33 runtime.mstart1+147 (/usr/share/grafana/bin/grafana)
        500bc7a runtime.mstart0+122 (/usr/share/grafana/bin/grafana)
        503b8e5 runtime.mstart.abi0+5 (/usr/share/grafana/bin/grafana)
        990ac68 start+114 (/usr/share/grafana/bin/grafana)

将地址解析为对应的函数名

1
2
3
4
5
6
7
8
bpftrace -e 'tracepoint:timer:hrtimer_start {@[ksym(args->function)] = count();}'

@[it_real_fn]: 2
@[sched_rt_period_timer]: 5
@[timerfd_tmrproc]: 5
@[watchdog_timer_fn]: 96
@[hrtimer_wakeup]: 4524
@[tick_sched_timer]: 10603

映射表函数

 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
# 对出现次数计数
bpftrace -e 'tracepoint:block:* { @[probe] = count(); }'

@[tracepoint:block:block_bio_frontmerge]: 2
@[tracepoint:block:block_rq_insert]: 15
@[tracepoint:block:block_plug]: 30
@[tracepoint:block:block_unplug]: 34
@[tracepoint:block:block_getrq]: 148
@[tracepoint:block:block_rq_issue]: 163
@[tracepoint:block:block_bio_backmerge]: 169
@[tracepoint:block:block_rq_complete]: 185
@[tracepoint:block:block_dirty_buffer]: 249
@[tracepoint:block:block_bio_complete]: 309
@[tracepoint:block:block_touch_buffer]: 463
@[tracepoint:block:block_bio_remap]: 624
@[tracepoint:block:block_bio_queue]: 628


# 聚合函数
# sum()、avg()、min()、max()
bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret > 0/ { @bytes = sum(args->ret); }'


# 打印柱状图(二次幂为区间)
bpftrace -e 'tracepoint:syscalls:sys_exit_read  { @ret = hist(args->ret); }'

# 打印线性直方图
bpftrace -e 'tracepoint:syscalls:sys_exit_read  { @ret = lhist(args->ret, 0, 1000, 100); }'

一个综合性的例子

1
2
3
4
bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; }
  kretprobe:vfs_read /@start[tid]/ {
    @ms[comm] = sum(nsecs - @start[tid]); delete(@start[tid]); }
  END { print(@ms, 0, 1000000); clear(@ms); clear(@start); }'

执行原理

  • bpftrace 使用 libbcc 和 libbpf 完成对探针的插桩、程序的加载,以及使用 USDT。它也使用 LLVM 将程序编译为 BPF 字节码。
  • bpftrace 语言是使用 lex 和 yacc 文件定义的,会分别经过 flex 和 bison 程序处理
  • 输出是一个作为抽象语法树存在的程序。跟踪点解析器和 Clang 解析器会对这个结构进行语法分析
  • 一个语法分析器会检查语言元素的使用,在出错时会抛出错误
  • 下一步是代码生成 —— 将 AST 节点转为 LLVM IR,最后再由 LLVM 编译为 BPF 字节码。 23.jpg

编程语言

使用 BPF 对一种语言进行跟踪所需要的能力,包括可回答以下问题:

  • 什么函数被调用?
  • 函数的参数是什么?
  • 函数的返回值是什么?有错误吗?
  • 引发了某个事件的什么代码路径(调用栈)?
  • 函数运行的时长是多久?可以用直方图表示吗?

C相关工具

工具 来源 目标 描述
funccount BCC 函数 对函数调用计数
stackcount BCC 调用栈 对导致某个事件发生的本地调用栈进行计数
trace BCC 函数 打印详细的函数调用和返回值
argdist BCC 函数 摘要统计函数参数和返回值
bpftrace BT All 自定义函数和调用栈插桩

bpftrace

 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
# 对以 “attach” 开头的内核函数调用进行计数:
bpftrace -e 'kprobe:attach* { @[probe] = count(); }'

# 对某个二进制(如 /bin/bash)文件中以 a 开头的函数调用计数:
bpftrace -e 'uprobe:/bin/bash:a* { @[probe] = count(); }'

# 对某个库(如 libc.6.so)文件中以 a 开头的函数调用计数:
bpftrace -e 'u:/lib/x86_64-linux-gnu/libc.so.6:a* { @[probe] = count(); }'

# 跟踪某个函数和它的参数(比如,bash 的 readline ()):
bpftrace -e 'u:/bin/bash:readline { printf("prompt: %s\n", str(arg0)); }'

# 跟踪某个函数和它的返回值(比如,bash 的 readline ()):
bpftrace -e 'ur:/bin/bash:readline { printf("read: %s\n", str(retval)); }'

# 跟踪某个库函数和它的参数(比如,libc 的 fopen ()):
bpftrace -e 'u:/lib/x86_64-linux-gnu/libc.so.6:fopen { printf("opening: %s\n",str(arg0)); }'

# 对某个库函数的返回值进行统计(比如,libc 的 fopen ()):
bpftrace -e 'ur:/lib/x86_64-linux-gnu/libc.so.6:fopen { @[retval] = count(); }'

# 对某个用户态函数调用栈进行计数(比如,bash 的 readline ()):
bpftrace -e 'u:/bin/bash:readline { @[ustack] = count(); }'

# 以 49Hz 的频率对用户态调用栈进行采样:
bpftrace -e 'profile:hz:49 { @[ustack] = count(); }'

Java相关工具

工具 来源 跟踪目标 描述
jnistacks 本书 libjvm 使用对象调用栈来展示JNI消费者
profile BCC CPU 基于时间的调用栈采样,包括Java方法
offcputime BCC Sched 未在CPU上运行的栈采样,包括Java方法
stackcount BCC Event 对任何事件获取其调用栈
javastat BCC USDT 高级语言操作统计
javathreads 本书 USDT 跟踪线程的开始和结束事件
javacalls BCC/本书 USDT 对Java方法调用进行计数
javaflow BCC USDT 展示Java方法的代码流
javagc BCC USDT 跟踪Java的垃圾回收
javaobjnew BCC USDT 对Java新对象的分配进行计数

Java 的 USDT 探针

  • 虚拟机生命周期
  • 线程生命周期
  • 类加载
  • 垃圾回收
  • 方法编译
  • 监控器
  • 应用跟踪
  • 方法调用
  • 对象分配
  • 监控器事件

上述探针只在 JDK 使用了 –enable-dtrace 编译选项后才能使用

应用程序

应用程序相关的工具

工具 来源 目标 工具介绍
execsnoop BCC/BT 调度器 列出新创建的进程
threadsnoop 本书 pthread 列出新创建的线程
profile BCC CPU on-CPU 调用栈采样
threaded 本书 CPU on-CPU 线程采样
offcputime BCC Sched 展示带调用栈的 off-CPU 执行时间
offcpuist 本书 Sched 展示带时间直方图的 off-CPU 调用栈
syscount BCC 系统调用 按类型计数的系统调用
ioprofile 本书 I/O I/O 调用栈计数
mysqld_qslower BCC/本书 MySQL服务器 展示慢于阈值的 MySQL 查询
mysqld_clat 本书 MySQL服务器 以直方图的形式展示 MySQL 命令延迟
signals 本书 信号 汇总目标进程发出的信号
killsnoop BCC/BT 系统调用 展示带发送进程信息的 kill(2) 系统调用
pmlock 本书 展示 pthread 互斥锁次数和用户调用栈
pmheld 本书 展示 pthread 互斥锁被持有次数和用户调用栈
naptime 本书 系统调用 展示被动睡眠调用

bpftrace

 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
# 带参数的新创建进程:
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { join(args->argv); }'
# 按进程对系统调用计数:
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[pid, comm] = count(); }'

# 按系统调用名称对系统调用计数:
bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'

# 对 PID 为 189 的进程,以 49Hz 进行用户态调用栈采样:
bpftrace -e 'profile:hz:49 /pid == 189/ { @[ustack] = count(); }'

# 对 mysqld 进程,以 49Hz 进行用户态调用栈采样:
bpftrace -e 'profile:hz:49 /comm == "mysqld"/ { @[ustack] = count(); }'

# 对 off-CPU 用户态调用栈计数:
bpftrace -e 'tracepoint:sched:sched_switch { @[ustack] = count(); }'

# 对所有调用栈和进程名采样:
bpftrace -e 'profile:hz:49 { @[ustack, stack, comm] = count(); }'

# 按用户态调用栈计算 malloc () 请求的字节总数(高开销):
bpftrace -e 'u:/lib/x86_64-linux-gnu/libc-2.27.so:malloc { @[ustack(5)] = sum(arg0); }'

# 跟踪 kill () 信号,显示发送进程名称、目标 PID 和信号号码:
bpftrace -e 't:syscalls:sys_enter_kill { printf("%s -> PID %d SIG %d\n", comm, args->pid, args->sig); }'

# 对 libpthread 互斥锁方法计数 1 秒:
bpftrace -e 'u:/lib/x86_64-linux-gnu/libpthread.so.0:pthread_mutex_*lock { @[probe] = count(); } interval:s:1 { exit(); }'

# 对 libpthread 条件变量相关函数计数 1 秒:
bpftrace -e 'u:/lib/x86_64-linux-gnu/libpthread.so.0:pthread_cond_* { @[probe] = count(); } interval:s:1 { exit(); }'

# 按进程对 LLC 缓存未命中计数:
bpftrace -e 'hardware:cache-misses: { @[comm] = count(); }'

内核

BPF 跟踪工具可以提供内核指标外的其他信息,包括回答以下问题:

  • 线程为什么离开 CPU,以及它们离开 CPU 多久了?
  • off-CPU 的线程在等待什么事件?
  • 谁正在使用内核的 slab 分配器?
  • 内核是否正在移动页面来平衡 NUMA?
  • 正在发生什么工作队列事件?延迟是多少?
  • 对于内核开发者:哪些函数被调用了?传入和返回的参数是什么?延迟是多少?

内核事件类型和插桩源

事件类型 事件源
内核函数执行 kprobes
调度器事件 sched 跟踪点
系统调用 syscalls 跟踪点和 raw_syscalls 跟踪点
内核内存分配 kmem 跟踪点
页面换出守护进程扫描 vmscan 跟踪点
中断 irq 跟踪点和 irq_vectors 跟踪点
工作队列执行 workqueue 跟踪点
计时器 timer 跟踪点
IRQ 和抢占禁用 preemptirq 跟踪点1

内核相关工具

工具 来源 目标 描述
loads BT CPU 显示平均负载
offcputime BCC/本书 调度器 总结off-CPU调用栈和时间
wakeuptime BCC 调度器 总结唤醒调用栈和阻塞时间
offwaketime BCC 调度器 总结带off-CPU调用栈的唤醒
mlock 本书 互斥锁 显示互斥锁时间和内核堆栈
mheld 本书 互斥锁 显示互斥保持时间和内核堆栈
kmem 本书 内存 总结内核内存分配
kpages 本书 内存页面 总结内核内存页面分配
memleak 本书 内存 显示可能的内存泄漏代码路径
slabratetop BCC/本书 slab 按缓存显示内核slab分配率
numamove 本书 NUMA 显示NUMA页面迁移统计信息
workq 本书 工作队列 显示工作队列函数的执行时间

bpftrace

 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
# 按进程对系统调用进行计数:
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[pid, comm] = count(); }'

# 按系统调用探针名称对系统调用进行计数:
bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'

#按系统调用函数对系统调用进行计数:
bpftrace -e 'tracepoint:raw_syscalls:sys_enter {
    @[ksym(*(kaddr("sys_call_table") + args->id * 8))] = count(); }'
	
# 对以 “attach” 开始的内核函数调用进行计数:
bpftrace -e 'kprobe:attach* { @[probe] = count(); }'

# 为内核函数 vfs_read () 计时并将其总结为直方图:
bpftrace -e 'k:vfs_read { @ts[tid] = nsecs; } kr:vfs_read /@ts[tid]/ {
    @ = hist(nsecs - @ts[tid]); delete(@ts[tid]); }'	
	
# 对内核函数 “func1” 的第一个整数参数出现的频率进行计数:
bpftrace -e 'kprobe:func1 { @[arg0] = count(); }'

# 对内核函数 “func1” 的返回值出现的频率进行计数:
bpftrace -e 'kretprobe:func1 { @[retval] = count(); }'

# 以 99Hz 对内核态调用栈采样,不包含 idle:
bpftrace -e 'profile:hz:99 /pid/ { @[kstack] = count(); }'

# 以 99Hz 对 on-CPU 内核函数采样:
bpftrace -e 'profile:hz:99 { @[kstack(1)] = count(); }'

# 对上下文切换调用栈计数:
bpftrace -e 't:sched:sched_switch { @[kstack, ustack, comm] = count(); }'

# 按内核函数对工作队列请求进行计数:
bpftrace -e 't:workqueue:workqueue_execute_start { @[ksym(args->function)] = count() }'

# 对内核函数开始的 hrtimer 进行计数:
bpftrace -e 't:timer:hrtimer_start { @[ksym(args->function)] = count(); }'	

容器

主要有两种实现容器的方法。

  • 操作系统级别的虚拟化:在 Linux 中这涉及使用命名空间(namespace)对系统进行分区,通常与 cgroup 结合使用进行资源控制。所有容器共享同一个运行着的内核。这是 Docker、Kubernetes 以及其他容器环境所使用的方法。
  • 硬件级别的虚拟化:这涉及运行轻量级虚拟机,每个虚拟机都有自己的内核。Intel 的 Clear Containers(即现在的 Kata Containers[165])和 AWS 的 Firecracker[166]使用这种方式。

容器专用工具

工具 来源 目标 工具描述
runqlat BCC Sched 按 PID 命名空间总结 CPU 运行队列延迟
pidnss 本书 Sched 对 PID 命名空间切换进行计数:容器共享一个 CPU
blkthrot 本书 块I/O 对被 blk cgroup 限制的块I/O进行计数
overlayfs 本书 Overlay文件系统 显示Overlay文件系统的读写延迟

bpftrace

1
2
3
4
5
6
7
# 以 99Hz 的频率对 cgroup ID 进行计数:
bpftrace -e 'profile:hz:99 { @[cgroup] = count(); }'

# 跟踪名为 “container1” 的容器(cgroup v2)中打开的文件名:
bpftrace -e 't:syscalls:sys_enter_openat
/cgroup == cgroupid("/sys/fs/cgroup/unified/container1")/ {
printf("%s\n", str(args->filename)); }'

虚拟机

类型

  • 配置 A:此配置被称为本机管理器或裸机管理器。虚拟机管理器直接运行在处理器上,它负责创建为运行访客虚拟机的域并将虚拟的访客系统 CPU 调度到真实 CPU 上,这种配置的一个流行的例子是 Xen 虚拟机管理器。
  • 配置 B: 虚拟机管理器由宿主机内核运行,可能由内核态模块和用户态进程组成。宿主机操作系统有管理虚拟机管理器的特权,其内核负责将虚拟机 CPU 以及宿主机上的其他进程一同调度。这种配置的一个流行的例子是 KVM 虚拟机管理器。

改进

  • 处理器虚拟化支持:在2005—2006年引入的AMD-V和Intel VT-x处理器扩展为虚拟机操作提供了更快的硬件支持。
  • 半虚拟化(paravirt或PV):通过半虚拟化运行一个修改过的操作系统,可以使操作系统知道它正在硬件虚拟机上运行,并对虚拟机管理器发起超级调用,以更高效地处理某些操作。为了提高效率,Xen将这些超级调用批处理为一个多重调用。
  • 硬件设备支持:为了进一步优化虚拟机性能,处理器之外的硬件设备已经添加了虚拟机支持。这包括用于网络和存储设备的SR-IOV,以及使用它们的特殊驱动程序:ixgbe、ena和nvme。

监控

GUI 和工具包括如下一些。

  • Vector 和 Performance Co-Pilot(PCP):用于远程 BPF 监控
  • Grafana 配合 PCP:用于远程 BPF 监控
  • eBPF Exporter:用于 BPF 同 Prometheus 和 Grafana 的集成
  • kubectl-trace:用于跟踪 Kubernetes 的 pods 和 nodes

参考