差生文具多之(一)eBPF

前言

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

  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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

掌握Maven和SpringBoot的灵活性:定制化lib目录和依赖范围

前言 在开发基于Maven和SpringBoot的项目时,我们经常会使用第三方库来满足需求。然而,有时候我们需要更灵活地控制这些库的依赖范围和加载方式。本文将介绍如何使用Maven和SpringBoot实现定制化的lib目录和依赖范围。经过如下定制化后,打包执…

【算法 | 哈希表 No.2】leetcode 219. 存在重复元素II

个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 🍔本专栏旨在提高自己算法能力的同时,记录一下自己的学习过程,希望…

JVM类的声明周期

文章目录 版权声明生命周期概述加载阶段查看内存中的对象 连接阶段连接阶段之验证连接阶段之准备连接阶段之解析 初始化阶段练习题目一练习题目二练习题目三练习题目四 使用阶段卸载阶段总结 版权声明 本博客的内容基于我个人学习黑马程序员课程的学习笔记整理而成。我特此声明…

Microsoft Edge不能工作了,可能原因不少,那么如何修复呢

Microsoft Edge打不开或不能加载网页是用户在Windows 10、Android、Mac和iOS设备上的网络浏览器上遇到的许多错误之一。其他Microsoft Edge问题可能包括浏览器窗口和选项卡冻结、网站崩溃、互联网连接错误消息以及丢失Microsoft Edge书签、收藏夹、密码和收藏。 Microsoft Edg…

【安全】Java幂等性校验解决重复点击(6种实现方式)

目录 一、简介1.1 什么是幂等?1.2 为什么需要幂等性?1.3 接口超时,应该如何处理?1.4 幂等性对系统的影响 二、Restful API 接口的幂等性三、实现方式3.1 数据库层面,主键/唯一索引冲突3.2 数据库层面,乐观锁…

学习Opencv(蝴蝶书/C++)相关——1. 前言 和 第1章.概述

文章目录 1. 整体架构1.1 OpenCV3.01.2 Opencv4.xX. 在线文档X.1 Opencv cheatsheet(小抄)1. 整体架构 1.1 OpenCV3.0 对于Opencv3.x版本,网上最常见的图,图自OpenCV Tutorial-Itseez 现在已经不是500+的算法了,而是2500+,详见:About

STM32G030F6P6 芯片实验 (二)

STM32G030F6P6 芯片实验 (二) Hello World - GPIO LED 尝试了下, 从 0 开始建 MDK HAL M0plus Project, 成功点亮 LED了。 但是 ST-LINK跑着跑着, 码飞了! 不知飞哪去了。 只好拿 MX 建了个 MDK Base。 呼叫 SysTick HAL_Delay(), 切换 LED。 基本上都是一样的用法, 只是换…

ICCV2023 Tracking paper汇总(一)(多目标跟随、单目标跟随等)

一、PVT: A Simple End-to-End Latency-Aware Visual Tracking Framework paper: https://openaccess.thecvf.com/content/ICCV2023/papers/Li_PVT_A_Simple_End-to-End_Latency-Aware_Visual_Tracking_Framework_ICCV_2023_paper.pdf github: https://…

java EE 进阶

java EE 主要是学框架(框架的使用,框架的原理) 框架可以说是实现了部分功能的半成品,还没装修的毛坯房,然后我们再自己打造成自己喜欢的成品 这里学习四个框架 : Spring ,Spring Boot, Spring MVC, Mybatis JavaEE 一定要多练习,才能学好 Maven 目前我们主要用的两个功能: …

图像新型拼接

道路摄像头拼接 拼接道路上的摄像头,比较麻烦,如图所示 前后的摄像头都是如此,那么如何拼接摄像头画面呢,像下面这样拼接 测试代码 测试一下代码,使用python import cv2 import numpy as npimg cv2.imread("…

antv/g6之交互模式mode

什么是mode 在 AntV G6 中,“mode” 是用于配置图表交互模式的一种属性。通过设置 “mode”,可以控制图表的行为,以满足不同的交互需求。可能在不同的场景需要展现的交互行为不一样。比如查看模式下点击一个点就选中的状态,在编辑…

数据可视化:折线图

1.初看效果 (1)效果一 (2)数据来源 2.JSON数据格式 其实JSON数据在JAVA后期的学习过程中我已经是很了解了,基本上后端服务器和前端交互数据大多是采用JSON字符串的形式 (1)JSON的作用 &#…

本地idea远程调试服务器程序

本文主要介绍idea本地调试远程服务器程序的方式。相信很多同行跟我一样,在最初接触公司项目的时候,遇到测试提出的缺陷,往往会在本地进行调试、替换jar包远程调试等方式,本地调试往往会导致数据和环境不一致的问题使得问题无法复现…

没想到这么齐全!这份 Python 实战干货yyds

今天我分享一些Python学习神器资料,有需要的小伙文末自行免费领取。 1.200Python练手案例: 2.Python全套视频教程等: 3.浙大Python学习套装: * 4.Python实战案例: 5.Pandas学习大礼包 6.学习手册大礼包 Python知识…

CSAPP BOMB LAB part3

CSAPP BOMB LAB part3 phase_4 bomb.s phase_4的代码: 格式: 40102e行,比较0x8rsp的值和0xe, 需要让0x8rsp小于0xe, 然后跳转到40103a, func函数根据bomb.s 转化为c代码: 这个直接参考了知乎网友的翻译, func4的返回值等于0, 跳转到40…

分治法——找众数

分治法——找众数 要求: 寻找整数数组的众数,如果存在多个众数,则返回权值最小的那个 第一步: 要利用分治法找众数,首先就先要使数组有序。这里,我们用C语言库中的qsort进行快排: qsort(nums…

3D高斯泼溅(Splatting)简明教程

在线工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 3D场景编辑器 3D 高斯泼溅(Splatting)是用于实时辐射场渲染的 3D 高斯分布描述的一种光栅化技术,它允许实时渲染从小图像样…

直流无刷电机(BLDC)六步换相驱动

直流无刷电机(BLDC)六步换相驱动 文章目录 直流无刷电机(BLDC)六步换相驱动1. 前言2. 六步换相原理3. 电角度与机械角度4. 动手实践4.1 霍尔输出表测量4.2 换向控制4.3 代码编写 5. 总结 1. 前言 直流无刷电机相对直流有刷电机具…

Redis之Java操作Redis的使用

🎉🎉欢迎来到我的CSDN主页!🎉🎉 🏅我是君易--鑨,一个在CSDN分享笔记的博主。📚📚 🌟推荐给大家我的博客专栏《Redis实战开发》。🎯🎯 …

Android java Handler sendMessage使用Parcelable传递实例化对象,我这里传递Bitmap 图片数据

一、Bundle给我们提供了一个putParcelable(key,value)的方法。专门用于传递实例化对象。 二、我这里传递Bitmap 图片数据,实际使用可以成功传统图像数据。 发送:Bundle bundle new Bundle();bundle.putParcelable("bitmap",bitmap);msg.setD…