前两天一个朋友说PG的热块冲突比Oracle更容易产生,并会产生比较严重的性能问题,特别是当系统中的一些大型的热表存在大量UPDATE操作的时候。确实PG的ASTORE机制使用多个版本的TUPLE来保存某一行的历史版本,这种机制导致了PG的SHARED BUFFERS的锁会比较复杂。和朋友讨论问题后,我根据以前学习过的一些关于PG BUFFER的知识,画了一个思维导图。
PG的BUFFER 居然和三种锁有关,一种是SPINLOCK,用于管理BUFFER的空闲链的,如果要分配空闲缓冲区,则需要通过一个SPINLOCK(Buffer Strategy Lock)来获得。另外两类异类是我们比较容易理解的用于保护PG内存结构的锁LWLOCK。最后异类就比较令人费解了,如果我们看PG的等待事件,里面有一类独特的分类。
这类等待事件称为BufferPin,而这个等待事件大类里面只有一种等待事件,BufferPIN。这些锁之间都是什么关系呢?我们可以看上面的思维导图。
比如我们模拟一个BUFFER的一生,首先当要访问某个PG PAGE的时候,先要从FREE的BUFFER中找到一个,此时需要一个SPINLOCK(Buffer Strategy Lock),然后从FREELIST上取下BUFFER,准备给新的PAGE使用,此时我们需要PIN住这个BUFFER,使之不能被BUFFER替换等操作使用。
然后需要申请一个BUFFER CONTENT锁,来修改这个BUFFER,通过加buffer header lock来修改BUFFER头上的访问指针计数器等信息。然后就要开始读取PAGE的IO操作了,此时需要获得一个BUFFER IO锁,指示该BUFFER正在进行IO操作,从而避免在同一个BUFFER上的多个IO并发进行。
IO结束后,这个BUFFER中已经包含了我们所需要的PAGE,此时我们需要把这个BUFFER加入到HASH CHAINS里,此时就需要一个buffer mapping锁,从而便于今后BUFFER扫描定位,这个锁有点类似Oracle的CBC闩锁,也是多个锁分区管理的,PG使用多个分区来提高并行效率。
下面我们来看一个例子:
此时我们在另外一个会话里查看一下BUFFER PIN的情况:
可以看到一个BUFFER是被PIN住了。此时我们如果执行VACCUM会发生什么呢?
可以看到VACUUM跳过了被PIN住的BUFFER,因为针对PIN住的BUFFER,PG无法对其中的PAGE做VACUUM这样的不兼容的操作。
此时如果做不兼容的vacuum freeze操作就会被锁住,要等待BUFFER PIN被移除。BUFFER PIN是一个共享锁,不会阻塞同一个PAGE上的并发写操作,不过这个共享锁还是会产生一些并发互斥的操作,比如会阻止VACUUM对这个PAGE进行回收整理操作,使VACUUM操作跳过这个PAGE,会阻止FREEZE操作,直到PIN住该BUFFER的所有锁全部移除。
因为PG数据库采用的是APPEND STORE模式,因此一个行的UPDATE会产生多个行副本,这对于PG的数据行的访问操作来说会增加额外的成本,在这里我们还需要考虑索引访问的成本问题。如果这些记录副本都存储在同一个PAGE里,那么处理起来成本相对还比较低,PG采用HOT来降低索引的维护和访问成本。
如果多个TUPLE是分布在多个PAGE中,那么这个成本的增加就不可避免了。如果我们的应用系统中的某些表上的UPDATE十分频繁,那么这种额外的成本就会更大。再加上PG在访问数据时的各种锁的开销,这个叠加成本就更大了。
以VACUUM为例,如果我们的应用出现了BUG,打开一个CURSOR后忘记关闭了,或者一个死会话没有释放相关的CURSOR,那么某个或者某些BUFFER会被长时间PIN住,VACUUM每次都会跳过这些PAGE,时间长了,就会引发一些莫名其妙的问题。
希望今天看了这篇文章后,我们再去看PG等待事件中关于BUFFER的事件,可以更准确的了解到哪些等待事件代表什么含义,从而可以更好的定位问题。
正是因为PG的这种特性,在使用PG数据库的时候我们不能像使用Oracle那样肆无忌惮,如果做UPDATE操作,尽可能优化应用逻辑,让一条数据的UPDATE次数尽可能的减少。另外对于UPDATE十分频繁的表,或者需要对很多列进行UPDATE的宽表,其表的FILLFACTOR参数要适当减少,尽可能利用HOT来优化访问性能。另外,如果某张经常UPDATE的宽表是可以分拆的,那么尽可能把这张表分拆为多张表。
我和很多使用PG数据库的人交流过,有些人就说PG很好用,我们用了PG后系统一直都很稳定。有些朋友就说经常踩坑。实际上很多数据库都是有各种各样的坑的,如果你知道坑的存在,那就不容易踩坑了。有坑不可怕,不知道前面有坑才更可怕。