使用eBPF观测Go程序的goroutine状态变化
eBPF号称能动态地对内核进行编程,可以实现高效的网络处理、观测、追踪和安全功能。 Dynamically program the kernel for efficient networking, observability, tracing, and security 这篇博客就来体验一下用eBPF观测Go的goroutine调度,会涉及到uprobe原理、eBPF开发框架选择、eBPF程序的编写、用户态程序加载eBPF程序以及用户态程序和eBPF程序通过map交互等内容。 uprobe原理 uprobe(user-space probe)是 Linux 内核提供的一种机制,用来在 用户态程序的特定指令处 插入动态探针(probe)。uprobe 通过在用户态程序代码段指定位置写入一个 1 字节的断点指令(INT3, 0xCC),触发内核陷入(trap)进入 uprobe handler,再运行hook的代码。由于eBPF具有很好的安全性,所以可以用eBPF来编写hook代码,保证安全。 eBPF开发框架选择 最近几年eBPF发展的非常迅速,出现了优秀的开源eBPF开发框架,使得eBPF的开发难度大大降低。下面选取一些我知道的eBPF开发框架做介绍: 项目 特点 语言 bcc eBPF开发工具箱,适合教学&运维,依赖LLVM,较重。 C、Python libbpf-bootstrap 用C开发eBPF的模版/框架,官方推荐。 C cilium/ebpf 用Go开发eBPF的首选,eilium开源,能用于生产环境,无LLVM依赖 C、Go bpftrace 用类似awk的脚本语言开发eBPF,适合快速调试、验证。 专用语法 eunomia-bpf 面向云原生的eBPF WASM化 Rust 经过对比最终选择了使用cilium/ebpf来实现观测goroutine状态变化,原因有:bcc是一个很重的项目,有很多依赖环境。libbpf-bootstrap开发用户态程序相对麻烦一些。使用bpftrace开发需要了解他的脚本语言的语法(并不是通用的脚本语言),有一定的学习成本,而且开发复杂的eBPF程序也不太灵活。eunomia-bpf还没仔细研究过,以后会尝试。经过对比,我最终选择了对我来说最简单的cilium/ebpf,而且这个项目里有现成的examples,可以照猫画虎,很快就能实现一个简单uprobe功能。 用eBPF观测goroutine状态变化 eBPF程序 Go支持高并发的利器就是goroutine,而goroutine之所以能轻而易举的实现上万的并发,要归功于Go的调度策略,Go使用GMP调度模型。核心就是在用户态实现调度逻辑,避免系统级线程调度时用户态和内核态之间的切换开销。 Go的调度逻辑在源码的src/runtime/proc.go中,当scheduler切换goroutine状态时会调用runtime.casgstatus,runtime.casgstatus的原型如下,它的作用是:检查当前goroutine的状态是否old,如果是就把状态原子的更新为new,如果不是,不做更新。 func casgstatus(gp *g, oldval, newval uint32) 要观测goroutine的状态切换,只需要把eBPF函数hook到这个函数的入口处,拿到这三个参数即可。 首先把cilium/ebpf拉到本地,进入examples目录,在创建一个uprobe_sched目录,创建一个uprobe_sched.c文件。 $ cd ebpf/examples $ mkdir uprobe_sched $ cd uprobe_sched $ touch uprobe_sched.c 然后参考隔壁的examples/uretprobe/uretprobe.c,编写eBPF程序,主要逻辑就是,获取三个参数,第一个参数是指向g结构体的指针,第二个参数是old,第三个参数是new。其中goid在g结构体偏移0xA0(这个偏移和Go的版本相关,具体计算方法后面介绍),需要借助bpf_probe_read_user函数读取,读到goid之后将data_t结构体写入map中。 ...