一、 函数代码
CheckpointerMain函数是checkpointer进程的主入口函数。
第一部分主要是些准备工作:
- 初始化变量
- 定义、重置一些信号量,包括前面提到的SIGINT
- 创建专用内存上下文 checkpointer_context
- 一些异常判断和处理
/*
* Main entry point for checkpointer process,checkpointer进程的主入口函数
* 由AuxiliaryProcessMain函数调用,其实早已经创建,只是尚未启用。
*/
void
CheckpointerMain(void)
{
sigjmp_buf local_sigjmp_buf; // 主要是跟异常处理有关的,不是我们想看的重点,略过
MemoryContext checkpointer_context; // 内存上下文
CheckpointerShmem->checkpointer_pid = MyProcPid; // 检查点进程pid
/*
* Properly accept or ignore signals the postmaster might send us
* 自定义 pg接收或忽略的信号量
*/
pqsignal(SIGHUP, SignalHandlerForConfigReload); /* reload配置文件 */
pqsignal(SIGINT, ReqCheckpointHandler); /* request checkpoint,这个就是前文提到的SIGINT,用于请求检查点 */
pqsignal(SIGTERM, SIG_IGN); /* ignore SIGTERM,SIG_IGN就是SIG ignore */
/* SIGQUIT handler was already set up by InitPostmasterChild */
pqsignal(SIGALRM, SIG_IGN);
pqsignal(SIGPIPE, SIG_IGN);
pqsignal(SIGUSR1, procsignal_sigusr1_handler);
pqsignal(SIGUSR2, SignalHandlerForShutdownRequest);
/*
* Reset some signals that are accepted by postmaster but not here,重置一些信号量
*/
pqsignal(SIGCHLD, SIG_DFL);
/*
* Initialize so that first time-driven event happens at the correct time.初始化上次检查点执行时间点、及上次xlog切换的时间点为当前时间(检查点开始时间)
*/
last_checkpoint_time = last_xlog_switch_time = (pg_time_t) time(NULL);
/*
创建一块专用的内存上下文,我们的工作都会在其中完成。创建的原因是在遇到recovery报错时我们可以reset上下文,避免可能的内存泄漏问题。以前这个工作都是直接在TopMemoryContext中完成,而重置TopMemoryContext真是一个坏主意。
*/
checkpointer_context = AllocSetContextCreate(TopMemoryContext,
"Checkpointer",
ALLOCSET_DEFAULT_SIZES);
MemoryContextSwitchTo(checkpointer_context);
//下面这段主要是跟异常处理有关的,不是我们想看的重点,略过
/*
* If an exception is encountered, processing resumes here.
*/
if (sigsetjmp(local_sigjmp_buf, 1) != 0)
……
下面这段很重要
Checkpointer进程的主流程是一个无条件的for循环,在未触发checkpoint时一直在WaitLatch中sleep,也就是在epoll_wait中观察list链表,查看是否有事件句柄已经就绪(某个条件触发checkpoint)。如果已经存在就绪事件,则通过SetLatch中write pipe的方式唤醒,执行checkpoint。
/*
* Loop forever,以下内容全都在这个大循环里
*/
for (;;)
{
bool do_checkpoint = false; //是否执行checkpoint
int flags = 0;
pg_time_t now;
int elapsed_secs; // 自从上次检查点已过去多长时间
int cur_timeout; // 超时时间倒计时(Latch开始等待的时间距离上次检查点超时的剩余时间)
// 开头这段对理解主要流程用处不大,略
// 下面这段很重要,判断自从上次检查点后是否已超时,如果超时则需要创建新检查点
now = (pg_time_t) time(NULL);
elapsed_secs = now - last_checkpoint_time; // 自从上次检查点已过去的时间
if (elapsed_secs >= CheckPointTimeout) //如果elapsed_secs超过了检查点超时时间
{
if (!do_checkpoint) // 如果没有请求做checkpoint
BgWriterStats.m_timed_checkpoints++; // ??
do_checkpoint = true; // 设置标记位
flags |= CHECKPOINT_CAUSE_TIME;
}
/*
* Do a checkpoint if requested.如果收到请求,执行checkpoint
*/
if (do_checkpoint)
{
bool ckpt_performed = false; // 标记检查点是否创建成功的标记位
bool do_restartpoint; // 判断是不是恢复阶段的检查点
/*
* 检查我们是否需要执行checkpoint或restartpoint。可能的影响是,如还未设置TimeLineID,那么RecoveryInProgress()会初始化TimeLineID
*/
do_restartpoint = RecoveryInProgress();
/*
* 加锁,修改CheckpointerShmemStruct
结构体内容。获取标记位,以确认需要执行哪种checkpoint;同时增加ckpt_started计数器,以确认已启动了新的checkpoint
*/
SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
flags |= CheckpointerShmem->ckpt_flags;
CheckpointerShmem->ckpt_flags = 0;
CheckpointerShmem->ckpt_started++;
SpinLockRelease(&CheckpointerShmem->ckpt_lck);
ConditionVariableBroadcast(&CheckpointerShmem->start_cv);
/*
* The end-of-recovery checkpoint is a real checkpoint that's
* performed while we're still in recovery.
*/
if (flags & CHECKPOINT_END_OF_RECOVERY)
do_restartpoint = false;
/*
* 如果本次检查点时间离上次太近,或者在上次检查点启动后某个进程设置了CHECKPOINT_CAUSE_XLOG标志,会产生告警。但注意如果CheckPointTimeout < CheckPointWarning,则不会产生告警。
*/
if (!do_restartpoint &&
(flags & CHECKPOINT_CAUSE_XLOG) &&
elapsed_secs < CheckPointWarning)
ereport(LOG,
(errmsg_plural("checkpoints are occurring too frequently (%d second apart)",
"checkpoints are occurring too frequently (%d seconds apart)",
elapsed_secs,
elapsed_secs),
errhint("Consider increasing the configuration parameter \"max_wal_size\".")));
/*
* 初始化在创建检查点期间需要用到的私有变量
*/
ckpt_active = true;
if (do_restartpoint) // restartpoint类型
ckpt_start_recptr = GetXLogReplayRecPtr(NULL); // 获取XLog恢复位置
else // 普通checkpoint
ckpt_start_recptr = GetInsertRecPtr();// 获取XLog Record插入位置
ckpt_start_time = now; // checkpoint开始时间
ckpt_cached_elapsed = 0; // 自上次checkpoint已过多长时间
/*
* Do the checkpoint.
*/
if (!do_restartpoint) // 普通checkpoint
{
CreateCheckPoint(flags);
ckpt_performed = true;
}
else // restartpoint类型
ckpt_performed = CreateRestartPoint(flags);
/*
* 在完成检查点后,关闭smgr(存储介质管理器)
*/
smgrcloseall();
/*
* 告诉其他在等待的进程,本次检查点执行完毕
*/
SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
CheckpointerShmem->ckpt_done = CheckpointerShmem->ckpt_started;
SpinLockRelease(&CheckpointerShmem->ckpt_lck);
ConditionVariableBroadcast(&CheckpointerShmem->done_cv);
if (ckpt_performed) //检查点创建成功
{
/*
* 还记得在函数的最开始我们把last_checkpoint_time设置成了检查点开始时间而不是结束时间,因此这里我们要修正一下,改为当前时间(检查点结束时间)
*/
last_checkpoint_time = now;
}
else //检查点创建失败
{
/*
* 如果这个值为false,说明restartpoint没有创建成功(可能是遇到报错)。最有可能是因为我们没有收到自上次restartpoint以来的WAL记录,在15秒后进行重试
*/
last_checkpoint_time = now - CheckPointTimeout + 15; //赋值为15秒后
}
ckpt_active = false;
}
/* 检查是否超过 archive_timeout,是否需要切换xlog日志 */
CheckArchiveTimeout();
/*
* 发送活动统计信息到统计信息收集器(这里复用bgwriter-related代码的原因是bgwriter 和 checkpointer以前就是一个进程,不值得将这部分代码拆分成两个独立的统计信息类型)
*/
pgstat_send_bgwriter();
/* 发送WAL统计信息到统计信息收集器 */
pgstat_send_wal(true);
/*
* 如果设置了任何检查点标记位,重新进行下一次循环去处理检查点而不是sleeping
*/
if (((volatile CheckpointerShmemStruct *) CheckpointerShmem)->ckpt_flags)
continue;
/*
* 否则,sleep直到收到对应信号量、或者到达超时时间、或者xlog切换,再进行下一次检查点创建
*/
now = (pg_time_t) time(NULL);
elapsed_secs = now - last_checkpoint_time;
if (elapsed_secs >= CheckPointTimeout) /* 到达超时时间 */
continue; /* no sleep for us ...进入下一次循环,创建检查点 */
cur_timeout = CheckPointTimeout - elapsed_secs; /* 如果没超时,记录剩余时间 */
if (XLogArchiveTimeout > 0 && !RecoveryInProgress())/* 根据归档日志的超时参数判断是否需要创建新检查点 */
{
elapsed_secs = now - last_xlog_switch_time;
if (elapsed_secs >= XLogArchiveTimeout) /* 到达日志切换超时时间 */
continue; /* no sleep for us ... */
cur_timeout = Min(cur_timeout, XLogArchiveTimeout - elapsed_secs);
}
/* 进行sleep的Latch */
(void) WaitLatch(MyLatch,
WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
cur_timeout * 1000L /* convert to ms */ ,
WAIT_EVENT_CHECKPOINTER_MAIN); /* 休眠… */
}
二、 代码调试
1. 调试方法
① vscode 设置断点函数 CheckpointerMain
② 跟踪checkpointer进程
ps -ef|grep checkpointer
vscode跟踪5513进程
③ psql执行checkpoint命令
刚进来会在休眠函数位置
④ 找到for循环部分再打一个断点,这部分是函数的重点
⑤ 点击继续按钮,会直接跳到这步,避免在其他函数里一直打转。
2. 调试过程
判断是不是恢复阶段的检查点
加锁,修改CheckpointerShmemStruct结构体内容。获取标记位,以确认需要执行哪种checkpoint;同时增加ckpt_started计数器,以确认已启动了新的checkpoint
点击CheckpointerShmem结构体可以看到其中变量的值,例如ckpt_started:12。
如果本次检查点时间离上次太近,或者在上次检查点启动后某个进程设置了CHECKPOINT_CAUSE_XLOG标志,会产生告警。这里CheckPointWarning的值为30,所以不符合这个if条件。
初始化在创建检查点期间需要用到的私有变量,包括checkpoint开始位置、开始时间、自上次checkpoint已过多长时间(这里因为刚开始,所以是0)。
判断restartpoint类型,若为恢复检查点,则checkpoint开始位置设置为XLog恢复位置;若为普通检查点,则设为XLog Record插入位置。
若为普通检查点,则通过下一节要介绍的核心函数CreateCheckPoint创建;若为恢复检查点,则通过CreateRestartPoint函数创建。
ckpt_done加一,表示当前检查点执行完成。
判断检查点是否创建成功:如果成功,则将last_checkpoint_time设置为当前时间(检查点结束时间);如果失败,等待15秒后重试。
然后,将ckpt_active设置为false。
- 检查是否超过 archive_timeout,是否需要切换xlog日志
- 发送活动统计信息到统计信息收集器
- 发送WAL统计信息到统计信息收集器
如果设置了任何检查点标记位,重新进行下一次循环去处理检查点而不是sleep。
否则,sleep直到收到对应信号量、或者到达超时时间、或者xlog切换,再进行下一次检查点创建。
参考
postgresql checkpoint原理浅析-杨向博.pdf - 墨天轮文档
http://blog.itpub.net/6906/viewspace-2374762/