ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 27.4\. 动态跟踪 PostgreSQL允许对数据库服务器进行动态跟踪。 这样就允许在代码内特定的点上调用外部工具来跟踪执行过程。 许多跟踪点(也被称为"探头")已经插入在源代码中了, 这些探针的目的是被用于数据库开发者和管理员,默认情况下, 探头不编译成PostgreSQL; 用户必须运行配置脚本时明确启用它们。 目前,只有[DTrace](https://en.wikipedia.org/wiki/DTrace)支持实用工具,在写这的时候, 它可在Solaris, Mac OS X, FreeBSD, NetBSD和Oracle Linux上使用。 [SystemTap](http://sourceware.org/systemtap/) 项目为Linux还提供了一个DTrace的等效并且也是可用的。 通过改变`src/include/utils/probes.h` 中的宏命令定义为支持其他的动态跟踪工具在理论上是可能的 。 ## 27.4.1\. 编译动态跟踪支持 跟踪点是默认禁止的,你必须明确告诉配置脚本以使得PostgreSQL中的探头可用。 使用`--enable-dtrace`选项来启用DTrace支持。 参见[Section 15.4](#calibre_link-637)获取更多信息。 ## 27.4.2\. 内置跟踪点 [Table 27-15](#calibre_link-1989)显示的是在源代码中提供的标准跟踪点, [Table 27-16](#calibre_link-1990)显示探测中使用的类型。 更多探测可以被添加以提高PostgreSQL的观测性。 **Table 27-15\. 内置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包含了增加,删除和循环回收的xlog文件的数目。 | | 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表示期望写的数目(arg1的buffer-sync-start); 任何的不同会导致另一个进程在检查点发生时刷新缓冲区。 | | 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包含page块的锁和派生的子进程数(如果是一个关系扩展请求,arg1会是-1)。 arg2,arg3和arg4包含表空间,数据库和关系OID,以识别关系。 arg5是为局部缓冲创建临时关系时后端ID,或者共享缓冲区InvalidBackendId (-1)。 arg6对关系扩展请求表示真,对正常读表示假。 | | buffer-read-done | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, bool, bool) | 当完成一次缓冲区读时触发的探测器。 arg0和arg1包含page块的锁和派生的子进程数 (如果是一个关系扩展请求,arg1会表示新增锁的数目)。 arg2,arg3和arg4包含表空间,数据库和关系OID,以识别关系。 arg5是为局部缓冲创建临时关系时后端ID,或者共享缓冲区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](#calibre_link-1370)太小, 或需要调整bgwriter控制参数。 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](#calibre_link-1991)设置的太小了。 | | wal-buffer-write-dirty-done | () | 当完成一次脏WAL写时触发的探测器。 | | xlog-insert | (unsigned char, unsigned char) | 当插入一条WAL记录时触发的探测器。 arg0表示记录的rm id。 arg1包含信息标志。 | | xlog-switch | () | 当要求进行WAL切换时触发的探测器。 | | smgr-md-read-start | (ForkNumber, BlockNumber, Oid, Oid, Oid, int) | 开始从一个关系中读取锁时触发的探测器。 arg0和arg1包含page块的锁和派生的子进程数。 arg2,arg3和arg4包含表空间,数据库和关系OID, 以识别关系。arg5是为局部缓冲创建临时关系时的后端ID,或者共享缓冲InvalidBackendId (-1) | | smgr-md-read-done | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, int, int) | 当一个锁读取完成时触发的探测器。 arg0和arg1包含page块的锁和派生的子进程数。 arg2,arg3和arg4包含表空间, 数据库和关系OID,以识别关系。 arg5是为局部缓冲创建临时关系时的后端ID,或者共享缓冲InvalidBackendId (-1), 而arg6表示实际读取的字节数,而arg7是要求数(如果不一样会报错) | | smgr-md-write-start | (ForkNumber, BlockNumber, Oid, Oid, Oid, int) | 当向一个关系中写入锁时触发的探测器。 arg0和arg1包含page块的锁和派生的子进程数。 arg2,arg3和arg4包含表空间,数据库和关系OID,以识别关系。 arg5是为局部缓冲创建临时关系时的后端ID,或者共享缓冲InvalidBackendId (-1)。 | | smgr-md-write-done | (ForkNumber, BlockNumber, Oid, Oid, Oid, int, int, int) | 当一个锁写进程完成时触发的探测器。 arg0和arg1表示page块的锁和派生的子进程数。 arg2,arg3和arg4包含表空间,数据库和关系OID,以识别关系。 arg5表示为局部缓冲创建临时关系时的后端ID,或者共享缓冲InvalidBackendId (-1), 而arg6表示实际读取的字节数,而arg7是要求数(如果不一样会报错)。 | | sort-start | (int, bool, int, int, bool) | 排序操作开始时触发的探测器。 arg0表示堆,索引或者基准点。 arg1对强制唯一值表示真。arg2表示键列的数目。 arg3表示允许使用的内存数目(以千字节为单位)。 如果要求随机访问排序结果,那么arg4表示真。 | | sort-done | (bool, long) | 排序操作结束时触发的探测器。 arg0对外部排序表示真,内部排序表示假。 arg1表示用于一个外部排序的磁盘锁的数目, 或用于一个内部排序的,以千字节为单位的内存数目。 | | lwlock-acquire | (LWLockId, LWLockMode) | 当成功获得一个LWLock时触发的探测器。 arg0是LWLock的ID号,arg1表明请求的锁模式,要么独占要么共享 | | lwlock-release | (LWLockId) | LWLock释放时触发的探测器 (但是请注意任何发布的等待者还未觉醒)。 arg0表示LWLock的ID号 | | lwlock-wait-start | (LWLockId, LWLockMode) | 当不能立即获得LWLock锁,同时服务进程进入等待时触发的探测器。 arg0是LWLock的ID号,arg1表明请求的锁模式,要么独占要么共享。 | | lwlock-wait-done | (LWLockId, LWLockMode) | 当从一个LWLock锁中释放服务进程时触发的探测器(实际上没有进行锁)。 arg0是LWLock的ID号,arg1表明请求的锁模式,要么独占要么共享。 | | lwlock-condacquire | (LWLockId, LWLockMode) | 当成功获得一个LWLock时触发的探测器(已声明调用无需等待)。 arg0是LWLock的ID号,arg1表明请求的锁的模式,要么独占要么共享。 | | lwlock-condacquire-fail | (LWLockId, LWLockMode) | 当没有成功获得一个LWLock时触发的探测器(已声明调用无需等待)。 arg0是LWLock的ID号,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 | () | 当死锁探测器发现死锁时触发的探测器 | **Table 27-16\. 定义用于探测器参数的类型** | 类型 | 定义 | | --- | --- | | LocalTransactionId | unsigned int | | LWLockId | int | | LWLockMode | int | | LOCKMODE | int | | BlockNumber | unsigned int | | Oid | unsigned int | | ForkNumber | int | | bool | char | ## 27.4.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 ``` > **Note:** SystemTap为跟踪脚本使用一个不同的标记而不是Dtrace, 即使底层的跟踪点是兼容的。有一点需要注意, 在这样写的时候,SystemTap脚本必须使用双下划线代替连字符来指向探测器名。 希望在未来SystemTap的版本中修复。 你应该记住DTrace脚本需要仔细的编写和充分的调试, 否则收集到的跟踪信息可能毫无意义。 大多数情况下问题是手段是错误的而不是底层系统。 在讨论使用动态跟踪发现的信息时,应确保包含允许检查和讨论使用的脚本。 更多的示例脚本可以在PgFoundry [dtrace project](http://pgfoundry.org/projects/dtrace/) 中找到。 ## 27.4.4\. 定义新的跟踪点 开发者可以在代码中任意位置定义新的跟踪点, 当然这要重新编译之后才能生效。下面是用于新探测器插入步骤: 1. 通过探头决定探头名字和可利用数据。 2. 新增探头定义为`src/backend/utils/probes.d` 3. 包括`pg_trace.h`,如果已经不在模块中包含探测点, 并且在所需源代码中期望的位置插入`TRACE_POSTGRESQL`探测宏。 4. 重新编译和验证新探头是可用的。 **例子:** 下面是一个例子,你将如何添加一个探头通过事务ID追踪所有新的事务。 1. 决定探测器将被命名为`transaction-start`并且需要LocalTransactionId类型参数。 2. 新增探头定义为`src/backend/utils/probes.d`: ``` probe transaction__start(LocalTransactionId); ``` 注意探测器名字中的双下划线的使用。在使用探测器的DTrace脚本中, 需要用一个连字符来替换双下划线, 因此,对用户而言,`transaction-start`是文档名。 3. 在编译时,`transaction__start`被转换成一个宏调用 `TRACE_POSTGRESQL_TRANSACTION_START`(注意这里是单下划线), 可以从`pg_trace.h`中获得。将宏调用放在源代码中的合适位置。 在这种情况下,类似于下面: ``` TRACE_POSTGRESQL_TRANSACTION_START(vxid.localTransactionId); ``` 4. 在重新编译和运行新的二进制文件之后, 通过运行下面的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`宏。