差生文具多之(一)eBPF

news2024/11/28 19:37:22

前言

在问题排查过程中, 通常包含: 整体观测, 数据采集, 数据分析这几个阶段. 对于简单问题的排查, 可以跳过前两个步骤, 无需额外收集数据, 直接通过分析日志中的关键信息就可以定位根因; 而对于复杂问题的排查, 为了对应用的行为有更完整的了解, 可以通过以下形式收集更多的行为数据帮助分析:

  1. 调高日志级别生成数据, 例如将内核的 /proc/sys/kernel/sched_schedstats 配置为 1, 例如将应用的日志级别从 INFO 调整为 DEBUG;

  2. 利用静态埋点生成数据, 例如内核的 tracepoint, 例如应用的 USDT;

  3. 利用动态埋点生成数据, 例如内核的 kprobe, 例如应用的 uprobe.

本篇主角 eBPF 提供 "动态将代码挂载在执行点(用户/内核/动态/静态)上进行数据采集, 通过内置数据结构(数组/哈希/函数映射)保存数据及和用户态程序通信" 的能力. 翻译成人话是:

  1. eBPF 通过验证器保证了挂载程序的安全性, 避免恶意程序的挂载;

  2. eBPF 虚拟机执行挂载程序, 进行数据采集, 数据过滤;

  3. eBPF 仅在用户空间请求时才将内核中的数据拷贝到用户空间, 保证消耗最小化.

eBPF 是如此强力(且流行)的机制, 我们怎么利用它对系统进行观测呢? eBPF 支持多种用户空间语言, 让我们先从简单的脚本语言 bpftrace 开始.

bpftrace

bpftrace 是基于 eBPF 的, 用于进行系统追踪的脚本语言, 语法和 awk 基本一致, 如下:

BEGIN
{
 // BEGIN 语句块, 一开始会被执行一次
} 

kprobe:foo // 指定挂载点
/condition/ // 仅当匹配该条件, 以下语句块才会被执行
{
 // 该脚本运行过程中, 每次匹配条件都会执行的语句块
}

END
{
 // END 语句块, 结束时会被执行一次
} 

bpftrace 指定 -e 可以指定需要解析的语句块, 例如以下的语句可以打印一句 "hello world":

$ bpftrace -e 'BEGIN{printf("hello world.\n")}'

bpftrace 指定 -l 可以列出可用的挂载点, 可用挂载点的数量一定程度上反映了 eBPF 的能力边界, 例如在 CentOS Linux release 8.4.2105 上存在 8w 多个挂载点可供挂载函数. (如果降低内核编译优化等级, 可以导出更多可供挂载的符号)

$ bpftrace -l
...
kprobe:do_sys_open
...
$ bpftrace -l | wc -l
82589

例如我们想查看系统中 open 系统调用正在打开哪些文件, 只需要在挂载点的位置填上 kprobe:do_sys_open 即可:

$ bpftrace -e 'kprobe:do_sys_open {printf("%-7d %-10s %s\n", pid, comm, str(uptr(arg1)))}'
9016    awk        /proc/self/maps
9016    awk        /dev/null
1095    ksmtuned   /sys/kernel/mm/ksm/run
9017    ksmtuned   /dev/null

其中:

  1. kprobe:do_sys_open: 指明挂载点, 即每次执行 do_sys_open 函数体执行前先执行一次语句块 {printf("%-7d %-10s %s\n", pid, comm, str(uptr(arg1)))};

  2. pid: bpftrace 的内置变量, 保存了 curr 进程的进程号;

  3. comm: bpftrace 的内置变量, 保存了 curr 进程的进程名;

  4. str: bpftrace 的内置函数, 用于标志字符串;

  5. uptr: bpftrace 的内置函数, 用于标识指针来自用户空间, 当打印 char __user * 类型变量时需要用到;

  6. arg1: bpftrace 的内置变量, 下标从 0 开始, 此处指代内核函数 do_sys_open(int dfd, const char __user *filename, ...) 第二个参数. bpftrace 提供了很多实用的内置变量, 具体参考 reference_guide.

当 eBPF 程序写得越来越长, 单独写成 bpftrace 脚本是更合适的. 以下脚本实现了每秒输出当前系统每个 CPU 运行队列中等待运行进程的个数:

$ wget -qO - https://raw.githubusercontent.com/lilstaz/perf-tool-examples/main/bpftrace/runqlen.bt
...
profile:hz:99 // 1
{
    $cfs_rq = curtask->se.cfs_rq; // 2
    @tmp[cpu] = $cfs_rq->nr_running; // 3
}

interval:s:1 // 4
{
 printf("@[%s]: %s\n", "CPU", "RQ_LEN");
 print(@tmp);
}
...
$ wget -qO - https://raw.githubusercontent.com/lilstaz/perf-tool-examples/main/bpftrace/runqlen.bt | bpftrace - # 5
@[CPU]: RQ_LEN
@[0]: 0
@[3]: 0
@[1]: 0
@[2]: 1

其中:

  1. 在 profile 模式中, 可以指定采样的频率, 当配置为 99HZ, 每个 CPU 每秒会产生大约 99 次时钟中断, 通过时钟中断注册的回调函数对所需的信息进行采集;

  2. bpftrace 脚本中变量前 $ 标志该变量是局部变量, @ 标志该变量是全局变量, 这里通过 bpftrace 内置变量 curtask 拿到了 curr 进程, 数据类型为 task_struct*, 通过 curtask->se.cfs_rq 获取到当前进程调度实体被挂载哪个运行队列;

  3. 将当前运行的 CPU 作为哈希 key, 将队列上调度实体的个数作为哈希 value, 保存到全局变量 @tmp 中;

  4. 每隔一秒钟输出一次列名及整个哈希表的内容;

  5. 运行效果, 当前虚拟机有 4 个核心, 其中 CPU 2 的运行队列中存在一个进程.

以上两个案例展示了: eBPF 程序可以挂载在内核函数上, 例如挂在 do_sys_open 函数上, 此时可以通过解析函数的输入获取所需的信息; 也可以注册为 perf_event 的回调函数, 利用 perf 提供的采样机制, 在中断上下文提取所需信息.

bcc

bcc 是一个开源的 Linux 动态跟踪工具. 无第三方模块依赖, 该工具继承 BPF 这个强大的内核中虚拟机的功能, 可对程序进行高效而且安全的跟踪. 在安装了 bcc 之后我们可以在目录 /usr/share/bcc 中找到它, 其 tools 子目录包含了大量实用的观测工具, 大部分由 python 代码写成:

$ ll /usr/share/bcc/tools/
....
-rwxr-xr-x. 1 root root  9528 Jan 24  2023 runqlat
-rwxr-xr-x. 1 root root  7919 Jan 24  2023 runqlen
-rwxr-xr-x. 1 root root  8929 Jan 24  2023 runqslower
...

以我们熟悉的 runqlen 作为例子, 该脚本几乎提供了上节例 2 一模一样的功能:

bpf_text = """
...
struct cfs_rq_partial { // 1
    struct load_weight load;
    RUNNABLE_WEIGHT_FIELD
    unsigned int nr_running, h_nr_running;
};
BPF_HISTOGRAM(dist, cpu_key_t, MAX_CPUS);

int do_perf_event()
{
    unsigned int len = 0;
    pid_t pid = 0;
    struct task_struct *task = NULL;
    struct cfs_rq_partial *my_q = NULL;

    task = (struct task_struct *)bpf_get_current_task(); // 2
    my_q = (struct cfs_rq_partial *)task->se.cfs_rq;
    len = my_q->nr_running; // 3

    if (len > 0) // 4
        len--;

    STORE
    return 0;
}

bpf_text.replace('STORE', ...) // 5
b = BPF(text=bpf_text, ...) // 6
b.attach_perf_event(ev_type=PerfType.SOFTWARE, // 7
    ev_config=PerfSWConfig.CPU_CLOCK, fn_name="do_perf_event",
    sample_period=0, sample_freq=99)

该 python 脚本主要包含三个部分: 挂载程序(bpf_text); 对挂载程序的修正(bpf_text.replace 对 bpf_text 做字符串替换); 使用 BPF 加载 eBPF 程序文本. 其中:

  1. 内核没有导出 cfs_rq 结构体, 需要我们自己定义一个数据结构, 目的是以正确的偏移获取 cfs_rq 的成员 nr_running;

  2. eBPF 程序通过 bpf_get_current_task 获取 curr 进程. 此处 task 等同于 bpftrace 脚本中的 curtask;

  3. 因为标号 1 处结构体的定义, 此处可以以正确的偏移量拿到 nr_running 数值;

  4. nr_running 计数包含当前运行的进程, 减去 1 之后才是等待队列的长度. 这里需要对 len == 0 的场景做特殊处理. 因为我们通过时钟中断注册的回调函数对所需的信息进行采集, 被中断打断的进程可能是 idle 进程, idle 进程运行是不计入 nr_running 的. 这里做了场景特判;

  5. 字符串处理, 此处替换 STORE 字符串. 脚本中有些字符串替换是为了兼容系统版本, 例如高版本 cfs_rq 才引入 runnable_weight 成员, 所以在高版本内核运行该脚本时, 将标号 1 数据结构中的 RUNNABLE_WEIGHT_FIELD 替换为 runnable_weight 成员, 而在低版本将 RUNNABLE_WEIGHT_FIELD 替换为空; 有些字符串替换是为了根据 bcc 脚本的输入做 bpf 代码的调整, 例如脚本输出的单位默认为纳秒, 可以指定参数 -m 将输出的单位替换为微秒;

  6. 验证并加载 BTF 元数据到内核, 创建相关的数据结构例如 dist;

  7. 这里指定使用 perf 基于时钟驱动的软件事件, 即利用 perf 的采样模式, frequency 指定为 99 HZ, 入口函数指定为 eBPF 程序中的 do_perf_event. 因此该语句主要涉及以下工作:
    1. 验证及加载 eBPF 程序到内核

    2. 为系统中每一个 CPU 注册 perf 软件事件

    3. 将每个 CPU 注册的软件事件的回调函数替换为 do_perf_event 函数

至此, 我们已经见识过用户态程序使用 bpftrace 及使用 python 写成的 runqlen, 用户态程序除了用于打印 eBPF 程序中收集得到的值, 还可以用于进行用户态内核态通信, 或者用于火焰图生成等数据后处理; 两份 runqlen 代码中 eBPF 程序的内容也基本一致. 下面让我们忽略这些微的差异, 从系统调用的角度看看 runqlen 和内核进行了哪些交互.

syscall

这一节采用 strace 工具对 bpftrace 写成的 runqlen 进行观测, 相关系统调用梳理如下:

bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_PERF_EVENT, insn_cnt=33, insns=0x5579141cf330, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(0, 0, 0), prog_flags=0, prog_name="99",..) = 15 // 1
/* 以下两句, 重复 CPU 个数次 */
perf_event_open({type=PERF_TYPE_SOFTWARE, size=0 /* PERF_ATTR_SIZE_??? */, config=PERF_COUNT_SW_CPU_CLOCK, sample_freq=99, sample_type=0, read_format=0, freq=1, precise_ip=0 /* arbitrary skid */, ...}, -1, 0, -1, PERF_FLAG_FD_CLOEXEC) = 16 // 2
ioctl(16, PERF_EVENT_IOC_SET_BPF, 15) // 3

bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_PERF_EVENT, insn_cnt=37, insns=0x5579141e1580, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(0, 0, 0), prog_flags=0, prog_name="1", ...) // 4
perf_event_open({type=PERF_TYPE_SOFTWARE, size=0 /* PERF_ATTR_SIZE_??? */, config=PERF_COUNT_SW_CPU_CLOCK, sample_period=1000000000, sample_type=0, read_format=0, precise_ip=0 /* arbitrary skid */, ...}, -1, 0, -1, PERF_FLAG_FD_CLOEXEC) = 23 // 5
ioctl(23, PERF_EVENT_IOC_SET_BPF, 24)   = 0 // 6

因为脚本中有两段 eBPF 程序, 所以可以看到系统调用分为两个部分(line 1-3, line 4-6), 其中:

  1. 加载采样频率为 99HZ 的 eBPF 程序;

  2. 为 0 号 CPU (CPU ID 由倒数第三个参数指定) 注册一个以时钟驱动的软件事件; 使用 perf_event 的 sampling 模式, 当频率被设置为 99HZ, 每秒采样的次数由 sample_freq 参数指定为 99;

  3. 将步骤 2 软件事件的回调函数修改为步骤 1 加载的 eBPF 程序, 假设系统中有 4 个核, 步骤 2 和 3 会重复 4 次.

  4. 加载 1 秒打印 1 次输出的 eBPF 函数;

  5. 为 0 号 CPU 注册一个 1 秒触发一次的软件事件;

  6. 将步骤 5 软件事件的回调函数修改为步骤 4 加载的 eBPF 程序. 因为每秒只需要输出一次, 此处让 0 号 CPU 负责周期性执行该段 eBPF 程序.

后记

本篇主要从一个小例子 runqlen 入手, 串联介绍了 bpftrace bcc 工具集和相关的系统调用. 因为 eBPF 挂载点渗透到内核每个子系统, 并且对 eBPF 程序提供了安全性验证, 极大增强了内核的可观测性. 如果将内核状态机看作时序维度上的一叠黑胶片, eBPF 就是你手里那杯显影液.

ref

  1. bcc python developer tutorial

  2. linux observability with bpf

  3. https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1174861.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

程序员成长树

- 10年以后我在做什么? 成为项目负责人(管理事、管理人) - 如何处理同事的关系: 平时生活中最简单的一句问候,闲暇时间的聊天了解,互帮互助 - miss yang: - 1、软件UI设计 - 2、需求分析 - 3、协调推进任务的安排 …

linux 驱动——字符设备驱动(自动生成设备节点文件)

文章目录 字符设备驱动字符设备 APP生成 dev 节点的原理配置内核自动创建设备节点模块使用 字符设备驱动 #include "linux/device/class.h" #include "linux/export.h" #include "linux/uaccess.h" #include <linux/types.h> #include &l…

10 DETR 论文精读【论文精读】End-to-End Object Detection with Transformers

DETR 这篇论文&#xff0c;大家为什么喜欢它&#xff1f;为什么大家说它是一个目标检测里的里程碑式的工作&#xff1f;而且为什么说它是一个全新的架构&#xff1f; 大家好&#xff0c;今天我们来讲一篇 ECC V20 的关于目标检测的论文。它的名字想必大家都不陌生&#xff0c;也…

Python---字符串中的查找方法--index()--括号里是要获取的字符串

index()方法其功能与find()方法完全一致&#xff0c;唯一的区别在于当要查找的子串没有出现在字符串中时&#xff0c;find()方法返回-1&#xff0c;而index()方法则直接 报错。 find()方法相关链接&#xff1a;Python---字符串中的查找方法--find&#xff08;&#xff09;--括…

排序——冒泡排序

冒泡排序的基本思想 从前往后&#xff08;或从后往前&#xff09;两两比较相邻元素的值&#xff0c;若为逆序&#xff08;即 A [ i − 1 ] < A [ i ] A\left [ i-1\right ]<A\left [ i\right ] A[i−1]<A[i]&#xff09;&#xff0c;则交换它们&#xff0c;直到序列…

【漏洞复现】Nginx_(背锅)解析漏洞复现

感谢互联网提供分享知识与智慧&#xff0c;在法治的社会里&#xff0c;请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞等级1.3、影响版本1.4、漏洞复现1、基础环境2、漏洞扫描3、漏洞验证 1.5、深度利用GetShell 1.1、漏洞描述 这个漏洞其实和代码执行没有太大关系&…

基础Redis-Java客户端操作介绍

Java客户端操作介绍 2.基础-Redis的Java客户端a.介绍b.Jedisc.Jedis连接池d.SpringDataRedise.SpringDataRedis的序列化方式f.StringRedisTemplate 2.基础-Redis的Java客户端 a.介绍 Jedis 以Redis命令作为方法名称&#xff0c;学习成本低&#xff0c;简单实用。但是Jedis实例…

C++: 类和对象(中)

文章目录 1. 类的6个默认成员函数2. 构造函数构造函数概念构造函数特性特性1,2,3,4特性5特性6特性7 3. 析构函数析构函数概念析构函数特性特性1,2,3,4特性5特性6 4. 拷贝构造函数拷贝构造函数概念拷贝构造函数特性特性1,2特性3特性4特性5 5. 运算符重载一般运算符重载赋值运算符…

大模型进展的主要观点综述

大模型模式的意义可以用两个词来概括&#xff1a;涌现和同质化。涌现意味着一个系统的行为是隐含诱导的&#xff0c;而不是明确构建的;它既是科学兴奋的源泉&#xff0c;也是对意外后果的一种焦虑。同质化表示在广泛的应用程序中构建机器学习系统的方法的整合;它为许多任务提供…

(14)学习笔记:动手深度学习(Pytorch神经网络基础)

文章目录 神经网络的层与块块的基本概念自定义块 问答 神经网络的层与块 块的基本概念 以多层感知机为例&#xff0c; 整个模型接受原始输入&#xff08;特征&#xff09;&#xff0c;生成输出&#xff08;预测&#xff09;&#xff0c; 并包含一些参数&#xff08;所有组成层…

FreeRTOS笔记【一】 任务的创建(动态方法和静态方法)

一、任务创建和删除API函数 函数描述xTaskCreate()使用动态的方法创建一个任务xTaskCreateStatic()使用静态的方法创建一个任务xTaskCreateRestricted()创建一个使用MPU进行限制的任务&#xff0c;相关内存使用动态内存分配vTaskDelete()删除一个任务 二、动态创建任务 2.1 …

国内外一级市场TOP10股权投资研究报告

前言 在金融领域&#xff0c;令人心跳加速的时刻往往来自于那些领先群雄的成就&#xff0c;无论是在科技创新、生产效率还是投资回报上。想象一下&#xff0c;如果财富的累积只是微不足道的&#xff0c;那又何异于日复一日的朝九晚五呢&#xff1f;随着时间的推移&#xff0c;…

【C++】详解IO流(输入输出流+文件流+字符串流)

文章目录 一、标准输入输出流1.1提取符>>&#xff08;赋值给&#xff09;与插入符<<&#xff08;输出到&#xff09;理解cin >> a理解ifstream&#xff08;读&#xff09; >> a例子 1.2get系列函数get与getline函数细小但又重要的区别 1.3获取状态信息…

数据包端到端的流程

流程 A给F发送一个数据包的流程&#xff1a; 首先 A&#xff08;192.168.0.1&#xff09;通过子网掩码&#xff08;255.255.255.0&#xff09;计算出自己与 F&#xff08;192.168.2.2&#xff09;并不在同一个子网内&#xff0c;于是决定发送给默认网关&#xff08;192.168.0.…

307. 区域和检索 - 数组可修改

给你一个数组 nums &#xff0c;请你完成两类查询。 其中一类查询要求 更新 数组 nums 下标对应的值 另一类查询要求返回数组 nums 中索引 left 和索引 right 之间&#xff08; 包含 &#xff09;的nums元素的 和 &#xff0c;其中 left < right 实现 NumArray 类&#xff…

数据分析与数据挖掘期末复习,附例题及答案

文章目录 一、概述1.数据挖掘能做什么&#xff1f;2.数据挖掘在哪些方面有应用&#xff1f;3.数据挖掘与数据分析的区别&#xff1f;4.数据挖掘的四大类模型5.什么是数据挖掘&#xff1f;6.数据挖掘的常用方法&#xff1f; 二、数据1.余弦相似度、欧几里得距离2.近似中位数 三、…

刚入职因为粗心大意,把事情办砸了,十分后悔

刚入职&#xff0c;就踩大坑&#xff0c;相信有很多朋友有我类似的经历。 5年前&#xff0c;我入职一家在线教育公司&#xff0c;新的公司福利非常好&#xff0c;各种零食随便吃&#xff0c;据说还能正点下班&#xff0c;一切都超出我的期望&#xff0c;“可算让我找着神仙公司…

数据结构与算法【02】—线性表

CSDN系列专栏&#xff1a;数据结构与算法专栏 针对以前写的数据结构与算法系列重写(针对文字描述、图片、错误修复)&#xff0c;改动会比较大&#xff0c;一直到更新完为止 前言 通过前面数据结构与算法基础知识我们知道了数据结构的一些概念和重要性&#xff0c;那么本章总结…

新技术前沿-2023-应用GPT提问模板写技术文章

参考一份万能的GPT提问模版&#xff01;直接套用&#xff01; 参考用GPT写技术文章是真爽&#xff01; 参考码住这篇 8200 字 ChatGPT 实战指南&#xff01;&#xff01; 1 GPT提问模板 想让GPT回答的内容符合我们所希望的&#xff0c;最最重要的一点就在于我们如何提问。提问…

NFS服务以及静态路由及临时IP配置

目录 一、NFC服务基础知识 1、NFS服务初相识 2、NFS服务工作原理 二、NFC服务基础操作 1、NFS服务端配置 2、NFS服务 - exports 相关参数 3、NFS服务 - 命令相关 三、RPC 远程调度 四、静态路由及临时IP配置 1、Linux 静态路由相关命令 2、Linux 临时IP地址添加与删除…