nfs_latency

用bpf获取rpc执行时间戳,计算函数执行时间这个代码是记录每调用一次rpc,就记录调用的rpc时间。代码具体用到了hashmap去存储每个进程号、当前时间戳。用数组去记录进程的pid和对应的耗时时间。

原始代码

nfs.bpf.c
// SPDX-License-Identifier: GPL-2.0
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

char LICENSE[] SEC("license") = "GPL";

struct event {
    u32 pid;       //pid进程
    u64 delta_ns;   //耗时时间
    char comm[16];   //进程名
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH); //指定类型是BPF_MAP_TYPE_HASH ----哈希表
    __uint(max_entries, 4096);          //最大key数量=4096
    __type(key, u64);       
    __type(value, u64);
} start SEC(".maps");  //SEC是指定将这个结构体放在BPF的.maps段中,在BPF中,段是一种组织代码和数据的方式

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);  //数组
} events SEC(".maps");

SEC("kprobe/rpc_call_start")   //表示这个 BPF 程序会被加载到内核中,并在每次rpc_call_start内核函数被调用时触发执行,也就是rpc_start开始执行时触发
int BPF_KPROBE(handle_rpc_start)  //BPF_KPROBE是一个宏用于定义一个 kprobe 类型的 BPF 程序
{
    u64 tid = bpf_get_current_pid_tgid();           //获取当前PID和TGID
    u64 ts = bpf_ktime_get_ns();                //获取当前的时间戳
    bpf_map_update_elem(&start, &tid, &ts, BPF_ANY); //向start哈希表里面更新数据,PF_ANY 表示如果键已存在,则更新值;如果键不存在,则插入新键值对
    return 0;
}

SEC("kretprobe/rpc_call_done")
int BPF_KRETPROBE(handle_rpc_done)
{
    u64 tid = bpf_get_current_pid_tgid();
    u64 *tsp = bpf_map_lookup_elem(&start, &tid);
    if (!tsp)
        return 0;

    u64 delta = bpf_ktime_get_ns() - *tsp;   //当前时间戳与hashmap中的时间戳进行计算,得到执行时间。
    bpf_map_delete_elem(&start, &tid);

    struct event e = {};
    e.pid = tid >> 32;
    e.delta_ns = delta;             
    bpf_get_current_comm(&e.comm, sizeof(e.comm));  //获取数据存储到event结构体里面

    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e)); //
    return 0;
}
nfs.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include "nfs.skel.h"

static volatile bool exiting = false;

//用户态
void handle_event(void *ctx, int cpu, void *data, __u32 size) //BPF 程序输出性能事件时,这个函数会被调用。
{
    struct event *e = data;
    printf("PID %d (%s): %.2f ms\n", e->pid, e->comm, e->delta_ns / 1e6);
}

void handle_lost(void *ctx, int cpu, __u64 lost) //当有事件数据丢失时,这个函数会被调用
{
    fprintf(stderr, "Lost %llu events on CPU %d\n", lost, cpu);
}

void sig_handler(int sig)
{
    exiting = true;
}

int main()
{
    struct nfs_bpf *skel;
    int err;

    signal(SIGINT, sig_handler);
    signal(SIGTERM, sig_handler); //注册信号处理程序,以便在收到 SIGINT 或 SIGTERM 时调用 sig_handler,

    skel = nfs_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open skeleton\n");
        return 1;
    }

    err = nfs_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load skeleton\n");
        return 1;
    }

    err = nfs_bpf__attach(skel);    //将 BPF 程序附加到内核中的 kprobe 和 kretprobe 点
    if (err) {
        fprintf(stderr, "Failed to attach\n");
        return 1;
    }

    printf("Tracing NFS client RPC latency... Ctrl+C to exit\n");

    struct perf_buffer *pb = NULL;
    pb = perf_buffer__new(bpf_map__fd(skel->maps.events), 8,
                          handle_event, handle_lost, NULL, NULL);  //创建缓冲区来接收程序输出的事件数据
    if (!pb) {
        fprintf(stderr, "Failed to open perf buffer\n");
        return 1;
    }

    while (!exiting)
        perf_buffer__poll(pb, 100);  //轮询缓冲区,等待事件数据的到来, poll轮询可以快速响应。

    perf_buffer__free(pb);
    nfs_bpf__destroy(skel);
    return 0;
}

修改部分

(1)error: 当前目录下没有 vmlinux.h 文件,这个文件在 nfs.bpf.c 中被引用

sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h 这条命令它从运行中的 Linux 内核中提取类型信息,并将其转换为 C 语言头文件格式

(2)error: struct_event没有在nfs.c定义

struct event {
    __u32 pid;       // 进程 ID
    __u64 delta_ns;  // 耗时时间
    char comm[16]; // 进程名
};

加入到nfs.c中。

(3)perf_buffer__new() 函数调用的参数数量不正确,我们使用的参数是 6 个,但函数定义只接受 3 个参数 那么我们来看一下函数正确的参数:

perf_buffer__new(int map_fd, size_t page_cnt,
                 const struct perf_buffer_opts *opts);

来看一下perf_buffer_opts结构体

struct perf_buffer_opts {
        /* if specified, sample_cb is called for each sample */
        perf_buffer_sample_fn sample_cb;
        /* if specified, lost_cb is called for each batch of lost samples */
        perf_buffer_lost_fn lost_cb;
        /* ctx is provided to sample_cb and lost_cb */
        void *ctx;
};

其实就可以看出,调用的API函数已经更新了,只有三个参数,新版本的API,是把事件回调、丢失事件回调、用户上下文放到结构体里面,然后进行结构体指针传参。

(4)内核中有 rpc_call_start 函数,但没有 rpc_call_done 函数。导致程序.nfs无法加载

同样可能是内核版本不同,将 rpc_call_done 改为 rpc_task_call_done

(5)map 'events' 创建失败的问题

map 'events': failed to create: Invalid argument 从这句提示可以看出 events的结构体定义不对,map定义,添加更多特定参数以满足内核要求:

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);  //数组
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(int));
    __uint(max_entries, 128);
} events SEC(".maps");

(6)无法找到 rpc_task_call_done 函数进行 kretprobe

sudo ls -la /sys/kernel/debug/tracing/events/sunrpc/ 输入指令后发现了 /sys/kernel/debug/tracing/events/sunrpc/rpc_task_call_done,这表明我们可以使用 tracepoint 而不是 kprobe,因此修改。

(7)由此可以看出大部分都是版本问题

  1. 我的libbpf版本是0.5.0, ubuntu的内核版本是6.8.0-59。
  2. perf_buffer API 变化,使用结构体传参。
  3. kprobe 到 tracepoint 的迁移、旧版本: 使用 kprobe/kretprobe 跟踪特定函数,新版本: 使用 tracepoint 跟踪标准化的事件点
  4. Map 定义的完整性要求,旧版本: 最小化定义可能足够,新版本: 需要更完整的定义,包括 key_size, value_size, max_entries

最后通过nfs挂载测试

本地挂载 NFS 共享

             
# 挂载本地共享 (使用NFSv4协议)
sudo mount -t nfs -o vers=4.1 localhost:/s_test /mnt                
# 验证挂载
df -hT | grep nfs4
# 应输出类似:
# localhost:/s_test 526802432 43963904 456005120    9% /mnt            
# 写操作
cd /mnt
touch file
dd if=/dev/urandom of=/mnt/file bs=1M count=10 status=progress

运行脚本文件

sudo make
sudo ./nfs
#先运行./nfs 自去进行写操作
#输出结果如下:
Tracing NFS client RPC latency... Ctrl+C to exit
PID 14807 (ls): 49.01 ms