检查点进程的主入口函数是CheckpointerMain,其中CreateCheckPoint这个函数是创建检查点最主要的函数。这两个函数都很复杂,我们留到后面再看。
本篇先来看一个相对简单的RequestCheckpoint函数。在手动执行checkpoint命令时,调用的就是RequestCheckpoint函数。
一、 RequestCheckpoint函数
首先判断是否运行在单用户模式下:
- 如果是,则直接调用CreateCheckPoint函数
- 不是,则获取自旋锁,保存 CheckpointerShmem中 ckpt_failed,ckpt_started 计数器的当前值(类似于打快照),修改标记位,然后释放自旋锁
/*
* RequestCheckpoint Called in backend processes to request a checkpoint
*/
void
RequestCheckpoint(int flags)
{
int ntries;
int old_failed,
old_started;
/*
* 如果是单用户模式运行,直接调用CreateCheckPoint函数创建检查点
*/
if (!IsPostmasterEnvironment)
{
CreateCheckPoint(flags | CHECKPOINT_IMMEDIATE);
/*
* 创建检查点后,关闭PG存储介质管理器(smgr)
*/
smgrcloseall();
return;
}
// 非单用户模式运行
/*
* 原子级地设置请求标记并给计数器打快照,当看到ckpt_started > old_started时,我们知道设置在这里的标签已经被checkpointer进程看到了。
*/
// 拿自旋锁,以下为原子级操作
SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
// 上次checkpoint的失败信息
old_failed = CheckpointerShmem->ckpt_failed;
// 上次checkpoint的开始位置
old_started = CheckpointerShmem->ckpt_started;
// 设置checkpoint标记位
CheckpointerShmem->ckpt_flags |= (flags | CHECKPOINT_REQUESTED);
// 释放自旋锁
SpinLockRelease(&CheckpointerShmem->ckpt_lck);
判断检查点进程是否启动。此时检查点进程可能后面启动,也可能正在重启,因此如果有需要的话我们会重试几次(最多重试600次)。在检查点进程启动时,它将会看到这个请求(无论有没有获得信号)。
如果检查点进程启动了,通过kill函数发送SIGINT信号,判断进程能不能接收。
SIGINT信号的定义在主函数CheckpointerMain,内容为 pqsignal(SIGINT,ReqCheckpointHandler);
这里SIGINT信号对应的处理函数是ReqCheckpointHandler函数,它通过SetLatch函数唤醒当前检查点进程(检查点进程正在等待触发消息),并设置checkpoint_requested标记为true,表明本次检查点由RequestCheckpoint函数触发。
#define MAX_SIGNAL_TRIES 600 /* max wait 60.0 sec,600次,每次等0.1秒 */
for (ntries = 0;; ntries++)
{
if (CheckpointerShmem->checkpointer_pid == 0) /* checkpointer进程没启动 */
{
if (ntries >= MAX_SIGNAL_TRIES || !(flags & CHECKPOINT_WAIT)) /* 等待超时 */
{
elog((flags & CHECKPOINT_WAIT) ? ERROR : LOG,
"could not signal for checkpoint: checkpointer is not running");
break;
}
}
/* request checkpoint */
else if (kill(CheckpointerShmem->checkpointer_pid, SIGINT) != 0)
{
if (ntries >= MAX_SIGNAL_TRIES || !(flags & CHECKPOINT_WAIT))
{
elog((flags & CHECKPOINT_WAIT) ? ERROR : LOG,
"could not signal for checkpoint: %m");
break;
}
}
else // 检查点进程已启动,且信号发送成功,退出重试循环,进行下一步
break; /* signal sent successfully */
CHECK_FOR_INTERRUPTS();
pg_usleep(100000L); /* wait 0.1 sec, then retry,等0.1秒,进入下一次循环 */
}
检查点进程已启动,且信号发送成功,则开始等待检查点创建。
下面这段检查的算法都是一样的:获取自旋锁,对比新旧计数器的值。
- new_started != old_started,说明新检查点已经开始,计数器值已经增加,退出循环。
- new_done - new_started >= 0,同理,说明检查点已经完成
- new_failed != old_failed,同理,说明检查点创建失败
/*
* 检查点进程已启动,且信号发送成功,继续后续操作
*/
if (flags & CHECKPOINT_WAIT)
{
int new_started,
new_failed;
/* Wait for a new checkpoint to start.等待新检查点开始 */
ConditionVariablePrepareToSleep(&CheckpointerShmem->start_cv);
for (;;)
{
SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
new_started = CheckpointerShmem->ckpt_started; // 新检查点开始,计数器+1
SpinLockRelease(&CheckpointerShmem->ckpt_lck);
if (new_started != old_started) // 当新旧计数器的值不相等,说明新检查点已经开始,计数器值已经增加,退出循环。如果值相等,则说明新检查点还未开始,继续循环等待。
break;
ConditionVariableSleep(&CheckpointerShmem->start_cv,
WAIT_EVENT_CHECKPOINT_START);
}
ConditionVariableCancelSleep();
/*
* We are waiting for ckpt_done >= new_started. 等待ckpt_done >= new_started,即检查点完成,这个检查算法跟前面是一样的
*/
ConditionVariablePrepareToSleep(&CheckpointerShmem->done_cv);
for (;;)
{
int new_done;
SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
new_done = CheckpointerShmem->ckpt_done;
new_failed = CheckpointerShmem->ckpt_failed;
SpinLockRelease(&CheckpointerShmem->ckpt_lck);
if (new_done - new_started >= 0) //检查点完成,退出循环
break;
ConditionVariableSleep(&CheckpointerShmem->done_cv,
WAIT_EVENT_CHECKPOINT_DONE);
}
ConditionVariableCancelSleep();
if (new_failed != old_failed) //如果fail计数器的值增加了,说明检查点创建失败,记录报错信息
ereport(ERROR,
(errmsg("checkpoint request failed"),
errhint("Consult recent messages in the server log for details.")));
}
}
参考
《PostgreSQL技术内幕:事务处理深度探索》第4章