28.5. 动态跟踪#
PostgreSQL提供了支持数据库服务器动态跟踪的功能。这允许在代码中的特定点调用外部实用程序,从而跟踪执行。
一些探测器或跟踪点已插入到源代码中。这些探测器旨在供数据库开发人员和管理员使用。默认情况下,探测器不会编译到PostgreSQL中;用户需要明确地告知 configure 脚本以使探测器可用。
目前,支持DTrace实用程序,在撰写本文时,可在 Solaris、macOS、FreeBSD、NetBSD 和 Oracle Linux 上使用。适用于 Linux 的SystemTap项目提供 DTrace 等效项,也可以使用。通过更改src/include/utils/probes.h
中宏的定义,理论上可以支持其他动态跟踪实用程序。
28.5.1. 编译以进行动态跟踪#
默认情况下,探针不可用,因此您需要明确告知配置脚本在PostgreSQL中提供探针。若要包含 DTrace 支持,请在配置中指定--enable-dtrace
。有关更多信息,请参见第 17.3.3.6 节。
28.5.2. 内置探针#
源代码中提供了许多标准探针,如表 28.48所示;表 28.49显示了探针中使用的类型。当然可以添加更多探针来增强PostgreSQL的可观察性。
表 28.48. 内置 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 是查询字符串。 |
查询重写开始 | (const char *) | 开始重写查询时触发探测。arg0 是查询字符串。 |
查询重写完成 | (const char *) | 完成查询重写时触发探测。arg0 是查询字符串。 |
查询计划开始 | () | 开始计划查询时触发探测。 |
查询计划完成 | () | 完成查询计划时触发探测。 |
查询执行开始 | () | 开始执行查询时触发探测。 |
查询执行完成 | () | 完成查询执行时触发探测。 |
语句状态 | (const char *) | 服务器进程更新其 pg_stat_activity .status 时触发探测。arg0 是新的状态字符串。 |
检查点开始 | (int) | 开始检查点时触发探测。arg0 保存用于区分不同检查点类型的位标志,例如关闭、立即或强制。 |
检查点完成 | (int, int, int, int, int) | 完成检查点时触发探测。(在检查点处理期间,接下来列出的探测按顺序触发。)arg0 是写入的缓冲区数。arg1 是缓冲区的总数。arg2、arg3 和 arg4 分别包含已添加、已删除和已回收的 WAL 文件数。 |
CLOG 检查点开始 | (bool) | 开始检查点的 CLOG 部分时触发探测。arg0 对于正常检查点为 true,对于关闭检查点为 false。 |
CLOG 检查点完成 | (bool) | 完成检查点的 CLOG 部分时触发探测。arg0 的含义与 clog-checkpoint-start 相同。 |
子事务检查点开始 | (bool) | 开始检查点的 SUBTRANS 部分时触发探测。arg0 对于正常检查点为 true,对于关闭检查点为 false。 |
子事务检查点完成 | (bool) | 完成检查点的 SUBTRANS 部分时触发探测。arg0 的含义与 subtrans-checkpoint-start 相同。 |
多事务检查点开始 | (bool) | 开始检查点的 MultiXact 部分时触发探测。arg0 对于正常检查点为 true,对于关闭检查点为 false。 |
多事务检查点完成 | (bool) | 完成检查点的 MultiXact 部分时触发探测。arg0 的含义与 multixact-checkpoint-start 相同。 |
缓冲区检查点开始 | (int) | 开始检查点的缓冲区写入部分时触发探测。arg0 保存用于区分不同检查点类型的位标志,例如关闭、立即或强制。 |
缓冲区同步开始 | (int, int) | 在检查点期间开始写入脏缓冲区时触发探测(在识别出必须写入哪些缓冲区之后)。arg0 是缓冲区的总数。arg1 是当前脏且需要写入的缓冲区数。 |
缓冲区同步已写入 | (int) | 在检查点期间写入每个缓冲区后触发探测。arg0 是缓冲区的 ID 号。 |
缓冲区同步完成 | (int, int, int) | 当所有脏缓冲区都已写入时触发的探测。arg0 是缓冲区的总数。arg1 是由检查点进程实际写入的缓冲区数。arg2 是预期写入的缓冲区数(buffer-sync-start 的 arg1);任何差异都反映了在检查点期间其他进程刷新缓冲区。 |
buffer-checkpoint-sync-start | () | 在脏缓冲区写入内核后,并在开始发出 fsync 请求之前触发的探测。 |
buffer-checkpoint-done | () | 当缓冲区与磁盘同步完成后触发的探测。 |
twophase-checkpoint-start | () | 当检查点的两阶段部分启动时触发的探测。 |
twophase-checkpoint-done | () | 当检查点的两阶段部分完成后触发的探测。 |
buffer-extend-start | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, unsigned int) | 当关系扩展启动时触发的探测。arg0 包含要扩展的分支。arg1、arg2 和 arg3 包含表空间、数据库和关系 OID,用于标识关系。arg4 是为本地缓冲区创建临时关系的后端 ID,或者对于共享缓冲区是 InvalidBackendId (-1)。arg5 是调用者想要扩展的块数。 |
buffer-extend-done | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, unsigned int, BlockNumber) | 当关系扩展完成后触发的探测。arg0 包含要扩展的分支。arg1、arg2 和 arg3 包含表空间、数据库和关系 OID,用于标识关系。arg4 是为本地缓冲区创建临时关系的后端 ID,或者对于共享缓冲区是 InvalidBackendId (-1)。arg5 是关系扩展的块数,由于资源限制,这可能小于 buffer-extend-start 中的数字。arg6 包含第一个新块的 BlockNumber。 |
buffer-read-start | (ForkNumber, BlockNumber, Oid, Oid, Oid, int) | 当缓冲区读取启动时触发的探测。arg0 和 arg1 包含页面的分支和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID,用于标识关系。arg5 是为本地缓冲区创建临时关系的后端 ID,或者对于共享缓冲区是 InvalidBackendId (-1)。 |
buffer-read-done | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, bool) | 当缓冲区读取完成后触发的探测。arg0 和 arg1 包含页面的分支和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID,用于标识关系。arg5 是为本地缓冲区创建临时关系的后端 ID,或者对于共享缓冲区是 InvalidBackendId (-1)。如果在池中找到缓冲区,arg6 为 true,否则为 false。 |
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 相同。 |
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 包含信息标志。 |
wal-switch | () | 请求 WAL 段切换时触发的探测。 |
smgr-md-read-start | (ForkNumber, BlockNumber, Oid, Oid, Oid, int) | 开始从关系中读取块时触发的探测。arg0 和 arg1 包含页面的 fork 和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID,用于标识关系。arg5 是为本地缓冲区创建临时关系的后端 ID,或共享缓冲区的 InvalidBackendId (-1)。 |
smgr-md-read-done | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, int, int) | 块读取完成后触发的探测。arg0 和 arg1 包含页面的 fork 和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID,用于标识关系。arg5 是为本地缓冲区创建临时关系的后端 ID,或共享缓冲区的 InvalidBackendId (-1)。arg6 是实际读取的字节数,而 arg7 是请求的字节数(如果不同,则表示有故障)。 |
smgr-md-write-start | (ForkNumber, BlockNumber, Oid, Oid, Oid, int) | 开始将块写入关系时触发的探测。arg0 和 arg1 包含页面的 fork 和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID,用于标识关系。arg5 是为本地缓冲区创建临时关系的后端 ID,或共享缓冲区的 InvalidBackendId (-1)。 |
smgr-md-write-done | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, int, int) | 块写入完成后触发的探测。arg0 和 arg1 包含页面的 fork 和块号。arg2、arg3 和 arg4 包含表空间、数据库和关系 OID,用于标识关系。arg5 是为本地缓冲区创建临时关系的后端 ID,或共享缓冲区的 InvalidBackendId (-1)。arg6 是实际写入的字节数,而 arg7 是请求的字节数(如果不同,则表示有故障)。 |
排序开始 | (int、bool、int、int、bool、int) | 在开始排序操作时触发的探测器。arg0 表示堆、索引或数据排序。arg1 为 true 表示强制唯一值。arg2 为键列数。arg3 为允许的工作内存千字节数。arg4 为 true 表示需要对排序结果进行随机访问。arg5 表示串行时为 0 ,并行工作进程时为 1 ,并行领导者时为 2 。 |
排序完成 | (bool、long) | 在排序完成后触发的探测器。arg0 为 true 表示外部排序,为 false 表示内部排序。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.49. 探测参数中使用的已定义类型
类型 | 定义 |
---|---|
LocalTransactionId | unsigned int |
LWLockMode | int |
LOCKMODE | int |
BlockNumber | unsigned int |
Oid | unsigned int |
ForkNumber | int |
bool | unsigned 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
,则包含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 代码添加跟踪宏时,有一些事项需要注意
您应该注意,为探测参数指定的数据类型与宏中使用的变量的数据类型相匹配。否则,您将收到编译错误。
在大多数平台上,如果 PostgreSQL 使用
--enable-dtrace
构建,则每当控制权通过宏时,都会评估跟踪宏的参数,即使没有进行任何跟踪。如果您只是报告几个局部变量的值,这通常不必担心。但请注意不要将昂贵的函数调用放入参数中。如果您需要这样做,请考虑使用检查来保护宏,以查看跟踪是否实际启用if (TRACE_POSTGRESQL_TRANSACTION_START_ENABLED()) TRACE_POSTGRESQL_TRANSACTION_START(some_function(...));
每个跟踪宏都有一个相应的
ENABLED
宏。