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.brendangregg.com/ebpf.htmlhttps://www.brendangregg.com/ebpf.html

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'

# 如ebpf会将地址0x000000000007c2eb 换成int3指令(即break)并把原来的指令保存,当程序执行到这时来执行我们定义的USDT/uprobe handler, 之后再将原来保存的指令恢复
# readelf -n /usr/bin/python3 |grep function__entry -A1
    Name: function__entry
    Location: 0x000000000007c2eb, Base: 0x00000000004e6420, Semaphore: 0x00000000005a1d60

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

20240425 - Using uprobe to probe python

想要打印下列函数的输入参数和输出:

vim ./neutron/plugins/ml2/extensions/dns_integration.py

    def _get_request_dns_name(self, port):
        dns_domain = self._get_dns_domain()
        if dns_domain and dns_domain != lib_const.DNS_DOMAIN_DEFAULT:
            return port.get(dns_apidef.DNSNAME, ''), False
        return '', True
       
    def _get_request_dns_name_and_domain_name(self, dns_data_db):               
        dns_domain = self._get_dns_domain()                                     
        dns_name = ''                                                           
        if dns_domain and dns_domain != lib_const.DNS_DOMAIN_DEFAULT:           
            if dns_data_db:                                                     
                dns_name = dns_data_db.dns_name                                 
                if dns_data_db.current_dns_domain:                              
                    dns_domain = dns_data_db.current_dns_domain                 
        return dns_name, dns_domain

函数_get_request_dns_name_and_domain_name有的参数是一个结构体dns_data_db会更麻烦,所以我们先测试函数_get_request_dns_name(它只有一个int型的参数port). 程序如下:

from bcc import BPF, USDT
import os
import sys

bpf_text = """
#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_handler(struct pt_regs *ctx) {
    uint64_t fnameptr;
    char fname[128] = {0}, searchname[50] = "_get_request_dns_name";
    bpf_usdt_readarg(2, ctx, &fnameptr);
    bpf_probe_read(&fname, sizeof(fname), (void *)fnameptr);
    bpf_trace_printk("quqi2 is here %s \\n", fname);
    if (strncmp(fname, searchname, sizeof(searchname)) == 0)
        bpf_trace_printk("quqi is here %s \\n", fname);
    return 0;
};
"""

neutron_pids = [int(pid) for pid in os.listdir('/proc') if pid.isdigit() and "neutron-server" in open(os.path.join('/proc', pid, 'cmdline'), 'r').read()]
neutron_pids = [581]
bpf_instances = []
for pid in neutron_pids:
    try:
        usdt = USDT(pid=pid)
        usdt.enable_probe(probe="function__entry", fn_name="trace_handler")
        bpf_instance = BPF(text=bpf_text, usdt_contexts=[usdt])
        bpf_instances.append(bpf_instance)
    except Exception as e:
        print(f"Failed to enable probe for PID {pid}: {e}")
try:
    while True:
        for bpf_instance in bpf_instances:
            try:
                (task, pid, cpu, flags, ts, msg) = bpf_instance.trace_fields()
                #print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))
            except ValueError:
                pass
            print("%-18.9f %s" % (ts, msg))
except KeyboardInterrupt:
    pass

这里面注意几点:

  • 语句’bpf_usdt_readarg(2, ctx,&fnameptr)'用于读函数名,究竟index用1还是2得一个个试,用1的时候是python文件的绝对路径,用2的时候是函数名,但是却没有_get_request_dns_name这个函数名,所以实际上用USDT程序无法打印_get_request_dns_name时的输入参数port. 命令‘bpftool prog tracelog’可以在运行了USDT程序后再来查看日志。
  • 因为用2的时候也没有打印出_get_request_dns_name这个函数名,所以出就无法继续用index=3来尝试读port了。输入参数dns_data_db实际来自_ml2_md_extend_port_dict
  • 但是bpf对python的function__entry只能probe标准库中的如eventlet和threading, 而这些库没有进一步的为neutron _get_request_dns_name等触发usdt事件
  • 因为bpf_usdt_readarg读出来的只是内核的内存地址,bpf出于安全性是无法直接通过内存地址拿到数据的,必须得通过bpf_probe_read来从内存读数据,如果输入参数是一个结构体的话如上面的dns_data_db就会很麻烦。下面是一种可能的方法,但dns_data_db是一个字典套字典的结构体也没弄成功,只是描述一个大致思路吧。

首先找到是_ml2_md_extend_port_dict最终调用了_get_request_dns_name_and_domain_name,

vim ./neutron/plugins/ml2/plugin.py
    @staticmethod
    @resource_extend.extends([port_def.COLLECTION_NAME])
    def _ml2_md_extend_port_dict(result, portdb):
        plugin = directory.get_plugin()
        session = plugin._object_session_or_new_session(portdb)
        plugin.extension_manager.extend_port_dict(session, portdb, result)

通过加LOG.debug得到它是一个字典:

LOG.debug("quqi Result object: %s", result)
LOG.debug("quqi Result object type: %s", type(result))
LOG.debug("quqi Result object attributes: %s", dir(result))

2024-04-25 11:19:43.047 86099 DEBUG neutron.plugins.ml2.plugin [req-54b77478-3bf1-400d-9760-301ba6daa236 - - - - -] quqi Result object: {'id': 'dae63c77-fb39-4726-9388-23d43275e3db', 'name': '', 'network_id': '5995fc04-23fb-4112-9c18-cd0cfcfd5678', 'tenant_id': '', 'mac_address': 'fa:16:3e:94:92:31', 'admin_state_up': True, 'status': 'ACTIVE', 'device_id': '570cff72-dd8c-47b4-aa8c-9c4ccbdc74e2', 'device_owner': 'network:router_gateway', 'standard_attr_id': 11, 'fixed_ips': [{'subnet_id': '49a79f73-f4e9-4ba1-85c4-f1787d8ac316', 'ip_address': '10.5.153.225'}], 'bulk': True, 'allowed_address_pairs': [], 'extra_dhcp_opts': [], 'security_groups': [], 'description': '', 'binding:vnic_type': 'normal', 'binding:profile': {}, 'binding:host_id': 'juju-67870a-ovn-11.cloud.sts', 'binding:vif_type': 'ovs', 'binding:vif_details': {'port_filter': True, 'connectivity': 'l2', 'bound_drivers': {'0': 'ovn'}}} _ml2_md_extend_port_dict /usr/lib/python3/dist-packages/neutron/plugins/ml2/plugin.py:906
2024-04-25 11:19:43.051 86099 DEBUG neutron.plugins.ml2.plugin [req-54b77478-3bf1-400d-9760-301ba6daa236 - - - - -] quqi Result object type: <class 'dict'> _ml2_md_extend_port_dict /usr/lib/python3/dist-packages/neutron/plugins/ml2/plugin.py:907
2024-04-25 11:19:43.051 86099 DEBUG neutron.plugins.ml2.plugin [req-54b77478-3bf1-400d-9760-301ba6daa236 - - - - -] quqi Result object attributes: ['__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__ior__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__ror__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values'] _ml2_md_extend_port_dict /usr/lib/python3/dist-packages/neutron/plugins/ml2/plugin.py:908

{
  'id': 'dae63c77-fb39-4726-9388-23d43275e3db',
  'name': '',
  'network_id': '5995fc04-23fb-4112-9c18-cd0cfcfd5678',
  'tenant_id': '',
  'mac_address': 'fa:16:3e:94:92:31',
  'admin_state_up': True,
  'status': 'ACTIVE',
  'device_id': '570cff72-dd8c-47b4-aa8c-9c4ccbdc74e2',
  'device_owner': 'network:router_gateway',
  'standard_attr_id': 11,
  'fixed_ips': [
    {
      'subnet_id': '49a79f73-f4e9-4ba1-85c4-f1787d8ac316',
      'ip_address': '10.5.153.225'
    }
  ],
  'bulk': True,
  'allowed_address_pairs': [
    
  ],
  'extra_dhcp_opts': [
    
  ],
  'security_groups': [
    
  ],
  'description': '',
  'binding:vnic_type': 'normal',
  'binding:profile': {
    
  },
  'binding:host_id': 'juju-67870a-ovn-11.cloud.sts',
  'binding:vif_type': 'ovs',
  'binding:vif_details': {
    'port_filter': True,
    'connectivity': 'l2',
    'bound_drivers': {
      '0': 'ovn'
    }
  }
}

所以设计了下列eBPF (基于python):

from bcc import BPF

# 定义eBPF程序
bpf_program = """
#include <linux/ptrace.h>

struct dns_data_db_struct {
    const char *id;
    const char *name;
    const char *network_id;
    const char *tenant_id;
    const char *mac_address;
    const char *admin_state_up;
    const char *status;
    const char *device_id;
    const char *device_owner;
    const char *standard_attr_id;
    const char *fixed_ips;
    const char *bulk;
    const char *allowed_address_pairs;
    const char *extra_dhcp_opts;
    const char *security_groups;
    const char *description;
    const char *binding_vnic_type;
    const char *binding_profile;
    const char *binding_host_id;
    const char *binding_vif_type;
    const char *binding_vif_details;
};

struct request_dns_name_and_domain_name_args {
    const struct dns_data_db_struct *dns_data_db;
    const char *dns_name;
    const char *dns_domain;
};

BPF_HASH(request_dns_name_and_domain_name_args_map, u64, struct request_dns_name_and_domain_name_args);

int trace_request_dns_name_and_domain_name(struct pt_regs *ctx, const struct dns_data_db_struct *dns_data_db) {
    struct request_dns_name_and_domain_name_args args = {};
    args.dns_data_db = dns_data_db;
    args.dns_name = dns_data_db->dns_name;
    args.dns_domain = dns_data_db->dns_domain;
    // 如何读结构体可参考 - https://github.com/iovisor/bcc/issues/2534
    u64 pid = bpf_get_current_pid_tgid();
    request_dns_name_and_domain_name_args_map.update(&pid, &args);
    return 0;
}
"""

# 加载eBPF程序并附加到neutron-server进程中
b = BPF(text=bpf_program)
b.attach_uprobe(name="/usr/bin/python3", sym="_get_request_dns_name_and_domain_name", fn_name="trace_request_dns_name_and_domain_name")

# 读取并打印函数调用信息
print("%-10s %-20s %-20s %-20s %-20s" % ("PID", "DNS Data DB", "DNS Name", "DNS Domain", "ID"))
while True:
    try:
        (_, pid, args) = b.trace_fields()
        print("%-10d %-20s %-20s %-20s %-20s" % (pid, args.dns_data_db, args.dns_name, args.dns_domain, args.id))
    except KeyboardInterrupt:
        exit()

但是却报了下列错:

Traceback (most recent call last):
  File "/home/ubuntu/trace_dns.py", line 54, in <module>
    b.attach_uprobe(name="/usr/bin/python3", sym="_get_request_dns_name_and_domain_name", fn_name="trace_request_dns_name_and_domain_name")
  File "/usr/lib/python3/dist-packages/bcc/__init__.py", line 1144, in attach_uprobe
    (path, addr) = BPF._check_path_symbol(name, sym, addr, pid, sym_off)
  File "/usr/lib/python3/dist-packages/bcc/__init__.py", line 781, in _check_path_symbol
    raise Exception("could not determine address of symbol %s" % symname)
Exception: could not determine address of symbol b'_get_request_dns_name_and_domain_name'

这还是因为对于解释型语言(如Python),BPF无法直接识别和跟踪函数符号,因为BPF需要知道函数在内存中的地址才能在运行时跟踪它。所以是不能用uprobe动态探针的,还是得用USDT静态探针.
但是bpf对python的function__entry只能probe标准库中的如eventlet和threading, 而这些库没有进一步的为neutron _get_request_dns_name等触发usdt事件, 所以对于解释型语言,最佳的办法还是日志。

Reference

[1] https://blog.csdn.net/hehuyi_in/article/details/108910781
[2] https://www.kawabangga.com/posts/4894

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

quqi99

你的鼓励就是我创造的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值