执行一次怎么会写入两次数据_wal页buffer写入wal文件的过程

87ebdcab22cc5642e32649c45e311429.png

在之前的博客中讲述了,多个backend如何能并发的向wal的页buffer中写入wal记录,这篇博客会讲述,wal页buffer刷写到磁盘文件的过程。

一、wal页buffer的产生

wal页buffer与数据buffer不同,正常情况下,wal页buffer初始化于内存,填充于内存,然后刷写入磁盘。只有在数据库启动时,会从磁盘中加载上一次停机未写完的page。

763bd7df91d65ed4a15203f87e5bdaac.png

buffer槽是指共享缓存中为wal页分配的固定数量(wal_buffers配置)的空间,相当于是一个page数组。给定一个lsn位置,可以通过如下宏计算出存储这个lsn的page从属的buffer槽的索引下标。

#define XLogRecPtrToBufIdx(recptr)  
    (((recptr) / XLOG_BLCKSZ) % (XLogCtl->XLogCacheBlck + 1))

wal页buffer初始化的方式:1. walwrite进程在完成一次wal整体写入后,会在未使用的buffer槽位置初始化下一个buffer 。 2. 一个backend进程需求的wal槽正在被一个较旧的page占用,那么本进程在等待旧的page刷盘后自己初始化这个buffer。关于wal页初始化可以参考AdvanceXLInsertBuffer()函数

二、wal页buffer的写入简述

在之前的博客中讲到过,将wal链数据从每一个backend中拷贝到wal页page的并发过程,这里再讲述一下wal页刷写到磁盘文件的过程。把wal页buffer写入wal文件分为几步?1.将wal页buffer写入操作系统缓存(write) 2.确保wal页buffer同步到磁盘(flush)。

但是在不同情况下,PostgreSQL对wal的write和flush是各不相同的,现在有一条DMl语句begin;insert into t1 values(1,1);执行后会产生wal日志,如下说明几种场景下其 write和flush的方式

2.1 无提交写入

本事务没有提交,而且没有并发事务提交,这种情况下,walwrite进程在timeout后会刷write并flush wal日志页buffer。函数调用关系为XLogBackgroundFlush()->XLogWrite()

2.2 发生事务同步提交

执行完上述insert后,如果本进程发生了同步提交或者有并发进程发生了同步提交,那么发生提交的进程会积极的去调用XLogFlush(lsn1)函数去write和flush数据。这会使lsn1之前的wal日志都完成write和flush(如果比lsn1大的wal位置也准备好了write,那么此时也不介意做点超前的flush)。

因为write的时候需要获取WALWriteLock锁,所以有可能发生提交的进程获取到这个锁后发现,自己想写的数据已经被别人写完了。

2.3 发生事务异步提交

执行完上述insert后,如果本进程发生了异步提交或者有并发进程发生了异步提交,那么发生提交的进程会唤醒walwrite进程。被唤醒的walwrite进程会执行write操作,同时根据wal_write_delay和wal_writer_flush_after参数判断此时walwrite进程是否进行flush操作。

也就是说异步提交之后,只会保证walwrite成功,而没有保证flush,也就是说在异步提交的情况下,如果操作系统崩溃可能会导致wal_write_delay时间或wal_writer_flush_after日志量的数据丢失。当然换来的是数据库性能的提升。

2.4 组提交

组提交发生在事务同步提交,且正确配置了commit_delay和commit_siblings参数。当一个backend提交事务且已经获得写wal的权限后,会看一下当前正在运行的事务是否多于commit_siblings个,如果是则会等待commit_delay时间,以期望一次完成更多的wal日志flush。这也是为了提升数据库整体性能。

2.5 backend初始化wal页buffer

当某些极端情况下,会出现一个进程去初始化wal页buffer的情况,此时这个进程可能需要先完成之前的wal buffer的刷写操作,详情参考AdvanceXLInsertBuffer()

三、wal页buffer写入调度

从上面的描述可以看出,walwrite和各个backend进程都可能会去写文件,而且还有了write和flush不同步的情况,这一节,简单说明PostgreSQL中对write和flush位置的记录和控制。

typedef struct XLogwrtRqst
{
    XLogRecPtr  Write;          /* last byte + 1 to write out */
    XLogRecPtr  Flush;          /* last byte + 1 to flush */
} XLogwrtRqst;

typedef struct XLogwrtResult
{
    XLogRecPtr  Write;          /* last byte + 1 written out */
    XLogRecPtr  Flush;          /* last byte + 1 flushed */
} XLogwrtResult;

typedef struct XLogCtlData
{
    ...
    XLogwrtRqst LogwrtRqst;
    ...
    XLogwrtResult LogwrtResult;
    ...
}

LogwrtRqst.Write意义是,一次wal写动作(不包括flush)能包括的最大lsn,因此当往一个新的wal页中写入一个完整记录时,会更新这个值。另外事务提交和一些必须要完成wal同步落盘的操作后也会更新这个值。

LogwrtRqst.Flush的意义是,一次wal写动作(包括flush)能包括的最大lsn,事务同步提交、一些必须要完成wal同步的操作和walwrite进程会更新这个值。

LogwrtResult.Write意义是,已经完成wal写的lsn。

LogwrtResult.Flush意义是,已经完成wal同步的lsn。

3.1 发生事务同步提交

这时会以事务提交的lsn值更新LogwrtRqst.Write和LogwrtRqst.Flush的值,并以此为基础去同步wal日志。完成后同时更新LogwrtResult.Write和LogwrtResult.Flush的值。

下面画一组图来说明这种情况下的刷写调度

图1-1 初始状态

dedb5c47086629dd31d69f2c4cbbc6a1.png

这时有ABCDE五个进程,获取了插入锁槽21643,并且预置了自己想要写的wal位置,现在是正在COPY的过程。

图1-2 一个进程完成COPY

c9a88bca5fc87790a8588cdd0bbd3db8.png

假设进程B现在完成了COPY过程,而却没有发生提交。那么它写完后产生上面红字部分的变化。最大的变化是插入锁槽1变为了空闲状态。

图1-3 一个进程发生提交

e59d3a0e22ce44d8afd40cfbf254edeb.png

这时进程D完成了COPY过程,而且假设进程D需要提交。那么此时会把LogwrtRqst.Write和LogwrtRqst.Flush都置为lsn_b,然后想调用`XLogWrite()函数去完成lsn_b之前的wal的write和flush。

但是此时进程A和进程C还没写完呢,所以在调用XLogWrite()之前需要等待lsn_b之前的wal数据都已经COPY完了,在这里就是指需要进程A和进程C完成。

代码中实现这个等待的方式是,遍历一次插入锁槽,比如现在的0,1号锁槽是空闲的,直接跳过;2号锁槽是被占用的,那么只需要等待2号锁槽被释放一次(说明进程A已经完成了这次预置的空间的写入);

图1-3-1 等待过程中,又并发发生了锁槽占用

a0409c1574d894b2d57b72ec6aea4497.png

现在因为进程A完成COPY,索引2号锁槽被释放了。但是我们现在假设又有两个并发进程F和G发生了waL预置,且占用了0和5号锁槽。

此时D进程会等待依次判断3,4,5,6,7等待其发生锁槽释放,或者等待锁槽发生LSN变化(有兴趣的同学我们可以探讨,这里不再展开)。当前不会再关心发生新变化的0号锁槽。

图1-3-2 完成锁槽遍历

aadb6c7a68104565b42d253295e09945.png

锁槽遍历完成之后,进程CEG一定都完成了当前预置空间的写入,现在可以保证lsn_b之前的wal日志都已经写完了(虽然我们可能多等了一些时间)。

图1-4 完成write和flush操作

d0820caaadba4c02a8750219731b4c5e.png

现在进程D终于可以愉快的完成写文件了,最后更新LogwrtResult.Write=lsn_b,LogwrtResult.Flush=lsn_b

(注意:在并发场景下最后LogwrtResult.Write和LogwrtResult.Flush的值可能会大于lsn_b)

3.2 walwrite进程写wal文件

这时会首先保证的LogwrtRqst.Write之前的wal位置都完成COPY,然后执行wal写文件(write),但是不一定会写到LogwrtRqst.Write的位置(这是效率为先的考虑点)。

正常情况下walwriter进程在一次唤醒中,只会调用一次write()系统接口,因此如果发生wal段切换,或者到达wal buffer数组的最后一个槽,那么会把后面的write任务交给下一次walwrite进程唤醒。

关于copy等待的部分我们在图1系列中已经讲述了,这里画一下在一个walwrite周期内为什么会写不完。

图2-1 跨wal段导致的部分写

16ee1f6e900a3557d23c13102ce660dd.png

如图中,在一个walwrite进程周期内,根据LogwrtRqst.Write的值,需要写page1~page4。但是page2和page4不在同一个wal段内,因此为了完成这四个page的写文件,就需要至少两次write()系统调用。但是walwrite进程太懒只想做一次write()调用,那只能把剩下的工作交给下一次walwrite进程被唤醒,或者交给backend进程完成写任务。

图2-2 到达buffer末尾导致的部分写

4e73ee2d7ba308ca0d9d302a94d48975.png

这种情况下,槽M是最后一个wal页buffer数组的最后一个位置,槽0是wal页buffer数组的第一个位置。因此M和0是不连续的,因此也无法在一次write()调用中同时完成page3和page4的写入。

3.3 发生事务异步提交时

这时会首先更新LogwrtRqst.Write的值,然后根据wal_write_delay和wal_writer_flush_after决定是否更新LogwrtRqst.Flush值,因此会发生只write而不flush的情况。然后根据实际情况更新LogwrtResult的值。

图3-1 异步提交的必达部分

06b149b9ee4975247dfcf1924d62adec.png

异步提交引起的wal日志写操作是由walwrite进程完成的,只不过异步提交进程给了一个asyncXactLSN值,walwrite进程必须完成asyncXactLSN位置的写入,在上图中的情况下必须调用两次write()系统接口。

四、结论

最近仔细研究了,wal的buffer的产生和写入文件的过程,因此写了《PostgreSQL的wal日志并发写入机制》和本文。但是也没有研究的非常透彻,很多地方也是知其然而不知其所以然,比如大多数设计都是为了更好的性能,但不知为什么这是最好的设计,或者我们可以有改进方案?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值