2021@SDUSC
目录
概述
上一周日志管理分析了一半分析了SLRU缓冲池,这周分析一下GLOG日志管理器,如果还有时间的话再看一下SUBTRANS日志管理器。
CLOG日志记录的是事务的最终状态。CLOG日志管理器管理着CLOG日志缓冲池,该日志缓
冲池是基于SLRU缓冲池实现的。
CLOG日志管理器
1.CLOG日志管理器相关数据结构
在PostgreSQL事务系统中一共定义了四种事务状态(数据结构7.24)。
#define TRANSACTION_ STATUS_ IN_ PROGRESS 0x00 /事务正在执行中 */
#define TRANSACT ION_ STATUS_ COMMITTED 0x01 /事务已提交*/
#define TRANSACTION STATUS ABORTED 0x021事务被中 止*/
#define TRANSACTION_ STATUS_ SUB_ COMMITTED 0x03/*事务的子事务已提交*/
状态TRANSACTION STATUS_ SUB_ COMMITTED是为嵌套事务而引入的(其他三种状态用于普
通事务),即对于嵌套事务,是存在子事务的,如果父事务没有提交,而子事务已提交,那么子事务的状
态就设置为TRANSACTION_ STATUS_ SUB_ _COMMITTED。
由于CLOG只记录事务的4个状态,因此记录一个事务状态信息的CLOG日志记录只需要2个比特,一个字节就可以存储4个事务的CLOG日志记录。在一个页面(大小为8KB)中,可记录事务CLOG日志记录条数为8K * 8b/2b =32K=2"。在一个由32个页面组成的段中,可记录的CLOG日志记录条数为32*2" =2”。
由于事务ID为32比特,因此可表示的事务数有22个,而CLOG日志文件是以段文件为单位的,这意味着可能存在的CLOG段的数量为2*/20=2"。CLOG日志段文件以段号命名,所以只需要12 位即可,而实际上,PostgreSQL以4位的16进制数来表示段号,并作为日志段文件的名称。CLOG日志的数据位于PGDATA/pg_ clog 目录下。
通过一个4元组< Segmentno, Pageno, Byte, Bindex> 就可以定位一条CLOG日志记录。其中
Segmentno为段号,即实际的段文件名称; Pageno 为日志记录所在的段内的页偏移; Byte 为页面偏移; Bindex 为字节内偏移。
通过一个事务的事务ID就可以获得其日志记录对应的4元组,具体方法如下:
/*根据事务ID获得对应事务所在页面*/
#define TransactionIdToPage (xid)
/*根据事务ID获得事务在对应页面上的偏移*/
tdefine TransactionIdToPgIndex (xid)
p*根据事务ID获得事务在对应页面上的字节偏移*/
#define TransactionIdToByte (xid)
*根据事务ID获得事务在记录其CLOG记录的字节上的位偏移*/
#define Transacti onIdToBIndex (xid)
由于一个日志段由32个页面所组成,所以通过计算CLOC日志记录所在页面的页面号就可得.到该日志记录所在的段。CLOG日志缓冲池就是一个SLRU缓冲池,在整个数据库系统中,CLOG 日志缓冲池只有一个,它注册在共享内存中,其名称为“CLOG Ctl"。PostgreSQL 定义了一个静态变量ClogCtl, 它是一个SLRU缓冲池控制结构,记录了CLOG的SLRU缓冲池的数据缓冲区在共享内存中的地址,以及对CLOG日志进行写操作的同步信息,默认要求CLOG日志的写操作是同步写操作。
2. CLOG日志管理器主要操作
CLOG日志管理器的操作主要包括对日志管理器的初始化、日志的读写操作、日志页面的初始
化,以及日志管理器的新建、启动、关闭、扩展、删除等操作。
(1)日志管理器的初始化
void
CLOGShmemInit(void)
{
ClogCtl->PagePrecedes = CLOGPagePrecedes;
SimpleLruInit(ClogCtl, "clog", CLOGShmemBuffers(), CLOG_LSNS_PER_PAGE,
CLogControlLock, "pg_xact", LWTRANCHE_CLOG_BUFFERS);
}
该操作定义在函数CLOGShmemlnit中,用于在共享内存中初始化CLOG缓冲池。主要是设置
CLOG共享内存控制信息中比较两个CLOG日志页面前后关系的函数信息,并调用SimpleLrulnit函
数在共享内存中初始化CLOG缓冲池,设置CLOG日志数据存储的磁盘路径,在共享内存中注册唯
一的CLOC缓冲池,并设置CLOG日志管理器的控制结构,最后分配-个缓冲池的控制锁给CLOG
日志管理器。
(2) CLOG 日志的写操作
该操作定义在函数TransactionIdSetStatus中,用于设置指定事务的状态,实际上就是在CLOC日
志文件中写该事务的CLOC日志记录。此函数是一个底层的操作,并不直接提供给其他一 般进程设置事务状态,具体对事务状态更新由事务日志接口例程的函数来完成。
static void
TransactionIdSetStatusBit(TransactionId xid, XidStatus status, XLogRecPtr lsn, int slotno)
{
int byteno = TransactionIdToByte(xid);
int bshift = TransactionIdToBIndex(xid) * CLOG_BITS_PER_XACT;
char *byteptr;
char byteval;
char curval;
byteptr = ClogCtl->shared->page_buffer[slotno] + byteno;
curval = (*byteptr >> bshift) & CLOG_XACT_BITMASK;
/*
* When replaying transactions during recovery we still need to perform
* the two phases of subcommit and then commit. However, some transactions
* are already correctly marked, so we just treat those as a no-op which
* allows us to keep the following Assert as restrictive as possible.
*/
if (InRecovery && status == TRANSACTION_STATUS_SUB_COMMITTED &&
curval == TRANSACTION_STATUS_COMMITTED)
return;
/*
* Current state change should be from 0 or subcommitted to target state
* or we should already be there when replaying changes during recovery.
*/
Assert(curval == 0 ||
(curval == TRANSACTION_STATUS_SUB_COMMITTED &&
status != TRANSACTION_STATUS_IN_PROGRESS) ||
curval == status);
/* note this assumes exclusive access to the clog page */
byteval = *byteptr;
byteval &= ~(((1 << CLOG_BITS_PER_XACT) - 1) << bshift);
byteval |= (status << bshift);
*byteptr = byteval;
/*
* Update the group LSN if the transaction completion LSN is higher.
*
* Note: lsn will be invalid when supplied during InRecovery processing,
* so we don't need to do anything special to avoid LSN updates during
* recovery. After recovery completes the next clog change will set the
* LSN correctly.
*/
if (!XLogRecPtrIsInvalid(lsn))
{
int lsnindex = GetLSNIndex(slotno, xid);
if (ClogCtl->shared->group_lsn[lsnindex] < lsn)
ClogCtl->shared->group_lsn[lsnindex] = lsn;
}
}
其主要流程如下:
1)首先判断设置的状态信息必须为“提交"、“中止”或“子事务提交”。写CLOG日志记录时,由于其初始化的日志页面为全0,即状态“ 正在处理",所以设置该状态是没有意义的。而且CLOG日志记录的是事务的最终状态,“ 正在处理”并不是一个事务的最终状态,所以设置一个事务的状态必须是“提交"、“中止”或“子事务提交”中的一种。由于涉及状态信息的修改,因此必须申请CLOG日志缓冲池的控制排他锁。
2)然后调用SimpleLruReadPage函数将指定事务的CLOG日志记录所在页面读人到CLOG日志缓冲池中的某一冲区中。如果指定页面已经在缓冲区中,则直接操作该缓冲区,而无需再次执行读入操作。然后,根据事务ID获得事务CLOG日志记录对应的四元组信息,进而设置该事务的CLOG日志中的状态信息。
3)最后置该条日志记录所在页面状态为" 脏",并释放之前持有的缓冲池控制锁。
(3) CLOG日志的读操作
XidStatus
TransactionIdGetStatus(TransactionId xid, XLogRecPtr *lsn)
{
int pageno = TransactionIdToPage(xid);
int byteno = TransactionIdToByte(xid);
int bshift = TransactionIdToBIndex(xid) * CLOG_BITS_PER_XACT;
int slotno;
int lsnindex;
char *byteptr;
XidStatus status;
/* lock is acquired by SimpleLruReadPage_ReadOnly */
slotno = SimpleLruReadPage_ReadOnly(ClogCtl, pageno, xid);
byteptr = ClogCtl->shared->page_buffer[slotno] + byteno;
status = (*byteptr >> bshift) & CLOG_XACT_BITMASK;
lsnindex = GetLSNIndex(slotno, xid);
*lsn = ClogCtl->shared->group_lsn[lsnindex];
LWLockRelease(CLogControlLock);
return status;
}
该操作定义在函数TransactionldSetStatus中,用于从CLOG日志缓冲池中读取事务的CLOG日志记录,从而获取指定事务的最终状态信息。同TransactionIdSetStatus函数一样,此函数也是一个底
层的操作,并不直接提供给其他一般进程获取事务状态,对事务状态的获取由事务日志接口例程的
函数来完成。
其主要流程如下:
1)首先需要请求CLOG日志缓冲池的控制锁。
2)然后调用SimpleLruReadPage函数将指定事务CLOG日志记录所在的页面读人到缓冲区中,如果所需页面已经在缓冲池中,则直接使用。根据事务ID获得事务CLOG日志记录对应的四元组信息。由缓冲区中读出指定事务的CLOG日志记录,从而获得指定事务的最终状态。
3)最后释放所持有的CLOG日志缓冲池的控制锁并返回指定事务的最终状态。
(4) CLOG日志页面的初始化
static int
ZeroCLOGPage(int pageno, bool writeXlog)
{
int slotno;
slotno = SimpleLruZeroPage(ClogCtl, pageno);
if (writeXlog)
WriteZeroPageXlogRec(pageno);
return slotno;
}
该操作定义在函数ZeroCLOGPage中,初始化指定页面为全0;根据writeXlog设置是否要写一
个XLOG日志记录。如果是为扩展CLOG日志页面创建一个新的CLOG8志页面并初始化该页面,
则需要创建一条XLOG日志记录,记录所扩展CLOG日志页面的页面号。
基本流程如下:
1)首先调用SimpleLnuZeroPage函数在缓冲池中选择一个可用缓冲区,并初始化该缓冲区为
全0。
2)然后判断是否需要写XL0G日志,如果writeXlog为TRUE,则调用WriteZeroPageXlogRec函
数将新创建的CLOG页面号记录到一条XL0G记录中;否则无需记录XLOC日志记录,直接返回。
(5) CLOG 日志段的创建
void
BootStrapCLOG(void)
{
int slotno;
LWLockAcquire(CLogControlLock, LW_EXCLUSIVE);
/* Create and zero the first page of the commit log */
slotno = ZeroCLOGPage(0, false);
/* Make sure it's written out */
SimpleLruWritePage(ClogCtl, slotno);
Assert(!ClogCtl->shared->page_dirty[slotno]);
LWLockRelease(CLogControlLock);
}
该操作定义在函数BootStrapCLOG中,在系统初始化时需要调用本函数,以创建第一个CLOG日志段。
执行基本流程如下:
1)首先获取CLOG日志缓冲池的控制锁。
2)然后在缓冲池中选择- -个可用缓冲区供初始CLOG日志页面(页面号为0)使用,调用ZeroCLOGPage函数将该缓冲区初始化为全0。然后,调用SimpleLuWritePage函数将这第一个CLOG日志页面由级冲区写到磁盘中。
3)最后,设置该页面的状态为“干净",并释放所持有的缓冲池控制锁。
(6) CLOG 日志的启动
void
StartupCLOG(void)
{
TransactionId xid = XidFromFullTransactionId(ShmemVariableCache->nextFullXid);
int pageno = TransactionIdToPage(xid);
LWLockAcquire(CLogControlLock, LW_EXCLUSIVE);
/*
* Initialize our idea of the latest page number.
*/
ClogCtl->shared->latest_page_number = pageno;
LWLockRelease(CLogControlLock);
}
该操作定义在函敷StartupCLOG中,基本功能是当Postmaster或一个单独的服务进程启动时,启动CLOG日志管理器。
基本流程如下:
1)首先由共享内存中的变量缓冲区获得下一可分配的事务ID,请求CLOG日志缓冲池的控制锁。
2)然后根据下一可分配的事务ID获得该事务的CLOG日志记录所在的CLOG日志页面,并设置该页面为当前最大的CLOG日志页面。
3)然后调用SimpleLruReadPage函数将该8志页面读人到某-缓冲区中: 根据下一可分配的事务ID获得该事务在页面中的偏移位置,将该CLOG日志页面从该偏移位置之后都设置为全0。
4)最后,设置该CLOG日志页面状态为“脏”,并释放所持有的CLOG日志缓冲池控制锁。
(7) CLOG 日志的关闭
void
ShutdownCLOG(void)
{
/* Flush dirty CLOG pages to disk */
TRACE_POSTGRESQL_CLOG_CHECKPOINT_START(false);
SimpleLruFlush(ClogCtl, false);
/*
* fsync pg_xact to ensure that any files flushed previously are durably
* on disk.
*/
fsync_fname("pg_xact", true);
TRACE_POSTGRESQL_CLOG_CHECKPOINT_DONE(false);
}
该操作定义在函数ShutdownCLOG中,当关闭Postmaster或单独的服务进程时,需要调用本函
数关闭CLOG日志管理器。它的实现是通过调用SimpleLruFlush函数将当前的CLOG日志缓冲池中
的“脏”页面写回到磁盘中而完成的。
(8)创建检查点时CLOG日志的操作
void
CheckPointCLOG(void)
{
/* Flush dirty CLOG pages to disk */
TRACE_POSTGRESQL_CLOG_CHECKPOINT_START(true);
SimpleLruFlush(ClogCtl, true);
/*
* fsync pg_xact to ensure that any files flushed previously are durably
* on disk.
*/
fsync_fname("pg_xact", true);
TRACE_POSTGRESQL_CLOG_CHECKPOINT_DONE(true);
}
该操作定义在函数CheckPointCLOG中,作用是执行一个CLOG日志检查点,在XLOG执行检查
点时被调用。该操作完成的任务就是通过调用SimpleLruFlush图数将当前CLOG日志缓冲池中的
"脏”页面写回到磁盘中。
(9) CLOG 日志的扩展
void
ExtendCLOG(TransactionId newestXact)
{
int pageno;
/*
* No work except at first XID of a page. But beware: just after
* wraparound, the first XID of page zero is FirstNormalTransactionId.
*/
if (TransactionIdToPgIndex(newestXact) != 0 &&
!TransactionIdEquals(newestXact, FirstNormalTransactionId))
return;
pageno = TransactionIdToPage(newestXact);
LWLockAcquire(CLogControlLock, LW_EXCLUSIVE);
/* Zero the page and make an XLOG entry about it */
ZeroCLOGPage(pageno, true);
LWLockRelease(CLogControlLock);
}
该操作定义在函数ExtendCLOC中,为新分配的事务ID创建CLOG日志空间。当一个事务的CLOG日志记录位于一个CLOG日志页面的首部时,若这个事务要写一条CLOG日志记录,就需要调用ExtendCLOG以创建一个新的CLOG日志页面供该条CLOG日志记录写入。
执行流程如下:
1)首先根据事务ID获得该事务CLOC日志记录所在的CLOG日志页面号。
2)然后请求CLOG日志缓冲池的控制锁。
3)将该日志页面初始化为全0,并写一条XLOG日志记录该页面号。
4)最后释放所持有的CLOG日志缓冲池控制锁。
(10) CLOG日志的删除
void
TruncateCLOG(TransactionId oldestXact, Oid oldestxid_datoid)
{
int cutoffPage;
cutoffPage = TransactionIdToPage(oldestXact);
/* Check to see if there's any files that could be removed */
if (!SlruScanDirectory(ClogCtl, SlruScanDirCbReportPresence, &cutoffPage))
return; /* nothing to remove */
AdvanceOldestClogXid(oldestXact);
/*
WriteTruncateXlogRec(cutoffPage, oldestXact, oldestxid_datoid);
/* Now we can remove the old CLOG segment(s) */
SimpleLruTruncate(ClogCtl, cutoffPage);
}
该操作定义在函数TruncateCLOG中。由于日志检查点的建立使得些事务的日志变成过时的,为了节省空间,应该把这些过时的日志记录删除。由于CLOC日志物理存储是以段为单位,因此该操作以段为单位,删除过时的CLOG日志记录。
基本执行流程如下:
1)首先根据指定的事务ID获得该事务的CLOG日志记录所在页面号。
2)接着调用SlnuScanDirectory函数,以该页面为参照页面,检查是否有过时的CLOG日志段需安期际,如来仅有则且按区回。
3)然后请求执行一个检查点。
4)最后调用SimpleLuTruncate函数将过时的CLOG日志段文件删除。
(12) CL0G日志的REDO操作
void
clog_redo(XLogReaderState *record)
{
uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK;
/* Backup blocks are not used in clog records */
Assert(!XLogRecHasAnyBlockRefs(record));
if (info == CLOG_ZEROPAGE)
{
int pageno;
int slotno;
memcpy(&pageno, XLogRecGetData(record), sizeof(int));
LWLockAcquire(CLogControlLock, LW_EXCLUSIVE);
slotno = ZeroCLOGPage(pageno, false);
SimpleLruWritePage(ClogCtl, slotno);
Assert(!ClogCtl->shared->page_dirty[slotno]);
LWLockRelease(CLogControlLock);
}
else if (info == CLOG_TRUNCATE)
{
xl_clog_truncate xlrec;
memcpy(&xlrec, XLogRecGetData(record), sizeof(xl_clog_truncate));
/*
* During XLOG replay, latest_page_number isn't set up yet; insert a
* suitable value to bypass the sanity test in SimpleLruTruncate.
*/
ClogCtl->shared->latest_page_number = xlrec.pageno;
AdvanceOldestClogXid(xlrec.oldestXact);
SimpleLruTruncate(ClogCtl, xlrec.pageno);
}
else
elog(PANIC, "clog_redo: unknown op code %u", info);
}
该操作定义在函数clog_redo 中,CLOG日志的REDO在执行XLOG的REDO操作时被调用,主
要是作为CLOG资源管理器的例程。
由于在扩展或创建一个新的CLOC页面时都要创建一条XL0G日志记录以保存该CLOG页面的页面号,因此在执行XL0G的REDO操作时,如果碰到资源管理器号为RM_ _CLOG_ ID、 类型为ZEROPAGE的XL0G日志记录,那么调用该操作。
执行流程如下:
1)从XLOG日志记录中获得该CLOG日志页面的页面号。
2)然后调用ZeroCLOCPage函数在CLOC日志缓冲池中选择一个可用的缓冲区供该页面使用,并设置该页面为全0。
3)调用SimpleIruWirtePage函数将该CLOG日志页面写回到磁盘中。
4)在XLOG的REDO过程中完成CLOG日志记录的重建过程。在这个过程中需要持有CLOG日志缓冲池的控制锁。
总结
这学期分析postgreSQL工作暂时告一段落,马上迎来考试周了。下面一段话来描述postgreSQL事务处理和并发控制我觉得十分恰当。
如果我们把整个数据库系统视为一个团队,那么事务系统在这个团队中扮演了“ 指挥官"的
角色,它根据外部用户命令以及系统内部状态决定当前数据库系统中操作的执行方向。
PostgreSQL事务管理器的操作其核心功能就是根据当前状态和接收到的外部状态(用户命令)决定当前需要执行的操作,所以它承担了整个系统的决策功能,是“指挥官”的大脑。
PostgreSQL的并发控制机制其核心功能是为了在保证数据一致性的前提下提高并发度,所以它承担了整个系统的调度协调功能,是“指挥官”的节拍器。
PostgreSQL的日志管理机制其核心功能是通过磁盘日志文件来记录数据库操作状态序列以及数据变化过程,所以它承担了整个系统的保障恢复功能。
总之,事务系统串联了整个数据库中各个不同的模块,事务系统的调度决策驱动了整个数据库系统的执行进程。