PostgreSQL + eBPF实现数据库服务可观测
引子
发这篇文章之前有点小兴奋,这篇文章可能是过去一年发表文章中最接近我技术领域和工作内容的了。BPF技术总的来说还是比较小众的,但在可观测领域还是有着非常重要的地位。我个人也在思考和设想如何把BPF和数据库结合起来,希望通过这篇文章能给DBA同学、国产数据库厂商提供一些数据库服务可观测的新思路。
什么是 eBPF?
eBPF(extended Berkeley Packet Filter)是一种在Linux内核中运行的虚拟机,可以动态地编写并注入内核级别的代码,用于网络、安全、性能等方面的监测和调试。
eBPF最初是为网络分析设计的,但现在已被广泛应用于系统性能分析、安全审计和容器监控等领域。它通过在内核中运行用户代码来收集和分析数据,而不需要修改内核或加载内核模块,因此具有高度灵活性和可移植性。
eBPF程序可以在内核态和用户态之间进行交互,从而允许用户从内核中获得关于系统和应用程序的详细信息。这些信息可以用于调试、优化和监控系统性能、网络流量、安全审计等方面。
eBPF已经成为Linux生态系统中不可或缺的一部分,被广泛应用于云原生、容器化、网络监控等场景中,成为了开发人员和运维人员的得力工具之一。
eBPF能做什么?
- 网络性能分析:通过在网络协议栈中插入eBPF程序,可以捕获和分析网络流量,例如统计各种网络协议的使用情况、计算网络延迟等。
- 系统性能分析:通过在内核中运行eBPF程序,可以监测系统各种资源的使用情况,例如CPU、内存、磁盘等,进而诊断系统性能问题。
- 安全审计:通过在内核中运行eBPF程序,可以监控系统的行为,例如检测恶意软件、跟踪文件系统活动等,从而实现安全审计和入侵检测。
- 容器监控:通过在容器内运行eBPF程序,可以监控容器内的资源使用情况和网络流量,从而实现容器性能分析和容器安全审计。
- 自定义触发器:通过在内核中运行eBPF程序,可以定义自定义触发器,例如当内核计数器达到特定阈值时,执行特定的操作,从而实现高级的自动化管理和响应。
总之,eBPF是一种高效的监控和分析工具,可以通过自定义程序实现各种系统性能分析、网络分析、安全审计和容器监控等功能。由于其高度灵活和可扩展的特性,eBPF已经被广泛应用于云原生、容器化和分布式系统中。
eBPF如何观测PostgreSQL运行
USDT
USDT(User Statically Defined Tracepoints)是eBPF的一种特性,可以让用户在应用程序中定义自己的跟踪点,从而实现更精细的性能分析和调试。使用eBPF USDT,需要分为以下几个步骤:
- 在应用程序中添加USDT跟踪点:在应用程序中,通过添加类似于以下代码的宏定义,在代码中定义自己的跟踪点:
c
#include <sys/sdt.h>
int main() {
DTRACE_PROBE(MyProvider, myprobe);
return 0;
}
- 编写eBPF程序:编写一个eBPF程序,用于捕获和处理USDT跟踪点生成的事件。可以使用libbpf等工具来简化eBPF程序的编写和管理。
- 加载eBPF程序:通过BPF_MAP_TYPE_PERF_EVENT_ARRAY类型的BPF map,将eBPF程序与USDT跟踪点关联起来,从而在跟踪点生成事件时,将事件发送到eBPF程序中处理。可以使用BCC等工具来简化eBPF程序的加载和管理。
- 分析数据:在eBPF程序中,可以使用BPF_PERF_OUTPUT类型的BPF map,将处理后的事件发送到用户空间,并使用BCC等工具进行数据分析和可视化。
总之,eBPF USDT可以让用户在应用程序中定义自己的跟踪点,从而实现更精细的性能分析和调试。使用eBPF USDT需要分为添加跟踪点、编写eBPF程序、加载eBPF程序和分析数据四个步骤。
PostgreSQL中的USDT跟踪点
在PostgreSQL中,您可以使用DTrace来进行系统级别的跟踪和分析。DTrace可以使用User-Level Statically Defined Tracing(USDT)跟踪点来跟踪PostgreSQL进程的执行情况。USDT是一种动态的、低影响的跟踪技术,它允许您将跟踪点注入到正在运行的二进制文件中,而无需重新编译代码。
PostgreSQL在版本9.3中添加了USDT支持。以下是一些示例USDT跟踪点,您可以使用它们来跟踪PostgreSQL进程的执行情况
BCC框架
BCC是一组用于创建eBPF工具和程序的Python库。它提供了许多有用的工具,例如tcpdump、execsnoop、biosnoop等,这些工具可以跟踪系统调用、网络流量、文件I/O等,并以可视化和易于理解的方式呈现结果。
范例
范例使用BCC框架。
Step 1 重新编译PostgreSQL
默认情况下PostgreSQL是不开启跟踪点的。需要重新编译,并在configure 增加–enable-dtrace选项
Step 2 查看跟踪点信息
通过readelf -n postgres
查看跟踪点信息。
同样,也可以使用tplist/tplist-bpfcc查看跟踪点的提供者和跟踪点名字。
frank@LAPTOP-4OF1323N:~/pgsql/bin$ tplist-bpfcc -l /home/frank/pgsql/bin/postgres
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'clog__checkpoint__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'clog__checkpoint__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'multixact__checkpoint__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'multixact__checkpoint__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'subtrans__checkpoint__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'subtrans__checkpoint__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'twophase__checkpoint__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'twophase__checkpoint__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'transaction__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'transaction__commit'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'transaction__abort'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__checkpoint__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__checkpoint__sync__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__checkpoint__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'wal__buffer__write__dirty__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'wal__buffer__write__dirty__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'wal__switch'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'checkpoint__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'checkpoint__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'wal__insert'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__flush__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__flush__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__read__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__read__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__write__dirty__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__write__dirty__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__sync__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__sync__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'buffer__sync__written'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'deadlock__found'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'lock__wait__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'lock__wait__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'lwlock__wait__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'lwlock__wait__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'lwlock__acquire'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'lwlock__condacquire'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'lwlock__condacquire__fail'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'lwlock__acquire__or__wait__fail'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'lwlock__acquire__or__wait'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'lwlock__release'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'smgr__md__read__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'smgr__md__read__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'smgr__md__write__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'smgr__md__write__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'query__parse__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'query__parse__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'query__rewrite__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'query__rewrite__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'query__plan__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'query__plan__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'query__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'query__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'query__execute__start'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'query__execute__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'statement__status'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'sort__done'
b'/home/frank/pgsql/bin/postgres' b'postgresql':b'sort__start'
Step 3 BCC编写eBPF代码
from __future__ import print_function
from bcc import BPF, USDT
from bcc.utils import printb
import sys
if len(sys.argv) < 2:
print("USAGE: PostgreSQL PID")
exit()
pid = sys.argv[1]
debug = 1
# load BPF program
bpf_text = """
#include <uapi/linux/ptrace.h>
int do_trace(struct pt_regs *ctx) {
uint64_t addr;
char query[128];
bpf_usdt_readarg(1, ctx, &addr);
bpf_probe_read_user(&query, sizeof(query), (void *)addr);
bpf_trace_printk("%s\\n", query);
return 0;
};
"""
# enable USDT probe from given PID
u = USDT(pid=int(pid))
u.enable_probe(probe="query__start", fn_name="do_trace")
if debug:
print(u.get_text())
print(bpf_text)
# initialize BPF
b = BPF(text=bpf_text, usdt_contexts=[u])
# header
print("%-18s %-16s %-6s %-6s %s" % ("TIME(s)", "COMM", "PID", "CPU", "QUERY"))
# format output
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
print("value error")
continue
except KeyboardInterrupt:
exit()
printb(b"%-18.9f %-16s %-6d %-6d %s" % (ts, task, pid, cpu, msg))
注解:
- bpf_text :该变量保存的是在内核空间执行的C代码,主要功能是捕获并打印query信息。
- u.enable_probe(probe=“query__start”, fn_name=“do_trace”):query__start是PostgreSQL中定义probe观察点。do_trace是观察点出发时要执行的跟踪函数。
- 上面代码的功能是,当postgres进程收到客户单的查询sql是触发观察点,并将获取的sql文本传递给观察者。
运行
- 获取要跟踪PostgreSQL 的PID
- 执行观察脚本——python代码。
- psql 执行查询语句
- 观测捕获的sql文本
源码分析
PostgreSQL中的USDT代码。
TRACE_POSTGRESQL_QUERY_START是个宏,定义为:
总结
本文通过一个demo简单介绍了如何使用eBPF技术观测PostgreSQL服务端,eBPF技术比较复杂,文章并没有详细的进行讲解(内容很多)。通常BCC框架的搭建也相对比较麻烦,感兴趣的小伙伴可以自行了解。PostgreSQL共有57个跟踪点供观测,大家可以根据具体场景的需要编写BCC的Python脚本。