28.5. 动态追踪 #
PostgreSQL 提供了功能来支持数据库服务器的动态追踪。这样就允许在代码中的特定点上调用外部工具来追踪执行过程。
一些探针或追踪点已经被插入在源代码中。这些探针的目的是被数据库开发者和管理员使用。默认情况下,探针不被编译到 PostgreSQL 中;用户需要显式地告诉配置脚本使得探针可用。
目前,在写本文当时 DTrace 已被支持,它在 Solaris、macOS、FreeBSD、NetBSD 和 Oracle Linux 上可用。Linux 的 SystemTap 项目提供了一种可用的 DTrace 等价物。支持其他动态追踪工具在理论上可以通过改变 src/include/utils/probes.h 中的宏定义实现。
28.5.1. 为动态追踪编译 #
默认情况下,探针是不可用的,因此你将需要显式地告诉配置脚本让探针在 PostgreSQL 中可用。要包括 DTrace 支持,在配置时指定 --enable-dtrace。更多信息请见第 16.4 节。
28.5.2. 内置探针 #
如表 28.23 所示,源代码中提供了一些标准探针。表 28.24 显示了在探针中使用的类型。当然,可以增加更多探针来增强 PostgreSQL 的可观测性。
表 28.23. 内置 DTrace 探针
| 名称 | 参数 | 描述 |
|---|---|---|
transaction-start | (LocalTransactionId) | 在一个新事务开始时触发的探针。arg0 是事务 ID。 |
transaction-commit | (LocalTransactionId) | 在一个事务成功完成时触发的探针。arg0 是事务 ID。 |
transaction-abort | (LocalTransactionId) | 当一个事务失败结束时触发的探针。arg0 是事务 ID。 |
query-start | (const char *) | 当一个查询的处理开始时触发的探针。arg0 是查询字符串。 |
query-done | (const char *) | 当一个查询的处理完成时触发的探针。arg0 是查询字符串。 |
query-parse-start | (const char *) | 当一个查询的解析开始时触发的探针。arg0 是查询字符串。 |
query-parse-done | (const char *) | 当一个查询的解析完成时触发的探针。arg0 是查询字符串。 |
query-rewrite-start | (const char *) | 当一个查询的重写开始时触发的探针。arg0 是查询字符串。 |
query-rewrite-done | (const char *) | 当一个查询的重写完成时触发的探针。arg0 是查询字符串。 |
query-plan-start | () | 当一个查询的规划开始时触发的探针。 |
query-plan-done | () | 当一个查询的规划完成时触发的探针。 |
query-execute-start | () | 当一个查询的执行开始时触发的探针。 |
query-execute-done | () | 当一个查询的执行完成时触发的探针。 |
statement-status | (const char *) | 任何时候当服务器进程更新它的 pg_stat_activity.status 时触发的探针。arg0 是新的状态字符串。 |
checkpoint-start | (int) | 当一个检查点开始时触发的探针。arg0 传递位标志来区分不同的检查点类型,例如关闭(shutdown)、立即(immediate)或强制(force)。 |
checkpoint-done | (int, int, int, int, int) | 当一个检查点完成时触发的探针(下面列出的探针会在检查点处理过程中依次触发)。arg0 是已写入的缓冲区数量。arg1 是缓冲区的总数。arg2、arg3 和 arg4 分别包含了增加、删除和循环回收的 WAL 文件的数量。 |
clog-checkpoint-start | (bool) | 当一个检查点的 CLOG 部分开始时触发的探针。arg0 为真表示正常检查点,为假表示关闭检查点。 |
clog-checkpoint-done | (bool) | 当一个检查点的 CLOG 部分完成时触发的探针。arg0 的含义与 clog-checkpoint-start 中相同。 |
subtrans-checkpoint-start | (bool) | 当一个检查点的 SUBTRANS 部分开始时触发的探针。arg0 为真表示正常检查点,为假表示关闭检查点。 |
subtrans-checkpoint-done | (bool) | 当一个检查点的 SUBTRANS 部分完成时触发的探针。arg0 的含义与 subtrans-checkpoint-start 中相同。 |
multixact-checkpoint-start | (bool) | 当一个检查点的 MultiXact 部分开始时触发的探针。arg0 为真表示正常检查点,为假表示关闭检查点。 |
multixact-checkpoint-done | (bool) | 当一个检查点的 MultiXact 部分完成时触发的探针。arg0 的含义与 multixact-checkpoint-start 中相同。 |
buffer-checkpoint-start | (int) | 当一个检查点的写缓冲区部分开始时触发的探针。arg0 传递位标志来区分不同的检查点类型,例如关闭(shutdown)、立即(immediate)或强制(force)。 |
buffer-sync-start | (int, int) | 当我们在检查点期间开始写脏缓冲区时(在标识哪些缓冲区必须被写之后)触发的探针。arg0 是缓冲区总数,arg1 是当前为脏并且需要被写的缓冲区数量。 |
buffer-sync-written | (int) | 在检查点期间当每个缓冲区被写完之后触发的探针。arg0 是缓冲区的 ID。 |
buffer-sync-done | (int, int, int) | 当所有脏缓冲区被写之后触发的探针。arg0 是缓冲区总数。arg1 是检查点进程实际写的缓冲区数量。arg2 是期望写的数目(buffer-sync-start 的 arg1);arg1 和 arg2 的任何的不同反映在该检查点期间有其他进程刷写了缓冲区。 |
buffer-checkpoint-sync-start | () | 在脏缓冲区被写入到内核之后并且在开始发出 fsync 请求之前触发的探针。 |
buffer-checkpoint-done | () | 当同步缓冲区到磁盘完成时触发的探针。 |
twophase-checkpoint-start | () | 当一个检查点的两阶段部分开始时触发的探针。 |
twophase-checkpoint-done | () | 当一个检查点的两阶段部分完成时触发的探针。 |
buffer-read-start | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, bool) | 当一次缓冲区读开始时触发的探针。arg0 和 arg1 包含该页的分支号和块号(如果这是一次关系扩展请求,arg1 为 -1)。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID 用以识别该关系。对一个本地缓冲区,arg5 是创建临时关系的后端的 ID;对于一个共享缓冲区,arg5 是 InvalidBackendId(-1)。arg6 为真表示一次关系扩展请求,为假表示正常读。 |
buffer-read-done | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, bool, bool) | 当一次缓冲区读完成时触发的探针。arg0 和 arg1 包含该页的分支号和块号(如果这是一次关系扩展请求,arg1 现在包含新增加块的块号)。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID 用以识别该关系。对一个本地缓冲区,arg5 是创建临时关系的后端的 ID;对于一个共享缓冲区,arg5 是 InvalidBackendId(-1)。arg6 为真表示一次关系扩展请求,为假表示正常读。arg7 为真表示在池中找到该缓冲区,为假表示没有找到。 |
buffer-flush-start | (ForkNumber, BlockNumber, Oid, Oid, Oid) | 在发出对一个共享缓冲区的任意写请求之前触发的探针。arg0 和 arg1 包含该页的分支号和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID 用以识别该关系。 |
buffer-flush-done | (ForkNumber, BlockNumber, Oid, Oid, Oid) | 当一个写请求完成时触发的探针(注意这只反映传递数据给内核的时间,它通常并没有实际地被写入到磁盘)。参数和 buffer-flush-start 的相同。 |
buffer-write-dirty-start | (ForkNumber, BlockNumber, Oid, Oid, Oid) | 当一个服务器进程开始写一个脏缓冲区时触发的探针(如果这经常发生,表示 shared_buffers 太小,或需要调整后台写入器的控制参数)。arg0 和 arg1 包含该页的分支号和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID 用以识别该关系。 |
buffer-write-dirty-done | (ForkNumber, BlockNumber, Oid, Oid, Oid) | 当一次脏缓冲区写完成时触发的探针。参数与 buffer-write-dirty-start 相同。 |
wal-buffer-write-dirty-start | () | 当一个服务器进程因为没有可用 WAL 缓冲区空间开始写一个脏 WAL 缓冲区时触发的探针(如果这经常发生,表示 wal_buffers 太小)。 |
wal-buffer-write-dirty-done | () | 当一次脏 WAL 缓冲区写入完成时触发的探针。 |
wal-insert | (unsigned char, unsigned char) | 当一个 WAL 记录被插入时触发的探针。arg0 是该记录的资源管理器(rmid)。arg1 包含 info 标志。 |
wal-switch | () | 当请求一次 WAL 段切换时触发的探针。 |
smgr-md-read-start | (ForkNumber, BlockNumber, Oid, Oid, Oid, int) | 当开始从一个关系读取一块时触发的探针。arg0 和 arg1 包含该页的分支号和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID 用以识别该关系。对一个本地缓冲区,arg5 是创建临时关系的后端的 ID;对于一个共享缓冲区,arg5 是 InvalidBackendId(-1)。 |
smgr-md-read-done | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, int, int) | 当一次块读取完成时触发的探针。arg0 和 arg1 包含该页的分支号和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID 用以识别该关系。对一个本地缓冲区,arg5 是创建临时关系的后端的 ID;对于一个共享缓冲区,arg5 是 InvalidBackendId(-1)。arg6 是实际读取的字节数,而 arg7 是请求读取的字节数(如果两者不同就意味着麻烦)。 |
smgr-md-write-start | (ForkNumber, BlockNumber, Oid, Oid, Oid, int) | 当开始向一个关系中写入一个块时触发的探针。arg0 和 arg1 包含该页的分支号和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID 用以识别该关系。对一个本地缓冲区,arg5 是创建临时关系的后端的 ID;对于一个共享缓冲区,arg5 是 InvalidBackendId(-1)。 |
smgr-md-write-done | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, int, int) | 当一个块写操作完成时触发的探针。arg0 和 arg1 包含该页的分支号和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID 来标识该关系。对于一个本地缓冲区,arg5 是创建临时关系的后端 ID;对于一个共享缓冲区,arg5 是 InvalidBackendId(-1)。arg6 是实际写的字节数,而 arg7 是要求写的字节数(如果这两者不同,则意味着麻烦)。 |
sort-start | (int, bool, int, int, bool) | 当一次排序操作开始时触发的探针。arg0 指示是堆排序、索引排序或 datum 排序。arg1 为真表示强制唯一值。arg2 是键列数。arg3 是允许使用的工作内存大小(以千字节计)。如果要求随机访问排序结果,则 arg4 为真。 |
sort-done | (bool, long) | 当一次排序完成时触发的探针。arg0 为真表示外部排序,为假表示内排序。arg1 是用于一次外部排序的磁盘块的数目,或用于一次内排序的以千字节计的内存。 |
lwlock-acquire | (char *, LWLockMode) | 当成功获得一个 LWLock 时触发的探针。arg0 是该 LWLock 所在的切片(Tranche)。arg1 是所请求的锁模式,是排他或共享。 |
lwlock-release | (char *) | 当一个 LWLock 被释放时(但是注意还没有唤醒任何一个被释放的等待者)触发的探针。arg0 是该 LWLock 所在的切片(Tranche)。 |
lwlock-wait-start | (char *, LWLockMode) | 当一个 LWLock 不是当即可用并且一个服务器进程因此开始等待该锁变为可用时触发的探针。arg0 是该 LWLock 所在的切片(Tranche)。arg1 是所请求的锁模式,是排他或共享。 |
lwlock-wait-done | (char *, LWLockMode) | 当一个进程从对一个 LWLock 的等待中被释放(它实际还没有得到该锁)时触发的探针。arg0 是该 LWLock 所在的切片(Tranche)。arg1 是所请求的锁模式,是排他或共享。 |
lwlock-condacquire | (char *, LWLockMode) | 当调用者指定无需等待而成功获得一个 LWLock 时触发的探针。arg0 是该 LWLock 所在的切片(Tranche)。arg1 是所请求的锁模式,是排他或共享。 |
lwlock-condacquire-fail | (char *, LWLockMode) | 当调用者指定无需等待而没有成功获得一个 LWLock 时触发的探针。arg0 是该 LWLock 所在的切片(Tranche)。arg1 是所请求的锁模式,是排他或共享。 |
lock-wait-start | (unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, LOCKMODE) | 当一个重量级锁(lmgr 锁)的请求由于锁不可用开始等待时触发的探针。arg0 到 arg3 是标识被锁定对象的标签字段。arg4 指示被锁对象的类型。arg5 表示被请求的锁类型。 |
lock-wait-done | (unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, LOCKMODE) | 当一个重量级锁(lmgr 锁)的请求结束等待时(即已经得到锁)触发的探针。参数与 lock-wait-start 一样。 |
deadlock-found | () | 当死锁检测器发现死锁时触发的探针。 |
表 28.24. 探针参数中使用的已定义类型
| 类型 | 定义 |
|---|---|
LocalTransactionId | unsigned int |
LWLockMode | int |
LOCKMODE | int |
BlockNumber | unsigned int |
Oid | unsigned int |
ForkNumber | int |
bool | char |
28.5.3. 使用探针 #
下面的示例展示了一个分析系统中事务计数的 DTrace 脚本,可以用来代替一次性能测试之前和之后的 pg_stat_database 快照:
#!/usr/sbin/dtrace -qs
postgresql$1:::transaction-start
{
@start["Start"] = count();
self->ts = timestamp;
}
postgresql$1:::transaction-abort
{
@abort["Abort"] = count();
}
postgresql$1:::transaction-commit
/self->ts/
{
@commit["Commit"] = count();
@time["Total time (ns)"] = sum(timestamp - self->ts);
self->ts=0;
}当被执行时,该示例 D 脚本给出这样的输出:
# ./txn_count.d `pgrep -n postgres` or ./txn_count.d <PID> ^C Start 71 Commit 70 Total time (ns) 2312105013
注意
SystemTap 的追踪脚本记法与 DTrace 不同,但底层探针是兼容的。需要注意的一点是,截至本文编写时,SystemTap 脚本必须使用双下划线来代替连字符引用探针名。预计未来的 SystemTap 版本会修复这一点。
你应该记住,DTrace 脚本需要细心地编写和调试,否则被收集的追踪信息可能会毫无意义。在大多数发现问题的情况下,出错的是插桩,而不是底层系统。当讨论使用动态追踪发现的信息时,一定要附上使用的脚本以便其也被检查和讨论。
28.5.4. 定义新探针 #
开发者可以在代码中任意位置定义新的探针,当然这要重新编译之后才能生效。下面是插入新探针的步骤:
决定探针名称以及要通过探针提供的数据
把该探针定义加入到
src/backend/utils/probes.d如果
pg_trace.h尚未被包含该探针点的模块引用,则将它包含进来,并且在源代码中期望的位置插入TRACE_POSTGRESQL探针宏重新编译并验证新探针是可用的
示例:. 这里是一个如何增加一个探针来用事务 ID 追踪所有新事务的示例。
决定探针将被命名为
transaction-start并且需要一个LocalTransactionId类型的参数将该探针定义加入到
src/backend/utils/probes.d:probe transaction__start(LocalTransactionId);
注意探针名字中双下划线的使用。在一个使用探针的 DTrace 脚本中,双下划线需要被替换为一个连字符,因此,文档中应向用户说明的名称是
transaction-start。在编译时,
transaction__start被转换成一个名为TRACE_POSTGRESQL_TRANSACTION_START的宏(注意这里是单下划线),可以通过包含头文件pg_trace.h获得。将宏调用加入到源代码中的合适位置。在这种情况下,看起来类似:TRACE_POSTGRESQL_TRANSACTION_START(vxid.localTransactionId);
在重新编译和运行新的二进制文件之后,通过运行下面的 DTrace 命令来检查新增的探针是否可用。你应该看到类似下面的输出:
# dtrace -ln transaction-start ID PROVIDER MODULE FUNCTION NAME 18705 postgresql49878 postgres StartTransactionCommand transaction-start 18755 postgresql49877 postgres StartTransactionCommand transaction-start 18805 postgresql49876 postgres StartTransactionCommand transaction-start 18855 postgresql49875 postgres StartTransactionCommand transaction-start 18986 postgresql49873 postgres StartTransactionCommand transaction-start
向 C 代码中添加追踪宏时,有一些事情需要注意:
需要小心的是,为探针参数指定的数据类型要匹配宏中使用的变量的数据类型,否则会发生编译错误。
在大多数平台上,如果用
--enable-dtrace编译了 PostgreSQL,无论何时当控制经过一个追踪宏时,都会对该宏的参数求值,即使没有进行追踪也会这样做。如果只是报告少数局部变量的值,通常无需担心这一点。但是要注意不要将开销大的函数调用放入参数中。如果你需要这样做,考虑通过检查追踪是否真的被启用来保护该宏:if (TRACE_POSTGRESQL_TRANSACTION_START_ENABLED()) TRACE_POSTGRESQL_TRANSACTION_START(some_function(...));每个追踪宏有一个对应的
ENABLED宏。