概述
CLOG日志记录的是事务的最终状态。CLOG日志管理器管理者CLOG日志缓冲池,该日志缓冲池基于SLRU缓冲池实现。
CLOG日志管理器相关结构
在pg中事务系统一共记录了四种事务状态,分别如下
#define TRANSACTION_STATUS_IN_PROGRESS 0x00 //事务正在运行中
#define TRANSACTION_STATUS_COMMITTED 0x01 //事务已提交
#define TRANSACTION_STATUS_ABORTED 0x02 //事务被终止
#define TRANSACTION_STATUS_SUB_COMMITTED 0x03 //事务的子事务已提交
由于需要记录的事务状态只有四种,所以只需要2个byte就能记录一个事务状态信息,这样的话,一个字节就能存储4个CLOG日志记录,一个页面(8K)就能保存32K个日志记录。而CLOG日志文件每个段文件由32个页组成,也就能保存1M个日志记录。
又由于事务ID最多能达到232个,所以理论上可以存在的CLOG日志文件数量等于232/220=212=4096个,当然由于事务ID会发生回卷和冻结,实际情况是不可能达到这么多的,不过这样亿段号来命名文件的话,最多就只需要12位就够了,而实际上段文件都是以4为16进制数字来命名的、CLOG日志文件保存在PGDATA/pg_xact目录下。
要定位一条CLOG日志记录,可以通过一个四元组表示,<segmentno, Pageno, Byte, Bindex>
- segmentno: 段号,即CLOG日志文件名
- Pageno: 页号
- Byte: 页面偏移
- Bindex: 字节内偏移
给定一个事务ID xid,可以通过四元组定位到其在日志中的位置,其计算公式为:
segmentno = xid /2^20
pageno = xid / 32K
Byte = xid % 32k
bindex = xid % 4
有了上面的四元组定位法,给定一个事务ID,我们就可以获取其日志记录对应的四元组,继而去对应位置读写数据.例如给定一个事务ID1688,其四元组为<0,0,1688,0>即第一个段文件的第0页的第1688个字节的前两位就是其对应的事务提交状态值。
#define CLOG_BITS_PER_XACT 2 //每个事务状态占的比特位
#define CLOG_XACTS_PER_BYTE 4 //每个Byte能保存的CLOG信息数量
#define CLOG_XACTS_PER_PAGE (BLCKSZ * CLOG_XACTS_PER_BYTE) //每个页能保存的CLOG数量=32K
#define CLOG_XACT_BITMASK ((1 << CLOG_BITS_PER_XACT) - 1) //每个CLOG对应的mask
#define TransactionIdToPage(xid) ((xid) / (TransactionId) CLOG_XACTS_PER_PAGE) //获取保存的页号
#define TransactionIdToPgIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_PAGE) //获取页内偏移
#define TransactionIdToByte(xid) (TransactionIdToPgIndex(xid) / CLOG_XACTS_PER_BYTE) //获取对应Byte
#define TransactionIdToBIndex(xid) ((xid) % (TransactionId) CLOG_XACTS_PER_BYTE)//获取Byte内偏移
clog日志缓冲池是一个SLRU缓冲池,在整个数据库系统中,CLOG日志缓冲池只有一个,它注册在共享内存中,其名为CxactCtl, 它的结构是SlruCtlData,记录了CLOG的SLRU缓冲池的数据缓冲区在共享内存中的地址
CLOG管理器主要操作函数
初始化函数
初始化的CLOG缓冲区大小由CLOGShmemSize函数确定,实际不会很大,一般几十到几百M之间。
CLOGShmemInit函数
- 注册比较CLOG日志页面的函数CLOGPagePrecedes
- 调用SimpleLruInit初始化SLRU缓冲池
- 执行测试PagePrecedes函数
XactCtl->PagePrecedes = CLOGPagePrecedes;
SimpleLruInit(XactCtl, "Xact", CLOGShmemBuffers(), CLOG_LSNS_PER_PAGE,
XactSLRULock, "pg_xact", LWTRANCHE_XACT_BUFFER,
SYNC_HANDLER_CLOG);
SlruPagePrecedesUnitTests(XactCtl, CLOG_XACTS_PER_PAGE);
写CLOG日志函数
TransactionIdSetTreeStatus
该函数在事务提交时调用,用于记录事务状态信息到CLOG日志缓冲池中。
- 根据事务ID获取对应的页号
pageno = TransactionIdToPage(xid); /* 获取父事务所在的页 */
- 判断事务状态是否为“TRANSACTION_STATUS_COMMITTED”或者“TRANSACTION_STATUS_ABORTED”,只能是这两种状态时才能记录,因为页面初始化时会全写0(TRANSACTION_STATUS_IN_PROGRESS)不用处理,而子事务那个会由父事务去处理,所以也不用处理。
Assert(status == TRANSACTION_STATUS_COMMITTED ||
status == TRANSACTION_STATUS_ABORTED);
- 检查是否所有子事务都与父事务在同一个页上
for (i = 0; i < nsubxids; i++)
{
if (TransactionIdToPage(subxids[i]) != pageno)
break;
}
- 若所有子事务跟父事务在同一个页上,直接调用TransactionIdSetPageStatus函数设置对应事务状态到CLOG日志缓冲去
if (i == nsubxids)
{
/*
* 使用单个调用设置父事务和所有子事务的状态
*/
TransactionIdSetPageStatus(xid, nsubxids, subxids, status, lsn,
pageno, true);
}
- 若有子事务跟父事务不在一个页上,先设置在同一个页上的子事务和父事务
/*
* 现在设置与父事务在同一页上的子事务,如果有的话
*/
pageno = TransactionIdToPage(xid);
TransactionIdSetPageStatus(xid, nsubxids_on_first_page, subxids, status,
lsn, pageno, false);
- 按页处理剩余的页上的子事务
/*
* 现在按页处理剩余的子事务,从第二个页开始,就像上面一样。
*/
set_status_by_pages(nsubxids - nsubxids_on_first_page,
subxids + nsubxids_on_first_page,
status, lsn);
TransactionIdSetPageStatus
该函数主要用于设置事务ID的状态,因为可能存在XactSLRULock锁的竞争,所以为了减少竞争,我们会尝试使用group分组的方式去更新事务ID,就是将多个要更新的事务放到一个group中,group中第一个事务作为leader统一去抢锁,如果抢到锁会将该组内所有的事务ID信息都更新到CLOG中,这样就可以减少抢锁的次数,从而提高并发性能。
- 若要使用group更新方式,需要保证所有事务在同一个页中,且事务ID子事务信息与当前进程Myproc中保存的信息一致才行,而且不能有太多的子事务。
if (all_xact_same_page && xid == MyProc->xid &&
nsubxids <= THRESHOLD_SUBTRANS_CLOG_OPT &&
nsubxids == MyProc->subxidStatus.count &&
(nsubxids == 0 ||
memcmp(subxids, MyProc->subxids.xids,
nsubxids * sizeof(TransactionId)) == 0))
{
/*
* 如果我们可以立即获取XactSLRULock,我们将更新我们自己的XID的状态并释放锁。
如果没有,尝试使用组XID更新。如果不起作用,退回到等待锁来只为该事务执行更新。
*/
if (LWLockConditionalAcquire(XactSLRULock, LW_EXCLUSIVE))
{
/* 无需等待即可获取锁!执行更新。 */
TransactionIdSetPageStatusInternal(xid, nsubxids, subxids, status,
lsn, pageno);
LWLockRelease(XactSLRULock);
return;
}
else if (TransactionGroupUpdateXidStatus(xid, status, lsn, pageno))
{
/* 组更新机制已经完成了工作。 */
return;
}
/* 只有在更新尚未完成时才通过。 */
}
- 组更新不适用,直接抢锁然后更新
LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);
TransactionIdSetPageStatusInternal(xid, nsubxids, subxids, status,
lsn, pageno);
LWLockRelease(XactSLRULock);
TransactionIdSetPageStatusInternal
设置事务ID页面状态
- 读取CLOG的缓冲页面位置
slotno = SimpleLruReadPage(XactCtl, pageno, XLogRecPtrIsInvalid(lsn), xid);
- 如果存在子事务,且事务状态是“已提交,先更新子事务
- 更新父事务
if (TransactionIdIsValid(xid))
{
/* 如果需要,先更新子事务... */
if (status == TRANSACTION_STATUS_COMMITTED)
{
for (i = 0; i < nsubxids; i++)
{
Assert(XactCtl->shared->page_number[slotno] == TransactionIdToPage(subxids[i]));
TransactionIdSetStatusBit(subxids[i],
TRANSACTION_STATUS_SUB_COMMITTED,
lsn, slotno);
}
}
/* ...然后更新主事务 */
TransactionIdSetStatusBit(xid, status, lsn, slotno);
}
- 更新子事务
/* 更新子事务 */
for (i = 0; i < nsubxids; i++)
{
Assert(XactCtl->shared->page_number[slotno] == TransactionIdToPage(subxids[i]));
TransactionIdSetStatusBit(subxids[i], status, lsn, slotno);
}
XactCtl->shared->page_dirty[slotno] = true;
- TransactionIdSetStatusBit
更新事务ID的状态位 - 获取事务ID对应的CLOG页的Byte位置以及Byte内偏移
int byteno = TransactionIdToByte(xid);
int bshift = TransactionIdToBIndex(xid) * CLOG_BITS_PER_XACT;
- 更新CLOG的值
假设该事务的Byte偏移是2,事务状态是3(已提交)
byteptr = XactCtl->shared->page_buffer[slotno] + byteno;
curval = (*byteptr >> bshift) & CLOG_XACT_BITMASK; //(1000 0010) & 0000 0011 = 1000 0010
byteval = *byteptr; //1000 1000
byteval &= ~(((1 << CLOG_BITS_PER_XACT) - 1) << bshift); // 1000 1000 & ~(0000 1100) = 1000 1000
byteval |= (status << bshift); //1000 1000 | 0000 1100 = 1000 1100
*byteptr = byteval;//10001100,Byte内偏移2即第三第四位表示当前事务状态,已提交标记为11
- 如果事务完成的LSN比当前页中LSN更高,则更新LSN
if (!XLogRecPtrIsInvalid(lsn))
{
int lsnindex = GetLSNIndex(slotno, xid);
if (XactCtl->shared->group_lsn[lsnindex] < lsn)
XactCtl->shared->group_lsn[lsnindex] = lsn;
}
TransactionGroupUpdateXidStatus
该函数用于在更新事务状态时,如果无法立即获取到XactSLRULock锁时,会将自己添加事务ID的等待组中,组内第一个事务作为leader,会代表所有成员去抢占XactSLRULock锁,如果抢到锁就将组内所有所有成员都更新,避免多个进程同事提交时抢锁竞争过大。
- 将当前进程添加到需要更新事务组的列表中
proc->clogGroupMember = true;
proc->clogGroupMemberXid = xid;
proc->clogGroupMemberXidStatus = status;
proc->clogGroupMemberPage = pageno;
proc->clogGroupMemberLsn = lsn;
- 获取组的leader,如果leader要更新的页与当前事务要更新的页不一致,表示当前进程还不在要更新的组中,退出;否则把当前事务ID标记为组的leader
nextidx = pg_atomic_read_u32(&procglobal->clogGroupFirst);
while (true)
{
// 如果当前进程需要更新的页与组领导者的页相同,则将进程添加到列表中
if (nextidx != INVALID_PGPROCNO &&
ProcGlobal->allProcs[nextidx].clogGroupMemberPage != proc->clogGroupMemberPage)
{
// 确保当前进程不在任何需要更新XID状态的事务组中
proc->clogGroupMember = false;
pg_atomic_write_u32(&proc->clogGroupNext, INVALID_PGPROCNO);
return false;
}
pg_atomic_write_u32(&proc->clogGroupNext, nextidx);
if (pg_atomic_compare_exchange_u32(&procglobal->clogGroupFirst,
&nextidx,
(uint32) proc->pgprocno))
break;
}
- 如果leader不为空,那么久等待leader更新完即可,等待proc->clogGroupMember为false
if (nextidx != INVALID_PGPROCNO)
{
int extraWaits = 0;
// 等待组领导者的XID状态更新
pgstat_report_wait_start(WAIT_EVENT_XACT_GROUP_UPDATE);
for (;;)
{
// 作为读屏障
PGSemaphoreLock(proc->sem);
if (!proc->clogGroupMember)
break;
extraWaits++;
}
pgstat_report_wait_end();
Assert(pg_atomic_read_u32(&proc->clogGroupNext) == INVALID_PGPROCNO);
// 为吸收的唤醒次数进行补偿
while (extraWaits-- > 0)
PGSemaphoreUnlock(proc->sem);
return true;
}
- 如果当前进程是leader,抢XactSLRULock锁,然后逐个更新组成员的事务状态
LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);
// 清除等待组XID状态更新的进程列表,并保存列表的头部指针
nextidx = pg_atomic_exchange_u32(&procglobal->clogGroupFirst,
INVALID_PGPROCNO);
// 保存列表的头部指针以便在释放锁后进行唤醒
wakeidx = nextidx;
// 遍历列表并更新所有XID的状态
while (nextidx != INVALID_PGPROCNO)
{
PGPROC *proc = &ProcGlobal->allProcs[nextidx];
// 事务具有超过THRESHOLD_SUBTRANS_CLOG_OPT个子XID时,不应使用组XID状态更新机制
Assert(proc->subxidStatus.count <= THRESHOLD_SUBTRANS_CLOG_OPT);
TransactionIdSetPageStatusInternal(proc->clogGroupMemberXid,
proc->subxidStatus.count,
proc->subids.xids,
proc->clogGroupMemberXidStatus,
proc->clogGroupMemberLsn,
proc->clogGroupMemberPage);
// 移动到列表中的下一个进程
nextidx = pg_atomic_read_u32(&proc->clogGroupNext);
}
// 现在可以释放锁了
LWLockRelease(XactSLRULock);
- 更新完之后唤醒所有成员,设置proc->clogGroupMember为false
while (wakeidx != INVALID_PGPROCNO)
{
PGPROC *proc = &ProcGlobal->allProcs[wakeidx];
wakeidx = pg_atomic_read_u32(&proc->clogGroupNext);
pg_atomic_write_u32(&proc->clogGroupNext, INVALID_PGPROCNO);
// 确保所有之前的写操作对跟随者可见
pg_write_barrier();
proc->clogGroupMember = false;
if (proc != MyProc)
PGSemaphoreUnlock(proc->sem);
}
set_status_by_pages
该函数是TransactionIdSetTreeStatus的辅助函数,用于设置一组事务的状态。它将事务分成不同的CLOG页面进行处理。该函数不会将整个事务树传递给它,只会传递与顶级事务ID不同的子事务
它会遍历每个子事务并调用TransactionIdSetPageStatus设置事务状态
// 获取第一个subxids的页码
int pageno = TransactionIdToPage(subxids[0]);
// 初始化偏移量
int offset = 0;
// 初始化循环计数器
int i = 0;
// 断言nsubxids大于0,否则上面的页码获取是不安全的
Assert(nsubxids > 0);
// 循环处理每个页码
while (i < nsubxids)
{
// 初始化当前页码上的事务数量
int num_on_page = 0;
// 获取下一个页码
int nextpageno;
// 循环判断当前页码是否与上一个页码相同
do
{
nextpageno = TransactionIdToPage(subxids[i]);
if (nextpageno != pageno)
break;
num_on_page++;
i++;
} while (i < nsubxids);
// 设置无效事务id在当前页码上的事务数量、事务id、状态、日志位置等信息
TransactionIdSetPageStatus(InvalidTransactionId,
num_on_page, subxids + offset,
status, lsn, pageno, false);
// 更新偏移量
offset = i;
// 更新下一个页码
pageno = nextpageno;
}
读CLOG日志
TransactionIdGetStatus
该函数用来查询CLOG中存储的事务的状态和LSN值
- 根据事务ID获取其页号,Byte号,Byte内位数
- 从SLRU缓冲池中读取对应内容
- 获取LSN值
假如事务ID等于17386,其四元组值为<0,0,4346,2>,假设读取的事务状态是3(已提交)
int pageno = TransactionIdToPage(xid);//页码为0
// 将事务ID转换为字节号
int byteno = TransactionIdToByte(xid); //byteno=4346
// 将事务ID转换为块索引,并乘以每个事务的位数
int bshift = TransactionIdToBIndex(xid) * CLOG_BITS_PER_XACT;//=2*2=4
int slotno; // 用于存储槽号
int lsnindex; // 用于存储LSN索引
char *byteptr; // 用于存储字节指针
XidStatus status; // 用于存储事务状态
// 锁已由SimpleLruReadPage_ReadOnly获取
// 通过SimpleLruReadPage_ReadOnly获取槽号
slotno = SimpleLruReadPage_ReadOnly(XactCtl, pageno, xid);
// 获取字节指针
byteptr = XactCtl->shared->page_buffer[slotno] + byteno;
// 获取事务状态
status = (*byteptr >> bshift) & CLOG_XACT_BITMASK;//(11110011 >> 4) & 00000011 = 00000011
// 获取LSN索引
lsnindex = GetLSNIndex(slotno, xid);
// 获取LSN
*lsn = XactCtl->shared->group_lsn[lsnindex];
// 释放锁
LWLockRelease(XactSLRULock);
return status;
CLOG 日志创建
BootStrapCLOG
该函数在系统安装的时候创建,用于创建初始的CLOG缓冲池
- 以排他模式获取XactSLRULock
- 创建第一个CLOG页并初始化为0
- 写入磁盘
- 释放锁
ZeroCLOGPage
该函数用于初始化一个CLOG页,并根据需要写XLOG日志。
CLOG日志启动
StartupCLOG
该函数会在每次数据库启动时调用,初始化最新的事务ID对应的页
XactCtl->shared->latest_page_number = pageno
CLOG日志关闭
TrimCLOG
该函数会在数据库关闭时将当前字节的剩余位置零。在正常情况下,它应该已经是零了,但似乎至少理论上可能会出现下一个XID值小于上一个数据库生命周期标记的实际使用的最大XID值的情况(因为子事务提交会写入clog,但不会写入WAL条目)。让我们保持安全。 (我们不需要担心超过当前页的页,因为第一次使用时会将它们置零。出于同样的原因,在nextXid正好位于页边界时也不需要做任何事情;而且很可能“当前”页在那种情况下还没有存在。)
int byteno = TransactionIdToByte(xid);
int bshift = TransactionIdToBIndex(xid) * CLOG_BITS_PER_XACT;
int slotno;
char *byteptr;
slotno = SimpleLruReadPage(XactCtl, pageno, false, xid);
byteptr = XactCtl->shared->page_buffer[slotno] + byteno;
/* 将目前为止未使用的字节位置置零 */
*byteptr &= (1 << bshift) - 1;
/* 将当前字节的其余部分置零 */
MemSet(byteptr + 1, 0, BLCKSZ - byteno - 1);
XactCtl->shared->page_dirty[slotno] = true;
CLOG日志的checkpoint
CheckPointCLOG
在checkpoint进程运行时会调用该函数将CLOG缓冲区中的所有脏数据写入磁盘,调用的是SimpleLruWriteAll函数
CLOG日志的扩展
ExtendCLOG
该函数用于确保CLOG有足够的空间来分配新的XID。该函数在持有XidGenLock时被调用。
int pageno;
/*
* 除了页面的第一个XID之外,没有其他工作。但是要注意:在循环之后,页面零的第一个XID是FirstNormalTransactionId。
*/
if (TransactionIdToPgIndex(newestXact) != 0 &&
!TransactionIdEquals(newestXact, FirstNormalTransactionId))
return;
pageno = TransactionIdToPage(newestXact);
LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);
/* 将页面清零并记录XLOG条目 */
ZeroCLOGPage(pageno, true);
LWLockRelease(XactSLRULock);
CLOG日志的删除
该函数用于删除在给定事务ID之前的CLOG段文件。在删除任何CLOG数据之前,必须将XLOG刷新到磁盘,以确保最近发出的FREEZE_PAGE记录已到达磁盘,否则在崩溃和重启时可能会留下一些未冻结的元组引用已删除的CLOG数据。我们选择发出一个特殊的TRUNCATE XLOG记录。从XLOG中重放删除操作不是关键,因为文件可以稍后删除,但这样做可以防止长时间运行的热备用服务器获得不合理膨胀的CLOG目录。由于CLOG段落包含大量事务,因此实际上可以删除一个段落的机会非常罕见,
因此似乎最好在确认存在可删除的段落时才进行XLOG刷新。
- 根据提供的事务ID计算出页号
- 遍历CLOG日志目录(PGDATA/pg_xact/)下的每个文件,并调用SlruScanDirCbReportPresence函数处理,若没有要处理的文件则直接返回。SlruScanDirCbReportPresence函数主要是判断文件是否可以删除。
- 如果存在可以删除的段文件,先更新全局的oldestClogXid
- 将当前的truncate CLOG操作写入XLOG日志
- 移除旧的CLOG文件段
TruncateCLOG(TransactionId oldestXact, Oid oldestxid_datoid)
{
int cutoffPage; // 定义截止页面变量
// 计算并设置截止页面,即包含oldestXact事务ID的页码
cutoffPage = TransactionIdToPage(oldestXact);
// 扫描目录下的每个文件,并对每个文件应用回调函数
if (!SlruScanDirectory(XactCtl, SlruScanDirCbReportPresence, &cutoffPage))
return; // 若没有需要移除的内容,则直接返回
// 在截断CLOG之前更新最旧的ClogXid,确保并发事务状态查询不会尝试访问已被截断的CLOG部分
// 只有在实际会截断CLOG页面时才需要执行此操作
AdvanceOldestClogXid(oldestXact);
// 写入XLOG记录并刷盘。记录下我们正在保留信息的最旧事务ID,
// 以便在崩溃情况下确保其始终领先于CLOG截断点,且备用节点能在下一次检查点前得知新的有效事务ID
WriteTruncateXlogRec(cutoffPage, oldestXact, oldestxid_datoid);
// 现在可以移除旧的CLOG段了
SimpleLruTruncate(XactCtl, cutoffPage);
}
CLOG日志的redo
在故障恢复时,需要回放所有XLOG,如果是CLOG类型的XLOG,则会调用clog_redo函数回放CLOG数据
clog_redo
- 根据XLOG中记录的clog info判断XLOG类型,主要有两种:CLOG_ZEROPAGE和CLOG_TRUNCATE
- 如果是CLOG_ZEROPAGE,表示执行的是clog页面清零操作
int pageno;
int slotno;
// 从日志记录中复制出页面号
memcpy(&pageno, XLogRecGetData(record), sizeof(int));
LWLockAcquire(XactSLRULock, LW_EXCLUSIVE);
slotno = ZeroCLOGPage(pageno, false);
SimpleLruWritePage(XactCtl, slotno); // 将页面写回缓存
LWLockRelease(XactSLRULock);
- 如果是CLOG_TRUNCATE,表示执行的是clog日志删除操作
xl_clog_truncate xlrec;
// 从日志记录中复制出截断记录结构体内容
memcpy(&xlrec, XLogRecGetData(record), sizeof(xl_clog_truncate));
// 更新最旧的ClogXid
AdvanceOldestClogXid(xlrec.oldestXact);
// 执行CLOG段的截断操作
SimpleLruTruncate(XactCtl, xlrec.pageno);
【参考】
- 《PostgreSQL数据库内核分析》
- 《Postgresql技术内幕-事务处理深度探索》
- pg14源码