Checkpoint介绍
1、介绍
PostgreSQL基于DRAM+HDD/SSD两层存储架构,所有读写都在DRAM中进行。由于数据写大部分都是随机的,为提高数据库性能,通过写前日志WAL将所有修改都记录到日志中,事务提交时,将日志持久化到磁盘即可,而脏数据页由后台进程异步刷写磁盘,从而将随机写转换成顺序写。
崩溃重启时通过WAL日志将修改回放出来,从而保证数据持久性和一致性,但是如果所有事务的WAL日志都需要回放的话,一旦日志量非常大时,恢复时间就会很长。为减小恢复时间,PostgreSQL通过checkpoint将脏数据刷写到磁盘,崩溃恢复时,只需将checkpoint之后的日志回放即可。
PostgreSQL由checkpoint进程完成刷写脏页(还有一个后台进程bgwriter也会刷写脏页,本文不做过多介绍),并生成一个CHECKPOINT的WAL日志,同时将checkpoint位置记录到pg_control文件中。重启恢复时,从pg_control文件中读取checkpoint位置,作为读取WAL日志和回放的起始位置。启动后可以看到checkpoint进程:
80259 ? Ss 0:00 /home/yzs/bin/postmaster -D /home/yzs/data/
80260 ? Ss 0:00 \_ postgres: logger
80262 ? Ss 0:00 \_ postgres: checkpointer
80263 ? Ss 0:00 \_ postgres: background writer
80264 ? Ss 0:00 \_ postgres: walwriter
80265 ? Ss 0:00 \_ postgres: autovacuum launcher
80266 ? Ss 0:00 \_ postgres: stats collector
80267 ? Ss 0:00 \_ postgres: autoprewarm master
80268 ? Ss 0:00 \_ postgres: logical replication launcher
当将checkpoint子进程kill -9杀掉后,其他进程也会重新启动,这也说明checkpoint子进程对数据一致性有很重要保护作用。
pg_control文件记录的checkpoint信息:
2、checkpoint相关参数
1)log_checkpoints
该参数默认关闭,开启后将每次checkpoint详细信息记录到错误日志中,可以通过此信息了解系统负载。一个相关信息如下:
“checkpoint starting: immediate force wait”//这条信息由函数LogCheckpointStart输出
“checkpoint complete: wrote 1 buffers (0.0%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.000 s, sync=0.000 s, total=0.001 s; sync files=1, longest=0.000 s, average=0.000 s; distance=0 kB, estimate=11948 kB”//这条信息由函数LogCheckpointEnd输出
2)checkpoint_warning
该参数默认30s,如果填充的WAL段文件导致检查点之间的间隔低于这个参数表示的时间,就向服务器日志写一个消息。0表示关闭警告。如果checkpoint_timeout小于checkpoint_warning值,则不会有警告产生。如果磁盘性能好,可以适当将这个值设置小点,但这个参数不会影响性能,仅用于检查检查点的频率。
3)checkpoint_timeout
自动检查点之间的最长时间。如果没有指定单位,则以秒为单位。默认值是5分钟。增加这个值,会增加崩溃恢复的时间。
checkpoint_timeout用到的位置:
- checkpoint进程距上次检查点时间超过这个值,就强制触发检查点。在3节 checkpoint发生时机详细介绍。
- Checkpoint进程执行完一次检查点后,如果执行的时间不够checkpoint_timeout 时间,则等待直到达到这个时间,流程简化后:
CheckpointerMainfor(;;){now = (pg_time_t) time(NULL);CreateCheckPoint(flags);//执行检查点last_checkpoint_time = now;//检查点开始时间now = (pg_time_t) time(NULL);elapsed_secs = now - last_checkpoint_time;//检查点执行的时间if (elapsed_secs >= CheckPointTimeout)continue;//检查点超时,不进行休眠cur_timeout = CheckPointTimeout - 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);//检查点休眠}
- 每次刷写完一个脏页后进行检查点调度判断,若刷写IO太过频繁则休息100ms
BufferSync->CheckpointWriteDelay->|-- IsCheckpointOnSchedulegettimeofday(&now, NULL);elapsed_time = ((double) ((pg_time_t) now.tv_sec - ckpt_start_time) +now.tv_usec / 1000000.0) / CheckPointTimeout;//脏页刷写率大于checkpoint消耗时间率的话,需要sleep 100msif (progress < elapsed_time){ckpt_cached_elapsed = elapsed_time;return false;}|-- pg_usleep(100000L);
注意:IsCheckpointOnSchedule中除了判断时间的进度外,会先对日志产生的进度进行判断,从而对刷新脏页进行限速:
if (RecoveryInProgress())recptr = GetXLogReplayRecPtr(NULL);//恢复场景:回放的WAL日志lsn
elserecptr = GetInsertRecPtr();//产生的WAL的lsn
elapsed_xlogs = (((double) (recptr - ckpt_start_recptr)) /wal_segment_size) / CheckPointSegments;//检查点以来产生WAL的进度
if (progress < elapsed_xlogs){ckpt_cached_elapsed = elapsed_xlogs;return false;
}
4)min_wal_size
用于回收WAL文件时设置未来日志文件段号值。段号值最小即为min_wal_size计算所得。所以只要WAL磁盘用量保持在这个设置之下,在检查点时旧的文件总是被回收以便未来使用,而不是直接删除。这个可以用来确保足够的WAL空间被保留下来应付WAL使用高峰,例如大量写任务下。如果指定时没有单位,则以MB为单位,默认80MB。
执行完checkpoint后,回收WAL文件时,用于判断回收后日志文件段号的范围:
XLOGfileslopminSegNo = RedoRecPtr / wal_segment_size +ConvertToXSegs(min_wal_size_mb, wal_segment_size) - 1;maxSegNo = RedoRecPtr / wal_segment_size +ConvertToXSegs(max_wal_size_mb, wal_segment_size) - 1;distance = (1.0 + CheckPointCompletionTarget) * CheckPointDistanceEstimate;/* add 10% for good measure. */distance *= 1.10;recycleSegNo = (XLogSegNo) ceil(((double) RedoRecPtr + distance) /wal_segment_size);if (recycleSegNo < minSegNo)recycleSegNo = minSegNo;if (recycleSegNo > maxSegNo)recycleSegNo = maxSegNo;return recycleSegNo;
说明:回收WAL日志文件后,增加的段号要在min_wal_size和max_wal_size范围内
5)max_wal_size
自动WAL检查点之间允许WAL增长到的最大尺寸。这是一个软限制,特殊情况下WAL尺寸会超过max_wal_size。如果没有指定单位,则以MB为单位,默认1GB。增加这个参数会增加崩溃恢复时间。
在回收WAL文件时,预留的日志文件号最大是这个值计算出的段文件号。也就是说超过这量的日志需要删除。
另外就是和checkpoint_complete_target联合使用。
用到该参数的地方除了上文介绍的回收WAL日志文件处外,还用于计算CheckPointSegments值(checkpoint触发值),该值同样用于判断是否进行检查点调度。该值计算方法:
CalculateCheckpointSegmentstarget = (double) ConvertToXSegs(max_wal_size_mb, wal_segment_size) /(1.0 + CheckPointCompletionTarget);CheckPointSegments = (int) target;if (CheckPointSegments < 1)CheckPointSegments = 1;
说明:如果检查点不能立即完成,则老WAL日志就不能立即删除。max_wal_size规定了WAL日志的最大值。达到WAL日志大小的顶峰是checkpoint即将执行完成的时刻,因为此时包含这次触发的WAL日志加上新增的WAL日志。假设触发值是target,则checkpoint_timeout时间内,WAL日志新增大小最多为target。假设WAL日志增长速度相同,则此时增长的日志大小为target*checkpoint_completion_target,为保证峰值,wal日志大小是max_wal_size,由此可以计算出触发值:
target+target*checkpoint_completion_target=max_wal_size
target=max_wal_size/(1+checkpoint_completion_target)
6)checkpoint_completion_target
该参数表示checkpoint的完成时间占两次checkpoint时间间隔的比例,默认值是0.5,也就是说每个checkpoint需要在checkpoint时间间隔checkpoint_timeout的50%内完成。Checkpoint执行会消耗大量系统资源,尤其时IO资源,需要对IO进行限速以减少系统影响,使得磁盘IO变得平缓。
这个值除了在计算WAL日志大小触发checkpoint阈值(上小节介绍)、计算回收日志后最大段号(min_wal_size部分介绍)外,还在判断是否进行checkpoint调度时用到:
IsCheckpointOnScheduleprogress *= CheckPointCompletionTarget;//将刷脏页率缩放后,用于后续判断....
7)checkpoint_flush_after
自动上次checkpoint以来刷写了这么多脏页后,发起sync操作。这样做限制操作系统缓冲中脏页数量,降低checkpoint末尾发出sync或者OS在后台大批量写回数据时被卡住的可能性。如果没有指定单位,默认是页数,指定MB则转换成页数。
由函数ScheduleBufferTagForWriteback进行sync调度。
SyncOneBufferFlushBuffer(bufHdr, NULL);//刷写一个脏页tag = bufHdr->tag;ScheduleBufferTagForWriteback(wb_context, &tag);//调度是否sync|-- if (*context->max_pending > 0){//将脏页sync请求放入请求队列pending = &context->pending_writebacks[context->nr_pending++];pending->tag = *tag;}if (context->nr_pending >= *context->max_pending)IssuePendingWritebacks(context);//若请求数>=checkpoint_flush_after则sync//context->max_pending来自checkpoint_flush_afterBufferSync->WritebackContextInit(&wb_context, &checkpoint_flush_after);context->max_pending = max_pending;context->nr_pending = 0;
3、checkpoint发生时机
Checkpoint触发时机分为以下多种:
1)时间触发:后台checkpoint进程,距上次执行时间超过checkpoint_timeout值就会触发。由时间超时触发的时机:
CheckpointerMain:for (;;){now = (pg_time_t) time(NULL);elapsed_secs = now - last_checkpoint_time;//距上次检查点时间if (elapsed_secs >= CheckPointTimeout){//超过checkpoint_timeoutdo_checkpoint = true;flags |= CHECKPOINT_CAUSE_TIME;//发起CHECKPOINT_CAUSE_TIME的检查点}if (do_checkpoint){CreateCheckPoint(flags);last_checkpoint_time = now;//checkpoint开始时间}...}
2)数据库正常关闭时,触发一次:
CheckpointerMainpqsignal(SIGUSR2, ReqShutdownHandler);/* 注册信号处理函数 */for (;;){if (shutdown_requested){//checkpoint进程接收到shutdown请求ExitOnAnyError = true;ShutdownXLOG(0, 0);|-- CreateCheckPoint(CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_IMMEDIATE);proc_exit(0);}...}
//接收到信号SIGUSR2后,发起shutdown checkpoint
static void ReqShutdownHandler(SIGNAL_ARGS)
{int save_errno = errno;shutdown_requested = true;SetLatch(MyLatch);errno = save_errno;
}
3)数据库崩溃恢复,修复完成后触发一次:
StartupXLOG//为恢复进程CreateCheckPoint(CHECKPOINT_END_OF_RECOVERY | CHECKPOINT_IMMEDIATE);
4)手动执行CHECKPOINT命令
5)基础备份:当进行数据库基础备份时,会执行pg_start_backup->do_pg_start_backup触发一次:
do_pg_start_backupRequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT |(fast ? CHECKPOINT_IMMEDIATE : 0));
6)每次将日志XLogWrite写入磁盘并且一个WAL日志文件写满后,自从上次checkpoint以来产生的超过max_wal_size放大checkpoint_completion_target后触发一次:
XLogWriteif (finishing_seg){//写满一个WAL段文件if (IsUnderPostmaster && XLogCheckpointNeeded(openLogSegNo)){(void) GetRedoRecPtr();if (XLogCheckpointNeeded(openLogSegNo))RequestCheckpoint(CHECKPOINT_CAUSE_XLOG);}}
//判断WAL自从上次checkpoint以来产生量是否超过阈值
static bool XLogCheckpointNeeded(XLogSegNo new_segno)
{XLogSegNo old_segno;XLByteToSeg(RedoRecPtr, old_segno, wal_segment_size);if (new_segno >= old_segno + (uint64) (CheckPointSegments - 1))return true;return false;
}
//其中CheckPointSegments来自函数CalculateCheckpointSegments,由max_wal_size放大
//checkpoint_completion_target倍后计算得来,在第2节的max_wal_size中介绍。
4、checkpoint监控
除了log_checkpoints监听日志的方式外,还可以通过pg_stat_bgwriter视图查看checkpoint。下面介绍与checkpoint相关的列:
checkpoints_timed | 因为时间触发的次数 |
checkpoints_req | 其他原因触发的次数 |
checkpoint_write_time | 缓存刷写到文件系统cache花费的时间 |
checkpoint_sync_time | 缓存对应的文件系统cache刷写到磁盘花费的时间 |
buffers_checkpoint | buffer刷新到磁盘的数目 |
buffers_backend | checkpoint接收到sync请求次数 |
buffers_backend_fsync | 后台因为发送sync请求失败,需要自身执行fsync操作的次数 |
stats_reset | 更新时间 |