一 、clog的作用与分析
1. clog作用及功能
Clog是记录事务状态的日志,由于其多版本特性,因此需要提交日志clog来记录事务的状态,从而判断其可见性。Clog分配于共享内存中,并作用于事务处理过程的全过程。
在PG数据库中事务状态有四种,分别是:IN_PROGRESS、COMMITED、ABORTED和SUB_COMMITED。
例如事务正在运行中,那么它的状态就是IN_PROGRESS。PG中是通过clog来存储事务xid和 事务xid对应的状态。 因此,若取消一个执行很长时间的事务,就可以基本在瞬间处理完成,只需要将事务状态从IN_PROGRESS设置成ABORTED即可;而Oracle却要等待undo表空间中的内容回滚完。
2. Clog源码分析
在clog.c文件中定义了,一个事务状态占用2bits,一个字节能表示4个事务状态,一个页表示 页的字节数*4 个 事务状态,一般一个页的大小位8K,即能表示32,768个事务状态 。
/* We need two bits per xact, so four xacts fit in a byte */
#define CLOG_BITS_PER_XACT 2 // each trancsation status use 2 bits
#define CLOG_XACTS_PER_BYTE 4 // each byte has 4 transactions's status
#define CLOG_XACTS_PER_PAGE (BLCKSZ * CLOG_XACTS_PER_BYTE) // each page has 8K*4 transactions status
#define CLOG_XACT_BITMASK ((1 << CLOG_BITS_PER_XACT) - 1) // 掩位码
事务id可以通过与CLOG_XACTS_PER_PAGE做除法来确定此事务的状态在第几个页上,对CLOG_XACTS_PER_PAGE取余来确定在某页的字节偏移位置 ,用字节偏移位置除以每字节最大事务状态数得到其具体位置。
#define TransactionIdToPage(xid) ((xid) / (TransactionId) CLOG_XACTS_PER_PAGE) // transactionID in which CLog page
#define TransactionIdToPgIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_PAGE)
#define TransactionIdToByte(xid) (TransactionIdToPgIndex(xid) / CLOG_XACTS_PER_BYTE)
#define TransactionIdToBIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_BYTE)
Clog写事务状态主要函数调用流程:
在clog事务状态写的过程实际上是写进缓冲区,并非是直接写进文件,这样的好处是减少磁盘的IO,提高性能,实际上的写文件操作是在SLRU里进行,其逻辑大体如下 :
先获取一个缓冲区buf页用来保存事务状态,在这其中就需要查找是否存在空的缓存页,如果有就用空页来写,否则就用LRU淘汰算法置换出一个页。在置换过程中要对页内的数据进行脏页判断,若是脏页就进行写文件操作,否则就直接清空来给新来事务使用。页内数据不仅是保存事务状态,在事务进行可见性判断的时候也会用到其内容进行事务可见性判断,或是获取某个事务状态等操作。流程图如下:
整个过程都是在XactSLRULock保护下进行,来保证写入的页的数据的真实可靠。Buf置换的时候还有相应的buf锁来保护,buf锁具有多个,根据buf页来使用,buf页的数量范围是4~128。
Size
CLOGShmemBuffers(void)
{
return Min(128, Max(4, NBuffers / 512));
}
……………………
typedef struct SlruSharedData
{
LWLock *ControlLock; /* 页锁 */
………………………………
LWLockPadded *buffer_locks; /* buf锁 */
………………………………
}SlruSharedData;
Clog整个过程如下:
二 、clog性能瓶颈
虽然Clog的事务状态处理非常的迅速,但是在某个表存在大量回滚的情况下,若是查询此表必然会进行扫描其clog的事务状态,性能肯定会很低。PG为解决此问题设计了一个缓冲池来缓存某些事务的状态。先将记录保存在缓冲池中,若缓冲池空间用尽则会利用LRU淘汰算法刷写到磁盘,若某些记录已经被记录则会直接替换新事务记录。
而缓冲池就要用到锁的保护,缓冲池内操作数据需要在锁的保护下进行才能保证数据的真实可靠,并且不会丢失与乱序。经过对代码的研读发现LRU页锁只有一个而缓冲buf锁却有4~128个,在高并发情况下必然会导致锁竞争,只有一个页锁的情况下,竞争尤为激烈,从而降低PG整体性能。
三、改进思路
经过调研,发现opengauss数据库对clog的缓冲池锁进行了分区处理,以达到在高并发下极大减少锁竞争,使得clog也可以高并发处理。我的思路是将opengauss对clog中SLRU锁的分区处理的思路应用于PG数据库上。本质是将一个锁变为多个锁,用页号(pageno)来哈希分区。在原有的轻量级锁(LWLock)上进行改进扩充,作为clogpartitionslocks,之后将页号对锁clogpartitionslocks的数量取余哈希到对应的锁上。优化模型图如下:
在考虑使用pageno作为页号进行所分区时,要分析子事务与主事务的记录是否从同一个页上的问题,即边界问题,这个问题pg本身已经帮我们解决,在调用clog模块的时候会先进行事务id对应的页号判断,如果其中一个或几个子事务的id映射的页号与主事务不在同一页上则会跳出此页的写操作,在另一个页上进行事务的写。因此将原先的轻量级锁改成用pageno哈希的分区锁是可行的,将一个锁扩到256个锁,在高并发写事务状态的情况下必然会大幅降低锁的竞争与等待, 高并发下性能将会得到一定量的提升。
代码实现可供参考:gitee(仅供参考)
标签:状态,openguass,clog,xid,可行性,CLOG,事务,PER,Clog From: https://www.cnblogs.com/supersimple/p/18517333