写入位置0x0000000时发生访问冲突_PostgreSQL的wal日志并发写入机制

1e7a53549852508b59036bdf42818f56.png

数据库用wal日志的顺序写,替代数据buffer的随机写既实现了事务的持久性,也完成了数据的高效写入。在PostgreSQL并发写入的情况下,每一个并发进程都有可能会去写wal日志,如果wal日志同时只允许一个进程写入,这样的设计会导致性能瓶颈,因此wal日志一定支持多个进程并发写入。一直比较好奇wal日志的并发写入机制,最近研究了下代码,因此写个博客记录研究结果。

wal日志写入过程简介

为了让更多读者能了解这篇博客的主旨,这里总结一下wal日志从产生到写入wal文件的一般过程,在我之前的博客中有比较深入内核的介绍,这里做一下简述。

d14c8d1bdb0d60225d02591a7746a1a9.png

一个PostgreSQL后端进程产生数据写入后,一定会先写入wal,具体流程是①通过一些高级接口注册wal数据,②将注册的wal数据重组为一个数据链表,③将数据链表中的数据拷贝到wal页buffer中,④是将wal页buffer刷写到磁盘中。之前的博客详细描述了①②的过程,在这个博客中将详细描述③过程。

wal并发写入综述

如下是wal并发写入综述图,在本模块中将会从整体上描述wal并发写入的概貌,后面的小节中会深入探究每一个实现细节。

f24d403b4b66d4e96ae6118d7396ce14.png

wal并发写入综述图

如图所示,一个PostgreSQl进程在组装wal数据之后,会在local内存中形成一个wal的数据链表。这个进程希望将链表中的数据拷贝到wal的buffer中。首先进程需要获取wal插入锁槽,然后根据当前已经写的wal位置和希望插入的wal记录的长度,在wal buffer中预置其wal位置,最后将local内存的wal数据链表中的数据拷贝到预置的wal页buffer中。

在整个写入过程中,插入锁槽是一直持有的,在图中我们可以看到一共有8个插入锁槽,也就是说PostgreSQL目前只支持8个并发进程同时修改wal日志buffer。

在预置wal位置时需要获取排他锁,因此预置wal位置的过程在PostgreSQL运行过程中是无法并行的。

wal记录插入锁槽

读者可能会疑惑这个插入锁槽的作用,看起来锁槽只有8个会降低wal插入的并发度,它存在的意义是什么呢?那我现在再抛出另外一个问题:下图中多个进程同时在写wal日志,此时进程4发生事务提交,那么就要立即触发wal刷写操作,此时进程应该刷写哪些wal日志呢?

adc3fe5dd5c52e8407a43763a275b0e9.png

那进程4发生了事务提交,为了保证刷写的数据的正确性和完整性,就需要等待lsn4_e之前的预留位置全部写完,事实上PostgreSQL的wal刷写代码对8个wal插入锁槽进行一次遍历(忽略未使用的锁槽,对于正在使用锁槽会等待其发生一次锁释放或锁槽状态改变),这个遍历的过程大家有兴趣可以阅读WaitXLogInsertionsToFinish()函数,遍历完成后就可以保证lsn4_e之前的预留空间的并发写入已经完成。

由于每一次wal刷写都会遍历wal插入锁槽,所以锁槽个数不宜过大,在PostgreSQL代码中将锁槽的个数写死为8个,可以参阅NUM_XLOGINSERT_LOCKS宏。

预置wal位置

预置wal位置时需要获取排他锁,此时无法与其他进程并发,因此预置wal位置是整个写wal页buffer的性能瓶颈模块,这个无法并发过程应该为极其少量的代码。因此现在需要一个机制在给定wal开始位置和wal数据链表总长度的情况下,尽量少的步骤计算出wal结束的位置。这个问题的难点是一个wal数据链表是可能跨wal的page,wal的segment,wal的文件的,因此要计算出wal结束位置,可能需要比较复杂的逻辑。

PostgreSQL对这一问题的解决方案是,在共享缓存中维护了一个CurrBytePos值,其意义是目前使用的wal的字节数(除去wal的page头所占用的字节数)。根据映射函数(XLogBytePosToRecPtr)每一个CurrBytePos唯一对应一个LSN值,因此在获取排他锁的过程中,PostgreSQL只需要完成CurrBytePos+wal数据链表总长度即可,释放排他锁后再计算CurrBytePos与真正LSN的映射关系。

2cd29b5b68ee16e21888163421554618.png

拷贝wal数据

现在,看起来只要把local内存中的wal数据链拷贝到wal页buffer中去就可以了。但还是有些问题存在,如一个进程预置的wal位置在现有的wal页buffer中是否命中,如何替换wal页buffer,一个wal数据链的数据可能跨wal页buffer。

1. wal页buffer的管理机制

wal的buffer管理简单粗暴,直接在共享缓存中开辟一定数量的wal页buffer,可以通过每一个lsn值,计算出这个lsn值所在的page在wal页buffer中的索引位置。

e93d4bd68cb2c838022782291fe63d44.png

2. 确认wal页buffer可用

一个进程在拷贝wal链表数据之前,会先根据预留的lsn_start值获取其wal页buffer的索引值,然后查询这个位置的存储的wal页是否为当前进程需要的wal页,如果是则将数据拷贝至这个buffer;如果不是(那么这个位置现在存储了一个更旧的wal页),这时当前进程需要等待这个旧的wal页buffer完成数据写入和刷盘后,在这个位置初始化自己需要的wal页。

3. 完成拷贝

ad08d779c20ab006a0919ad5a5b191d1.png

获取到需要的buffer之后,将wal链表中的数据拷贝到预留的buffer中即可。当wal链表中的数据太多而导致需要跨wal页存储时,这个进程还要再进行一次确认wal页buffer可用的过程。同时需要向wal插入锁槽汇报写入信息,这个信息可以使wal刷写进程,不必等待这个wal插入锁槽释放即可完成刷写。

总结

PostgreSQL中使用了大量的机制保证wal日志持久化的效率,本博客着重描述的是并发写入的部分,还有wal页buffer刷写的一些机制,会在以后的博客中体现。在本博客的wal并发写入综述小节中,描述了wal并发写入的大体原理,到这里笔者的思路还是很清晰的,但是在后面的具体描述的小节中,由于涉及到一些wal页刷写的知识,所以难以清晰明了讲明白这些知识点,后面还会展开写一个wal日志write和flush的博客,写完wal日志write和flush后再回来完善没有讲明白的地方吧。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值