BPF for Rust/Go/Python Application

BPF(eBPF, extended Berkeley Packet Filter)是Linux内核中的一种通用执行引擎,它通过将代码逻辑JIT编译成内核虚拟机中间语言的方式来提供了一种更安全灵活的系统运行观察手段,相比传统的工具,由于代码逻辑是内核中执行,数据不需要从内核态拷贝到用户态,性能要优秀很多很多。BPF目前主要应用在网络、调试和安全领域,本文主要介绍在高层业务中如何通过BPF来观察调试应用的运行。

在应用语言提供的调式工具(譬如Go的pprof)外,我们观察系统或程序的运作一般有以下手段:

  • kprobes
  • uprobes
  • Tracepoint
  • USDT

这些技术基本上都是通过在机器码对应位置插入 int3/jmp/noop 等来实现关键位置的埋点,然后运行时按需动态替换为观察逻辑片段来实现对程序运行状态的观察。相比语言级的调试工具,这些技术的性能损失近乎可以忽略,也不需要对生产和调试版本分开编译,随用随开即可。Linux内核中就包含了大量的埋点(kprobes/Tracepoint),平时丝毫不会影响系统的性能。

实际上Linux成熟的工具链就已经大量使用了这些埋点的信息,BPF的出现提供了一个统一的接口和编程语言来实现从系统到应用的全链路调试:

  • bcc : Tools for BPF-based Linux IO analysis, networking, monitoring, and more

  • bpftrace : High-level tracing language for Linux eBPF

更多关于BPF技术和系统相关的调试监控方式可以去查看Brendan博客的文章,下面演示在Rust/Go/Python应用中如何利用BPF。

Rust

Rust应用目前可以使用 probe 来在代码中自定义USDT,但由于实现上使用了 asm,所以当前需要使用 +nightly 版本来编译。

lib.rs

#![feature(asm)]

#[macro_use]
#[no_link]
extern crate probe;

#[no_mangle]
pub fn fib(n: i64) -> i64 {
    probe!(rs_fib, my_probe, n);
    match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 1) + fib(n - 2),
    }
}

main.rs

use std::{thread, time};

use rs_fib::fib;

fn main() {
    for i in 0..1000 {
        println!("Fibonacci {}: {}", i % 20, fib(i % 20));
        thread::sleep(time::Duration::from_secs(1));
    }
}

编译完成后,可以通过 bpftrace -l 查看程序中定义的USDT:

# bpftrace -l 'usdt:./target/debug/rs-fib:*'
usdt:./target/debug/rs-fib:rs_fib:my_probe

usdt 可以替换成 uprobe 等其他类型,具体可以查看 bpftrace 的帮助文档:

# bpftrace -l 'uprobe:./target/debug/rs-fib:*' | grep 'rs-fib:fib'
uprobe:./target/debug/rs-fib:fib

如果我们前面没有指定 no_mangle ,Rust会在编译时进行混淆,得到的会是 _ZN6rs_fib3fib17h0feebae60a2979d7E 这样的结果。

运行程序,我们可以看到Fibonacci的结果在循环输出,这时在另一个控制台中通过下面命令观察程序:

# bpftrace -e 'usdt:./target/debug/rs-fib:rs_fib:* { @[probe] = count(); }'
Attaching 1 probe...
^C

@[usdt:./target/debug/rs-fib:rs_fib:my_probe]: 57

输出结果表示在观察的时间段内 my_probe 执行了57次,CPU时间、焰火图等更多进阶用法请查看 bcc bpftrace 的文档或者项目代码下的例子。

Go

Go目前可以通过 libstapsdt 来插入USDT:

fib.go

package main

import (
	"fmt"
	"os"
	"os/signal"
	"time"

	"github.com/mmcshane/salp"
)

var (
	probes = salp.NewProvider("go-fib")
	p = salp.MustAddProbe(probes, "my_probe", salp.Int64)
)

func fib(i int64) int64 {
	p.Fire(i)
	switch i {
	case 0:
		return 0
	case 1:
		return 1
	default:
		return fib(i-1) + fib(i-2)
	}
}

func main() {
	defer salp.UnloadAndDispose(probes)

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)

	salp.MustLoadProvider(probes)
	var i int64 = 0
	for {
		select {
		case <-c:
			return
		case _ = <-time.After(1 * time.Second):
			fmt.Printf("Fibonacci %d: %d\n", i, fib(i))
			i++
		}
	}

}

但是由于 libstapsdt 是Dynamic USDT的实现方式,我们无法通过 bpftrace -l 查询程序的USDT,需要在程序运行后通过指定PID的方式观察:

# bpftrace -p $(pgrep fib) -e 'usdt:./fib:* { @[probe] = count(); }'
Attaching 71 probes...
^C

@[usdt:./fib:libpthread:pthread_start]: 2
@[usdt:./fib:libc:setjmp]: 2
@[usdt:./fib:libc:memory_arena_new]: 2
@[usdt:./fib:libc:memory_heap_new]: 2
@[usdt:./fib:libpthread:pthread_create]: 2
@[usdt:./fib:go-fib:my_probe]: 2228183

程序居然有71个USDT,而且由于Go版本例子中没有限制Fibonacci计算的输入范围,在观察一段时间后,除了我们的Probe外,还可以看到系统库的USDT,看起来Go并没有为我们优化这个递归版本的实现。

Python

Python应用目前也是通过 libstapsdt 实现,因此在使用方式上与Go类似:

lib.py

from time import sleep

import stapsdt

provider = stapsdt.Provider("py_fib")
probe = provider.add_probe("my_probe", stapsdt.ArgTypes.int64)
provider.load()


def fib(i: int) -> int:
    probe.fire(i)
    return i if i < 2 else fib(i - 1) + fib(i - 2)


if __name__ == '__main__':
    while True:
        print(f"Fibonacci 20: {fib(20)}")
        sleep(1)

同样需要在程序运行后观察:

# bpftrace -p $(pgrep -f 'python fib.py') -e 'usdt:python:* { @[probe] = count(); }'
Attaching 87 probes...
^C

@[usdt:/usr/bin/python:py_fib:my_probe]: 108006
@[usdt:/usr/bin/python:python:function__return]: 319789
@[usdt:/usr/bin/python:python:function__entry]: 322284
@[usdt:/usr/bin/python:libpthread:cond_signal]: 434944
@[usdt:/usr/bin/python:libpthread:mutex_entry]: 652416
@[usdt:/usr/bin/python:python:line]: 1333385

如果只想关注业务定义的USDT,还可以先获取动态库的路径再观察(同样适用于Go):

# bpftrace -p $(pgrep -f 'python fib.py') -l 'usdt:*' | grep my_probe
usdt:/tmp/py_fib-OUkkLF.so:py_fib:my_probe

# bpftrace -p $(pgrep -f 'python fib.py') -e 'usdt:/tmp/py_fib-OUkkLF.so:py_fib:* { @[probe] = count(); }'
Attaching 1 probe...
^C

@[usdt:/tmp/py_fib-OUkkLF.so:py_fib:my_probe]: 197019