从一道PG知识的选择题谈起,你悟到了些什么?

数据库 其他数据库
稍微懂点PG数据库的人不难回答,答案是A、B、C。一些Oracle DBA可能会觉得这个答案有点出乎意料。因为在Oracle数据库中,回写DB CACHE脏数据的只有DBWR。

昨天一个网友问我一道关于PG的选择题:Postgresql数据库中哪些进程可以将shared buffers中的脏数据回写到数据文件?A) BACKEND B) BGWRITER C)CHECKPOINTER D) WALWRITER。

稍微懂点PG数据库的人不难回答,答案是A、B、C。一些Oracle DBA可能会觉得这个答案有点出乎意料。因为在Oracle数据库中,回写DB CACHE脏数据的只有DBWR。可能这些人不太清楚的CKPT负责回写部分脏数据是80年代早期关系型数据库的共同特点,Oracle数据库中,CKPT也曾经负责过写脏块。后来随着数据库规模的增大,CKPT的功能被独立出来了,只负责CKPT的推进工作,不再负责写脏块了。在Oracle数据库中,为了更快的将脏块回写,通过将DB CACHE分区,采用多个DBWR进程可以并发写脏块,从而满足大型数据库的需要。

PG在刷脏块的算法方面的设计比较传统,这种设计是从90年代一脉相承的,改起来成本较大,所以就一直沿用下来了。只不过让backend也去写脏块,这未免也有点太出乎一般人的想象了。这会产生几个比较奇怪的现象,一个是某条SQL语句,可能在执行计划相同、数据量相同的情况下,不同的时间执行,其执行效率有较大的不同。另外一个奇怪现象是某条SELECT语句可能会产生较多的写操作。实际上Oracle数据库中也会存在类似的现象,这种情况是由延迟块清理产生的。当一个大型事务结束后,ITL的状态清理可能并没有完成,如果马上有SELECT操作访问这些数据,那么执行SELECT的前台进程将负责完成这些块的清理,这时候select操作也会产生大量的写操作,产生大量的REDO,同时这条SELECT比起平时会慢很多。    

图片图片

通过pg_stat_bgwriter系统表中的buffers_backend和buffers_backend_fsync可以查询到系统中的backend写脏块和fsync的计数。

图片图片

从我们的一个D-SMART的PG数据库中看到一个十分神奇的情况,按理说被用来写脏块的bgwriter居然很小(buffers_clean),大多数脏块都是checkpointer和backend写的 。如果你去检查一下你们的PG数据库,可能bgwriter写脏块的比例也不是很高。

为什么会出现这种情况,为什么大量的写脏块工作不由本应该处理脏块的 bgwriter去做,而让Backend去写脏块呢?这方面的资料很少,我们只能从 PG的代码上去找找答案。首先我们看一下src/backend/storage/bufmgr.c,在这里可以看到backend从shared buffers中分配空闲buffer的算法。一般我们都能理解从shared buffers中查找空闲的buffer,如果找不到非脏的空闲块,那么就有可能找到一个存储了脏数据的数据块,这时候才需要backend去写脏块。在Oracle数据库中,前台进程是顺着lru链的冷端去查找空闲缓冲块的,如果前台进程发现了某个没有被Pin住的块是脏块,就会把这个数据块移到lru-w中,然后继续往下搜索,如果连续搜素到了N个脏块,无法获得所需要的空闲块的时候,就会发出一个free buffer requests的事件,让DBWR加快刷脏块,然后再去重试。从PG的代码上看,PG的大体思想类似,不过策略要复杂得多。    

图片图片

BufferAlloc里包含了backend查找某个buffer的顶层逻辑。不阅读一下还真没发现PG这方面的代码逻辑会搞得如此复杂。要想访问某个buffer,先要生成一个BUFFER TAG(关于这方面的详细算法请参考我2021年写过的一篇8000多字的长篇《PG SHARED BUFFER POOL的优化》)。然后查找这个BUFFER。    

图片图片

如果BUFFER存在,还有两种可能性,一种是成功的PIN住了这个BUFFER,那么就可以返回这个BUFFER了。不过BUFFER存在还有一种可能性是无法PIN住,无法PIN住的原因是可能被其他的会话PIN住了,也可能是一些其他的原因。这种情况,Oracle被称为read by other session或者buffer busy waits。这个部分不是我们今天分析的重点,我们继续往下看代码。    

图片图片

GetVictimBuffer函数通过时钟扫描算法去找一个空闲的buffer。这里就涉及到查找空闲shared buffers了。我们下钻到这个函数的代码中去继续分析。

图片图片

首先调用StrategyGetBuffer去找一个BUFFER。    

图片图片

如果发现找到的是一个脏块,那么就把脏块刷盘,这就是BACKEND也需要刷脏块的原因之一。作为数据库缓冲的算法,我们肯定应该尽可能的找到非脏块来复用,总是让BACKEND写脏块肯定会降低数据库的整体性能。    

StrategyGetBuffer函数在src/backend/buffer/freelist.c中定义。首先,它会检查是否有一个策略对象(strategy),如果有,就调用GetBufferFromRing函数,从策略对象的环形缓冲区(ring buffer)中获取一个缓冲区。如果获取成功,就返回这个缓冲区,并设置from_ring标志为true。如果没能正常找到free buffer,它会尝试唤醒bgwriter,让它刷新脏的缓冲区到磁盘,以便释放一些空间。接下来,backend会检查StrategyControl->firstFreeBuffer变量,如果大于等于0,就表示有空闲的缓冲区,那么就通过一个循环从空闲链表中获取一个缓冲区。这部分算法与Oracle的free buffer requests十分类似。

此时如果空闲链表为空,backend会进入另一个循环,尝试从victim pool中选择一个缓冲区,victim pool是一个循环队列,存储了最近被访问过的缓冲区的编号。从nextVictimBuffer的当前位置开始,顺时针扫描victim pool,寻找一个既不被锁定,也没有被引用,也不是脏的缓冲区。如果找到了,就返回这个缓冲区,并将其从victim pool中移除。如果没有找到合适的缓冲区,它会继续扫描victim pool,寻找一个既不被锁定,也没有被引用,但是是脏块的缓冲区。如果找到了,就将这个缓冲区的内容写入磁盘,然后返回这个缓冲区(这是代码中backend中另外一个写脏块的地方),并将其从victim pool中移除,并添加到策略对象的环形缓冲区中。

我们先不去吐槽PG在这块代码的质量问题,仅仅从算法来看,backend直接刷脏块的机会也应该是比较小的。那么为什么我们会在pg_stat_bgwriter中看到如此奇葩的数据呢?这里面其实也有一个十分有意思的逻辑。

首先是关于buffers_backend这个指标,本身这个指标就有一定的误导性,我们今天看的代码上包含了处写脏块的地方,其实不用看代码我们都能想到第三处,那就是VACUUM。因为backend中还包含了auto vacuum,vacuum操作等写脏块的统计数据,因此我们可能会被这个指标误导。PG社区中十多年前就有人希望PG代码中把这些情况区分开来,从而让buffers_clean更有指向性,不过没有获得PG社区核心研发的认同。    

另外一点是PG数据库的策略是尽可能让数据在内存中多存放一段时间,而不急着把脏数据写盘。因此在PG数据库中,还是将检查点进程作为写脏块的主力,如果你的系统中的buffers_alloc增长很缓慢的话,那么只要按照checkpointer的节奏慢慢写脏块就可以了,backend总是能够找到所需要的buffer,因此也就没必要让bgwriter去写脏块了。这种算法对于早些年比较缓慢的IO子系统来说是十分友好的,不过对于当今高性能的IO系统来说,不够高效,比较适合目前IO性能一般的云上小型数据库,而对于采用高性能IO设备的大型数据库来说,并不一定是很优化的。

基于上面的分析我们可以了解到,如果你看到你的系统中的buffers_clean总是为0或者总是慢速增长,那么并不说明系统存在问题,而是说明你的系统写负载还不算太高,bgwriter还犯不着去帮你刷盘而已。对于IO性能还不错的系统,或者说规模不算太大的数据库来说,PG的这种刷脏块的方法还是可以胜任的,在一些超大型系统中,可能这方面会成为瓶颈。我看到Polardb-PG、openGauss等基于PG代码的数据库产品中,对这方面都做了一些优化,引入了专门的机制来替换BGWRITER。目前还没有对这些代码进行分析,因此不知道这方面的改善如何。

今天分析PG这方面源代码的另外一个收获是从中学到一些PG的SHARED BUFFERS相关的优化策略的。首先shared buffers不能设置得太少,否则backend真正开始大量刷脏块了,那么SQL的性能是会受到很大的影响的。其次是CHECKPOINTER的相关参数设置要合理,根据底层IO的能力配置合适的参数,让CHECKPOINTER刷盘的速度能够跟得上buffers_alloc的速度。如果我们发现buffers_clean的增长比较快了,那么说明目前系统的负载对shared buffers 有一定的压力了,那么我们就需要考虑调整bgwriter相关的参数了。       

最后的源码链接是我两年多前写的一篇关于PG SHARED BUFFERS的内部结构的分析文章,文章很长,有8000多字,有兴趣的朋友可以阅读一下。文中有些观点可能和今天的文章有些不大一致了,如果存在这方面的观点,那么就以今天的文章为准吧。对PG数据库的理解都是一点一点的从模糊到清晰,从不大准确到相对准确的。认知的提升是从一个个案例,一段段源码的分析中逐渐完成的。

责任编辑:武晓燕 来源: 白鳝的洞穴
相关推荐

2021-04-29 21:06:49

有序数组算法

2018-03-13 16:04:45

Promise执行顺序

2024-02-07 01:32:48

页面前端项目

2022-01-19 11:39:15

数据治理大数据数据

2021-03-27 10:59:45

JavaScript开发代码

2022-08-02 07:57:54

RAC故障运维

2023-04-26 01:25:05

案例故障模型

2018-04-26 11:23:01

Linuxfork程序

2021-04-13 08:50:21

JS作用域面试题

2024-08-12 15:44:06

2018-01-29 15:25:05

前端JSDate对象

2024-03-18 13:32:11

2015-01-14 19:48:06

移动CRM

2020-03-26 15:29:35

全球关注涉密会议

2023-11-29 12:12:24

Oceanbase数据库

2021-03-02 11:29:50

算法算法分析前端

2010-08-18 10:27:56

2009-01-08 21:21:45

程序员笔记

2021-10-28 11:40:58

回文链表面试题数据结构

2016-01-04 09:27:56

TCP网络协议
点赞
收藏

51CTO技术栈公众号