2021@SDUSC
目录
概述
日志是数据库系统必不可少的一部分,它以一种安全的方式记录数据库变更的历史。当系统出现故障后,数据库系统通过使用日志来重建对数据库所做更新的过程,以恢复数据库到一致状态,从而保证数据库的一致性和完整性。
PostgreSQL采用的日志主要有XLOC和CLOG,即事务日志和事务提交日志。XLOG是一般的日志记录,即通常意义上所认识的日志记录,它记录了事务对数据更新的过程和事务的最终状态。CLOG是XLOG的一种辅助形式,记录了事务的最终状态。因为每一条XLOG日志记录相对较大,如果需要通过日志判断一个事务的状态,那么使用CLOG比使用XLOG要高效得多。同时CLOG占用的空间也非常有限。此外,为支持嵌套事务,PostgreSQL还引入了SUBTRANS日志记录一即子事务 日志,记录每个事务的父事务的事务ID,这样通过一个事务可以递归查找到其父事务,但是并不能通过一个事务 查找到其子事务。同时,为了支持多版本并发控制,PostgreSQL 引入了组合事务ID ( MultiXactID),记录事务的组合关系,并维护从众多事务ID到MultiXactID的映射关系。
在PostgreSQL中,日志通过日志文件来存放。如果每个日志记录在创建时都被立即写到磁盘中,那么将增加大量I/O开销。因为通常向磁盘的写人是以块为单位进行的,而在大多数情况下,一个日志记录比一个块要小得多,为了降低写人日志带来的IO开销,数据库系统在实现时往往设置了日志缓冲区,即先将日志记录写到主存中的日志缓冲区中,当日志缓冲区满了以后以块为单位向磁盘写出。
在数据库系统中,日志缓冲区通过日志管理器来管理。数据库系统并不直接操作磁盘日志文件,一般是通过日志缓冲区来使用日志文件,而缓冲区和磁盘之间的交互、同步则由日志管理器来完成。PostgreSQL 有四种日志管理器,即XLOG (事务日志)、CLOG (事务提交日志)、SUBTRANS(子事务日志)以及MULTIXACT (组合事务日志)。
PostgreSQL通过同一种缓冲区来实现对CLOG日志、SUBTRANS日志以及MULTIXACT日志的管理,即SLRU缓冲池一采用简单LRU算法作为页面置换算法的缓冲池。
如果要获取一个事务的状态,并不是直接通过日志管理器操作日志缓冲池,而是要通过事务日志接口例程进行操作。在事务日志接口例程中,一方面定义了可使用事务ID号的范围、可使用对象ID号的范围,另一方面提供了可设置和获取事务状态信息的接口例程。此外,为减少I/O次数,也使用了缓冲区机制,即建立一个单独的缓冲区,用于缓存最近获取的事务ID及其状态。而且为方便事务管理,建立了一个共享变量缓冲数据结构,存储下一可分配事务ID、对象ID以及已分配对象数。由于使用日志的资源有很多种,PostgreSQL为对日志进行分类,使用了资源管理器的概念。资源管理器主要用于在日志系统中把各种需要记录日志的数据分类,通过在日志中标识资源管理器号,使系统在恢复或者读取日志记录时,能够很方便地知道该日志记录的源数据属于哪一类,从而通过资源管理器的方法可以准确地选择对应的方法。
SLRU缓冲池
在介绍SLRU缓冲池之前,先说明一下PostgreSQL对CLOG和SUBTRANS日志的物理存储组织方法。CLOG和SUBTRANS日志在磁盘中由一个个小的物理文件组成,每一个 物理日志文件定义为一个段,一个段由32个磁盘页面组成,每-一个磁盘页面的大小为8KB。每一个段文件以段号来命名,通过一个段号就可以找到对应的日志文件。进一步,只要有日志的页面号,就可以找到该页面所在的段文件及该页在段文件中的偏移,从而将该日志页面调人到内存缓冲区中,供日志管理器使用。也就是说,通过二元组< Segmentno, Pageno >就可以定位日志页在哪个段文件中以及在该文件中的偏移位置。
SLRU缓冲池由8个缓冲区组成。每一个缓冲区为一个页面,对应一个磁盘块,大小为8KB。页面调度算法采用Slru算法,即简单的最近最少使用算法。因为缓冲区只有8个页面,所以实现搜索时,只需要使用简单的线性搜索算法。
写日志记录时,先写人到对应日志页面所在的缓冲区,再进一步写人到磁盘上的日志段文件。而读日志文件首先将要读的日志记录所在页面读人到缓冲池的某一个页面,再在日志缓冲区中读具体的日志记录。
1.缓冲池的并发控制
PostgreSQL使用轻量级锁实现对LrU缓冲区的并发控制。其中使用一个控制锁(用轻量级锁实现)来保护整个数据缓冲区,而对于每一个缓冲区,当进行I/0同步时,还使用一个LWLock来保护。一个进程在读或写一个页面缓冲区时并不持有控制锁,而只是对当前所读人或写出的页面所在缓冲区加一个缓冲区锁。当改变一个缓冲区所对应页面或其状态时,需要持有控制锁。但当一个写进程在持有一个缓冲区锁并将缓冲区状态由DIRTY改为WRITE__IN_PROGRESS时,此时改变状态并不需要持有控制锁。
当缓冲区状态不是EMPTY或CLEAN时,可能有进程正在对缓冲区进行I/0操作,所以此时页面号码不能改变;此时唯一可以进行状态改变的是在页脏了以后从状态WRITE_ IN_ PROGRESS转换为状态DIRTY。
当一个缓冲区的任何状态转换动作涉及潜在的I/0操作时,进程需要同时持有控制锁和缓冲区锁。请求锁的过程是先获取一个缓冲区锁再获取控制锁,即不要在获取控制锁后再等待缓冲区锁。如果一个进程试图读取一个页面,则将其缓冲区状态标识为READ _IN_ PROGRESS, 然后释放控制锁,再请求一个缓冲区锁,在继续执行前重新检查缓冲区的状态(目的在于防止其他人试图将相同的页面读取到另外一个缓冲区中)。
2.缓冲区相关数据结构
缓冲池除了共享的数据缓冲区外,还有一个非共享的控制结构,提供缓冲池的控制信息。
typedef struct Slruct1Data
{
Sl ruShared shared;//共享内存中共享的数据壤冲区指针
bool do_ fsync;/*判断在写文件时是否进行同步写。默认对于pg_ _clog
/*为真,对于pg_ subtrans为假*/
bool (* PagePrecedes) (int,int); //比较两个页面前后关系的方法接口
char Dir[64];//物理文件在磁盘的存储目录
}Sl ruCtl Data;
typedef SlruCt1Data * SlruCt1;
SLRU缓冲池使用SlruFlushData记录打开的物理磁盘文件。当进行刷写操作时,将对多个文件的更新数据一次性刷写到磁盘。
typedef struct Sl ruFlushData
{
int num_ files ;//实际打开的文件个数
int fd[ NUM SLRU BUFFERS] ; //缓冲区对应的已打开文件的FD
int segno[ NUM_ SLRU _BUFFERS]; //缓冲区对应页面所在的段号
} SlruFlushData;
3.缓冲池主要操作
缓冲池主要的主要操作包括缓冲池的初始化、缓冲池页面的选择、页面的初始化、缓冲池页面
的换人换出、缓冲池页面的删除等。
(1)缓冲池的初始化
缓冲池的初始化定义在函数SimpleLuInit中,主要功能是在共享内存中初始化SLRU缓冲池,并分配内存空间供数据缓冲区使用。
在共享内存中记录了已存在的SLRU缓冲池,并提供了根据其名称找到该缓冲池共享数据缓冲区地址的索引,如CLOC,其名称为“CLOG Ctu"; 而对于SUBTRANS,其名称为“SUBTRANSCtl"。如果所需类型的缓冲池已经存在,那么直接使用该缓冲池,并不需要重新建立;如果不存在,那么在共享内存中申请空间并建立,后需要在共享内存中用其名称注册该缓冲池。
(2)缓冲区的选择
该操作定义在函数SlruSelectLRUPage中,主要用于获取一个缓冲区。当需要-一个缓冲区用于读
入新页面时,需要调用这个函数获得一个可用的缓冲区,当需要创建并初始化一个缓冲区时, 也需要调用本函数。
static int
SlruSelectLRUPage(SlruCtl ctl, int pageno)
{
SlruShared shared = ctl->shared;
/* Outer loop handles restart after I/O */
for (;;)
{
int slotno;
int cur_count;
int bestvalidslot = 0; /* keep compiler quiet */
int best_valid_delta = -1;
int best_valid_page_number = 0; /* keep compiler quiet */
int bestinvalidslot = 0; /* keep compiler quiet */
int best_invalid_delta = -1;
int best_invalid_page_number = 0; /* keep compiler quiet */
/* See if page already has a buffer assigned */
for (slotno = 0; slotno < shared->num_slots; slotno++)
{
if (shared->page_number[slotno] == pageno &&
shared->page_status[slotno] != SLRU_PAGE_EMPTY)
return slotno;
}
/*
* If we find any EMPTY slot, just select that one. Else choose a
* victim page to replace. We normally take the least recently used
* valid page, but we will never take the slot containing
* latest_page_number, even if it appears least recently used. We
* will select a slot that is already I/O busy only if there is no
* other choice: a read-busy slot will not be least recently used once
* the read finishes, and waiting for an I/O on a write-busy slot is
* inferior to just picking some other slot. Testing shows the slot
* we pick instead will often be clean, allowing us to begin a read at
* once.
*
* Normally the page_lru_count values will all be different and so
* there will be a well-defined LRU page. But since we allow
* concurrent execution of SlruRecentlyUsed() within
* SimpleLruReadPage_ReadOnly(), it is possible that multiple pages
* acquire the same lru_count values. In that case we break ties by
* choosing the furthest-back page.
*
* Notice that this next line forcibly advances cur_lru_count to a
* value that is certainly beyond any value that will be in the
* page_lru_count array after the loop finishes. This ensures that
* the next execution of SlruRecentlyUsed will mark the page newly
* used, even if it's for a page that has the current counter value.
* That gets us back on the path to having good data when there are
* multiple pages with the same lru_count.
*/
cur_count = (shared->cur_lru_count)++;
for (slotno = 0; slotno < shared->num_slots; slotno++)
{
int this_delta;
int this_page_number;
if (shared->page_status[slotno] == SLRU_PAGE_EMPTY)
return slotno;
this_delta = cur_count - shared->page_lru_count[slotno];
if (this_delta < 0)
{
/*
* Clean up in case shared updates have caused cur_count
* increments to get "lost". We back off the page counts,
* rather than trying to increase cur_count, to avoid any
* question of infinite loops or failure in the presence of
* wrapped-around counts.
*/
shared->page_lru_count[slotno] = cur_count;
this_delta = 0;
}
this_page_number = shared->page_number[slotno];
if (this_page_number == shared->latest_page_number)
continue;
if (shared->page_status[slotno] == SLRU_PAGE_VALID)
{
if (this_delta > best_valid_delta ||
(this_delta == best_valid_delta &&
ctl->PagePrecedes(this_page_number,
best_valid_page_number)))
{
bestvalidslot = slotno;
best_valid_delta = this_delta;
best_valid_page_number = this_page_number;
}
}
else
{
if (this_delta > best_invalid_delta ||
(this_delta == best_invalid_delta &&
ctl->PagePrecedes(this_page_number,
best_invalid_page_number)))
{
bestinvalidslot = slotno;
best_invalid_delta = this_delta;
best_invalid_page_number = this_page_number;
}
}
}
/*
* If all pages (except possibly the latest one) are I/O busy, we'll
* have to wait for an I/O to complete and then retry. In that
* unhappy case, we choose to wait for the I/O on the least recently
* used slot, on the assumption that it was likely initiated first of
* all the I/Os in progress and may therefore finish first.
*/
if (best_valid_delta < 0)
{
SimpleLruWaitIO(ctl, bestinvalidslot);
continue;
}
/*
* If the selected page is clean, we're set.
*/
if (!shared->page_dirty[bestvalidslot])
return bestvalidslot;
/*
* Write the page.
*/
SlruInternalWritePage(ctl, bestvalidslot, NULL);
/*
* Now loop back and try again. This is the easiest way of dealing
* with corner cases such as the victim page being re-dirtied while we
* wrote it.
*/
}
}
换人换出策略(LRU)就体现在该函数中,即每一个 读人缓冲区的页面都有一个LRU值,其初始值为0。当需要替换一个页面时,选取用于替换的页面的准则就是该页面的LRU值最大,即该页面最近最少使用。此外,由于对于日志记录,都是不断往日志文件中写人记录,所以对于当前日志文件中的页面号最大的页面,如果该页面在缓冲区中,那么并不把它替换出去,即不参与LRU替换,原因是该页面随时都需要被写人新数据,为提高效率,所以并不替换该页面。
(3)缓冲池页面的初始化
该操作定义在函数SimplelruZeroPage中,主要功能是在缓冲池中选择一个缓冲区供指定页面使
用,将该页面初始化为全0,并设置该页面标识值为当前最大值。在扩展日志文件时,该操作可以.
创建一个新的日志页面,方便后继的日志记录的写入,并最终写到磁盘。
int
SimpleLruZeroPage(SlruCtl ctl, int pageno)
{
SlruShared shared = ctl->shared;
int slotno;
/* Find a suitable buffer slot for the page */
slotno = SlruSelectLRUPage(ctl, pageno);
Assert(shared->page_status[slotno] == SLRU_PAGE_EMPTY ||
(shared->page_status[slotno] == SLRU_PAGE_VALID &&
!shared->page_dirty[slotno]) ||
shared->page_number[slotno] == pageno);
/* Mark the slot as containing this page */
shared->page_number[slotno] = pageno;
shared->page_status[slotno] = SLRU_PAGE_VALID;
shared->page_dirty[slotno] = true;
SlruRecentlyUsed(shared, slotno);
/* Set the buffer to zeroes */
MemSet(shared->page_buffer[slotno], 0, BLCKSZ);
/* Set the LSNs for this new page to zero */
SimpleLruZeroLSNs(ctl, slotno);
/* Assume this page is now the latest active page */
shared->latest_page_number = pageno;
return slotno;
}
(4)缓冲池页面的读操作
该操作定义在函数SimpleLruReadPage中,主要作用是在缓冲池中找到所需要的页面。如果指
定页面已经在缓冲池中,那么直接返回其所在的缓冲区;否则调用SlruPhysicalReadPage函数物理读入指定页面到某一缓冲区中,并返回该缓冲区号。
int
SimpleLruReadPage(SlruCtl ctl, int pageno, bool write_ok,
TransactionId xid)
{
SlruShared shared = ctl->shared;
/* Outer loop handles restart if we must wait for someone else's I/O */
for (;;)
{
int slotno;
bool ok;
/* See if page already is in memory; if not, pick victim slot */
slotno = SlruSelectLRUPage(ctl, pageno);
/* Did we find the page in memory? */
if (shared->page_number[slotno] == pageno &&
shared->page_status[slotno] != SLRU_PAGE_EMPTY)
{
/*
* If page is still being read in, we must wait for I/O. Likewise
* if the page is being written and the caller said that's not OK.
*/
if (shared->page_status[slotno] == SLRU_PAGE_READ_IN_PROGRESS ||
(shared->page_status[slotno] == SLRU_PAGE_WRITE_IN_PROGRESS &&
!write_ok))
{
SimpleLruWaitIO(ctl, slotno);
/* Now we must recheck state from the top */
continue;
}
/* Otherwise, it's ready to use */
SlruRecentlyUsed(shared, slotno);
return slotno;
}
/* We found no match; assert we selected a freeable slot */
Assert(shared->page_status[slotno] == SLRU_PAGE_EMPTY ||
(shared->page_status[slotno] == SLRU_PAGE_VALID &&
!shared->page_dirty[slotno]));
/* Mark the slot read-busy */
shared->page_number[slotno] = pageno;
shared->page_status[slotno] = SLRU_PAGE_READ_IN_PROGRESS;
shared->page_dirty[slotno] = false;
/* Acquire per-buffer lock (cannot deadlock, see notes at top) */
LWLockAcquire(&shared->buffer_locks[slotno].lock, LW_EXCLUSIVE);
/* Release control lock while doing I/O */
LWLockRelease(shared->ControlLock);
/* Do the read */
ok = SlruPhysicalReadPage(ctl, pageno, slotno);
/* Set the LSNs for this newly read-in page to zero */
SimpleLruZeroLSNs(ctl, slotno);
/* Re-acquire control lock and update page state */
LWLockAcquire(shared->ControlLock, LW_EXCLUSIVE);
Assert(shared->page_number[slotno] == pageno &&
shared->page_status[slotno] == SLRU_PAGE_READ_IN_PROGRESS &&
!shared->page_dirty[slotno]);
shared->page_status[slotno] = ok ? SLRU_PAGE_VALID : SLRU_PAGE_EMPTY;
LWLockRelease(&shared->buffer_locks[slotno].lock);
/* Now it's okay to ereport if we failed */
if (!ok)
SlruReportIOError(ctl, pageno, xid);
SlruRecentlyUsed(shared, slotno);
return slotno;
}
}
总结
阅读postgreSQL的日志管理源码工作量还是很大的,这周看了一半。下周打算看完。欢迎批评指正。