日志
目的是以安全的方式记录数据库变更的历史,当出错后可以用日志来重建对数据库所作更新的过程从而恢复到一致的状态,保证数据库的一致性和完整性。
用到了XLOG和CLOG,分别是事务日志和提交日志。
- XLOG
- 是通常意义的日志记录,记录更新过程和事务最终状态
- CLOG
- 记录事务的最终状态,比XLOG小也比XLOG高效
为了支持嵌套,引入了SUBTRANS日志记录父事务的ID从而通过一个事务可以递归查找到父事务,然而不能从父事务查找到子事务。
为了支持多版本并发控制,引入了MultiXactID组合事务ID来记录事务的组合关系。
日志中的LSN,log sequence number。
- 每个页面都会有一个LSN,保存最新的影响该页的LOG。
- 当写出一个脏页时,需要保证xlog已经被flush到对应的LSN。这样当进行WAL回放时,如果LSN大于等于日志项的WAL位置,则说明该页面对应的时间点在该日志之后,则不需要再次应用。如此可以加速WAL回放速度。
日志的存储
日志通过日志文件存放,设置了日志缓冲区,先将日志记录写到内存的缓冲区,满了之后再按块写道外存。
缓冲区使用SLRU缓冲区,采用LRU算法作为页面置换算法的缓冲池。
SLRU缓冲池
每一个日志文件为一个段,一个段由32个磁盘页面组成,每个磁盘页面8KB,每一个段文件以段号命名。日志页面号即可计算具体的段文件和偏移<Segmentno, Pageno>
SLRU缓冲池由8个缓冲区,每一个为一个页面,对应一个磁盘块,用SLRU算法调度页面。读写写日志先写到对应日志页面的缓冲区,再写入磁盘的日志段文件。
在对缓冲区进行I/O时,我们在释放控制锁之前独占地获取每个缓冲区的锁。在完成I/O、重新获取控制锁并更新共享状态之后,释放每个缓冲区的锁。(在这里不可能发生死锁,因为我们永远不会在其他进程正在对同一缓冲区进行I/O时尝试启动I/O。)为了等待I/O完成,释放控制锁,以共享模式获取每个缓冲区的锁,立即释放每个缓冲区的锁,重新获取控制锁,然后重新检查状态(因为在我们没有锁定时可能发生任意的事情)。
共享内存中的缓冲池数据结构:
|
|
缓冲池的控制结构:
|
|
记录打开的物理磁盘文件,在写入时一次性写入:
|
|
这记录了SimpleLruWriteAll()时用到的文件及日志段号,并定义SlruWriteAll为对应的指针类型。
主要操作
SimpleLruInit
初始化缓冲池,分配共享内存空间。
|
|
-
参数
- ctl:本地非共享缓冲池控制结构指针
- 缓冲池名称:用户可见
- nslots:缓冲池中的页面数
- ctllock:用于控制对共享控制结构的访问的轻量锁
- subdir:相对PGDATA的用于防止数据的相对目录
- tranche_id:轻量级锁的组编号
- sync_handler:用于处理LRU的同步请求的函数
-
操作
-
1 2 3
shared = (SlruShared) ShmemInitStruct(name, SimpleLruShmemSize(nslots, nlsns), &found);
通过缓冲池名称查找并获取共享内存中的缓冲池。
在VC中,__int64表示64位整型,其他环境用32位int
1 2 3 4 5 6 7 8 9 10 11
#ifdef _WIN64 typedef unsigned __int64 size_t; typedef __int64 ptrdiff_t; typedef __int64 intptr_t; #else typedef unsigned int size_t; typedef int ptrdiff_t; typedef int intptr_t; #endif typedef size_t Size;
获取共享内存中Slru控制结构的大小:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Size SimpleLruShmemSize(int nslots, int nlsns) { Size sz; /* we assume nslots isn't so large as to risk overflow */ sz = MAXALIGN(sizeof(SlruSharedData)); sz += MAXALIGN(nslots * sizeof(char *)); /* page_buffer[] */ sz += MAXALIGN(nslots * sizeof(SlruPageStatus)); /* page_status[] */ sz += MAXALIGN(nslots * sizeof(bool)); /* page_dirty[] */ sz += MAXALIGN(nslots * sizeof(int)); /* page_number[] */ sz += MAXALIGN(nslots * sizeof(int)); /* page_lru_count[] */ sz += MAXALIGN(nslots * sizeof(LWLockPadded)); /* buffer_locks[] */ if (nlsns > 0) sz += MAXALIGN(nslots * nlsns * sizeof(XLogRecPtr)); /* group_lsn[] */ return BUFFERALIGN(sz) + BLCKSZ * nslots; }
在共享内存中寻找/分配一个数据结构,如果没有创建过则会为这个结构分配内存,如果创建过则会把已有的架构返回。
return一个指向共享内存中对应位置的指针。如果是已有则会把foundPtr设置成true。
9.0之前该函数出错会返回NULl,15.4中会抛出错误。
1 2
void * ShmemInitStruct(const char *name, Size size, bool *foundPtr)
-
操作:
-
获取共享内存索引锁:
1
LWLockAcquire(ShmemIndexLock, LW_EXCLUSIVE);
该锁在lwlognames.h中define,对应MainLWLockArray[1]
1 2 3 4
#define ShmemIndexLock (&MainLWLockArray[1].lock) #define OidGenLock (&MainLWLockArray[2].lock) #define XidGenLock (&MainLWLockArray[3].lock) ...
-
检查ShmemIndex是否已经被分配,没有的话先分配
-
在ShememIndex中搜索name对应的位置
1 2
result = (ShmemIndexEnt *) hash_search(ShmemIndex, name, HASH_ENTER_NULL, foundPtr);
HASH_ENTER_NULL的含义是如果内存不足则返回null,如果不存在则创建一个并返回表项。
表项数据结构:
1 2 3 4 5 6 7 8
/* this is a hash bucket in the shmem index table */ typedef struct { char key[SHMEM_INDEX_KEYSIZE];//索引名 void *location; //共享内存指针位置 Size size; //请求分配的大小 Size allocated_size; //实际分配的大小 } ShmemIndexEnt;
-
如果没找到且内存不足,释放索引锁,退出。
-
如果找到了
- 如果找到的size与函数的输入size不相等,报错
- 设置structPtr为result的location
-
如果没找到需要分配
-
调用ShememAllocaRaw分配内存并保存分配到的大小
1
structPtr = ShmemAllocRaw(size, &allocated_size);
如果没分配到,报错。
-
设置索引项的信息
1 2 3
result->size = size; result->allocated_size = allocated_size; result->location = structPtr;
-
-
释放索引锁,返回structPtr
-
-
-
如果当前处于Postmaster运行下,所需的SLRU缓冲池还不存在,初始化该缓冲池,否则理应已有。
-
清空shared中的结构部分,将传入的信息放到结构中:
1 2 3 4 5 6 7 8 9
memset(shared, 0, sizeof(SlruSharedData)); shared->ControlLock = ctllock; shared->num_slots = nslots; shared->lsn_groups_per_page = nlsns; shared->cur_lru_count = 0; shared->slru_stats_idx = pgstat_get_slru_index(name);
-
用char指针ptr指向shared。
- 设置pagebuffer指向ptr=ptr+MAXALIGN(sizeof(SlruSharedData))
- 设置page_status指向ptr=ptr+MAXALIGN(nslots * sizeof(char *));
- 设置page_dirty指向ptr=ptr+MAXALIGN(nslots * sizeof(SlruPageStatus));
- 设置page_number指向ptr=ptr+MAXALIGN(nslots * sizeof(bool));
- 设置page_lru_count指向ptr=ptr+MAXALIGN(nslots * sizeof(int));
- 设置buffer_locks指向ptr=ptr+MAXALIGN(nslots * sizeof(int));
- 如果nlsns大于0,设置group_lsn指向ptr=ptr+offset += MAXALIGN(nslots * sizeof(LWLockPadded));
- ptr指向当前所有数据结构的末尾,重复nslots次初始化每个slot对应的轻量级锁、page_buffer、page_status为SLRU_PAGE_EMPTY、page_dirty为false、page_lru_count为0。其中page_buffer从ptr开始排布。
-
-
最后,设置ctr的shared为这个初始化完成/找到的shared,设置sync_handler,最后将subdir拷贝到ctl->Dir中,作为存储的目录。
-
slruSelectLRUPage
用于从缓冲池中替换掉一个page,从而获取一个空白槽。
|
|
需要传入pageno是因为可能其它进程正在读取这个页面,这时要检查是否页面已经存在于槽中。所以返回值要么是一个已经存储这个页面的槽,或者是一个清空的槽。
|
|
先初始化best_valid和best_invalid
|
|
遍历所有槽,如果由槽的pageno就是所需且不是EMPTY状态,则直接返回。
随后开始正式寻找
|
|
如果best_valid_delta小于0,说明没找到。
|
|
这时选择等待它的io,因为它最老,所以按照预期它会最先完成io
如果找到了,判断是否是dirty,如果不是dirty说明可以直接用:
|
|
否则要写入外存并再次判断
SlruInternalWritePage(ctl, bestvalidslot, NULL);
SimpleLruZeroPage
|
|
用SlruSelectLruPage
选择slot号,设置相关参数
|
|
其中SlruRecentlyUsed如下:
|
|
由于对最新页面的访问可能连续进行,所以用if进行判断来比卖你老页面的count_delta增长的过快。
只有当这个页面不是最新访问的页面时,才让cur_lru_count递增,并设置slotno的lru_cout为递增后的count。
|
|
SimpleLruReadPage
从缓存中找到对应的页面,当有需要时读取。如果write_ok,则可以返回正在写入的页面,否则要等待IO完成。
输入的xid用来进行错误报告。进入该函数前ctl内部的控制锁需要被获取。
返回值是一个缓存槽号。
|
|
在外层无条件for中,先调用SlruSelectLRUPage来获取到可以存这个页面的槽号
|
|
随后判断对应的是已经在内存中的缓存还是一个空的缓存,如果是已有的且可用则直接返回。
|
|
完成后,得到了内部是空的槽
标记为正在读取:
|
|
管理锁:
|
|
进行实际的物理读
|
|
具体的物理读操作再后文说明。
设置这个页面的LSN为空并重新获取控制锁:
|
|
|
|
根据是否成功读取设置SLRU_PAGE的状态,释放buffer锁。如果没能成功读取,则报告IO错误。最后更新LRU计数和统计数据。
slru物理读SlruPhysicalReadPage
|
|
获取段号、偏移、目录、文件号
|
|
调用宏,将DIR和seg合并为最终文件路径放到path内,其中用至少4位16进制大写作为文件名。
随后调用OpenTransientFile打开文件
|
|
值得注意的是,在崩溃重启情况下,可能会出现事务对应的数据处于截断的提交日志中,故允许文件不存在并当作读入全0数据。
随后判断fd的数值,如果小于0说明出错。此时如果不在恢复状态,说明确实有问题返回false,处于恢复状态则记录log,随后项buffer中写入全0。
调用pgstat_report_wait_start来报告一个需要等待的事件信息,其中摄制my_wait_event_info全局变量为传入的info
随后真正开始读:
|
|
如果读取不成功,则先汇报等待事件结束,设置全局变量说明为什么不成功,最后关闭打开的文件。
如果读取成功,进行最后的操作:
|
|
其中同样先汇报等待事件结束,随后关闭文件描述符并捕获记录的错误,返回结果true/false
SimpleLruWritePage->SlruInternalWritePage
将一个指定的slot号中的页面由缓冲区写入磁盘。与Read类似,调用前需要获取控制锁。
值得注意的是如果对应的页面不是dirty的,则不会写入。如果在写入的时候其它人再次污染了这个页面,不会有并发问题但是会导致离开函数的时候依然是脏页。
先计算出pageno,如果正在写入,则等待IO直到没写入。
如果不是dirty的,直接返回。
否则标记页面为正在写入,并且不是脏页,获取buffer锁,释放控制锁。
调用SlruPhysicalWritePage写入页面,如果失败了且fdata不为空,处于flush状态,则关闭fdata内的所有文件指针。
获取控制锁,如果没能成功写入则继续标记为dirty;设置页面状态为valid,释放buffer锁,如果fdata不为空则递增buff_written计数。
SimpleLruWriteAll
写入所有脏页,在checkpoint和数据库关闭时被调用。
首先记录flush统计数据,定义一个fdata局部变量。
设置写入文件数量为0,获取控制锁:
|
|
对所有slot,调用slruInternalWritePage刷写脏页,释放控制锁
对fdata中的所有文件,关闭文件。
SimpleLruTruncate
把所有比传入页面老的脏页面写入并删除文件,因为它们已经随着检查点建立而变得没用了。
获取控制锁,确保最新的latest_page_number不应当满足移除条件。
遍历所有比传入页面老的非空待写入页面,空闲写入,不空闲等待。
最后删除掉对应文件。
日志目录遍历 SlruScanDirectory
|
|
遍历ctl对应日志目录下的所有文件,向回调函数传入stl、文件名、文件内第一个页面的页面号、data指针。
日志的管理
通过日志管理器来管理日志缓冲区,数据库不操作磁盘上的日志文件,而是通过日志缓冲区。日志缓冲区和外存之间的交互由日志管理器来完成。
postgreSQL有四种日志管理器:
- XLOG
- CLOG
- SUBTRANS子事务日志
- MULTIXACT组合事务日志
获取事务的状态通过事务日志接口程序而非日志管理器进行操作。该程序定义了可使用的ID范围、可使用对象ID范文、设置和获取事务状态信息的接口,同时也建立了缓冲区来缓存最近获取的事务ID及其状态。建立了共享变量缓冲区数据结构,存储下一可分配事务ID、对象ID及已分配对象数。
资源管理器再日志系统中把各种需要记录日志的数据分类,在日志中标识资源管理器号来标记源数据属于哪一类,进而选择资源管理器的对应方法。
CLOG
数据提交日志
记录是无状态和事务嵌套关系
CLOG数据结构
|
|
表示执行中、已提交、被终止、子事务已提交,四种状态两位比特表示
如果子事务已提交,但父事务没有提交,则设置子事务状态为SUB_COMMITED
通过理论计算CLOG的占用内存可以发现,共有$2^{32}$个事务,共需要$2^{33}$比特,$2^{17}$个页面,$2^{12}$个段,共需要与段数对应的文件来存储。
此处,PG定义了四个计算宏来从id计算对应的文件保存位置:
|
|
相关操作
内存初始化——CLOGShmemInit
CLOG全局有一个指向共享Slru控制器的非共享变量
在此函数中,设置其的比较器为COGPagePrecedes
调用SimpleLruInit初始化该LRU缓冲
最后,调用了一个UnitTest来确认正确性。
|
|
值得注意的是其共享内存索引名是Xact,子目录名是pg_xact,锁的组是LWTRANCHE_XACT_BUFFER,slot数量为CLOGShmemBuffers()
CLOG缓冲数量
|
|
含义是按照NBuffers的全局量定义来计算,大于4小于128。
写入
TransactionIdSetPageStatusInternal
设置的status要求不能是inProgress,进入前要获取XactSLRULock
|
|
根据pageno用XactCtl读取对应的page并读取到缓存
|
|
如果xid有效则设置xid,如果要设置的是COMMITED,则先设置所有子事务为SUB_COMMIT提交状态。
|
|
最后,设置所有子事务状态为对应值,标记脏页面
|
|
TransactionIdSetStatusBit
用来被内部调用,写入对应位
|
|
需要先获取XactSLRULock
获取到Byteno和bshuft,用临时char指针byteptr指向对应的字节,随后获取到当前的事务状态。
|
|
设置对应状态。
读取
TransactionIdGetStatus
|
|
返回xid状态,向*lsn中存储晚于xid对应CLOG的LSN
同样先计算页号及偏移:
|
|
如果整个clog页面已经存到了磁盘里,则会返回非法XLogRecPtr;如果log所在组有其它更晚的clog,同样会返回同组更晚的CLOG的LSN
|
|
先用只读模式读页面并获取slotno,用类似set的方法获取状态,写入group_lsn到*lsn,最后释放锁。
日志页面初始化
ZeroCLOGPage
|
|
如果writeXlog为True,则会同时写一个Xlog record
用pageno和XactCtl调用SimpleLruZeroPage,根据writeXlog调用WriteZeroPageXlogRec。
日志段创建
BootStrapCLOG
在整个系统的启动过程中只被调用一次,用来创建最初的CLOG段。
其中CLOG文件目录被initdb创建
先获取XactSLRULock锁,再用ZeroCLOGPage获取一个可用的空页面,最后调用SimpleWritePage确保页面被写入文件,最后释放锁。
CLOG日志启动
StartupCLOG
当Postmaster或者一个服务进程启动时,启动CLOG日志管理器,仅被调用一次。
先从ShmemVaribleCache共享变量中获取下一个xid保存到xid中,再用xid得到正确的pageno。
获取XactSLRULock锁,设置XactCtl内保存的处于共享内存的缓冲区的最新page_number为下一个xid对应的pageno,最后释放锁。
对比新老版本,此处不再对对应页面进行清空处理。
CLOG日志保存点
在CLOG日志关闭时或SAVEPOINT,调用SimpleLruWriteAll刷新页面。
CLOG日志清理
TruncateCLOG
|
|
删除oldestXact之前的CLOG日志段,先找到传入的xactid对应的页面作为最晚页面,调用SlruScanDIrectory,用SlruScanDirCbReportPresence和最晚页面号遍历所有Clog文件。如果找不到能删除的,直接退出。
|
|
随后先将共享变量的oldestClogXid提前到传入的xid,写入XLOG并刷写XLOG到磁盘。(?)
最后传入最老页面号调用SimpleLruTruncate。
构建CLOG日志时写入XLOG
info为CLOG_ZEROPAGE写入XLOG
|
|
在XLOG部分详述
CLOG的REDO
在rmgrlist中注册为clog的redo方法
|
|
计算出info后根据info进行redo,只会有两种操作,ZEROPAGE和TRUNCATE,进行对应redo操作。
XLOG
数据库日志主体,记录所有数据操作的变化过程