数据库内核月报 - 2019 / 07

PgSQL · 最佳实践 · pg_cron 内核分析及用法简介

pg_cronPostgreSQL(9.5或更高版本)的一个简单的基于cron的作业调度程序,它作为扩展在数据库中运行。 它与常规 cron 保持相同的语法,但它允许直接从数据库安排 PostgreSQL 命令。作为一个独立运行的工作者进程,其生命周期管理、内存空间都依赖于 postgreSQL 。本文主要从启动、生命周期、状态机、用法介绍该插件在 postgresQL 数据库中的应用。

postgresql 后台的主进程是 postmaster, 在其启动时,会调用一个函数process_shared_preload_libraries(); 这个函数被 postgresql 用来预加载外部插件。该函数会遍历参数列表,然后对列表参数中的插件依次进行加载。对于插件来说加载的过程包括了除了检查环境 和 注册主函数,主函数由插件中的 _PG_init() 完成注册,这个函数从外部环境中 加载进来,被postmaster执行(类似 Python 的反射)。

PG_init = (PG_init_t) pg_dlsym(file_scanner->handle, "_PG_init");
if (PG_init)
    (*PG_init) ();

对于 pg_cron 插件来说,插件的 _PG_init 函数对于主函数进行了注册:将 PgCronWorkerMain配置为一个后台 worker 并且注册到列表中。到这里系统回到了 postmaster 进程中继续执行任务,直到执行到 maybe_start_bgworkers() 函数,尝试将 workerlist 列表中的worker启动。(这个执行的过程还与数据库的模式有关,处于 standby mode 状态下的数据库不会去启动 pg_cron) postmaster 会分配一个 background workpg_cron , 之后pg_cron 进程独立运行。

pg_cron 生命周期

pg_cron 插件的主体是围绕 PG_CRON_TASK 进行,从内部来说, PG_CRON_TASK 有自己的生命周期,其生命周期的轮转过程就是插件的运行过程,从外部来说 PG_CRON_TASKPG_CRON_JOB 通信取得当前的任务列表,在运行状态与 POSTMASTER 通信 完成定时任务的运行。

pic

图 1 pg_cron_task 示意图

pg_cron 生命周期中涉及到的主要涉及环境信息 如 图2 所示,主要分为三类:即状态检查信息、标志位返回信息 和 错误返回信息 。而pg_cron 的生命周期的状态转移就由这些信息控制。状态转移状态一般来说也分为三类:状态等待中,正常执行进入下一个状态,错误信息返回。进程从 CRON_TASK_WAITING 开始,依次进入每一个对应的状态,最后流转到 CRON_TASK_DONECRON_TASK_ERROR 中的一个。 最后这些状态信息被重置,pg_cron 进入下一个生命周期。

pic

图 2 pg_cron_task 生命周期

pg_cron 通信状态机

pg_cron 是单进程单线程运行插件,由 postmaster 进程启动, 属于其子进程。在执行多任务的进程中,采用了 生命周期流转 和 多路 IO复用来 模拟了多任务的并行处理,下面介绍该机制。

pg_cron 模拟了一个生命周期队列(如图2所示)来维护所有的任务状态,每个任务都由以下状态组成:等待、开始、连接、发送、运行、接收、完成、错误。其中,大部分状态被认为不会IO阻塞(例如等待状态、开始状态),但是有一些进程可能会由于 socket 阻塞 (例如 连接、发送、运行 )。pg_cron 使用了poll 函数 完成 IO复用。该过程如图3 所示,由于可能的网络时延、任务执行时间不确定,这些任务的并发状况是未知的,而poll函数便是遍历文件句柄, 接收到 IO 数据后会更新文件句柄,唤醒进程进行 IO ,从而避免了 IO 阻塞。当IO结束后 任务接受一个标志位 任务完成或 任务失败,结束本次生命周期。

pic

图3 pg_cron_task 多路IO复用模型

pg_cron 用法手册

每一个定时任务分为两部分: 定时计划 和 定时任务。定时计划规定了用户 使用 插件的计划(例如:每隔1分钟执行一次该任务),定时任务是用户具体的任务内容(例如:select * from some_table

普通用户一共有三个可选函数:增加任务项、删除任务项、查看当前任务项。

  • 增加任务项
-- 周六3:30am (GMT) 删除过期数据 
SELECT cron.schedule('30 3 * * 6', $$DELETE FROM events WHERE event_time < now() - interval '1 week'$$);
 schedule
----------
       42

-- 每天的 10:00am (GMT) 执行磁盘清理
SELECT cron.schedule('0 10 * * *', 'VACUUM');
 schedule
----------
       43
 
-- 每分钟执行 指定脚本
SELECT cron.schedule('* * * * *', 'select 1;');
 schedule
----------
       44

-- 每个小时的 23分 执行 指定脚本
SELECT cron.schedule('23 * * * *', 'select 1;');
 schedule
----------
       45
       
-- 每个月的 4号 执行 指定脚本
SELECT cron.schedule('* * 4 * *', 'select 1;');
 schedule
----------
       46

pg_cron 计划使用标准的 cron 语法,其中 * 表示“每个该时间运行”,特定数字表示“仅在 这个数字时 运行”

 ┌───────────── 分钟 (0 - 59)
 │ ┌────────────── 小时 (0 - 23)
 │ │ ┌─────────────── 日期 (1 - 31)
 │ │ │ ┌──────────────── 月份 (1 - 12)
 │ │ │ │ ┌───────────────── 一周中的某一天 (0 - 6) (0 到 6 表示周末到下周六,
 │ │ │ │ │                   7 仍然是周末)
 │ │ │ │ │
 │ │ │ │ │
 * * * * *
  • 删除任务项
-- 停止、删除一个任务
SELECT cron.unschedule(42);
 unschedule
------------
          t
  • 查看当前任务
SELECT * FROM cron.job;

 jobid | schedule   |  command  | nodename  | nodeport | database | username | active 
-------+------------+-----------+-----------+----------+----------+----------+--------
    43 | 0 10 * * * |   VACUUM; | localhost |     5433 | postgres | test     | t