• postgresql源码学习(35)—— 检查点⑤-检查点中的XLog清理机制


           前篇我们提到,检查点的工作之一是删除无用的日志文件,本篇我们来看看其中具体的计算和删除函数。前文中相关代码如下(在6. 删除无用的日志文件):

    postgresql源码学习(32)—— 检查点④-核心函数CreateCheckPoint_Hehuyi_In的博客-CSDN博客

    1. /*
    2. * 如果前一个检查点存在,更新检查点之间的平均距离
    3. */
    4. if (PriorRedoPtr != InvalidXLogRecPtr)
    5. /* 估算两次checkpoint之间产生的xlog量,假如上次估算量比这次估算的小,则更新为这次的估算量,否则适量增加CheckPointDistanceEstimate =(0.90 * CheckPointDistanceEstimate + 0.10 * (double) nbytes); */
    6. UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);
    7. /* 获取redo点的日志段号,作为最旧的需要保留的_logSegNo */
    8. XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
    9. /* 根据max_slot_wal_keep_size和wal_keep_size两个参数设置,再次调整最旧的需要保留的_logSegNo */
    10. KeepLogSeg(recptr, &_logSegNo);
    11. /* 如果_logSegNo是已经过时的复制槽,需要重新计算 */
    12. if (InvalidateObsoleteReplicationSlots(_logSegNo))
    13. {
    14. /*
    15. * Some slots have been invalidated; recalculate the old-segment
    16. * horizon, starting again from RedoRecPtr.
    17. */
    18. XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
    19. KeepLogSeg(recptr, &_logSegNo);
    20. }
    21. /* 前面_logSegNo是最旧的需要保留的段号,因此减1则是最新的可以删除的段号 */
    22. _logSegNo--;
    23. /* 删除已无用的日志文件 */
    24. RemoveOldXlogFiles(_logSegNo, RedoRecPtr, recptr);

    主要函数和宏定义如下

    • UpdateCheckPointDistanceEstimate函数:更新检查点之间的平均距离,估算两次checkpoint之间产生的xlog量
    • XLByteToSeg宏定义:根据RedoRecPtr(参数1)计算对应日志段号
    • KeepLogSeg函数:确定需要保留的日志(即待清理XLog的终点)
    • RemoveOldXlogFiles函数:真正删除XLog

    一、 UpdateCheckPointDistanceEstimate函数

          估算两次checkpoint之间产生的xlog量,主要用于后面XLOGfileslop函数的日志预分配。

    1. // RedoRecPtr是本次检查点位置,PriorRedoPtr是上次检查点位置。因此,传入的参数是本次实际产生的日志量。
    2. UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);

           如果上次估算量比这次实际产生的要小,则将估算值更新为这次产生的量。否则,采用平滑算法对估计值进行平滑估算,即增量内容占10%的比重。

    1. /*
    2. * Update the estimate of distance between checkpoints.
    3. *
    4. * The estimate is used to calculate the number of WAL segments to keep
    5. * preallocated, see XLOGfileslop().
    6. */
    7. static void
    8. UpdateCheckPointDistanceEstimate(uint64 nbytes)
    9. {
    10. /* 本次产生的日志量 */
    11. PrevCheckPointDistance = nbytes;
    12. /* 如果上次估算量CheckPointDistanceEstimate比这次实际产生的要小,则将估算值更新为这次产生的量 */
    13. if (CheckPointDistanceEstimate < nbytes)
    14. CheckPointDistanceEstimate = nbytes;
    15. else
    16. /* 否则,按下面的算法估算 */
    17. CheckPointDistanceEstimate =
    18. (0.90 * CheckPointDistanceEstimate + 0.10 * (double) nbytes);
    19. }

    例如上次估算值为100,实际值为50,则本次估算值应为:0.9*100+0.1*50=95,缓缓缩小。

    二、 XLByteToSeg定义

    1. // 获取redo点的日志段号_logSegNo
    2. XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);

    很简单的一个定义

    1. /*
    2. * Compute a segment number from an XLogRecPtr.
    3. */
    4. #define XLByteToSeg(xlrp, logSegNo, wal_segsz_bytes) \
    5. logSegNo = (xlrp) / (wal_segsz_bytes)

    三、 KeepLogSeg函数

    在学习这个函数之前,有两个与XLog清理相关的参数需要了解:

    • max_slot_wal_keep_size:日志复制进程能落后于当前事务的写入点。如果超过此大小,则可以撤销该日志复制的WalSender进程。
    • wal_keep_size:要为日志复制进程保留多少日志量。旧版本使用的是wal_keep_segments参数(要为日志复制进程保留多少个日志文件),在pg 13中,该参数已被移除

    在pg 14版本已查询不到该参数

           由于主库需要为备库保留日志,这里 KeepLogSeg(recptr, &_logSegNo); 对比redo点日志段号&_logSegNo、当前事务日志段号以及上面两个参数,初步计算清理XLog的位置信息。

    1. /*
    2. * 由于wal_keep_size或者复制槽有额外保留XLog的要求,本函数重新定位*logSegNo指针至最旧的需要保留的日志段
    3. */
    4. static void
    5. KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
    6. {
    7. XLogSegNo currSegNo;
    8. XLogSegNo segno;
    9. XLogRecPtr keep;
    10. //根据recptr计算当前事务日志位置段号currSegNo
    11. XLByteToSeg(recptr, currSegNo, wal_segment_size);
    12. segno = currSegNo;
    13. /*
    14. * 第一次调整,根据max_slot_wal_keep_size参数,先计算复制槽需要保留多少日志
    15. */
    16. // 每个日志复制槽位中都记录了当前复制的最小LSN,下面函数进行汇总,并得到所有日志槽中最最小的LSN,大于此LSN的日志即尚未复制完成。
    17. keep = XLogGetReplicationSlotMinimumLSN();
    18. if (keep != InvalidXLogRecPtr)
    19. {
    20. /* 获取复制槽最小LSN对应的日志段号segno */
    21. XLByteToSeg(keep, segno, wal_segment_size);
    22. /* 如果设置了 max_slot_wal_keep_size */
    23. if (max_slot_wal_keep_size_mb >= 0)
    24. {
    25. uint64 slot_keep_segs;
    26. /* 由参数设置的size转换为需保留的日志段数 */
    27. slot_keep_segs =
    28. ConvertToXSegs(max_slot_wal_keep_size_mb, wal_segment_size);
    29. /* 计算当前日志段号currSegNo与最早可清理的segno间的差值,如果已经超出需保留的日志段数slot_keep_segs,则将segno往前推进,此时主从复制可能会受影响 */
    30. if (currSegNo - segno > slot_keep_segs)
    31. segno = currSegNo - slot_keep_segs;
    32. }
    33. }
    34. /*
    35. * 第二次调整,根据wal_keep_size参数,再计算需要保留多少日志
    36. */
    37. if (wal_keep_size_mb > 0)
    38. {
    39. uint64 keep_segs;
    40. /* 还是由参数设置的size转换为需保留的日志段数 */
    41. keep_segs = ConvertToXSegs(wal_keep_size_mb, wal_segment_size);
    42. if (currSegNo - segno < keep_segs)
    43. {
    44. /* avoid underflow, don't go below 1,避免相减后段号小于1 */
    45. if (currSegNo <= keep_segs)
    46. segno = 1;
    47. else
    48. /* 否则,继续前推segno,此时主从复制可能会受影响 */
    49. segno = currSegNo - keep_segs;
    50. }
    51. }
    52. /* don't delete WAL segments newer than the calculated segment,如果segno与*logSegNo不相等,则重新设置*logSegNo为新计算出的最旧XLog需保留点segno */
    53. if (segno < *logSegNo)
    54. *logSegNo = segno;
    55. }

    原理如下图所示

    四、 RemoveOldXlogFiles函数

        在确定可清理位置之后,调用RemoveOldXlogFiles函数清理旧的XLog。该函数会调用RemoveXlogFile,真正清理日志文件。

        同样在学习函数定义前,我们先看一些参数定义:

    • wal_recycle:待删除日志是否以重命名方式循环利用
    • min_wal_size:日志清理后,WAL文件中最少需要保证有多少空间。若小于此参数,需要额外分配
    • max_wal_size:日志清理后,WAL文件中最多能有多少空间。若大于此参数,多余部分也会被清理
    • checkpoint_completion_target:检查点目标完成时间
    • checkpoint_timeout:检查点超时时间

    1. /*
    2. * Recycle or remove all log files older or equal to passed segno.
    3. *
    4. * endptr is current (or recent) end of xlog, and lastredoptr is the
    5. * redo pointer of the last checkpoint. These are used to determine
    6. * whether we want to recycle rather than delete no-longer-wanted log files.
    7. */
    8. static void
    9. RemoveOldXlogFiles(XLogSegNo segno, XLogRecPtr lastredoptr, XLogRecPtr endptr)
    10. {
    11. DIR *xldir;
    12. struct dirent *xlde;
    13. char lastoff[MAXFNAMELEN];
    14. XLogSegNo endlogSegNo;
    15. XLogSegNo recycleSegNo;
    16. /* 获取endptr对应的日志段号endlogSegNo */
    17. XLByteToSeg(endptr, endlogSegNo, wal_segment_size);
    18. /*在本次检查点,有多少 WAL 段需要作为预分配的未来 XLOG 段回收?返回应预分配的最大段号*/
    19. recycleSegNo = XLOGfileslop(lastredoptr);
    20. /*构建一个XLog日志名,用于判断,该文件之前的xlog可以删除。用不到时间线,所以可以使用0 */
    21. XLogFileName(lastoff, 0, segno, wal_segment_size);
    22. elog(DEBUG2, "attempting to remove WAL segments older than log file %s",
    23. lastoff);
    24. /* 获取XLog目录 */
    25. xldir = AllocateDir(XLOGDIR);
    26. /* 读取目录中的文件 */
    27. while ((xlde = ReadDir(xldir, XLOGDIR)) != NULL)
    28. {
    29. /* 忽略非XLog文件 */
    30. if (!IsXLogFileName(xlde->d_name) &&
    31. !IsPartialXLogFileName(xlde->d_name))
    32. continue;
    33. /* 跳过时间线部分比较日志文件名,如果当前段号<=回收点段号,并且该日志已经归档(开归档的情况下),就可以回收或者删除 */
    34. if (strcmp(xlde->d_name + 8, lastoff + 8) <= 0)
    35. {
    36. /* 如果没有开启归档:总是TRUE;否则,检查日志是否归档完成(即pg_wal/archive_status目录下是不是已经存在对应的.done文件) */
    37. if (XLogArchiveCheckDone(xlde->d_name))
    38. {
    39. /* Update the last removed location in shared memory first,首先在共享内存中更新已被删除的位置 */
    40. UpdateLastRemovedPtr(xlde->d_name);
    41. /* 调用RemoveXlogFile函数真正进行删除,函数里使用unlink删除日志 */
    42. RemoveXlogFile(xlde->d_name, recycleSegNo, &endlogSegNo);
    43. }
    44. }
    45. }
    46. FreeDir(xldir);
    47. }

    XLog的重命名原理

    这里我们要再详细看两个函数:

    1. /* 计算回收文件重命名的未来最大文件段号recycleSegNo */
    2. recycleSegNo = XLOGfileslop(lastredoptr);
    1. /* 调用RemoveXlogFile函数真正进行删除 */
    2. RemoveXlogFile(xlde->d_name, recycleSegNo, &endlogSegNo);

    五、 XLOGfileslop

           在本次检查点,有多少 WAL 段需要作为预分配的未来 XLOG 段回收?返回应预分配的最大段号。函数参数为最近一次redo点位置。

           预分配的过程是,为所有不再需要的旧文件重命名一个未来的日志号,直到预分配的文件数量达到XLOGfileslop返回的recycleSegNo。

    1. /*
    2. * At a checkpoint, how many WAL segments to recycle as preallocated future XLOG segments? Returns the highest segment that should be preallocated.
    3. */
    4. static XLogSegNo
    5. XLOGfileslop(XLogRecPtr lastredoptr)
    6. {
    7. XLogSegNo minSegNo;
    8. XLogSegNo maxSegNo;
    9. double distance;
    10. XLogSegNo recycleSegNo;
    11. /* 根据min_wal_size和max_wal_size参数设置,计算最小和最大段号 */
    12. minSegNo = lastredoptr / wal_segment_size +
    13. ConvertToXSegs(min_wal_size_mb, wal_segment_size) - 1;
    14. maxSegNo = lastredoptr / wal_segment_size +
    15. ConvertToXSegs(max_wal_size_mb, wal_segment_size) - 1;
    16. /*估算下一次checkpoint结束时日志位置*/
    17. distance = (1.0 + CheckPointCompletionTarget) * CheckPointDistanceEstimate;
    18. /* add 10% for good measure. */
    19. distance *= 1.10;
    20. recycleSegNo = (XLogSegNo) ceil(((double) lastredoptr + distance) /
    21. wal_segment_size);
    22. /* recycleSegNo不能小于minSegNo,也不能大于maxSegNo */
    23. if (recycleSegNo < minSegNo)
    24. recycleSegNo = minSegNo;
    25. if (recycleSegNo > maxSegNo)
    26. recycleSegNo = maxSegNo;
    27. return recycleSegNo;
    28. }

    六、 RemoveXlogFile

           RemoveXlogFile中进行日志回收以及删除,回收是从不需要保留的日志中选择一部分来给未来使用(回收数量和两次checkpoint间产生wal量有关系),其余的会被删除掉。

    1. /*
    2. * Recycle or remove a log file that's no longer needed.
    3. *
    4. * segname为待处理文件名; recycleSegNo为待回收段号;endlogSegNo为当前(或最近)的XLog结束段号。如果是对段进行回收,endlogSegNo会增加,这样它就不会在未来调用此函数时被重复检查。
    5. */
    6. static void
    7. RemoveXlogFile(const char *segname, XLogSegNo recycleSegNo,
    8. XLogSegNo *endlogSegNo)
    9. {
    10. char path[MAXPGPATH];
    11. #ifdef WIN32
    12. char newpath[MAXPGPATH];
    13. #endif
    14. struct stat statbuf;
    15. snprintf(path, MAXPGPATH, XLOGDIR "/%s", segname);
    16. /*
    17. * 首先判断是回收还是直接删除日志。
    18. * 如果启用了wal_recycle、并且当前wal序列号小于最大回收号、并且(中间那个条件没看懂),使用InstallXLogFileSegment函数回收日志,并增加ckpt_segs_recycled和endlogSegNo
    19. */
    20. if (wal_recycle &&
    21. *endlogSegNo <= recycleSegNo &&
    22. lstat(path, &statbuf) == 0 && S_ISREG(statbuf.st_mode) &&
    23. InstallXLogFileSegment(endlogSegNo, path,
    24. true, recycleSegNo, true))
    25. {
    26. /* 服务器日志级别为debug2时,会提示当前正在回收wal*/
    27. ereport(DEBUG2,
    28. (errmsg_internal("recycled write-ahead log file \"%s\"",
    29. segname)));
    30. CheckpointStats.ckpt_segs_recycled++;
    31. /* Needn't recheck that slot on future iterations */
    32. (*endlogSegNo)++;
    33. }
    34. /* 否则删除文件 */
    35. else
    36. {
    37. int rc;
    38. ereport(DEBUG2,
    39. (errmsg_internal("removing write-ahead log file \"%s\"",
    40. segname)));
    41. /* 如果是windows */
    42. #ifdef WIN32
    43. snprintf(newpath, MAXPGPATH, "%s.deleted", path);
    44. if (rename(path, newpath) != 0)
    45. {
    46. ereport(LOG,
    47. (errcode_for_file_access(),
    48. errmsg("could not rename file \"%s\": %m",
    49. path)));
    50. return;
    51. }
    52. /* 删除日志文件 */
    53. rc = durable_unlink(newpath, LOG);
    54. /* 否则直接删除 */
    55. #else
    56. /* 删除日志文件 */
    57. rc = durable_unlink(path, LOG);
    58. #endif
    59. if (rc != 0)
    60. {
    61. /* Message already logged by durable_unlink() */
    62. return;
    63. }
    64. CheckpointStats.ckpt_segs_removed++;
    65. }
    66. /* 清除.ready, .done标签 */
    67. XLogArchiveCleanup(segname);
    68. }

    这里还有一个比较重要的函数是InstallXLogFileSegment

    七、 InstallXLogFileSegment函数

    InstallXLogFileSegment函数负责日志回收重用,回收至recycleSegNo返回false

    1. /*
    2. * Install a new XLOG segment file as a current or future log segment.
    3. *
    4. * 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.
    5. *
    6. */
    7. static bool
    8. InstallXLogFileSegment(XLogSegNo *segno, char *tmppath,
    9. bool find_free, XLogSegNo max_segno,
    10. bool use_lock)
    11. {
    12. char path[MAXPGPATH];
    13. struct stat stat_buf;
    14. XLogFilePath(path, ThisTimeLineID, *segno, wal_segment_size);
    15. /*
    16. * We want to be sure that only one process does this at a time.
    17. */
    18. if (use_lock)
    19. LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
    20. /*
    21. * 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.
    22. 1) 在endlogSegNo和recycleSegNo之间找一个free slot num,即没有该段文件号的xlog文件
    23. 2) 将需要删除的WAL文件重命名为该free slot号的文件名
    24. 3) 如果没有找到free slot则直接删除WAL文件
    25. */
    26. if (!find_free)
    27. {
    28. /* Force installation: get rid of any pre-existing segment file没有找到free slot,直接删除WAL文件 */
    29. durable_unlink(path, DEBUG1);
    30. }
    31. else
    32. {
    33. /* Find a free slot to put it in,如果能找到,将需要删除的WAL文件重命名为该free slot号的文件名*/
    34. while (stat(path, &stat_buf) == 0)
    35. {
    36. /*如果段号已经到达recycleSegNo,直接返回False,在上层函数RemoveXlogFile中进入删除逻辑*/
    37. if ((*segno) >= max_segno)
    38. {
    39. /* Failed to find a free slot within specified range */
    40. if (use_lock)
    41. LWLockRelease(ControlFileLock);
    42. return false;
    43. }
    44. /*段号+1,直到到达recycleSegNo*/
    45. (*segno)++;
    46. /* 回收并重命名 */
    47. XLogFilePath(path, ThisTimeLineID, *segno, wal_segment_size);
    48. }
    49. }
    50. /*
    51. * Perform the rename using link if available, paranoidly trying to avoid
    52. * overwriting an existing file (there shouldn't be one).
    53. */
    54. if (durable_rename_excl(tmppath, path, LOG) != 0)
    55. {
    56. if (use_lock)
    57. LWLockRelease(ControlFileLock);
    58. /* durable_rename_excl already emitted log message */
    59. return false;
    60. }
    61. if (use_lock)
    62. LWLockRelease(ControlFileLock);
    63. return true;
    64. }

    XLogFilePath定义如下

    1. #define XLogFilePath(path, tli, logSegNo, wal_segsz_bytes) \
    2. snprintf(path, MAXPGPATH, XLOGDIR "/%08X%08X%08X", tli, \
    3. (uint32) ((logSegNo) / XLogSegmentsPerXLogId(wal_segsz_bytes)), \
    4. (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

  • 相关阅读:
    如何恢复被MALLOX勒索病毒加密的重要数据?
    vue2/vue3 v-if与v-show的区别 | 触发的生命周期
    一文搞定异步爬虫框架Scrapy环境的安装
    前端进击笔记第十一节 改善编程思维:从事件驱动到数据驱动
    【CKA考试笔记】六、存储管理
    使用.Net对图片进行裁剪、缩放、与加水印
    MySQL数据库查询对象空值判断与Java代码示例【含面试题】
    基于多种优化算法及神经网络的光伏系统控制(Matlab代码实现)
    STL容器适配器之stack与queue
    球体投影到像素空间的大小
  • 原文地址:https://blog.csdn.net/Hehuyi_In/article/details/126209094