postgresql源码学习(28)—— 事务日志⑧ - 日志真正落盘函数 XLogWrite

67 篇文章 51 订阅
34 篇文章 3 订阅

一、 XLogWrite概述

XLogWrite是XLOG落盘的最底层函数,负责将XLOG真正写入磁盘。

static void XLogWrite(XLogwrtRqst WriteRqst, bool flexible)

参数1表示请求写入的起点LSN,参数2表示是否灵活写入(If flexible == true, we don't have to write as far as WriteRqst)

1. 概念回顾

这个函数的细节较多,我们来慢慢分析,首先回顾几个概念

  • LSN:日志序列号,初始值为0,单调递增
  • log buffer:一个循环队列,队尾写满之后,会循环到队头继续写入。

      前面提到过,GetXLogBuffer会定位每个LSN对应的XLOG应该写入哪个buffer page。在上图中,LSN的1号页面中的内容会写入log buffer的1号页面,LSN的2号页面中的内容会写入log buffer的2号页面…。实际上LSN中没有页面的概念,这里只是方便说明。

     

      3号页面比较特殊,是一个partial page。partial page是指当前页面还未写满,由于log buffer是顺序写的,所以有且仅有一个partial page。由于落盘也是顺序落盘,所以当partial page(3号页面)落盘后,就说明相关的XLOG都已经落盘。

  • disk:物理的xlog文件。

         如何知道log buffer中的XLOG和物理块的对应关系呢?实际上也是通过LSN。一个segment对应一个单独的物理文件,一个page对应物理文件中的一个页,它们的大小相等、一一对应。所以通过简单的映射规则就能知道一个LSN对应的XLOG应该写入哪个物理块。

  • 段号 = LSN / XLogSegSize
  • 段内块偏移 = LSN % XLogSegSize

       当一个物理文件写满之后,会写下一个。在实际落盘时,是以页面为单位进行落盘,所以在落盘前需要找到每个页面的起始位置,然后将整个页面进行落盘。

2. 总体流程

综上所述,XLOG的落盘流程如下:

  • LogwrtResult(最后已落盘的LSN)为起点,依据LSN及映射规则在log buffer中获取对应的页面。
  • 依据LSN及映射规则获取页面需要写入的段号及段内页号。
  • buffer page写入对应的物理页面。

3. 对应XLogWrite代码实现流程 

  • part1:调用wirte,对WriteRqst.Write之前的XLOG进行写入(写入操作系统缓存,如果是异步提交,则不保证一定落盘)。这个部分是XLogWrite的核心,包含上述所有流程。
  • part2:调用flush,对part1写入的XLOG进行落盘。
  • part3:更新XLogCtl(全局)的LogwrtResult及LogwrtRqst。

       注意:上面说的write和flush指的是下文代码中的pg_pwrite函数 和 issue_xlog_fsync函数(会再调用pg_fdatasync函数),而不是XLogWrite和XLogFlush函数,不要弄混。

二、 调用write前的准备工作(part1)

        注意part 1部分所有的写入(可能也有些地方会写成落盘)都是指的写入os缓存,对于异步提交需要注意区分。

       part 1是XLogWrite的核心,分为多种场景。下面我们先讲解主要流程,然后再根据情况来添加代码。

最简单的场景:log buffer没有循环,segment 1也没有写满,可以容纳所有的XLOG

我们将XLogWrite的代码进行了简化,代码如下:

static void
XLogWrite(XLogwrtRqst WriteRqst, bool flexible)
{
    bool        ispartialpage;
    bool        last_iteration;
    bool        finishing_seg;
    bool        use_existent;
    int         curridx;
    int         npages;
    int         startidx;
    uint32      startoffset;

    /* We should always be inside a critical section here */
    Assert(CritSectionCount > 0);

    /*
     * Update local LogwrtResult (caller probably did this already, but...),更新本地已落盘的XLOG LSN
     */
    LogwrtResult = XLogCtl->LogwrtResult;

//npages用于记录需要落盘的页面数量;startidx表示第一个需要落盘的页面的下标;startoffset表示页面的起始写入偏移。这几个变量详见图3。
    npages = 0;
    startidx = 0;
    startoffset = 0;

/* 通过XLogRecPtrToBufIdx将LogwrtResult.Write转换为buffer page的下标,LogwrtResult.Write是第一个需要落盘的page对应的LSN,* 所以curridx为第一个page的数组下标。
*/
    curridx = XLogRecPtrToBufIdx(LogwrtResult.Write);

//当已落盘Write的LSN小于请求Write的LSN时,循环操作。每次循环对一个buffer page进行一系列处理,curridx表示当前正在处理的buffer page
    while (LogwrtResult.Write < WriteRqst.Write)
    {
        /*
         * 首先从xlblocks获取一个页面当前可以存放的XLOG的最大LSN
         */
        XLogRecPtr  EndPtr = XLogCtl->xlblocks[curridx];

// 做一个判断,已落盘Write的LSN不应该大于等于该buffer page的LSN上限,否则就说明后者已经Write完了。
        if (LogwrtResult.Write >= EndPtr)
            elog(PANIC, "xlog write request %X/%X is past end of log %X/%X",
                 LSN_FORMAT_ARGS(LogwrtResult.Write),
                 LSN_FORMAT_ARGS(EndPtr));

         /* Advance LogwrtResult.Write to end of current buffer page,将本地缓存的LogwrtResult.Write修改为当前log buffer page的结束位置。 */
        LogwrtResult.Write = EndPtr;
        /* 判断当前写入的page是不是partial page(结束页)。如果页是被填满的,这两个值应该相等,只有在页只被写入一部分时,WriteRqst.Write < LogwrtResult.Write */
        ispartialpage = WriteRqst.Write < LogwrtResult.Write;

        /* Add current page to the set of pending pages-to-dump,npages为0时,(重新)获取startidx,startoffset */
        if (npages == 0)
        {
            /* first of group */
            startidx = curridx;
            startoffset = XLogSegmentOffset(LogwrtResult.Write - XLOG_BLCKSZ,
                                            wal_segment_size);
        }
        npages++;

        /*
         * 判断当前是否为最后一次循环或最后一个页
         */
        last_iteration = WriteRqst.Write <= LogwrtResult.Write;

        /*
         * 判断当前是否为最后一个segment,后面场景会提到
         */
        finishing_seg = !ispartialpage &&
            (startoffset + npages * XLOG_BLCKSZ) >= wal_segment_size;

        // 这三个落盘条件对应我们要讨论的3种场景,后面会提到
        if (last_iteration ||
            curridx == XLogCtl->XLogCacheBlck ||
            finishing_seg)
        {
           // 落盘代码,暂时省略 
        }

        /* 如果落盘则表示WriteRqst.Write之前的所有XLOG都已经落盘,则结束循环
         * 注意在结束循环前,需要将LogwrtResult.Write改为WriteRqst.Write
         * 因为我们前面将LogwrtResult.Write改为了XLogCtl->xlblocks[curridx]
         * 而我们实际写入的内容仅限于WriteRqst.Write之前
         */

// ispartialpage用于表示partial page是否已经落盘
        if (ispartialpage)
        {
            /* Only asked to write a partial page */
            LogwrtResult.Write = WriteRqst.Write;
            break;
        }
        curridx = NextBufIdx(curridx);

        /* If flexible, break out of loop as soon as we wrote something */
        if (flexible && npages == 0)
            break;
    }

    Assert(npages == 0);
…

代码分析:

首先对三个非常重要的成员进行了初始化

        npagesstartidxstartoffset。这三个成员的作用如图3所示:npages用于记录需要落盘的页面数量;startidx表示第一个需要落盘的页面的下标;startoffset表示页面的起始写入偏移。

② 通过XLogRecPtrToBufIdx函数找到LogwrtResult.Write对应的buffer page。这个page其实就是第一个需要落盘的page。

③ 进入循环,每次循环对一个buffer page进行一系列处理,curridx表示当前正在处理的buffer page。

 while (LogwrtResult.Write < WriteRqst.Write)
  • 在循环体内部,首先从xlblocks获取一个页面当前可以存放的XLOG的最大lsn。
        /*
         * 从xlblocks获取一个页面当前可以存放的XLOG的最大LSN
         */
        XLogRecPtr  EndPtr = XLogCtl->xlblocks[curridx];

xlblocks

        xlblocks是XLogCtlData的又一个成员,是一个XLogRecPtr的数组,数组元组的个数为log buffer的页面数,xlblocks与buffer page一一对应

typedef struct XLogCtlData {
...
XLogRecPtr *xlblocks;		/* 1st byte ptr-s + XLOG_BLCKSZ */
...
}

typedef uint64 XLogRecPtr;

       前面讲过LSN到buffer page的转换,也就是说每个LSN都对应一个buffer page。反之每个buffer page都对应一个范围内的LSN。由于log buffer是循环队列,所以用xlblocks数组来表示某个buffer page当前可写入的XLOG的LSN的上限。

       由于buffer page的大小固定为XLOG_BLCKSZ,所以通过xlblocks-XLOG_BLCKSZ就可以得到该page可写入XLOG的LSN的下限。所以一个buffer page当前可以写入XLOG的LSN的范围为 [xlblocks-XLOG_BLCKSZ, xlblocks]。这个范围主要用来判断当前写入的XLOG是否会覆盖页面中之前写入的XLOG。

  • 在获取到EndPtr之后,首先进行校验,由于LogwrtResult.Write之后的页面都是需要落盘的,所以LogwrtResult.Write不可能>=EndPtr。
// 做一个判断,已落盘Write的LSN不应该大于等于该buffer page的LSN上限,否则就说明后者已经Write完了。
        if (LogwrtResult.Write >= EndPtr)
            elog(PANIC, "xlog write request %X/%X is past end of log %X/%X",
                 LSN_FORMAT_ARGS(LogwrtResult.Write),
                 LSN_FORMAT_ARGS(EndPtr));
  • 校验之后将LogwrtResult.Write修改为EndPtr。注意:LogwrtResult是本地缓存的LogwrtResult而不是全局LogwrtResult。接着判断当前写入的页面是不是partial page。
/* Advance LogwrtResult.Write to end of current buffer page,将本地缓存的LogwrtResult.Write修改为当前log buffer page的结束位置。 */
        LogwrtResult.Write = EndPtr;
        /* 判断当前写入的page是不是partial page(结束页)。如果页是被填满的,这两个值应该相等,只有在页只被写入一部分时,WriteRqst.Write < LogwrtResult.Write */
        ispartialpage = WriteRqst.Write < LogwrtResult.Write;
  • 接下来,在npages为0时获取startidx以及startoffset。
/* Add current page to the set of pending pages-to-dump */
        if (npages == 0)
        {
            /* first of group */
            startidx = curridx;
            startoffset = XLogSegmentOffset(LogwrtResult.Write - XLOG_BLCKSZ,
                                            wal_segment_size);
        }
        npages++;

这里需要特别注意startoffset的运算。如图4所示:

        假设初始时LogwrtResult.Write对应一个页面的中间位置,通过前面的流程,已经将LogwrtResult.Write修改为页面结束位置对应的LSN。所以LogwrtResult.Write - XLOG_BLCKSZ对应页面起始位置的LSN。由于是以页面为单位进行落盘,所以很显然,我们需要获取log buffer对应的物理页偏移。而前面说过将LSN % XLogSegSize就可以实现LSN到物理偏移的转换。所以startoffset = (LogwrtResult.Write - XLOG_BLCKSZ) % XLogSegSize;就得到了log buffer写盘的起始物理偏移

  • 接下来需要对页面数进行累加。然后判断当前页是否为最后一个需要落盘的页面。对于图2的场景,只有在这个时候才会开始真正的落盘(Flush)操作。
        /*
         * 判断当前是否为最后一次循环或最后一个页
         */
        last_iteration = WriteRqst.Write <= LogwrtResult.Write;

        // 实际上在最简单的场景只要判断 if (last_iteration) 即可,另外两个条件在后面的场景会提到
if (last_iteration ||
            curridx == XLogCtl->XLogCacheBlck ||
            finishing_seg)
        {
           // 正式调用Write写入os缓存,暂时省略 
        }
  • 最后,判断当前页面是否为partial page,如果是则表示WriteRqst.Write之前的所有XLOG都已经落盘,循环结束。否则继续循环。
/* 如果落盘则表示WriteRqst.Write之前的所有XLOG都已经落盘,则结束循环
         * 注意在结束循环前,需要将LogwrtResult.Write改为WriteRqst.Write
         * 因为我们前面将LogwrtResult.Write改为了XLogCtl->xlblocks[curridx]
         * 而我们实际写入的内容仅限于WriteRqst.Write之前
         */

// ispartialpage用于表示partial page是否已经落盘
if (ispartialpage)
        {
            /* Only asked to write a partial page */
            LogwrtResult.Write = WriteRqst.Write;
            break;
        }
//获取下一个buffer page
        curridx = NextBufIdx(curridx);

        /* If flexible, break out of loop as soon as we wrote something */
        if (flexible && npages == 0)
            break;

       OK,我们现在已经讲完了while (LogwrtResult.Write < WriteRqst.Write)循环的主要框架。在这个循环中主要做以下事情:

  • 每次获取一个buffer page
  • 校验每个page的xlblocks与LogwrtResult.Write是否合法
  • 第一次循环时获取startidx、startoffset
  • 累加npages
  • 最后一次循环时执行真正的落盘(FLush)操作

三、 调用write,写入os缓存(part1)

来看之前省略的正式调用Write写入os缓存的代码。

if (last_iteration ||
            curridx == XLogCtl->XLogCacheBlck ||
            finishing_seg)
        {
            char       *from;
            Size        nbytes;
            Size        nleft;
            int         written;
            instr_time  start;

            /* OK to write the page(s),写入的起点 */
            from = XLogCtl->pages + startidx * (Size) XLOG_BLCKSZ;
            // 总共要写入的bytes
            nbytes = npages * (Size) XLOG_BLCKSZ;
            // 剩余要写入的bytes
            nleft = nbytes;
            do
            {
                errno = 0;

                /* Measure I/O timing to write WAL data */
                if (track_wal_io_timing)
                    INSTR_TIME_SET_CURRENT(start);

                pgstat_report_wait_start(WAIT_EVENT_WAL_WRITE);
                /* 打开LogFile,写入数据到os缓存。written为已写入的量 */
                written = pg_pwrite(openLogFile, from, nleft, startoffset);
                pgstat_report_wait_end();

                /*
                 * Increment the I/O timing and the number of times WAL data
                 * were written out to disk.
                 */
                if (track_wal_io_timing)
                {
                    instr_time  duration;

                    INSTR_TIME_SET_CURRENT(duration);
                    INSTR_TIME_SUBTRACT(duration, start);
                    WalStats.m_wal_write_time += INSTR_TIME_GET_MICROSEC(duration);
                }

                WalStats.m_wal_write++;

                /* written<=0,说明写入报错了 */
                if (written <= 0)
                {
                    char        xlogfname[MAXFNAMELEN];
                    int         save_errno;

                    if (errno == EINTR)
                        continue;

                    save_errno = errno;
                    XLogFileName(xlogfname, ThisTimeLineID, openLogSegNo,
                                 wal_segment_size);
                    errno = save_errno;
                    ereport(PANIC,
                            (errcode_for_file_access(),
                             errmsg("could not write to log file %s "
                                    "at offset %u, length %zu: %m",
                                    xlogfname, startoffset, nleft)));
                }

                /* 下一次写入的起点 */
                nleft -= written;
                from += written;
                startoffset += written;
            // 当剩余要写入的量>0(还有要处理的),继续循环写入
            } while (nleft > 0);

            npages = 0;
        }

四、 两个特殊场景(part 1)

       第二部分我们介绍了最简单的一种场景,实际上还可能出现一些特殊场景需要处理。注意一下这部分还是属于part 1的。

1. 场景2

        当前segment也可以容纳所有的XLOG,但是log buffer已经写满一轮然后循环到了队头。此时page1和page2、page3不再连续,所以需要分两次进行落盘。也就是说当while (LogwrtResult.Write < WriteRqst.Write) 循环到log buffer的最后一个页面时,需要先将LogwrtResult.Write到该页面之间的所有页面都写盘,然后再继续循环。

        因此除了last_iteration时需要写盘,在curridx == XLogCtl->XLogCacheBlck时也需要写盘。XLogCacheBlck存放最大的log buffer页面下标,当curridx等于它时,说明已经到log buffer最后一个page了。

参考 https://blog.csdn.net/Hehuyi_In/article/details/125447500?spm=1001.2014.3001.5501

还记得之前这个写入的判断,场景2对应的就是 curridx == XLogCtl->XLogCacheBlck 这个条件。

if (last_iteration ||
            curridx == XLogCtl->XLogCacheBlck ||
            finishing_seg)
        {
           // 调用Write写入os缓存的代码
                 …
                 nleft -= written;
                from += written;
                startoffset += written;
            } while (nleft > 0);

            npages = 0;
             …
        }

这里重要的是将npages重置为0。针对上图的场景,page1落盘之后会获取下一个buffer page:

  • 显然,此时的curridex值为0指向队头的页面page2,然后继续循环。
  • 再次循环时,由于npages被重置为0,所以会重新获取startidx和startoffset,而startidx就指向了page2。
  • 如此当遍历到page3这个partial page时就会再次将page2和page3写盘。

2. 场景3

       还记得文章最开始这张图,这就是最后一种场景 —— 当前segment中空闲空间不足,需要写入到两个segment中。这种情况下需要先将buffer page中的一部分写入当前segment,并将这部分内容强制落盘(调用fsync),防止后面还需要将重新打开这个segment再进行落盘。

所以,我们需要添加finishing_seg相关的代码:

last_iteration = WriteRqst.Write <= LogwrtResult.Write;

        finishing_seg = !ispartialpage &&
            (startoffset + npages * XLOG_BLCKSZ) >= wal_segment_size;

        if (last_iteration ||
            curridx == XLogCtl->XLogCacheBlck ||
            finishing_seg)
        {
            // 调用Write写入os缓存的代码
                …
 nleft -= written;
                from += written;
                startoffset += written;
            } while (nleft > 0);

            npages = 0;

            /*
             * If we just wrote the whole last page of a logfile segment,fsync the segment immediately.  This avoids having to go back and re-open prior segments when an fsync request comes along later.
             * Doing it here ensures that one and only one backend will perform this fsync.
             * This is also the right place to notify the Archiver that the segment is ready to copy to archival storage, and to update the timer for archive_timeout, and to signal for a checkpoint if too many logfile segments have been used since the last checkpoint.
             */
            if (finishing_seg)
            {
                 // 强制执行fsync
                issue_xlog_fsync(openLogFile, openLogSegNo);

                /* signal that we need to wakeup walsenders later,发送请求唤醒walsenders进程 */
                WalSndWakeupRequest();

                LogwrtResult.Flush = LogwrtResult.Write;    /* end of page,更新已完成落盘Flush的位置 */

                 // XLog归档
                if (XLogArchivingActive())
                    XLogArchiveNotifySeg(openLogSegNo);

                 // 更新XLog日志的切换时间和LSN
                XLogCtl->lastSegSwitchTime = (pg_time_t) time(NULL);
                XLogCtl->lastSegSwitchLSN = LogwrtResult.Flush;

                /*
                 * Request a checkpoint if we've consumed too much xlog since
                 * the last one.  For speed, we first check using the local
                 * copy of RedoRecPtr, which might be out of date; if it looks
                 * like a checkpoint is needed, forcibly update RedoRecPtr and
                 * recheck. 判断是否需要执行检查点,如果需要则请求
                 */
                if (IsUnderPostmaster && XLogCheckpointNeeded(openLogSegNo))
                {
                    (void) GetRedoRecPtr();
                    if (XLogCheckpointNeeded(openLogSegNo))
                        RequestCheckpoint(CHECKPOINT_CAUSE_XLOG);
                }
            }

 至此,XLogWrite第一部分的核心功能终于走完。

五、 调用flush,对part1写入的数据强制落盘(part2)

         在第一部分,我们只是调用write将log buffer中的内容写入日志文件,但如果日志文件不是以O_SYNC或者O_DSYNC的方式打开的话,write函数无法保证成功即落盘(日志可能只是写入了操作系统缓存),所以还需要调用fsync来将系统缓存中的数据强制落盘。

/*
     * If asked to flush, do so
     */
    if (LogwrtResult.Flush < WriteRqst.Flush &&
        LogwrtResult.Flush < LogwrtResult.Write)
    {
        /*
         * Could get here without iterating above loop, in which case we might
         * have no open file or the wrong one.  However, we do not need to
         * fsync more than one file.
         */
        if (sync_method != SYNC_METHOD_OPEN &&
            sync_method != SYNC_METHOD_OPEN_DSYNC)
        {
            if (openLogFile >= 0 &&
                !XLByteInPrevSeg(LogwrtResult.Write, openLogSegNo,
                                 wal_segment_size))
                XLogFileClose();
            if (openLogFile < 0)
            {
                XLByteToPrevSeg(LogwrtResult.Write, openLogSegNo,
                                wal_segment_size);
                openLogFile = XLogFileOpen(openLogSegNo);
                ReserveExternalFD();
            }

            issue_xlog_fsync(openLogFile, openLogSegNo);
        }

        /* signal that we need to wakeup walsenders later */
        WalSndWakeupRequest();

        LogwrtResult.Flush = LogwrtResult.Write;
    }

六、 修改全局LogwrtResult和LogwrtRqst(part3)

完成了落盘操作之后,最后就是修改全局LogwrtResult和LogwrtRqst。

/*
     * Update shared-memory status
     *
     * We make sure that the shared 'request' values do not fall behind the
     * 'result' values.  This is not absolutely essential, but it saves some
     * code in a couple of places.
     */
    {
        SpinLockAcquire(&XLogCtl->info_lck);
        XLogCtl->LogwrtResult = LogwrtResult;
        if (XLogCtl->LogwrtRqst.Write < LogwrtResult.Write)
            XLogCtl->LogwrtRqst.Write = LogwrtResult.Write;
        if (XLogCtl->LogwrtRqst.Flush < LogwrtResult.Flush)
            XLogCtl->LogwrtRqst.Flush = LogwrtResult.Flush;
        SpinLockRelease(&XLogCtl->info_lck);
    }
}

     前面讲过,对于全局LogwrtResult和LogwrtRqst的修改需要同时持有WALWriteLock锁和info_lck锁,在XLogFlush调用XLogWrite之前就已经持有了WALWriteLock,所以这里只需要持有info_lck锁即可。

     至此,XLogWrite函数终于结束了,感天动地!

参考:PostgreSQL重启恢复---Log Buffer_obvious__的博客-CSDN博客

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hehuyi_In

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值