前篇我们提到,checkpoint的核心作用之一——计算哪些WAL日志是过时可以清理的,并将其清理(删除或重命名),前文中相关代码如下(6. 删除无用的日志文件):
postgresql源码学习(32)—— 检查点④-核心函数CreateCheckPoint_checkpoint函数_Hehuyi_In的博客-CSDN博客
/*
* 如果前一个检查点存在,更新检查点之间的平均距离
*/
if (PriorRedoPtr != InvalidXLogRecPtr)
/* 估算两次checkpoint之间产生的xlog量,假如上次估算量比这次估算的小,则更新为这次的估算量,否则适量增加CheckPointDistanceEstimate =(0.90 * CheckPointDistanceEstimate + 0.10 * (double) nbytes); */
UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);
/* 获取redo点的日志段号,作为最旧的需要保留的_logSegNo */
XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
/* 根据max_slot_wal_keep_size和wal_keep_size两个参数设置,再次调整最旧的需要保留的_logSegNo */
KeepLogSeg(recptr, &_logSegNo);
/* 如果_logSegNo是已经过时的复制槽,需要重新计算 */
if (InvalidateObsoleteReplicationSlots(_logSegNo))
{
/*
* Some slots have been invalidated; recalculate the old-segment
* horizon, starting again from RedoRecPtr.
*/
XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
KeepLogSeg(recptr, &_logSegNo);
}
/* 前面_logSegNo是最旧的需要保留的段号,因此减1则是最新的可以删除的段号 */
_logSegNo--;
/* 删除已无用的日志文件 */
RemoveOldXlogFiles(_logSegNo, RedoRecPtr, recptr);
估算两次checkpoint之间产生的xlog量,主要用于后面XLOGfileslop函数的日志预分配。
// RedoRecPtr是本次检查点位置,PriorRedoPtr是上次检查点位置。因此,传入的参数是本次实际产生的日志量。
UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);
如果上次估算量比这次实际产生的要小,则将估算值更新为这次产生的量。否则,采用平滑算法对估计值进行平滑估算,即增量内容占10%的比重。
/*
* Update the estimate of distance between checkpoints.
*
* The estimate is used to calculate the number of WAL segments to keep
* preallocated, see XLOGfileslop().
*/
static void
UpdateCheckPointDistanceEstimate(uint64 nbytes)
{
/* 本次产生的日志量 */
PrevCheckPointDistance = nbytes;
/* 如果上次估算量CheckPointDistanceEstimate比这次实际产生的要小,则将估算值更新为这次产生的量 */
if (CheckPointDistanceEstimate < nbytes)
CheckPointDistanceEstimate = nbytes;
else
/* 否则,按下面的算法估算 */
CheckPointDistanceEstimate =
(0.90 * CheckPointDistanceEstimate + 0.10 * (double) nbytes);
}
例如上次估算值为100,实际值为50,则本次估算值应为:0.9*100+0.1*50=95,缓缓缩小。
// 获取redo点的日志段号_logSegNo
XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
很简单的一个定义
/*
* Compute a segment number from an XLogRecPtr.
*/
#define XLByteToSeg(xlrp, logSegNo, wal_segsz_bytes) \
logSegNo = (xlrp) / (wal_segsz_bytes)
在学习这个函数之前,有两个与XLog清理相关的参数需要了解:
- max_slot_wal_keep_size:日志复制进程能落后于当前事务的写入点。如果超过此大小,则可以撤销该日志复制的WalSender进程。
- wal_keep_size:要为日志复制进程保留多少日志量。旧版本使用的是wal_keep_segments参数(要为日志复制进程保留多少个日志文件),在pg 13中,该参数已被移除
在pg 14版本已查询不到该参数
由于主库需要为备库保留日志,这里 KeepLogSeg(recptr, &_logSegNo); 对比redo点日志段号&_logSegNo、当前事务日志段号以及上面两个参数,初步计算清理XLog的位置信息。
/*
* 由于wal_keep_size或者复制槽有额外保留XLog的要求,本函数重新定位*logSegNo指针至最旧的需要保留的日志段
*/
static void
KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
{
XLogSegNo currSegNo;
XLogSegNo segno;
XLogRecPtr keep;
//根据recptr计算当前事务日志位置段号currSegNo
XLByteToSeg(recptr, currSegNo, wal_segment_size);
segno = currSegNo;
/*
* 第一次调整,根据max_slot_wal_keep_size参数,先计算复制槽需要保留多少日志
*/
// 每个日志复制槽位中都记录了当前复制的最小LSN,下面函数进行汇总,并得到所有日志槽中最最小的LSN,大于此LSN的日志即尚未复制完成。
keep = XLogGetReplicationSlotMinimumLSN();
if (keep != InvalidXLogRecPtr)
{
/* 获取复制槽最小LSN对应的日志段号segno */
XLByteToSeg(keep, segno, wal_segment_size);
/* 如果设置了 max_slot_wal_keep_size */
if (max_slot_wal_keep_size_mb >= 0)
{
uint64 slot_keep_segs;
/* 由参数设置的size转换为需保留的日志段数 */
slot_keep_segs =
ConvertToXSegs(max_slot_wal_keep_size_mb, wal_segment_size);
/* 计算当前日志段号currSegNo与最早可清理的segno间的差值,如果已经超出需保留的日志段数slot_keep_segs,则将segno往前推进,此时主从复制可能会受影响 */
if (currSegNo - segno > slot_keep_segs)
segno = currSegNo - slot_keep_segs;
}
}
/*
* 第二次调整,根据wal_keep_size参数,再计算需要保留多少日志
*/
if (wal_keep_size_mb > 0)
{
uint64 keep_segs;
/* 还是由参数设置的size转换为需保留的日志段数 */
keep_segs = ConvertToXSegs(wal_keep_size_mb, wal_segment_size);
if (currSegNo - segno < keep_segs)
{
/* avoid underflow, don't go below 1,避免相减后段号小于1 */
if (currSegNo <= keep_segs)
segno = 1;
else
/* 否则,继续前推segno,此时主从复制可能会受影响 */
segno = currSegNo - keep_segs;
}
}
/* don't delete WAL segments newer than the calculated segment,如果segno与*logSegNo不相等,则重新设置*logSegNo为新计算出的最旧XLog需保留点segno */
if (segno < *logSegNo)
*logSegNo = segno;
}
原理如下图所示
在确定可清理位置之后,调用RemoveOldXlogFiles函数清理旧的XLog。该函数会调用RemoveXlogFile,真正清理日志文件。
同样在学习函数定义前,我们先看一些参数定义:
- wal_recycle:待删除日志是否以重命名方式循环利用
- min_wal_size:日志清理后,WAL文件中最少需要保证有多少空间。若小于此参数,需要额外分配
- max_wal_size:日志清理后,WAL文件中最多能有多少空间。若大于此参数,多余部分也会被清理
- checkpoint_completion_target:检查点目标完成时间
- checkpoint_timeout:检查点超时时间
函数主要作用
/*
* Recycle or remove all log files older or equal to passed segno.
*
* endptr is current (or recent) end of xlog, and lastredoptr is the
* redo pointer of the last checkpoint. These are used to determine
* whether we want to recycle rather than delete no-longer-wanted log files.
*/
static void
RemoveOldXlogFiles(XLogSegNo segno, XLogRecPtr lastredoptr, XLogRecPtr endptr)
{
DIR *xldir;
struct dirent *xlde;
char lastoff[MAXFNAMELEN];
XLogSegNo endlogSegNo;
XLogSegNo recycleSegNo;
/* 获取endptr对应的日志段号endlogSegNo */
XLByteToSeg(endptr, endlogSegNo, wal_segment_size);
/*在本次检查点,有多少 WAL 段需要作为预分配的未来 XLOG 段回收?返回应预分配的最大段号*/
recycleSegNo = XLOGfileslop(lastredoptr);
/*构建一个XLog日志名,用于判断,该文件之前的xlog可以删除。用不到时间线,所以可以使用0 */
XLogFileName(lastoff, 0, segno, wal_segment_size);
elog(DEBUG2, "attempting to remove WAL segments older than log file %s",
lastoff);
/* 获取XLog目录 */
xldir = AllocateDir(XLOGDIR);
/* 读取目录中的文件 */
while ((xlde = ReadDir(xldir, XLOGDIR)) != NULL)
{
/* 忽略非XLog文件 */
if (!IsXLogFileName(xlde->d_name) &&
!IsPartialXLogFileName(xlde->d_name))
continue;
/* 跳过时间线部分比较日志文件名,如果当前段号<=回收点段号,并且该日志已经归档(开归档的情况下),就可以回收或者删除 */
if (strcmp(xlde->d_name + 8, lastoff + 8) <= 0)
{
/* 如果没有开启归档:总是TRUE;否则,检查日志是否归档完成(即pg_wal/archive_status目录下是不是已经存在对应的.done文件) */
if (XLogArchiveCheckDone(xlde->d_name))
{
/* Update the last removed location in shared memory first,首先在共享内存中更新已被删除的位置 */
UpdateLastRemovedPtr(xlde->d_name);
/* 调用RemoveXlogFile函数真正进行删除,函数里使用unlink删除日志 */
RemoveXlogFile(xlde->d_name, recycleSegNo, &endlogSegNo);
}
}
}
FreeDir(xldir);
}
XLog重命名大致原理(实际日志选择不是按顺序的)
这里我们要再详细看两个函数:
/* 计算回收文件重命名的未来最大文件段号recycleSegNo */
recycleSegNo = XLOGfileslop(lastredoptr);
/* 调用RemoveXlogFile函数真正进行删除 */
RemoveXlogFile(xlde->d_name, recycleSegNo, &endlogSegNo);
在本次检查点,有多少 WAL 段需要作为预分配的未来 XLOG 段回收?返回应预分配的最大段号。函数参数为最近一次redo点位置。
预分配的过程是,为所有不再需要的旧文件重命名一个未来的日志号,直到预分配的文件数量达到XLOGfileslop返回的recycleSegNo。
/*
* At a checkpoint, how many WAL segments to recycle as preallocated future XLOG segments? Returns the highest segment that should be preallocated.
*/
static XLogSegNo
XLOGfileslop(XLogRecPtr lastredoptr)
{
XLogSegNo minSegNo;
XLogSegNo maxSegNo;
double distance;
XLogSegNo recycleSegNo;
/* 根据min_wal_size和max_wal_size参数设置,计算最小和最大段号 */
minSegNo = lastredoptr / wal_segment_size +
ConvertToXSegs(min_wal_size_mb, wal_segment_size) - 1;
maxSegNo = lastredoptr / wal_segment_size +
ConvertToXSegs(max_wal_size_mb, wal_segment_size) - 1;
/*估算下一次checkpoint结束时日志位置*/
distance = (1.0 + CheckPointCompletionTarget) * CheckPointDistanceEstimate;
/* add 10% for good measure. */
distance *= 1.10;
recycleSegNo = (XLogSegNo) ceil(((double) lastredoptr + distance) /
wal_segment_size);
/* recycleSegNo不能小于minSegNo,也不能大于maxSegNo */
if (recycleSegNo < minSegNo)
recycleSegNo = minSegNo;
if (recycleSegNo > maxSegNo)
recycleSegNo = maxSegNo;
return recycleSegNo;
}
RemoveXlogFile中进行日志回收以及删除,回收是从不需要保留的日志中选择一部分来给未来使用(回收数量和两次checkpoint间产生wal量有关系),其余的会被删除掉。
/*
* Recycle or remove a log file that's no longer needed.
*
* segname为待处理文件名; recycleSegNo为待回收段号;endlogSegNo为当前(或最近)的XLog结束段号。如果是对段进行回收,endlogSegNo会增加,这样它就不会在未来调用此函数时被重复检查。
*/
static void
RemoveXlogFile(const char *segname, XLogSegNo recycleSegNo,
XLogSegNo *endlogSegNo)
{
char path[MAXPGPATH];
#ifdef WIN32
char newpath[MAXPGPATH];
#endif
struct stat statbuf;
snprintf(path, MAXPGPATH, XLOGDIR "/%s", segname);
/*
* 首先判断是回收还是直接删除日志。
* 如果启用了wal_recycle、并且当前wal序列号小于最大回收号、中间的条件排除符号链接并确保待重命名文件为普通文件,使用InstallXLogFileSegment函数回收日志,并增加ckpt_segs_recycled和endlogSegNo
*/
if (wal_recycle &&
*endlogSegNo <= recycleSegNo &&
lstat(path, &statbuf) == 0 && S_ISREG(statbuf.st_mode) &&
InstallXLogFileSegment(endlogSegNo, path,
true, recycleSegNo, true))
{
/* 服务器日志级别为debug2时,会提示当前正在回收wal*/
ereport(DEBUG2,
(errmsg_internal("recycled write-ahead log file \"%s\"",
segname)));
CheckpointStats.ckpt_segs_recycled++;
/* Needn't recheck that slot on future iterations */
(*endlogSegNo)++;
}
/* 否则删除文件 */
else
{
int rc;
ereport(DEBUG2,
(errmsg_internal("removing write-ahead log file \"%s\"",
segname)));
/* 如果是windows */
#ifdef WIN32
snprintf(newpath, MAXPGPATH, "%s.deleted", path);
if (rename(path, newpath) != 0)
{
ereport(LOG,
(errcode_for_file_access(),
errmsg("could not rename file \"%s\": %m",
path)));
return;
}
/* 删除日志文件 */
rc = durable_unlink(newpath, LOG);
/* 否则直接删除 */
#else
/* 删除日志文件 */
rc = durable_unlink(path, LOG);
#endif
if (rc != 0)
{
/* Message already logged by durable_unlink() */
return;
}
CheckpointStats.ckpt_segs_removed++;
}
/* 清除.ready, .done标签 */
XLogArchiveCleanup(segname);
}
这里还有一个比较重要的函数是InstallXLogFileSegment
InstallXLogFileSegment函数负责日志回收重用,回收至recycleSegNo返回false。
/*
* Install a new XLOG segment file as a current or future log segment.
*
* This is used both to install a newly-created segment (which has a temp filename while it's being created) and to recycle an old segment.
*
*/
static bool
InstallXLogFileSegment(XLogSegNo *segno, char *tmppath,
bool find_free, XLogSegNo max_segno,
bool use_lock)
{
char path[MAXPGPATH];
struct stat stat_buf;
XLogFilePath(path, ThisTimeLineID, *segno, wal_segment_size);
/*
* We want to be sure that only one process does this at a time.
*/
if (use_lock)
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
/*
* find_free: if true, install the new segment at the first empty segno number at or after the passed numbers. If false, install the new segment exactly where specified, deleting any existing segment file there.
1) 在endlogSegNo和recycleSegNo之间找一个free slot num,即没有该段文件号的xlog文件
2) 将需要删除的WAL文件重命名为该free slot号的文件名
3) 如果没有找到free slot则直接删除WAL文件
*/
if (!find_free)
{
/* Force installation: get rid of any pre-existing segment file
没有找到free slot,直接删除WAL文件 */
durable_unlink(path, DEBUG1);
}
else
{
/* Find a free slot to put it in,
如果能找到,将需要删除的WAL文件重命名为该free slot号的文件名*/
while (stat(path, &stat_buf) == 0)
{
/*如果段号已经到达recycleSegNo,直接返回False,在上层函数RemoveXlogFile中进入删除逻辑*/
if ((*segno) >= max_segno)
{
/* Failed to find a free slot within specified range */
if (use_lock)
LWLockRelease(ControlFileLock);
return false;
}
/*段号+1,直到到达recycleSegNo*/
(*segno)++;
/* 回收并重命名 */
XLogFilePath(path, ThisTimeLineID, *segno, wal_segment_size);
}
}
/*
* Perform the rename using link if available, paranoidly trying to avoid
* overwriting an existing file (there shouldn't be one).
*/
if (durable_rename_excl(tmppath, path, LOG) != 0)
{
if (use_lock)
LWLockRelease(ControlFileLock);
/* durable_rename_excl already emitted log message */
return false;
}
if (use_lock)
LWLockRelease(ControlFileLock);
return true;
}
XLogFilePath定义如下
#define XLogFilePath(path, tli, logSegNo, wal_segsz_bytes) \
snprintf(path, MAXPGPATH, XLOGDIR "/%08X%08X%08X", tli, \
(uint32) ((logSegNo) / XLogSegmentsPerXLogId(wal_segsz_bytes)), \
(uint32) ((logSegNo) % XLogSegmentsPerXLogId(wal_segsz_bytes)))
参考
《PostgreSQL技术内幕:事务处理深度探索》第4章
PostgreSQL如何删除不使用的xlog文件 - 腾讯云开发者社区-腾讯云
PostgreSQL如何删除XLOG文件【补充】_yzs87的博客-CSDN博客
PostgreSQL 清理redo(xlog,wal,归档)的机制 及 如何手工清理 - Digoal.Zhou’s Blog
Postgresql中xlog生成和清理逻辑操作 - PostgreSQL - 服务器之家
PostgreSQL如何管理无用的WAL文件
PgSQL · 追根究底 · WAL日志空间的意外增长_weixin_30646505的博客-程序员信息网 - 程序员信息网
https://blog.csdn.net/qq_43687755/article/details/108968461