DeepSeek总结的使用 eBPF 和硬件断点跟踪 PostgreSQL
来源https://jnidzwetzki.github.io/2026/05/08/ebpf-hw-breakpoints-postgresql.html使用 eBPF 和硬件断点跟踪 PostgreSQL作者: Jan Nidzwetzki日期: 2026 年 5 月 8 日当特定内存地址被访问时硬件断点可以利用 CPU 硬件支持以较低的开销触发 eBPF 程序。通过利用这些硬件断点我们可以有效地监控 PostgreSQL 的内部变量更新例如事务 ID 生成和 OID 分配。在这篇文章中我们将讨论什么是硬件断点它们是否比 uprobe 的开销更低以及如何使用 bpftrace 回答诸如“每秒执行多少个事务”或“哪个后端进程消耗的 OID 最多”等问题。在之前的一篇博文中我讨论了如何使用 eBPF、uprobe/uretprobe 和 bpftrace 来监控 PostgreSQL 的内部函数例如 vacuum。当进入或退出用户空间中的函数时uprobe 和 uretprobe 会触发 Linux 内核中的 eBPF 代码。尽管 uprobe 和 uretprobe 的开销非常低它们仍然需要通过软件中断来检测函数的入口或出口。对于调用非常频繁的函数来说这种开销尤其值得关注。相比之下硬件断点使用 CPU 硬件特性来监控特定的内存地址并在被监控的地址被访问时触发真正的硬件中断。因此它们也能让我们捕获对特定变量的所有更新即使该变量在多个函数中被更新而无需检测每个触及它的函数。Uprobe 在底层是如何工作的Uprobe 和 uretprobe 通过将函数入口或出口的前几条指令替换为一个软件int3中断来检测函数。当函数被调用时CPU 执行该软件中断触发 CPU 模式切换从而使 eBPF 程序能够运行。当 eBPF 程序完成时内核需要执行被 int3 替换的指令。这被称为线下执行out-of-line execution需要内核单独运行原始指令这增加了额外的开销。在将 uprobe 附加到函数之前和之后可以通过在 gdb 中检查函数的前几个字节来观察指令替换。例如让我们检查 PostgreSQL 中的bms_is_member函数(gdb) x/10bx bms_is_member 0x55e0c2f7242c bms_is_member: 0x55 0x48 0x89 0xe5 0x48 0x83 0xec 0x20 0x55e0c2f72434 bms_is_member8: 0x89 0x7dbms_is_member函数的第一个字节是0x55对应push rbp指令。当运行一个将 uprobe 附加到bms_is_member函数的 eBPF 程序时例如funccount-bpfcc /home/jan/postgresql-sandbox/bin/REL_17_1_DEBUG/bin/postgres:bms_is_member函数的第一个字节会改变(gdb) x/10bx bms_is_member 0x55e0c2f7242c bms_is_member: 0xcc 0x48 0x89 0xe5 0x48 0x83 0xec 0x20 0x55e0c2f72434 bms_is_member8: 0x89 0x7d执行funccount-bpfcc命令后bms_is_member函数的第一个字节被替换为0xcc这是 x86_64 CPU 上int3指令的操作码。这允许内核在调用bms_is_member函数时执行 eBPF 程序。注意在 gdb 中运行disassemble bms_is_member将显示原始指令因为 gdb 使用相同的int3指令来设置断点并在反汇编时将int3指令替换为原始指令。硬件断点是如何工作的与 uprobe 相比硬件断点不需要任何指令替换。相反它们使用 CPU 硬件特性来监控特定的内存地址并在被监控的地址被访问时触发真正的硬件中断。当 CPU 试图访问该特定地址读、写或执行时硬件比较器会触发这可以在监控频繁访问的函数或变量时实现更低的开销。在 x86_64 CPU 上硬件断点通常是可用的但确切的数量取决于 CPU。一个快速的检查方法是使用以下命令查找de标志该标志表示调试扩展debug extensionsgrep-m1flags /proc/cpuinfo|grep-odeechoCPU supports hardware breakpoints||echoCPU does not support hardware breakpoints不幸的是没有简单的方法来检查有多少个硬件断点可用但 x86_64 CPU 通常支持最多四个硬件断点。确定可用硬件断点数量的一种方法是使用 gdb 设置硬件断点直到失败为止。例如gdb 命令hbreak可用于在特定内存地址设置硬件断点。示例用例在本节中我们将讨论如何使用 eBPF 硬件断点来监控 PostgreSQL 的内部操作例如事务 ID 生成和 OID 分配。为了能够正确地将 uprobe 附加到 PostgreSQL以下示例中使用的是 PostgreSQL 的调试构建版本。监控 PostgreSQL 事务 ID 生成为了在访问特定变量时使用硬件断点来触发 eBPF 程序我们可以使用bpftrace工具。第一步是确定要监控的变量的内存地址。例如为了监控 PostgreSQL 的事务 ID 生成我们可以检查nextXid变量。为了确定nextXid的内存地址我们可以使用 gdb 附加到一个正在运行的 PostgreSQL 进程并打印变量的地址gdb -p $(pgrep -o postgres) (gdb) print TransamVariables-nextXid $1 (FullTransactionId *) 0x7f6791925608之后我们可以在 bpftrace 中使用该信息在TransamVariables-nextXid的内存地址上设置一个硬件断点并在其被访问时触发 eBPF 程序。此外eBPF 程序可以读取nextXid的值参见下面 bpftrace 命令中的*(uint64 *)0x7f6791925608表达式并连同访问它的进程 ID 和命令名一起打印出来sudobpftrace-e watchpoint:0x7f6791925608:8:w { \$val *(uint64 *)0x7f6791925608; printf(\[XID Event] PID: %-6d | comm: %-10s | next xid: %lu\n\, pid, comm, \$val); }在此示例中watchpoint探针用于在内存地址0x7f6791925608对应TransamVariables-nextXid上设置一个硬件断点。:8:w后缀表示我们想要监控对该地址的 8 字节写访问。当在第二个终端中调用pg_current_xact_id()时PostgreSQL 会分配一个新的事务 ID这会更新nextXid。这会触发硬件断点并执行 eBPF 程序该程序会打印nextXid的新值。例如test2# SELECT pg_current_xact_id();pg_current_xact_id--------------------2246(1row)test2# SELECT pg_current_xact_id();pg_current_xact_id--------------------2247(1row)bpftrace 命令的输出显示了每次nextXid更新时的进程 ID、命令名和新值Attaching 1 probe... [XID Event] PID: 117447 | comm: postgres | next xid: 2247 [XID Event] PID: 117447 | comm: postgres | next xid: 2248为了监控事务 ID 生成速率我们可以使用 bpftrace 计算每秒硬件断点被触发的次数。eBPF 程序将这些计数存储在一个名为count的 eBPF 映射中。interval:s:1探针每秒打印一次count的内容然后为下一个间隔清空映射sudobpftrace-e watchpoint:0x7f6791925608:8:w { count[comm] count(); } interval:s:1 { time(%H:%M:%S: ); print(count); clear(count); }运行上述 bpftrace 命令时它会每秒打印硬件断点被触发的次数这对应于 PostgreSQL 中创建的事务数。例如输出可能如下所示21:49:45: 21:49:46: count[postgres]: 1 21:49:47: count[postgres]: 23 21:49:48: count[postgres]: 24 21:49:49: count[postgres]: 2 21:49:50: 21:49:51: count[postgres]: 1 21:49:52: count[postgres]: 1 21:49:53: count[postgres]: 2这意味着在 21:49:46 开始的一秒间隔内硬件断点被触发了一次对应于 PostgreSQL 中创建了一个事务。在下一个从 21:49:47 开始的一秒间隔内硬件断点被触发了 23 次对应于 PostgreSQL 中创建了 23 个事务依此类推。监控 PostgreSQL OID 分配使用相同的方法我们也可以通过设置硬件断点在TransamVariables-nextOid变量上来监控 PostgreSQL 的 OID 分配。第一步是使用 gdb 确定nextOid变量的内存地址(gdb) print TransamVariables-nextOid $1 (Oid *) 0x7f6791925600为了监控 OID 分配我们可以使用一个简单的 bpftrace 命令在TransamVariables-nextOid的内存地址上设置一个硬件断点并在其更新时打印nextOid的新值sudobpftrace-e watchpoint:0x7f6791925600:4:w { \$val *(uint32 *)0x7f6791925600; printf(\[OID Event] PID: %-6d | comm: %-10s | next oid: %lu\n\, pid, comm, \$val); }当在第二个终端中PostgreSQL 分配一个新的 OID 时例如通过创建一个新表nextOid变量会被更新这会触发硬件断点并执行 eBPF 程序打印出nextOid的新值test2# CREATE TABLE test100();CREATETABLEtest2# CREATE TABLE test101();CREATETABLEtest2# CREATE TABLE test102();CREATETABLEtest2# SELECT test100::regclass::oid;oid-------57539(1row)bpftrace 命令的输出显示了每次nextOid更新时的进程 ID、命令名和新值。它还显示了表test100的 OID 是 57539。输出的第一行对应于表test100的 OID 分配表创建后nextOid递增到 57540。[OID Event] PID: 117447 | comm: postgres | next oid: 57540 [OID Event] PID: 117447 | comm: postgres | next oid: 57541 [OID Event] PID: 117447 | comm: postgres | next oid: 57542为了监控哪个后端进程消耗的 OID 最多我们可以使用一个 eBPF 程序计算每个后端进程触发硬件断点的次数。interval:s:5探针每五秒打印一次count映射的内容然后为下一个间隔清空映射sudobpftrace-e watchpoint:0x7f6791925600:4:w { count[tid, comm] count(); } interval:s:5 { time(%H:%M:%S: ); print(count); clear(count); }上述 bpftrace 命令的输出将显示每五秒每个后端进程触发硬件断点的次数这对应于每个 PostgreSQL 后端分配的 OID 数量。例如输出可能如下所示21:47:15: 21:47:20: 21:47:25: count[519125, postgres]: 6 21:47:30: count[519125, postgres]: 6 21:47:35: count[673992, postgres]: 6 count[519125, postgres]: 6 21:47:40: 21:47:45: count[673992, postgres]: 18这意味着 ID 为 519125 的进程在 21:47:25 开始的五秒间隔内触发了 6 次硬件断点在下一个五秒间隔内也触发了 6 次。ID 为 673992 的进程在 21:47:35 开始的五秒间隔内触发了 6 次硬件断点在下一个五秒间隔内触发了 18 次这表明它比 ID 为 519125 的进程消耗了更多的 OID。基准测试硬件断点与 Uprobe 的对比为了比较硬件断点和 uprobe 的开销我们可以使用一个简单的 C 程序该程序在循环中执行大量计算并更新一个全局变量然后分别通过硬件断点或 uprobe 对其进行监控。#includestdio.h#includestdint.h#includetime.h#includestdbool.h#includemath.h// 用于硬件监视点的全局变量volatileuint64_ttarget_var0;// 用于 uprobe 的函数__attribute__((noinline))voidtrace_target_func(uint64_tval){target_varval;}intmain(){uint64_titerations0;structtimespecstart,now;doubleelapsed;doubledummy_math0.0;printf(Target address for watchpoint: %p\n,(void*)target_var);printf(Symbol for uprobe: trace_target_func\n\n);clock_gettime(CLOCK_MONOTONIC,start);while(true){// 执行一些高负荷计算以模拟工作负载for(inti0;i500;i){dummy_mathsin(i)*cos(iterations);dummy_mathsqrt(fabs(dummy_math1.0));}// 触发探针trace_target_func((uint64_t)dummy_mathiterations);iterations;// 每秒测量一次吞吐量clock_gettime(CLOCK_MONOTONIC,now);elapsed(now.tv_sec-start.tv_sec)(now.tv_nsec-start.tv_nsec)/1e9;if(elapsed1.0){printf(Throughput: %.2f thousand iterations/s | Current Value: %.2f\n,(iterations/elapsed)/1e3,dummy_math);iterations0;clock_gettime(CLOCK_MONOTONIC,start);}}return0;}在下面的示例中程序使用gcc -O3 perf_test.c -lm -o perf_test编译然后执行。在我的机器上使用低功耗优化的 Intel® Pentium® Silver J5005 CPU运行该程序时输出如下Target address for watchpoint: 0x55c55f9eb040 Symbol for uprobe: trace_target_func Throughput: 56.13 thousand iterations/s | Current Value: 1.51 Throughput: 55.80 thousand iterations/s | Current Value: 1.84 Throughput: 56.11 thousand iterations/s | Current Value: 1.55 Throughput: 56.42 thousand iterations/s | Current Value: 1.80当使用sudo bpftrace -e uprobe:./perf_test:trace_target_func { count(); }将 uprobe 附加到trace_target_func函数上来计算函数被调用的次数时吞吐量显著下降Throughput: 34.46 thousand iterations/s | Current Value: 1.32 Throughput: 34.99 thousand iterations/s | Current Value: 1.32 Throughput: 35.07 thousand iterations/s | Current Value: 1.63 Throughput: 34.78 thousand iterations/s | Current Value: 1.58当使用sudo bpftrace -e watchpoint:0x558b32ba9040:8:w { count(); }将硬件断点附加到target_var变量上时输出也有所下降但幅度略小于 uprobeThroughput: 38.61 thousand iterations/s | Current Value: 1.46 Throughput: 38.80 thousand iterations/s | Current Value: 1.84 Throughput: 38.75 thousand iterations/s | Current Value: 1.66 Throughput: 39.09 thousand iterations/s | Current Value: 1.84因此在没有探针的情况下我们大约有 56.115 千次迭代/秒使用 uprobe 时约为 34.825 千次迭代/秒使用硬件断点时约为 38.8125 千次迭代/秒。这意味着在这个特定的基准测试中uprobe 的开销约为 38%而硬件断点的开销约为 30%。确切的开销可能因 CPU 架构、工作负载以及被监控函数或变量被访问的频率而异。在这两种情况下开销仍然显著的原因在于当探针被触发时执行 eBPF 程序所需的 CPU 模式切换。触发 eBPF 程序的方式会影响开销但 CPU 模式切换本身以及 eBPF 程序的执行是主要的贡献因素。结论在这篇博文中我们讨论了如何使用 eBPF 硬件断点来监控 PostgreSQL 的内部操作例如事务 ID 生成和 OID 分配。我们还比较了硬件断点与 uprobe 的开销发现硬件断点的开销可能略低于 uprobe。