PostgreSQL autovacuum launcher 进程处理逻辑分析
PostgreSQL autovacuum 模块主要由两类进程组成,分别是 autovacuum launcher 进程和 autovacuum worker 进程。其中 launcher 进程负责 worker 进程的创建,worker 进程是实际干活的进程,对数据库中的表、索引等对象做 vacuum 操作。本文主要介绍 autovacuum launcher 进程的处理逻辑。以下代码分析基于 PG 13.3 版本。
1. autovacuum launcher 进程核心处理逻辑
autovacuum launcher 进程是 PostgreSQL 数据库的辅助进程之一,在数据库启动时就被创建出来。launcher 进程从元数据表 pg_database 中获取数据库信息并放入 DatabaseList 链表,然后在主循环中定时地为需要做 vacuum 操作的数据库创建 worker 进程,并检测 worker 进程是否在规定的时间内创建成功。launcher 进程控制着 worker 进程的总量,在数据库较多的情况下,通过一些策略优先为年龄较大的数据库创建 worker 进程。
autovacuum launcher 进程主函数为 AutoVacLauncherMain,其核心调用堆栈如下:
AutoVacLauncherMain() { // 1. 获取数据库相关的元数据,主要从 pg_database 表中获取 rebuild_database_list() while() { // 2. 计算出 sleep 时间,该时间在 WaitLatch 中使用 launcher_determine_sleep() // 3. 等待被唤醒或者超时被唤醒 WaitLatch() // 4. 处理中断 HandleAutoVacLauncherInterrupts() // 5. 处理 SIGUSR2 信号相关的逻辑 if (got_SIGUSR2) { ... } // 加共享锁 LWLockAcquire(AutovacuumLock, LW_SHARED); // 6. 如果没有空闲的 worker,则不能启动新的 autovacuum worker 进程 can_launch = !dlist_is_empty(&AutoVacuumShmem->av_freeWorkers); // 7. 检查当前正在启动的 worker 进程是否超时 if (AutoVacuumShmem->av_startingWorker != NULL) { if (正在启动的 autovacuum worker 进程是否启动超时?) { LWLockRelease(AutovacuumLock); // 释放锁 LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE); // 加排他锁 // 如果仍然有正在启动的 autovacuum worker 进程 if (AutoVacuumShmem->av_startingWorker != NULL) { // 将该 worker 放入空闲链表,设置正在启动的进程为 NULL AutoVacuumShmem->av_startingWorker = NULL // 记录告警信息:worker took too long to start; canceled } } else can_launch = false; // 不能启动新的 autovacuum 进程 } LWLockRelease(AutovacuumLock); // 释放锁 // 8. 如果不能启动新的 autovacuum worker 进程,则直接进入下一轮循环 if (!can_launch) continue; // 9.下面是启动 autovacuum worker 的逻辑 if (dlist_is_empty(&DatabaseList)) { // 10. 如果数据库列表为空,直接启动一个 autovacuum worker 进程 launch_worker(current_time); } else { // 11. 从 DatabaseList 末尾取出一个元素,并判断其下一次启动时间是否超过当前时间 if (该元素的下一次启动时间超过当前时间?) { // 12. 启动一个新的 autovacuum worker 进程 launch_worker(current_time); } } } // 13. 进程退出 AutoVacLauncherShutdown() }
2. autovacuum launcher 进程处理细节
2.1 rebuild_database_list() 函数
rebuild_database_list() 函数的核心逻辑是查询 pg_database 表,然后将该表中记录放入 DatabaseList 链表中。
详细逻辑如下:
- 先检查上一次读统计数据的时间,如果超过 STATS_READ_DELAY 值,则清除缓存的统计数据。见函数:autovac_refresh_stats()
- 创建哈希表 dbhash
- 如果指定了参数 newdb,并且 newdb 对应的统计数据不为 NULL,则将 newdb 放入哈希表 dbhash
- 遍历已有的 DatabaseList 链表,如果链表中的 db 对应的统计数据不为 NULL,并且 dbhash 表中没有该 db,则将 db 放入哈希表 dbhash
- 查询 pg_database 元数据表中的所有记录,如果 db 对应的统计数据不为 NULL,并且 db 不在 dbhash 中,则将 db 放入哈希表 dbhash
- 清空 DatabaseList,遍历 dbhash 中的元素放入数组,对数据中的元素按 adl_score 进行倒序排序,为数组中的每个元素设置 adl_next_worker 值,即下一次启动的时间,将元素放入 DatabaseList 链表。
2.2 主循环的 sleep 时间
launcher_determine_sleep() 函数用于计算主循环中的 sleep 时间,其逻辑如下:
- 如果没有可用的空闲 worker,即 AutoVacuumShmem->av_freeWorkers 为空,那么 sleep 时间为 autovacuum_naptime 参数指定的时间。
- 如果 DatabaseList 为空,那么 sleep 时间为 autovacuum_naptime 参数指定的时间。
- 根据 DatabaseList 最后一个元素的 adl_next_worker 值计算 sleep 时间
- 根据上述计算出的结果,再限制一下最小时间和最大时间,最小 sleep 时间不能小于 100ms,最大 sleep 时间不能大于 300s。
2.3 launch_worker() 函数,启动 autovacuum worker 进程
launch_worker() 函数用于启动 autovacuum worker 进程,主要调用函数 do_start_worker() 实现,该函数如果能够启动 autovacuum worker 进程,则返回该 worker 进程对应的数据库 oid,即 dbid,如果 dbid 在 DatabaseList 链表中,则将其下一次的启动时间 adl_next_worker 增加 autovacuum_naptime * 1000,并将该元素放到链表头部。如果 dbid 不在链表中,则重建 DatabaseList 链表。如下所示:
launch_worker(TimestampTz now) { dbid = do_start_worker(); // 启动 autovacuum worker 进程 if (dbid 是有效的) { dlist_foreach(iter, &DatabaseList) { 如果在 DatabaseList 中找到了 dbid 对应的元素, 设置其 avdb->adl_next_worker = now + autovacuum_naptime * 1000 并且将其放到 DatabaseList 的头部 } if (如果在 DatabaseList 中没有找到 dbid 对应的元素) { rebuild_database_list(dbid); // 重建 DatabaseList 链表 } } }
do_start_worker() 函数以数据库作为基本单位来启动 autovacuum worker 进程,但可能因为条件不满足而不启动 autovacuum worker 进程,比如没有空闲的 worker 进程可用(超过参数 autovacuum_max_workers 限制的最大值),或者数据库的年龄不大并且不久前刚进行过 vacuum 操作,那么就不会对这个库进行 vacuum 操作。如果有合适的库需要做 vacuum,则启动 autovacuum worker 进程并返回该数据库 dbid。
如果有多个库,优先选择哪个库进行 vacuum 操作也是有固定策略的,如下:
- 第一优先处理 datfrozenxid 年龄超过 autovacuum_freeze_max_age 值的数据库,如果有多个库都超过了该值,则优化处理年龄更大的库。
- 第二优先处理 datminmxid 年龄超过阈值的库,如果有多个库超过阈值,则选取年龄较大的库。
- 如果数据库对应的统计信息为 NULL,否则忽略该数据库。
- 如果数据库在 DatabaseList 链表中,则需要判断该数据库下一次启动时间是否在 now 和 now + autovacuum_naptime * 1000 之间,如果是的话则忽略该数据库。
- 最后根据统计信息中的上一次 vacuum 时间,选择一个最旧的数据库进行 vacuum 操作。
详细的逻辑如下:
do_start_worker() { 1. 如果 AutoVacuumShmem->av_freeWorkers 为空,直接返回 InvalidOid 2. autovac_refresh_stats(); //更新 pg_stat 缓存 3. dblist = get_database_list(); // 获取数据库信息 4. 各种策略选择一个候选的数据库做 vacuum 操作 foreach(cell, dblist) { 4.1 优先 datfrozenxid 年龄超过 autovacuum_freeze_max_age 并且年龄最大的数据库,如果有的话则不考虑其他数据库。 4.2 在 4.1 不满足的情况下,优先处理 datminmxid 年龄超过阈值的最大的数据库,如果有的话则不考虑其他数据库。 4.3 有 4.1 和 4.2 都不满足的情况下,如果数据库的统计信息为 NULL, 则忽略该数据库 4.4 如果数据库在 DatabaseList 链表中,并且其下一次 vacuum 执行时间介于 now 与 now + autovacuum_naptime * 1000 之间,则忽略该数据库 4.5 如果有多个候选数据库,则根据统计信息中上一次 vacuum 时间,从中选择一个时间最早的数据库进行 vacuum 操作。 } 5. 启动 autovacuum worker 进程 if (如果候选数据库不为空) { 5.1 从 AutoVacuumShmem->av_freeWorkers 中取一个空闲 worker 元素,设置相关的值 5.2 向 PostMaster 进程发送信号 PMSIGNAL_START_AUTOVAC_WORKER } else { // 重建 DatabaseList 链表 rebuild_database_list(InvalidOid); } 6. 返回 dbid }
2.4 autovacuum launcher 相关参数
- autovacuum_max_workers,autovacuum worker 进程的最大数量
- autovacuum_naptime,autovacuum 启动 worker 进程间隙需要 sleep 的时间
- autovacuum_freeze_max_age,阻止事务号回卷的最大年龄限制