最近因为WAL日志重命名踩到大坑,一直很纠结WAL日志在什么情况下会被删除,什么情况下会被重命名,钻研一下这个部分。
一、 准备工作
1. 主要函数调用栈
首先无用WAL日志的清理发生检查点执行时,检查点执行核心函数为CreateCheckPoint。其中核心调用栈为CreateCheckPoint->RemoveOldXlogFiles->RemoveXlogFile。
CreateCheckPoint函数很复杂,从外往里看会很容易晕,所以我们倒过来,先从最内层的RemoveXlogFile开始研究。
2. Debug 准备
因为debug跑了很多次,后文各日志段号等不会完全一致,但本质是相同的。
- 先把wal_keep_size和max_wal_size调大(例如800M)
sql
alter system set wal_keep_size='800MB';
alter system set max_wal_size='800MB';
- 插入数据,生成一批wal日志(超过80M,即5个)
sql
truncate table tmp003;
insert into tmp003 select * from pgbench_accounts limit 100000;
insert into tmp003 select * from pgbench_accounts limit 100000;
- 然后调小(例如80M)
sql
alter system set wal_keep_size='80MB';
alter system set max_wal_size='80MB';
- Vscode跟踪checkpoint进程,在RemoveOldXlogFile函数打断点
- 客户端执行checkpoint命令,将触发日志回收操作
sql
checkpoint ;
注意这个跟踪属于高危操作,db进程有可能挂掉,千万别在生产环境随便执行
二、 RemoveXlogFile 函数
1. 源码学习
RemoveXlogFile中进行日志回收以及删除,回收是从不需要保留的日志中选择一部分重命名给未来使用(回收数量和两次checkpoint间产生wal量有关),其余的会被删除掉。
三个入参:* segname为正在处理的wal日志名;recycleSegNo为最大可重命名的段号;endlogSegNo为当前(最新)的wal日志end段号。
cpp
/*
* Recycle or remove a log file that's no longer needed.
* segname为正在处理的wal日志名;recycleSegNo为最大可重命名的段号;endlogSegNo为当前(最新)的wal日志段号。
*/
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);
}
2. Debug 过程(rename )
- *segname为正在处理的wal日志名:000000050000000300000092,对应日志段号为914
- recycleSegNo为最大可重命名的段号:937
- endlogSegNo为当前(最新)的wal日志段号:933
段号与日志名是怎么转换的?
Segname:000000050000000300000092 -> 392(16进制) -> 914(10进制)
recycleSegNo:937(10进制) -> 397(16进制) -> 000000050000000300000097
endlogSegNo:933(10进制) -> 3A5(16进制) -> 0000000500000003000000A5,这里它对应的是3A5而不是最新的日志名3A4,因为它对应的是日志段的结尾
符合*endlogSegNo <= recycleSegNo,将调用InstallXLogFileSegment为segname重命名。当前最新为0000000500000003000000A4,因此新名字为0000000500000003000000A5。
重命名后
随后跳出函数将ckpt_segs_recycled和endlogSegNo加一,调用XLogArchiveCleanup清理.ready和.done文件。
最后跳出RemoveXlogFile函数,回到RemoveOldXlogFiles(也可能下一次循环又进到RemoveXlogFile)。
3. Debug 过程(delete )
多轮重命名后,segname=0x1b13c3b "00000005000000030000009B",*endlogSegNo=938,不再符合*endlogSegNo <= recycleSegNo,进入else,本文件将被删除。
删除前后对比
下一轮segname=0x1b13d8b "000000050000000300000095",此后endlogSegNo不会再增加,将一直删除多余的文件。
这里有个问题,endlogSegNo和recycleSegNo是怎么来的?需要回到上层函数去看。
三、 RemoveOldXlogFiles 函数
RemoveOldXlogFiles 函数主要作用
- 获取endptr对应的日志段号endlogSegNo
- 调用XLOGfileslop函数,计算可预分配(重命名)的最大段号recycleSegNo
- 根据segno(最新可删除段号)构建日志名lastoff
- 读取pg_wal目录,循环读取其中日志文件(xlde->d_name),与最新可删除日志名lastoff对比
- 对未开启归档,或归档已完成的日志,调用UpdateLastRemovedPtr在共享内存中更新已被删除的位置
- 再调用RemoveXlogFile函数,真正进行删除(unlink)或重命名
cpp
/*
* 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);
}
四、 XLOGfileslop 函数
在本次检查点,有多少 WAL 段需要作为预分配的未来 XLOG 段回收,返回应预分配的最大段号。函数参数为最近一次redo点位置。预分配的过程是,为所有不再需要的旧文件重命名一个未来的日志号,直到预分配的文件数量达到XLOGfileslop返回的recycleSegNo。
cpp
/*
* 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;
}
所以本质上recycleSegNo是根据两次检查点之间的"距离"估算出来的。这里其他的值都是由pg参数指定的,例如CheckPointCompletionTarget就对应checkpoint_completion_target(默认0.9),唯独CheckPointDistanceEstimate还不知道是从哪来的,需要去外层找。这个外层其实还不在RemoveOldXlogFiles函数,而在更上层的CreateCheckPoint函数里。
五、 CreateCheckPoint 函数
终于我们又回到了开头,从整体看一下checkpoint在执行WAL日志清理时到底会干什么,以及CheckPointDistanceEstimate是在哪里算出来的。
checkpoint的核心作用之一------计算哪些WAL日志是过时可以清理的,并将其清理(删除或重命名):
- 根据两次checkpoint之间产生的wal日志量,计算CheckPointDistanceEstimate的值
- 获取redo点的日志段号,作为最旧的需要保留的_logSegNo(redo点之前的数据均已落盘,此前的wal日志就可以删除)
- 但实际中还会有一些参数控制wal日志保留量,因此需要用KeepLogSeg函数再次调整_logSegNo
- _logSegNo是最旧的需要保留的段号,因此减1则是最新的可以清理的段号
- 调用RemoveOldXlogFiles,清理已无用的日志文件
cpp
/*
* 如果前一个检查点存在,更新检查点之间的平均距离
*/
if (PriorRedoPtr != InvalidXLogRecPtr)
/* 估算两次checkpoint之间产生的wal日志量,假如上次估算量比这次估算的小,则更新为这次的估算量,否则适量增加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);
六、 UpdateCheckPointDistanceEstimate 函数
经过上面的分析,很明显CheckPointDistanceEstimate就是在这个函数算出来的,并用于后面recycleSegNo的计算。
cpp
UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);
1. 函数代码
cpp
/*
* 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,缓缓缩小。
2. 函数debug
UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);
- RedoRecPtr= 17997616128
- PriorRedoPtr= 17800674440
- nbytes=17997616128- 17800674440 = 196941688
- CheckPointDistanceEstimate= 105898150.9891763
- 符合CheckPointDistanceEstimate< nbytes,因此CheckPointDistanceEstimate被赋值为196941688
- lastredoptr=17997616128
- wal_segment_size=16777216
- min_wal_size_mb=80
- max_wal_size_mb=80
- 因此 minSegNo= maxSegNo= 17997616128/16777216 + 80/16 -1= 1072+5-1=1076
- 因此distance=(1+0.9)*196941688*1.1=216635856.8
- recycleSegNo=(17997616128+216635856.8)/16777216=1085
- 1085>1076,因此recycleSegNo=1076
最后,如果想看从上至下顺序的分析,可以参考:https://blog.csdn.net/Hehuyi_In/article/details/126209094