Using BPF USDT to trace OpenStack (by quqi99)

作者:张华 发表于:2021-01-14
版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明

问题

./nova/compute/resource_tracker.py#_update_available_resource中的_update_usage_from_instances函数没有DEBUG LEVEL日志,有办法通过probe则不是通过改代码写日志的方法来调试该函数吗?

def _update_available_resource(self, context, resources, startup=False):
...
cn = self.compute_nodes[nodename]

确认python版本是否支持USDT探针

sudo apt install python3-bpfcc libbpfcc bpfcc-tools -y
# if the output is empty, it means we need to compile python with dtrace
# for >>python3.7 supports '--with-dtrace'
# https://bugs.launchpad.net/ubuntu/+source/python3.8/+bug/1818778
tplist-bpfcc -l $(which python3)

探针种类

https://www.collabora.com/news-and-blog/blog/2019/05/14/an-ebpf-overview-part-5-tracing-user-processes/
有三种探针:
1, USDT静态探针, 主要是针对所依赖的二进制模块(eg: libc.so, libpthread.so, libvirt.so etc)的预定义探针。python是解释型语言,看来函数./nova/compute/resource_tracker.py#_update_available_resource只能通过下列function__entry与function__return两种探针来做。

# https://github.com/iovisor/bcc/blob/master/tools/tplist.py
# tplist-bpfcc -p $(ps -ef |grep nova-compute |grep -v grep |awk '{print $2}') |grep python |grep function
b'/proc/1551047/root/usr/bin/python3.8' b'python':b'function__entry'
b'/proc/1551047/root/usr/bin/python3.8' b'python':b'function__return'

2, 自定义探针(tracepints), 需要在你的python代码中通过provider.add_probe添加探针,这种和打日志没啥区别啊。略。
3, uprobes动态探针,不需要改运行代码,可以通过下列类似b.attach_uprobe来添加探针,但这种探针显示也是针对模块的,对解释型的python不适用。

b.attach_uprobe(name="c", sym="getaddrinfo", fn_name="do_entry", pid=args.pid)
b.attach_uretprobe(name="c", sym="getaddrinfo", fn_name="do_return", pid=args.pid)

一个例子

root@demo:~# ./test.py $(ps -ef |grep nova-compute |grep -v grep |awk '{print $2}')
207625.396728000   b'_update_available_resource here here!'

root@demo:~# cat test.py 
#!/usr/bin/env python3
from bcc import BPF, USDT
import sys

bpf = """
#include <uapi/linux/ptrace.h>

static int strncmp(char *s1, char *s2, int size) {
    for (int i = 0; i < size; ++i)
        if (s1[i] != s2[i])
            return 1;
    return 0;
}

int trace_file_transfers(struct pt_regs *ctx) {
    uint64_t fnameptr;
    char fname[128]={0}, searchname[30]="_update_available_resource";

    bpf_usdt_readarg(2, ctx, &fnameptr);
    bpf_probe_read(&fname, sizeof(fname), (void *)fnameptr);

    if (!strncmp(fname, searchname, sizeof(searchname)))
        bpf_trace_printk("_update_available_resource here here!\\n");
    return 0;
};
"""

u = USDT(pid=int(sys.argv[1]))
u.enable_probe(probe="function__entry", fn_name="trace_file_transfers")
b = BPF(text=bpf, usdt_contexts=[u])
while 1:
    try:
        (_, _, _, _, ts, msg) = b.trace_fields()
    except ValueError:
        continue
    print("%-18.9f %s" % (ts, msg))

打印变量

https://zhuanlan.zhihu.com/p/138887361
bcc自带的脚本已经能够满足一般的需求, 但是也不能满足所有需求. 这里以uprobe例, 看一下在bcc里面怎么访问变量, 很大程度上取决于probe的位置:

  • 如果在函数的入口, 那么可以通过PT_REGS_PARM很方便读取到入参
  • 如果在函数中间, 上面的方式就不在工作了, PT_REGS_PARM这些宏其实就是一些寄存器, 在函数中间入参所对应的寄存器可能已经被修改. 如果想要访问函数的入参或者局部变量,需要反汇编并找到对应的寄存器或者地址
  • 如果是return probe, 这个时候的sp/bp已经是caller的栈了, 需要小心计算在栈上的偏移 bcc目前还不支持读取dwarf信息
    在这里插入图片描述
$sudo ./write_local.py
a: 1, b: 2, uninit_c: 0, c: 80
$./foo
83
#!/usr/bin/python

from __future__ import print_function
import bcc
import ctypes as ct

text = """
#include <uapi/linux/ptrace.h>

struct data_t {
    int a;
    int b;
    int uninit_c;
    int c;
};

BPF_PERF_OUTPUT(events);

int foo(struct pt_regs *ctx) {
    struct data_t data = {};
    int c = 80;
    void *bp = (void *)ctx->bp;

    data.a = PT_REGS_PARM1(ctx);
    data.b = PT_REGS_PARM2(ctx);
    bpf_probe_read(&data.uninit_c, sizeof(data.uninit_c), bp - 4);
    bpf_probe_write_user(bp - 4, &c, 4);
    bpf_probe_read(&data.c, sizeof(data.c), bp - 4);

    events.perf_submit(ctx, &data, sizeof(struct data_t));
    return 0;
}
"""

b = bcc.BPF(text=text)
b.attach_uprobe(name="/home/wufei/work/test/foo", addr=0x40053a, fn_name="foo")

class Data(ct.Structure):
    _fields_ = [
        ("a", ct.c_int),
        ("b", ct.c_int),
        ("uninit_c", ct.c_int),
        ("c", ct.c_int),
    ]

def print_event(cpu, data, size):
    event = ct.cast(data, ct.POINTER(Data)).contents
    print("a: %d, b: %d, uninit_c: %d, c: %d" % (event.a, event.b, event.uninit_c, event.c))

# loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
    b.kprobe_poll()

似乎trace变量并不容易:

  • 一是python代码怎么反编译找到cn变量的位置呢?这种方法(python3 -m dis ./nova/compute/resource_tracker.py |grep ‘Disassembly of <code object _update_available_resource’ -A 10)似乎是伪码。tracing变量只对cython有效(是cython,不是cpython, cython是python的C扩展用于在python解释器中运行编译后的C代码, apt install cython3 cython3-dbg)
  • cn变量不是基本变量,而是一个结构体,这样类似于应用态的systemtap一样结构体所依赖的结构体层层定义在bpf中,这样非常麻烦的。
  • 传入参数似乎很容易打印,但也只是涉及基本变量,若是结构体也蛮麻烦的。
  • 本例中的cn变量是一个全局变量,而且依赖于位置,更麻烦。

Appendix - py-spy (python tool)

pip3 install py-spy
py-spy record -o profile.svg --pid $PID
py-spy top --pid $PID
py-spy dump --pid $PID

Reference

[1] https://blog.csdn.net/hehuyi_in/article/details/108910781

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页