postgresql源码学习(58)—— 删除or重命名WAL日志?这是一个问题

news2024/11/25 10:51:31

       最近因为WAL日志重命名踩到大坑,一直很纠结WAL日志在什么情况下会被删除,什么情况下会被重命名,钻研一下这个部分。

一、 准备工作

1. 主要函数调用栈

       首先无用WAL日志的清理发生检查点执行时,检查点执行核心函数为CreateCheckPoint。其中核心调用栈为CreateCheckPoint->RemoveOldXlogFiles->RemoveXlogFile。

      CreateCheckPoint函数很复杂,从外往里看会很容易晕,所以我们倒过来,先从最内层的RemoveXlogFile开始研究。

2. Debug准备

因为debug跑了很多次,后文各日志段号等不会完全一致,但本质是相同的。

  • 先把wal_keep_size和max_wal_size调大(例如800M)
alter system set wal_keep_size='800MB';
alter system set max_wal_size='800MB';
  • 插入数据,生成一批wal日志(超过80M,即5个)
truncate table tmp003;
insert into tmp003 select * from pgbench_accounts limit 100000;
insert into tmp003 select * from pgbench_accounts limit 100000;
  • 然后调小(例如80M)
alter system set wal_keep_size='80MB';
alter system set max_wal_size='80MB';
  • Vscode跟踪checkpoint进程,在RemoveOldXlogFile函数打断点

  • 客户端执行checkpoint命令,将触发日志回收操作
checkpoint ;

注意这个跟踪属于高危操作,db进程有可能挂掉,千万别在生产环境随便执行

二、 RemoveXlogFile函数

1. 源码学习

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

       三个入参:* segname为正在处理的wal日志名;recycleSegNo为最大可重命名的段号;endlogSegNo为当前(最新)的wal日志end段号。

/*
 * 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)或重命名
/*
 * 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。

/*
 * 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,清理已无用的日志文件
/*
     * 如果前一个检查点存在,更新检查点之间的平均距离
     */
    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的计算。

UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);

1. 函数代码

/*
 * 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/778818.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

96、Kafka中Zookeeper的作用

Kafka中zk的作用 它是一个分布式协调框架。很好的将消息生产、消息存储、消息消费的过程结合在一起。在典型的Kafka集群中, Kafka通过Zookeeper管理集群配置&#xff0c;选举leader&#xff0c;以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到…

PyQt5:使用PyQtWebEngine

1. PyQt 5.13.0 1.1 安装PyQt pip install PyQt55.13.0 -i https://pypi.tuna.tsinghua.edu.cn/simple1.2 安装PyQtWebEngine pip install PyQtWebEngine5.13.0 -i https://pypi.tuna.tsinghua.edu.cn/simplepip list 1.3 测试 python文件 import sys from PyQt5.QtCore imp…

ARM——点灯实验

循环点灯 RCC寄存器使能GPIOE、GPIOF组寄存器 修改GPIOx组寄存器下的值 通过GPIOx_MODER寄存器设置为输出模式通过GPIOx_OTYOER寄存器设置为推挽输出类型通过GPIOx_OSPEEDR寄存器设置为低速输出通过GPIOx_PUPDR寄存器设置为禁止上下拉电阻点灯 通过GPIOx_ODR寄存器设置为高电…

day33哈希表

1.哈希表 常见的哈希表分为三类&#xff0c;数组&#xff0c;set&#xff0c;map&#xff0c;C语言的话是不是只能用数组和 2.例题 题目一&#xff1a; 分析&#xff1a;题目就是判断两个字符串出现的次数是否相同&#xff1b; 1&#xff09;哈希表26个小写字母次数初始化为0&…

K8S初级入门系列之一-概述

一、前言 K8S经过多年的发展&#xff0c;构建了云原生的基石&#xff0c;成为了云原生时代的统治者。我将用三个博客系列全面&#xff0c;循序渐进的介绍K8S相关知识。 初级入门系列&#xff0c;主要针对K8S初学者&#xff0c;以及希望对K8S有所了解的研发人员&#xff0c;重点…

【贪心算法part05】| 435.无重叠区间、763.划分字母区间、56.合并区间

目录 &#x1f388;LeetCode435. 无重叠区间 &#x1f388;LeetCode763.划分字母区间 &#x1f388;LeetCode 56.合并区间 &#x1f388;LeetCode435. 无重叠区间 链接&#xff1a;435.无重叠区间 给定一个区间的集合 intervals &#xff0c;其中 intervals[i] [starti, …

【雕爷学编程】Arduino动手做(55)--DHT11温湿度传感器模块3

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

力扣C++|一题多解之数学题专场(2)

目录 50. Pow(x, n) 60. 排列序列 66. 加一 67. 二进制求和 69. x 的平方根 50. Pow(x, n) 实现 pow(x,n)&#xff0c;即计算 x 的 n 次幂函数&#xff08;即x^n&#xff09;。 示例 1&#xff1a; 输入&#xff1a;x 2.00000, n 10 输出&#xff1a;1024.00000 示例…

【SQL应知应会】表分区(五)• MySQL版

欢迎来到爱书不爱输的程序猿的博客, 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 本文收录于SQL应知应会专栏,本专栏主要用于记录对于数据库的一些学习&#xff0c;有基础也有进阶&#xff0c;有MySQL也有Oracle 分区表 • MySQL版 前言一、分区表1.非分区表2.分区…

day42-servlet下拉查询/单例模式

0目录 1.Servlet实现下拉查询&#xff08;两表&#xff09; 2.单例模式 1.实战 1.1 创建工程&#xff0c;准备环境... 1.2 接口 1.3 重写方法 1.4 servlet 1.5 list.jsp list.jsp详解 2.单例模式 2.1 饿汉模式&#xff1a;在程序加载时直接创建对象&#…

8.4 利用集成运放实现的信号转换电路

在控制、遥控、遥测、近代生物物理和医学等领域&#xff0c;常常需要将模拟信号进行转换&#xff0c;如将信号电压转换成电流&#xff0c;将信号电流转换成电压&#xff0c;将直流信号转换成交流信号&#xff0c;将模拟信号转换成数字信号&#xff0c;等等。 一、电压 - 电流转…

【网络】socket——TCP网络通信 | 日志功能 | 守护进程

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《网络》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 上篇文章中本喵介绍了UDP网络通信的socket代码&#xff0c;今天介绍TCP网络通信的socket代码。 TCP &a…

Flutter系列(2):解决Flutter打包成APP无法访问网络资源

将flutter项目打包成Android后&#xff0c;发现无法访问网络图片&#xff0c;权限不足&#xff0c;没有授权网络权限&#xff0c;解决办法如下&#xff1a; 在android/app/src/main/AndroidManifest.xml中添加如下代码即可 <uses-permission android:name"android.perm…

c语言修炼之指针和数组笔试题解析(1.2)

前言&#xff1a; 书接上回&#xff0c;让我们继续开始今天的学习叭&#xff01;废话不多说&#xff0c;还是字符数组的内容上代码&#xff01; char *p是字符指针&#xff0c;*表示p是个指针&#xff0c;char表示p指向的对象类型是char型&#xff01; char*p"abcdef&q…

第一百一十四天学习记录:C++提高:类模板案例(黑马教学视频)

类模板案例 main.cpp代码&#xff1a; #include "myarray.hpp"void printIntArray(MyArray <int>& arr) {for (int i 0; i < arr.getSize(); i){cout << arr[i] << " ";}cout << endl; }void test01() {MyArray <int&…

AIGC分享交流平台、GPT-4、GPT实时联网、Claude

拥有无限畅谈的AI个人助理&#xff0c;提高效率和创造力&#xff0c;引领未来的智能生活&#xff1b; 不仅承载着最前沿的科技理念&#xff0c;更集成了对人工智能可能性的深度理解。 已支持基于GPT、Claude等主流大模型的对话内容生成、支持GPT联网查询实时信息&#xff1b;基…

241. 为运算表达式设计优先级

题目描述&#xff1a; 主要描述&#xff1a; 区间dp问题。 class Solution { public:vector<int> diffWaysToCompute(string expression) {vector<int> ops;for(int i0;i<expression.length();i){if(expression[i]>0&&expression[i]<9){int num…

Vue整体架构分解

Vue.js的整体架构可以分解为以下几个部分: 文章目录 1. 数据驱动2. 组件化3. 响应式系统4. 虚拟DOM5. 插件系统6. 单文件组件7. 模板编译总结 1. 数据驱动 Vue的一个核心特点是数据驱动。Vue会在初始化的时候给数据提供一个observe监听&#xff0c;当数据变化时&#xff0c;会…

醉梦仙踪:二叉树狂想曲,中序遍历的华丽穿梭

本篇博客会讲解力扣“94. 二叉树的中序遍历”的解题思路&#xff0c;这是题目链接。 如何对二叉树进行中序遍历呢&#xff1f;所谓中序遍历&#xff0c;即先遍历左子树&#xff0c;接着遍历根节点&#xff0c;最后遍历右子树的一种遍历方式。具体来说&#xff0c;假设有某一种“…

苹果11手机设置手机跟踪功能

苹果11手机设置手机跟踪功能&#xff0c;就算是手机丢了&#xff0c;也能通过查询手机定位在哪里。 第一步&#xff1a;点击Apple ID进入详情 第二步&#xff1a;点击“查找” 第三步&#xff1a; 第四步&#xff1a; 到了这步&#xff0c;就算是设置成功。 下面需要到官方查询…