1.触发机制
WAL 和 Check Point:
WAL 是预写日志是大部分数据库异常恢复的标准手段,在 commit 将数据修改记录到库、表、索引文件之前,先更新内存中 page 页,再将操作记录写入到 WAL 日志中,保证数据库崩溃恢复时内存中还未来得及持久化的 page 缺失的操作记录可以从 WAL 日志中找,另外 WAL 日志也同时作为数据备份、远程复制使用。但是 WAL 日志不可能无限大、且越大恢复时间越长,所以需要 WAL 搭配 Check Point 技术,定时将缓冲区中的 page 脏页持久化到数据库。通常发生在下面几种情况:(1)内存中 page 脏页太多需要进行持久化;(2)内存中 page 缓冲区不够了需要持久化一部分腾出空间来缓存新的脏页;(3)WAL 日志文件过大了在清除一部分 WAL 内容时如果这些数据时 page 脏页需要进行持久化防止恢复时永久丢失。page 持久化后这一部分 WAL 内容也可以清除了。
PostgreSQL 触发 checkpoint 主要是下面几个流程中:
(1)停库(smart者fast模式)
(2)数据库recovery完成
(3)开始备份(pg_start_backup,也包括备份工具例如 pg_basebackup)
(4)手动执行checkpoint命令
(5)生成WAL日志达到一定数量,由几个参数共同决定(参考下面)
(6)超时机制,默认值 checkpoint_timeout = 5min
(7)删除数据库也会触发
(8)wal日志总大小超过参数max_wal_size设置
在 xlog.h 中定义了一些产生 checkpoint 的流程:
#define CHECKPOINT_IS_SHUTDOWN 0x0001 /* Checkpoint is for shutdown */
#define CHECKPOINT_END_OF_RECOVERY 0x0002 /* Like shutdown checkpoint, but issued at end of WAL recovery */
#define CHECKPOINT_IMMEDIATE 0x0004 /* Do it without delays */
#define CHECKPOINT_FORCE 0x0008 /* Force even if no activity */
#define CHECKPOINT_FLUSH_ALL 0x0010 /* Flush all pages, including those belonging to unlogged tables These are important to RequestCheckpoint */
#define CHECKPOINT_WAIT 0x0020 /* Wait for completion */
#define CHECKPOINT_REQUESTED 0x0040 /* Checkpoint request has been made */
#define CHECKPOINT_CAUSE_XLOG 0x0080 /* XLOG consumption */
#define CHECKPOINT_CAUSE_TIME 0x0100 /* Elapsed time */
控制后台 CheckPoint 调度的几个参数:
checkpoint_segments 指定产生多少 xlog 记录后执行 ChcekPoint。
checkpoint_completion_target 新产生的 xlog 除以 checkpoint_segments 得到一个数,如果小于这个配置则执行 CheckPonit。这个思想来源于根据 shard buffer 中脏数据达到一定比例来执行 CheckPoint。
checkpoint_timeout 在系统 for(;;) 中判断两次 CheckPoint 是否超过这个时间,超过了则强制执行一次 CheckPoint 。默认五分钟保证间隔执行。
以上便是checkpoint的调度机制。我们要注意调整上述几个参数时,不要让checkpoint产生过于频繁,否则频繁的fsync操作会是系统不稳定。比如,checkpoint_segments一般设置为16个以上,checkpoint_completion_target设为0.9,checkpoint_timeout为300秒,这样一般checkpoint的间隔能达到1分钟以上。
2.WAL文件、控制文件、LSN、Checkpoint、时间线
WAL、LSN、CheckPoint、控制文件的关系:
WAL 文件顺序记录了提交的数据修改记录,每个操作都有一条 xlog 日志而 LSN 就是这一条条日志的编号,在某一个 LSN 执行 Checkpoint 后,这个 LSN 之前的 WAL 日志所对应的内存的脏数据就持久化了,WAL 在这个 LSN 之前的日志也就不需要了,在执行了 CheckPoint 之后,控制文件会记录这一次执行的 CheckPoint 具体在哪个 WAL 文件的哪一条 xlog 记录上。控制文件主要的作用是记录系统状态,而其中最新的检查点只是其中一项。而出现在这些文件中的TimeLine 时间线字段主要是用于基于时间点恢复,看下面的过程就能很好理解这几个文件的依赖作用以加深源码理解。
另外 WAL(pgData/pg_wal)、Controller(pgData/global/pg_control)两个文件的存储目录不一样。这两个文件都比较重要,控制文件损坏影响的是系统恢复、wal 文件损坏影响的是备份。
首先查看 WAL 日志文件的规则:
00000001 | 00000000 | 000000 | 01
时间线 | wal逻辑ID | 段号
查看当前的在线 WAL 文件(PGDATD/pg_wal下面):
select * from pg_ls_waldir() order by modification asc;
查询结果:
可以看到当前仅一个 WAL 日志,一个 WAL 日志中有多条 xlog 记录,查询 xlog 记录需要使用 pg_waldump 工具:
pg_waldump /home/pgData/pg_wal/000000010000000000000001
查询结果如下:
可以看到每行 xlog 都有个编号也就是 lsn,一些典型的 xlog 记录描述(INSERT、COMMIT、CHECKPOINT_ONLINE)比如记录了一些历史已经执行过的 CheckPoint:
记录如下:
rmgr: XLOG len (rec/tot): 114/ 114, tx: 0, lsn: 0/0188BD38, prev 0/0188BD00, desc: CHECKPOINT_ONLINE redo 0/188BD00; tli 1
而控制文件则记录了最新的 CheckPoint,查询控制文件需要使用 pg_controldata 指令:
查询结果如下,其中和 CheckPoint 关键的几行, 0/188BD00 这个编号,就是 WAL 文件中的一条 xlog 的 LSN。查询:
pg_controldata -D /home/pgData
查询结果:
Latest checkpoint location: 0/188BD38
Latest checkpoint's REDO location: 0/188BD00
Latest checkpoint's REDO WAL file: 000000010000000000000001
其中 checkpoint's REDO location、checkpoint location 两个点的区别在于,前者是重做点用于在崩溃恢复时重做的起点,特性是重做点之前的 xlog 都落盘了不需要在恢复时应用。而后者是执行完 checkPonit 之后记录的 xlog。
这个和检查点创建的流程有关,创建检查点时先记录 redo point,然后将脏页落盘,然后更新控制文件,最后记录 check point。所以看到会有这两个记录。
3.入口函数
调用入口:
PostmasterMain() =>
StartupDataBase() =>
StartChildProcess() =>
AuxiliaryProcessMain() =>
checkpointer.c CheckpointerMain() => 检查点主进程入口函数,checkpoint 也在这里面 for(;;) 执行。
xlog.c GetInsertRecPtr() => 获取当前插入位置 XLogCtl->LogwrtRqst.Write
xlog.c CreateCheckPoint() 或者 xlog.c CreateRestartPoint() =>
xlog.c CheckPointGuts() => 就是底层 SharedMemory 到 Disk 进行 Flush 的入口函数
relmapper.c CheckPointRelationMap() => 将共享内存里系统表和对应物理文件映射的map文件刷到磁盘。
slot.c CheckPointReplicationSlots() => 将 ShardMemory 中所有 Slots 进行 Flush
snapbuild.c CheckPointSnapBuild() => 移除所有复制操作不在需要的可串行化快照
rewriteheap.c CheckPointLogicalRewriteHeap() => 移除逻辑复制的restart LSN位点前不再需要的mapping、将剩余的mappings刷盘,在后续回放是只需要处理已写入的mapping
origin.c CheckPointReplicationOrigin() => 对重放的 remote_lsn 执行每个复制源的进度检查点。 确保我们在检查点 (local_lsn) 中引用的所有事务实际上都在磁盘上。
clog.c CheckPointCLOG() => 将CLOG脏数据刷至磁盘,调用SimpleLruWriteAll接口实现
commit_ts.c CheckPointCommitTs() => 将脏 commit 页写入磁盘
subtrans.c CheckPointSUBTRANS() => 脏 subtrans 页写入磁盘
multixact.c => 将脏的 multixact页写入磁盘
predicte.c CheckPointPredicate()
bufmgr.c CheckPointBuffers() => 将共享缓冲池中的所有脏页刷盘
sync.c ProcessSyncRequests() => 处理同步队列请求
twophase.c CheckPointTwoPhase() => 处理两阶段提交事务工作
smgr.c smgrcloseall() => 完成 checkpoint 后 关闭 smgr 存储介质管理器引用
xlog.c UpdateCheckPointDistanceEstimate() => 估算两次 CheckPoint 之间的 xlog 日志量
xlog.c RemoveOldXlogFiles() => 删除无用的xlog记录 LSN=0
4.几个重要的结构体
和检查点相关的有几个比较关键的结构体:
CheckpointerShmemStruct:CheckpointerShmemStruct 这个是 CheckPoint 进程和其他进程通信的内存数据结构。
CheckPoint:检查点结构体,在执行完 CheckPoint 之后这个结构体作为一条 xlog 记录到 WAL 文件中。这个结构体封装了一次检查点操作而被其他结构体例如 ControlFileData 使用。
XLogCtl:用于操作更新 xlog
ControlFileData:控制文件 ControlFileData 用来保存服务状态,记录检查点等信息在系统恢复时起作用:
ControlFileData 代码如下:
{
uint64 system_identifier; // 数据库实例编号,确保当前控制文件由一个 pg 实例管理
uint32 pg_control_version; // 控制文件版本号,控制文件的修改会随时更新这个版本号
uint32 catalog_version_no; /* see catversion.h */
DBState state; // 数据库当前状态,例如正常运行、正常退出、异常退出等等
pg_time_t time; // 控制文件修改的时间
XLogRecPtr checkPoint; // 最近一次执行 CheckPoint 的记录
CheckPoint checkPointCopy; // 最近一次执行 CheckPoint 的记录的备份
XLogRecPtr unloggedLSN;
XLogRecPtr minRecoveryPoint; // 归档恢复时必须恢复到的最小 xlog 的 LSN
TimeLineID minRecoveryPointTLI; // 归档恢复时必须恢复到的最小时间线
XLogRecPtr backupStartPoint; // 在线备份时进行 CheckPoint 的开始的 xlog 的 LSN
XLogRecPtr backupEndPoint; // 在线备份时进行 CheckPoint 的结尾的 xlog 的 LSN
bool backupEndRequired; // 用于判断是否基于正确的在线备份集恢复
int wal_level; // 系统配置: wal 级别,控制 xlog 需要至少记录哪些东西(默认minimal只提供崩溃恢复级别)
bool wal_log_hints;
int MaxConnections;
int max_worker_processes;
int max_wal_senders;
int max_prepared_xacts;
int max_locks_per_xact;
bool track_commit_timestamp;
} ControlFileData;
5.几个重要的函数
核心源码在检查点检测入口函数的 for(;;) 中,会检测两次 checkpoint 的间隔(默认300毫秒)并执行检查点。
checkpointer.c CheckpointerMain()
void CheckpointerMain(void)
{
sigjmp_buf local_sigjmp_buf;
MemoryContext checkpointer_context;
CheckpointerShmem->checkpointer_pid = MyProcPid;
pqsignal(SIGHUP, SignalHandlerForConfigReload);
pqsignal(SIGINT, ReqCheckpointHandler); /* request checkpoint */
pqsignal(SIGTERM, SIG_IGN); /* ignore SIGTERM */
/* SIGQUIT handler was already set up by InitPostmasterChild */
pqsignal(SIGALRM, SIG_IGN);
pqsignal(SIGPIPE, SIG_IGN);
pqsignal(SIGUSR1, procsignal_sigusr1_handler);
pqsignal(SIGUSR2, SignalHandlerForShutdownRequest);
pqsignal(SIGCHLD, SIG_DFL);
last_checkpoint_time = last_xlog_switch_time = (pg_time_t) time(NULL);
before_shmem_exit(pgstat_before_server_shutdown, 0);
checkpointer_context = AllocSetContextCreate(TopMemoryContext,"Checkpointer",ALLOCSET_DEFAULT_SIZES);
MemoryContextSwitchTo(checkpointer_context);
if (sigsetjmp(local_sigjmp_buf, 1) != 0)
{
/* Since not using PG_TRY, must reset error stack by hand */
error_context_stack = NULL;
/* Prevent interrupts while cleaning up */
HOLD_INTERRUPTS();
/* Report the error to the server log */
EmitErrorReport();
LWLockReleaseAll();
ConditionVariableCancelSleep();
pgstat_report_wait_end();
AbortBufferIO();
UnlockBuffers();
ReleaseAuxProcessResources(false);
AtEOXact_Buffers(false);
AtEOXact_SMgr();
AtEOXact_Files(false);
AtEOXact_HashTables(false);
/* Warn any waiting backends that the checkpoint failed. */
if (ckpt_active)
{
SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
CheckpointerShmem->ckpt_failed++;
CheckpointerShmem->ckpt_done = CheckpointerShmem->ckpt_started;
SpinLockRelease(&CheckpointerShmem->ckpt_lck);
ConditionVariableBroadcast(&CheckpointerShmem->done_cv);
ckpt_active = false;
}
MemoryContextSwitchTo(checkpointer_context);
FlushErrorState();
/* Flush any leaked data in the top-level context */
MemoryContextResetAndDeleteChildren(checkpointer_context);
/* Now we can allow interrupts again */
RESUME_INTERRUPTS();
pg_usleep(1000000L);
smgrcloseall();
}
/* We can now handle ereport(ERROR) */
PG_exception_stack = &local_sigjmp_buf;
PG_SETMASK(&UnBlockSig);
UpdateSharedMemoryConfig();
ProcGlobal->checkpointerLatch = &MyProc->procLatch;
//无条件for,未触发 checkpoint 时一直在 waitLatch 和 sleep
for (;;)
{
// 是否在本次 loop 执行 checkpoint
bool do_checkpoint = false;
int flags = 0;
pg_time_t now;
// 自从上次 checkpoint 已经过去了多久
int elapsed_secs;
// 剩余的超时时间
int cur_timeout;
ResetLatch(MyLatch);
AbsorbSyncRequests();
HandleCheckpointerInterrupts();
if (((volatile CheckpointerShmemStruct *) CheckpointerShmem)->ckpt_flags)
{
do_checkpoint = true;
PendingCheckpointerStats.requested_checkpoints++;
}
// 计算 elapsed_secs 也就是上次 checkpoint 之后已经过了多久
now = (pg_time_t) time(NULL);
elapsed_secs = now - last_checkpoint_time;
// 上一次 checkpoint 之后超过 CheckPointTimeout 都未触发过 checkpoint 则强制触发一次
if (elapsed_secs >= CheckPointTimeout)
{
// 如果没有外部 checkpoint 请求则记录一下 PendingCheckpointerStats
if (!do_checkpoint)
PendingCheckpointerStats.timed_checkpoints++;
// 设置需要进行 checkpoint
do_checkpoint = true;
flags |= CHECKPOINT_CAUSE_TIME;
}
if (do_checkpoint)
{
bool ckpt_performed = false;
bool do_restartpoint;
// 检查现在是否在 recovery 中
do_restartpoint = RecoveryInProgress();
// 针对 CheckpointerShmem->ckpt_lck 加锁(自旋)
// 修改变量设置当前正在执行 checkpoint 标记
SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
flags |= CheckpointerShmem->ckpt_flags;
CheckpointerShmem->ckpt_flags = 0;
CheckpointerShmem->ckpt_started++;
// 解锁
SpinLockRelease(&CheckpointerShmem->ckpt_lck);
ConditionVariableBroadcast(&CheckpointerShmem->start_cv);
if (flags & CHECKPOINT_END_OF_RECOVERY)
do_restartpoint = false;
// 如果本次 checkpoint 距离上次太近会产生告警日志
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\".")));
// 下面检测 checkpoint 类型,读取需要从哪里开始执行 checkpoint
ckpt_active = true;
// 如果是恢复阶段,从 xlog 上获取当前恢复点开始执行 checkpoint
if (do_restartpoint)
ckpt_start_recptr = GetXLogReplayRecPtr(NULL);
// 如果是普通checkpoint,从 xlog 记录插入位置开始执行 checkpoint
else
ckpt_start_recptr = GetInsertRecPtr();
ckpt_start_time = now;
ckpt_cached_elapsed = 0;
// 如果普通 checkpoint
if (!do_restartpoint)
{
CreateCheckPoint(flags);
ckpt_performed = true;
}
// 如果是恢复阶段 checkpoint
else
ckpt_performed = CreateRestartPoint(flags);
// 每次 checkpoint 后关闭 smgr 文件,这样避免对存储介质管理器的无限应用
smgrcloseall();
// 本次 checkpoint 完毕,解锁
SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
CheckpointerShmem->ckpt_done = CheckpointerShmem->ckpt_started;
SpinLockRelease(&CheckpointerShmem->ckpt_lck);
ConditionVariableBroadcast(&CheckpointerShmem->done_cv);
// 成功执行 checkpoint 记录本次执行的时间
if (ckpt_performed)
{
last_checkpoint_time = now;
}
else
{
last_checkpoint_time = now - CheckPointTimeout + 15;
}
ckpt_active = false;
HandleCheckpointerInterrupts();
}
CheckArchiveTimeout();
/* 报告活动统计信息到收集器 */
pgstat_report_checkpointer();
/* 报告wal统计信息到收集器 */
pgstat_report_wal(true);
if (((volatile CheckpointerShmemStruct *) CheckpointerShmem)->ckpt_flags)
continue;
// 如果设置了任何检查点标记位,重新进行下一次循环去处理检查点而不是sleeping
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);
}
(void) WaitLatch(MyLatch,
WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
cur_timeout * 1000L /* convert to ms */ ,
WAIT_EVENT_CHECKPOINTER_MAIN);
}
}
后台定时、或者客户端请求 checkpoint 操作时,调用真正执行检查点的函数如下是 xlog.c CreateCheckPoint(),大体流程将内存中的 page 脏数据进行持久化、同时将当前执行 checkPoint 点对应 xlog 记录 lsn 保存到控制文件中。这个函数实现了下面几个重要功能:
(1)脏页刷入
(2)删除旧WAL:创建检查点后此位置之前的WAL可以删除
(3)故障恢复起点。
(4)更新控制文件中检查点信息,记录本次 CheckPoint 信息崩溃恢复时以做判断。
(5)Checkpoint skipped机制,判断是否有提交,没有新的提交可以跳过 CheckPoint。
xlog.c CreateCheckPoint() 代码如下:
void CreateCheckPoint(int flags)
{
bool shutdown; // 是否在 shutdown 中
CheckPoint checkPoint;
XLogRecPtr recptr; // xlog 记当前录位置
XLogSegNo _logSegNo; // 日志段号
XLogCtlInsert *Insert = &XLogCtl->Insert; // 用于 checkpoint 后更新控制文件
uint32 freespace;
XLogRecPtr PriorRedoPtr; // xlog 上一次重做位置
XLogRecPtr curInsert; // xlog 当前插入位置
XLogRecPtr last_important_lsn; // 上一个重要的 lsn
VirtualTransactionId *vxids; // 虚拟事务id
int nvxids;
int oldXLogAllowed = 0;
if (flags & (CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_END_OF_RECOVERY))
shutdown = true;
else
shutdown = false;
if (RecoveryInProgress() && (flags & CHECKPOINT_END_OF_RECOVERY) == 0)
elog(ERROR, "can't create a checkpoint during recovery");
MemSet(&CheckpointStats, 0, sizeof(CheckpointStats));
CheckpointStats.ckpt_start_t = GetCurrentTimestamp();
// 告知smgr准备好,要开始建检查点了
SyncPreCheckpoint();
START_CRIT_SECTION();
// 如果是 shutdown 时执行 checkpoint 更新一下控制文件
if (shutdown)
{
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->state = DB_SHUTDOWNING;
UpdateControlFile();
LWLockRelease(ControlFileLock);
}
MemSet(&checkPoint, 0, sizeof(checkPoint));
checkPoint.time = (pg_time_t) time(NULL);
if (!shutdown && XLogStandbyInfoActive())
checkPoint.oldestActiveXid = GetOldestActiveTransactionId();
else
checkPoint.oldestActiveXid = InvalidTransactionId;
// 获取 wal 中上一个 xlog 插入位置,CheckPoint之后需要从这里继续插入新的 CheckPoint-xlog
last_important_lsn = GetLastImportantRecPtr();
WALInsertLockAcquireExclusive();
curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
if ((flags & (CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_END_OF_RECOVERY |
CHECKPOINT_FORCE)) == 0)
{
if (last_important_lsn == ControlFile->checkPoint)
{
WALInsertLockRelease();
END_CRIT_SECTION();
ereport(DEBUG1,
(errmsg_internal("checkpoint skipped because system is idle")));
return;
}
}
if (flags & CHECKPOINT_END_OF_RECOVERY)
oldXLogAllowed = LocalSetXLogInsertAllowed();
checkPoint.ThisTimeLineID = XLogCtl->InsertTimeLineID;
if (flags & CHECKPOINT_END_OF_RECOVERY)
checkPoint.PrevTimeLineID = XLogCtl->PrevTimeLineID;
else
checkPoint.PrevTimeLineID = checkPoint.ThisTimeLineID;
checkPoint.fullPageWrites = Insert->fullPageWrites;
freespace = INSERT_FREESPACE(curInsert);
if (freespace == 0)
{
if (XLogSegmentOffset(curInsert, wal_segment_size) == 0)
curInsert += SizeOfXLogLongPHD;
else
curInsert += SizeOfXLogShortPHD;
}
checkPoint.redo = curInsert;
RedoRecPtr = XLogCtl->Insert.RedoRecPtr = checkPoint.redo;
WALInsertLockRelease();
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->RedoRecPtr = checkPoint.redo;
SpinLockRelease(&XLogCtl->info_lck);
if (log_checkpoints)
LogCheckpointStart(flags, false);
update_checkpoint_display(flags, false, false);
TRACE_POSTGRESQL_CHECKPOINT_START(flags);
LWLockAcquire(XidGenLock, LW_SHARED);
checkPoint.nextXid = ShmemVariableCache->nextXid;
checkPoint.oldestXid = ShmemVariableCache->oldestXid;
checkPoint.oldestXidDB = ShmemVariableCache->oldestXidDB;
LWLockRelease(XidGenLock);
LWLockAcquire(CommitTsLock, LW_SHARED);
checkPoint.oldestCommitTsXid = ShmemVariableCache->oldestCommitTsXid;
checkPoint.newestCommitTsXid = ShmemVariableCache->newestCommitTsXid;
LWLockRelease(CommitTsLock);
LWLockAcquire(OidGenLock, LW_SHARED);
checkPoint.nextOid = ShmemVariableCache->nextOid;
if (!shutdown)
checkPoint.nextOid += ShmemVariableCache->oidCount;
LWLockRelease(OidGenLock);
MultiXactGetCheckptMulti(shutdown,
&checkPoint.nextMulti,
&checkPoint.nextMultiOffset,
&checkPoint.oldestMulti,
&checkPoint.oldestMultiDB);
END_CRIT_SECTION();
vxids = GetVirtualXIDsDelayingChkpt(&nvxids, DELAY_CHKPT_START);
if (nvxids > 0)
{
do
{
pg_usleep(10000L); /* wait for 10 msec */
} while (HaveVirtualXIDsDelayingChkpt(vxids, nvxids,
DELAY_CHKPT_START));
}
pfree(vxids);
// 执行 ShardBuffer 中数据写入磁盘
CheckPointGuts(checkPoint.redo, flags);
vxids = GetVirtualXIDsDelayingChkpt(&nvxids, DELAY_CHKPT_COMPLETE);
if (nvxids > 0)
{
do
{
pg_usleep(10000L); /* wait for 10 msec */
} while (HaveVirtualXIDsDelayingChkpt(vxids, nvxids,
DELAY_CHKPT_COMPLETE));
}
pfree(vxids);
if (!shutdown && XLogStandbyInfoActive())
LogStandbySnapshot();
START_CRIT_SECTION();
// 写入一条 CheckPoint Start 的 xlog
XLogBeginInsert();
XLogRegisterData((char *) (&checkPoint), sizeof(checkPoint));
recptr = XLogInsert(RM_XLOG_ID,
shutdown ? XLOG_CHECKPOINT_SHUTDOWN :
XLOG_CHECKPOINT_ONLINE);
XLogFlush(recptr);
if (shutdown)
{
if (flags & CHECKPOINT_END_OF_RECOVERY)
LocalXLogInsertAllowed = oldXLogAllowed;
else
LocalXLogInsertAllowed = 0; /* never again write WAL */
}
if (shutdown && checkPoint.redo != ProcLastRecPtr)
ereport(PANIC,
(errmsg("concurrent write-ahead log activity while database system is shutting down")));
// 修改控制文件变量记录本次 CheckPoint 对应的 xlog
PriorRedoPtr = ControlFile->checkPointCopy.redo;
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
if (shutdown)
ControlFile->state = DB_SHUTDOWNED;
ControlFile->checkPoint = ProcLastRecPtr;
ControlFile->checkPointCopy = checkPoint;
/* crash recovery should always recover to the end of WAL */
ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
ControlFile->minRecoveryPointTLI = 0;
SpinLockAcquire(&XLogCtl->ulsn_lck);
ControlFile->unloggedLSN = XLogCtl->unloggedLSN;
SpinLockRelease(&XLogCtl->ulsn_lck);
// 更新控制文件
UpdateControlFile();
LWLockRelease(ControlFileLock);
/* Update shared-memory copy of checkpoint XID/epoch */
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->ckptFullXid = checkPoint.nextXid;
SpinLockRelease(&XLogCtl->info_lck);
END_CRIT_SECTION();
SyncPostCheckpoint();
// 估算本次 CheckPoint 和上次的距离,也就是中间包含多上 xlog 量
if (PriorRedoPtr != InvalidXLogRecPtr)
UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);
// 获取日志段号,计算执行需要保留的 xlog 的 LSN
XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
// 据max_slot_wal_keep_size和wal_keep_size两个参数设置,再次调整最旧的需要保留的_logSegNo
KeepLogSeg(recptr, &_logSegNo);
if (InvalidateObsoleteReplicationSlots(_logSegNo))
{
XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
KeepLogSeg(recptr, &_logSegNo);
}
// 从这个 xlog 往前一条开始就是可以删除的 xlog 了
_logSegNo--;
// 删除无用 xlog
RemoveOldXlogFiles(_logSegNo, RedoRecPtr, recptr,checkPoint.ThisTimeLineID);
if (!shutdown)
PreallocXlogFiles(recptr, checkPoint.ThisTimeLineID);
if (!RecoveryInProgress())
TruncateSUBTRANS(GetOldestTransactionIdConsideredRunning());
LogCheckpointEnd(false);
update_checkpoint_display(flags, false, true);
TRACE_POSTGRESQL_CHECKPOINT_DONE(CheckpointStats.ckpt_bufs_written,
NBuffers,
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled);
}
接下来将 ShardBuffer 缓存数据写入磁盘,xlog.c CheckPointGuts() 调用 bufmgr.c CheckPointBuffers() 就是将内存页进行 flush。
会遍历所有 Buffer,判断是否是脏页 BM_DIRTY,如果是脏页则标记为 BM_CHECKPOINT_NEEDED 需要 flush,然后对这些 Page 进行排序减少随机IO,最后调用 SyncOneBuffer() 一次一个 Page 进行写入。
bufmgr.c BufferSync() 代码如下:
static void BufferSync(int flags)
{
// 省略
// 遍历缓存,使用 BM_CHECKPOINT_NEEDED 标记需要写入的 page.
// 计数值保存在 mun_to_scan 中用来估计工作量
num_to_scan = 0;
for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
// 获取 buffer 的 desc
BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
// 给 buffer 加入自旋锁
buf_state = LockBufHdr(bufHdr);
// mask = BM_DIRTY 也就是脏页
if ((buf_state & mask) == mask)
{
CkptSortItem *item;
// 标记为需要 flush
buf_state |= BM_CHECKPOINT_NEEDED;
item = &CkptBufferIds[num_to_scan++];
item->buf_id = buf_id;
item->tsId = bufHdr->tag.spcOid;
item->relNumber = BufTagGetRelNumber(&bufHdr->tag);
item->forkNum = BufTagGetForkNum(&bufHdr->tag);
item->blockNum = bufHdr->tag.blockNum;
}
UnlockBufHdr(bufHdr, buf_state);
if (ProcSignalBarrierPending)
ProcessProcSignalBarrier();
}
if (num_to_scan == 0)
return; /* nothing to do */
WritebackContextInit(&wb_context, &checkpoint_flush_after);
TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_scan);
// 对需要 flush 的 page 进行排序减少随机磁盘IO
sort_checkpoint_bufferids(CkptBufferIds, num_to_scan);
num_spaces = 0;
// 为每一个需要刷脏页的表空间分配进度状态.
last_tsid = InvalidOid;
for (i = 0; i < num_to_scan; i++)
{
// 省略
}
// 在单个标记的写进度上构建最小堆,并计算单个处理缓冲区占比多少.
ts_heap = binaryheap_allocate(num_spaces,ts_ckpt_progress_comparator,NULL);
for (i = 0; i < num_spaces; i++)
{
CkptTsStatus *ts_stat = &per_ts_stat[i];
ts_stat->progress_slice = (float8) num_to_scan / ts_stat->num_to_scan;
binaryheap_add_unordered(ts_heap, PointerGetDatum(ts_stat));
}
binaryheap_build(ts_heap);
// 迭代处理to-be-checkpointed buffers,刷脏页.循环调用 SyncOneBuffer 一处处理一个 page
num_processed = 0;
num_written = 0;
while (!binaryheap_empty(ts_heap))
{
BufferDesc *bufHdr = NULL;
CkptTsStatus *ts_stat = (CkptTsStatus *)
DatumGetPointer(binaryheap_first(ts_heap));
buf_id = CkptBufferIds[ts_stat->index].buf_id;
Assert(buf_id != -1);
bufHdr = GetBufferDescriptor(buf_id);
num_processed++;
if (pg_atomic_read_u32(&bufHdr->state) & BM_CHECKPOINT_NEEDED)
{
// 一次处理一个 page 的写入
if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
{
TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
PendingCheckpointerStats.buf_written_checkpoints++;
num_written++;
}
}
CheckpointWriteDelay(flags, (double) num_processed / num_to_scan);
}
// 省略
}