PG 中的 statistics collector

Posted by w@hidva.com on November 22, 2019

本文章内容根据 PG9.6 代码而来.

PG 中的 statistics collector 负责收集, 保存, 持久化 PG 运行中产生的各种 metric 信息, 如表的增, 删, 改行数等. statistics collector 收集到的所有信息都保存在内存中, 存放在 pgStatDBHash 指向的哈希表中. 该哈希表的结构等同于 std::unordered_map<Oid, PgStat_StatDBEntry>, 此时 key 为 db oid, 取值为 0 表明对应 PgStat_StatDBEntry 中存放着 global object 相关统计信息. 在 PgStat_StatDBEntry 内部, PgStat_StatDBEntry::tables 以哈希表的形式存放着该库内所有表的统计信息, 其结构等同于 std::unordered_map<Oid, PgStat_StatTabEntry>, 其中 key 为 table oid. PgStat_StatDBEntry::functions 以哈希表的形式存放着该库内所有函数的使用信息, 其结构等同于 std::unordered_map<Oid, PgStat_StatFuncEntry>, 其中 key 为 function oid. 在 stat collector 进程关闭时, 会将内存中的统计信息通过 pgstat_write_statsfiles(true, true); 函数调用持久化到磁盘文件中, 统计信息在磁盘上的目录布局大概为:

${DataDir}
└── pg_stat
    ├── db_0.stat
    ├── db_333.stat
    └── global.stat

其中 db_${DbOid}.stat 存放着每个 db 对应 PgStat_StatDBEntry 内字段 tables,functions 的信息; global.stat 存放着其他所有信息. 在 stat collector 启动时, 也会从上述文件中读取之前持久化的统计信息到内存中.

statistics collector 自身同时也是一个 udp server, 监听着一个特定端口. postmaster 会在启动时调用 pgstat_init() 来完成 stat collector 初始化工作, 在合适时通过调用 pgstat_start() 来启动 stat collector 子进程. 在 PG 运行中, backend 会在适当时候将自身收集到的 metric 打包成 udp message 并发送给 stat collector. 下面以 PgStat_StatTabEntry 中信息的收集为例演示一下此过程. 如下图所示.

pgstats

其中 pgStatTabList 指针指向的结构等同于 std::list<PgStat_TableStatus>, 每个 backend 都会将自身收集到表级别的 metric 存放在该数组相应 PgStat_TableStatus 中, pgStatTabList 数组容量只增不减, 因此其内元素的地址总是有效. PgStat_TableStatus::trans 指针指向的结构等同于 std::vector<PgStat_TableXactStatus>, 其内通过 PgStat_TableXactStatus 存放着当前表在每个事务级别内的统计信息. PgStat_SubXactStatus 结构存放着一个特定事务级别内所有 PgStat_TableXactStatus 结构, 存放着该事务级别内发生的所有统计信息. pgStatXactStack 总是指向着当前事务级别对应的 PgStat_SubXactStatus 结构.

当在某个事务级别内 backend 在打开一个 relation 时, 会调用 pgstat_initstats() 从 pgStatTabList 指向的数组中选择一个 PgStat_TableStatus 元素赋值给 RelationData::pgstat_info. 之后在执行 IUD(Insert/Update/Delete) 操作时, backend 会调用 pgstat_count_heap_xxxx() 函数来更新当前事务级别上特定表对应的 PgStat_TableXactStatus 结构. 每当一个子事务 commit/rollback 时都会调用 AtEOSubXact_PgStat() 函数来将本级别事务内所有 PgStat_TableXactStatus 统计信息合并到父事务中. 在 PG 中, 子事务的 commit/rollback 总是自底向上(子事务 -> 父事务方向)一级一级进行的, 所以 AtEOSubXact_PgStat 调用序列中, 参数 nestDepth 的值是递减的. 在顶层事务 commit/rollback 时会调用 AtEOXact_PgStat() 函数将 PgStat_TableXactStatus 的统计信息合并到 PgStat_TableStatus.t_counts 中. 最后每当 backend 进入 idle 状态(或者退出)时, 通过调用 pgstat_report_stat() 函数将 pgStatTabList 中所有具有有效信息的 PgStat_TableStatus 打包成 PGSTAT_MTYPE_TABSTAT 类型 udp message 发送给 stat collector, 之后将 pgStatTabList 中统计信息清零.

statistics collector 只负责消息的收集与保存, 未提供查询接口. 而是通过将统计信息持久化到某个特定位置的文件中, 之后 backend 通过读取该文件来获取统计信息. 具体一点是, 在 backend 需要统计信息, 其会调用 backend_read_statsfile() 函数, 该函数内会检查持久化文件是否存在以及是否过老, 若是则通过一条 PGSTAT_MTYPE_INQUIRY message 通知 statistics collector 刷新持久化文件, 并等待 statistics collector 刷新完成后读取文件. 若持久化文件已经存在且较新, 则会直接读取持久化文件. 在 backend 一次事务内, 只会读取持久化文件一次, 从而确保整个事务看到的统计信息是一致的. 不过在事务内, backend 可以通过主动调用 pgstat_clear_snapshot() 来使 backend 内存中的统计信息失效, 从而再下次 backend_read_statsfile() 调用时, 会再次读取持久化文件来刷新统计信息.

GP 中的 statistics collector, 目前来看 GP 未对 stat collector 进行过任何分布式化改造, 所以 master 的 stat collector 收到的统计信息全部来源于 master backend 自身, 并不包含 segment 上信息.