前言
各位小伙伴们,非常感谢您对我们eBPF专题系列文章的持续关注和热情支持。在先前的文章《如何使用USDT探针定位MySQL异常访问》中,我们探讨了MySQL中DTrace的应用,该方法需要修改数据库内核代码(嵌入静态钩子),然后利用eBPF进行探测。而通过uprobe(User-space Probes)方式无需修改数据库内核源码即可探测。那么uprobe(User-space Probes)方式和USDT(User Statically Defined Tracing)相比,哪种方式更好呢?
本文我们将与您深入探讨USDT的预埋,并从性能和扩展上进行对比分析。
一. 如何定USDT(DTrace)的探针?
那我们该如何在代码中定义这样的静态探针呢?
对于C++中的DTrace探针的定义也是非常简单的,只需要包含头文件
// 模拟执行数据库查询的函数void executeQuery(const std::string &query) {int status = 0;// 触发查询开始的探针DTRACE_PROBE1(myprovider, query_start, query.c_str());std::cout << "Executing query: " << query << std::endl;// 模拟查询执行(此处可以放置实际的数据库操作逻辑)// ...// 假设查询执行成功,设置status为1status = 1;// 触发查询结束的探针DTRACE_PROBE2(myprovider, query_end, query.c_str(), status);}int main() {std::string query = "SELECT * FROM users;";executeQuery(query);return 0;}
我们上面写完了带有DTrace探针的demo,接下来我们对该demo使用eBPF进行探测。
#!/usr/bin/pythonfrom bcc import BPF, USDT# 创建USDT探针u = USDT(path="./main")u.enable_probe(probe="query_start", fn_name="trace_query_start")u.enable_probe(probe="query_end", fn_name="trace_query_end")# 定义eBPF程序bpf_text = """#include//处理 query_start 探针事件int trace_query_start(struct pt_regs *ctx) {char query[256];// 从探针中读取参数bpf_usdt_readarg_p(1, ctx, &query, sizeof(query));bpf_trace_printk("Query start: %s\\n", query);return 0;}// 处理 query_end 探针事件int trace_query_end(struct pt_regs *ctx) {char query[256];int status = 0;// 从探针中读取参数bpf_usdt_readarg_p(1, ctx, &query, sizeof(query));bpf_usdt_readarg(2, ctx, &status);bpf_trace_printk("Query end: %s, Status: %d\\n", query, status);return 0;}"""# 加载eBPF程序b = BPF(text=bpf_text, usdt_contexts=[u])# 输出探针捕获的信息print("Tracing USDT probes... Hit Ctrl-C to end.")b.trace_print()
在trace_query_start及trace_query_end函数中分别对query_start和query_end两个探针进行探测,并打印了探针中的值。
Demo 示例
g++ -o main main.cpppython trace.py./main[root@localhost C]# python trace.pyTracing USDT probes... Hit Ctrl-C to end.b' main-1176932 [012] d... 1028814.624214: bpf_trace_printk: Query start: SELECT * FROM users;'b''b' main-1176932 [012] d... 1028814.624283: bpf_trace_printk: Query end: SELECT * FROM users;, Status: 1'
这样我们就可以看到通过该探测脚本获取到了demo中的SQL以及执行后的status。
我们接下来分为两组进行测试:
void executeQuery(const std::string &query) {int status = 0;// 触发查询开始的探针//DTRACE_PROBE1(myprovider, query_start, query.c_str());// 模拟查询执行(此处可以放置实际的数据库操作逻辑)// ...// 假设查询执行成功,设置status为1status = 1;}int main() {std::string query = "SELECT * FROM users;";// 获取开始时间戳struct timespec start, end;clock_gettime(CLOCK_MONOTONIC, &start);// 执行二十亿次查询for (int i = 0; i < 2000000000; ++i) {executeQuery(query);}// 获取结束时间戳clock_gettime(CLOCK_MONOTONIC, &end);// 计算执行时间(纳秒)long long duration = (end.tv_sec - start.tv_sec) * 1000000000LL + (end.tv_nsec - start.tv_nsec);std::cout << "Total execution time for queries: " << duration << " ns" << std::endl;return 0;}
循环调用20亿次该函数,并打印纳秒级时间戳。
次数 | 1 | 2 | 3 | 4 | 5 | avg |
时间(ns) | 5303264202 | 5227036785 | 5301668029 | 5336016048 | 5335845259 | 5300766064.6 |
将DTRACE_PROBE1(myprovider, query_start, query.c_str());注释删除,再次编译执行。
次数 | 1 | 2 | 3 | 4 | 5 | avg |
时间(ns) | 8692785702 | 8717224846 | 8667429411 | 8585855930 | 8687992775 | 8670257732.8 |
小结:使用DTtrace后所需时间增幅较大,由于executeQuery函数没有任何逻辑处理,直接对比两组时间意义不大。可通两组数据的差值得出DTrace单次执行的时间消耗,20亿次DTrace的时间消耗:8670257732.8 - 5300766064.6 = 3369491668.2, 根据该数据可估计出单条DTrace时间消耗在1.68ns,该值放应用链路探测中消耗非常低可忽略不计。
仍使用上面的demo代码,将循环执行次数改为一千万次 ,为了尽可能减少变量,我们在eBPF的逻辑中不做任何处理,用来直接对比USDT和uprobe陷入的性能。使用USDT探测:
#!/usr/bin/pythonfrom bcc import BPF, USDT# 创建USDT探针u = USDT(path="./main")u.enable_probe(probe="query_start", fn_name="trace_query_start")# 定义eBPF程序bpf_text = """#include <uapi/linux/ptrace.h>// 处理 query_start 探针事件int trace_query_start(struct pt_regs *ctx) {//char query[256];// 从探针中读取参数//bpf_usdt_readarg_p(1, ctx, &query, sizeof(query));return 0;}"""# 加载eBPF程序b = BPF(text=bpf_text, usdt_contexts=[u])# 输出探针捕获的信息print("Tracing USDT probes... Hit Ctrl-C to end.")b.trace_print()
次数 | 1 | 2 | 3 | 4 | 5 | avg |
时间(ns) | 10561827908 | 10702234936 | 10307730644 | 10934509995 | 10393669721 | 10579994640.8 |
使用uprobe进行探测:
from bcc import BPF# 定义eBPF程序bpf_text = """#include <uapi/linux/ptrace.h>// 处理 uprobe 事件int trace_execute_query(struct pt_regs *ctx) {return 0;}"""# 加载eBPF程序b = BPF(text=bpf_text)# 设置 uprobeb.attach_uprobe(name="./main", sym="_Z12executeQueryRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE", fn_name="trace_execute_query")# 输出探针捕获的信息print("Tracing uprobe... Hit Ctrl-C to end.")b.trace_print()
次数 | 1 | 2 | 3 | 4 | 5 | avg |
时间(ns) | 11745072927 | 10933262397 | 10971148300 | 11217268481 | 11511004602 | 11275551341.4 |
通过对比可知,在数字上我们能看到USDT耗时比uprobe少69.5ns,但该值放应用链路探测中消耗也非常低可忽略不计。
实际在使用eBPF进行工程化探测时,由于程序没有预埋静态探针,很多时候还会用到uretprobe, 使用同样的demo对uretprobe 进行一组测试
from bcc import BPF# 定义eBPF程序bpf_text = """#include <uapi/linux/ptrace.h>// 处理 uprobe 事件int trace_execute_query(struct pt_regs *ctx) {return 0;}"""# 加载eBPF程序b = BPF(text=bpf_text)# 设置 uprobeb.attach_uretprobe(name="./main", sym="_Z12executeQueryRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE", fn_name="trace_execute_query")# 输出探针捕获的信息print("Tracing uprobe... Hit Ctrl-C to end.")b.trace_print()
运行 python trace.py后,执行./main 统计执行时间
次数 | 1 | 2 | 3 | 4 | 5 | avg |
时间(ns) | 22906574920 | 22257599559 | 21733037699 | 21800112726 | 21897448725 | 22118954725.8 |
由该组数据可知uretprobe触发耗时是USDT和uprobe的两倍,但该值放应用链路探测中消耗也非常低可忽略不计。