今天来从理论上了解一下postgresql的full-page-writes(全页写),我们平时在管理数据库的过程中,经常都会因为接触不多而忽略这个特性,但是了解这个特性,对于数据库的管理技术是非常有帮助的,对于数据库原理、数据库的调优以及数据库的备份恢复的时候都会有更加全面深入的一个了解。
全页写的概述
在数据库发生一次checkpoint检查点后,需要往数据库的一个数据块里面插入数据,数据库在修改前需要把这个数据块从磁盘读到内存中数据缓冲区(shared buffer pool)里,然后再内存中进行数据块的修改插入。当我们执行insert语句时,对数据块进行插入数据a,内存中的数据块里面就会新增一条数据a。在commit提交后,pg数据块就会将这整个块写到wal buffer日志缓冲区,然后再写到wal日志文件中。然后我们再次对数据块进行插入数据b,内存中的数据缓冲区同样会再次新增一条数据b。在commit提交后,这个时候pg数据库就会将新插入数据b的事务日志条目写到wal buffer日志缓冲区,最后再将这条数据库b的事务日志条目写到wal日志文件中。
全页写就是把整个数据库块的内容写到wal buffer日志缓冲区和wal日志文件中。一个wal记录长度是8字节,每个wal段文件默认为16mb。一个wal段可以记录将近200万事务。而如果存储8kb大小的数据块,只能储存2048个。就会导致wal的写入量是非常大的。
全页写的特点
-
全页写的概念
将整个数据块写入到wal日志文件中。
-
全页写的优点
提高数据库的安全性,解决块不一致问题。
-
全页写的缺点
导致wal日志膨胀;
增加额外的磁盘i/o,影响数据库整体性能;
导致主备延迟变大。
-
全页写的控制
full_page_writes(默认on)。
全页写的模式
非强制模式
对于修改操作,当启用全页写时,pg会在每个检查点之后、每个页面第一次发生变更时,将头数据和整个页面作为一条wal记录写入wal缓冲区。
- 最近一次检查点之后,第一次修改的数据块会进行全页写,后续再修改时不会进行全页写,直到下一次检查点发生。
强制模式
对于备份操作,强制启用全页写,只要块发生变化,就会被整块写入wal文件(不管是不是第一次,也不管有没有检查点)。因此,它写入的量是更大的。
-
当用pg_basebackup对数据库进行备份时,会自动执行强制模式,在备份期间被修改的数据块会全部写入wal当中。
-
pg_start_backup命令,对应函数do_pg_start_backup(xlog.c文件),其中开启强制全页写。
pg_stop_backup对应的函数do_pg_stop_backup,有一句关闭强制全页写。
因此手动执行pg_start_backup命令之后,备份完一定要执行pg_stop_backup,避免wal暴增
建议数据库备份时间点选在业务空闲时间段进行。
oracle full-page-writes
1、不提供full-page-writes开关控制。
2、以下备份发生时自动启动全页写。
alter tablespace xxx begin backup; alter database begin backup;
块不一致的场景
对postgresql来说,块不一致可以发生在两种场景:
- pg异常宕机(或者出现磁盘错误)时,数据文件中的页只写入了一部分。
- 使用操作系统命令备份正在运行的数据库,备份途中源数据库可能被修改,此时得到的备份数据状态就是不一致的
无论是崩溃恢复还是备份还原的恢复,都无法基于不一致的数据块进行。
块不一致的原因
-
操作系统进行i/o操作时,总是以块为单位,比如512字节、1kb等等。
-
数据库块一般是操作系统块的整数倍,比如2k、4k、8k等等。
-
块是数据库最小的i/o单位,当数据库写一个数据块时,操作系统需要i/o多次,可能在i/o过程中系统断电、磁盘故障等等原因导致一个数据块没有完整的写入,导致块不一致。
块不一致恢复
崩溃恢复
- 通过checksum发现“部分写”的数据页,并将wal中保存的这个完整数据页覆盖当前损坏的数据页,然后再继续redo恢复整个数据库。
备份恢复
- restore阶段,会直接还原不一致的块;但在recover阶段,会直接用wal中一致的块对其进行覆盖,然后开始应用日志。
heap_xlog_insert
可以参考xlog的恢复代码
static void
heap_xlog_insert(xlogreaderstate *record)
{
xlogrecptr lsn = record->endrecptr;
xl_heap_insert *xlrec = (xl_heap_insert *) xlogrecgetdata(record);
buffer buffer;
page page;
union
{
heaptupleheaderdata hdr;
char data[maxheaptuplesize];
} tbuf;
heaptupleheader htup;
xl_heap_header xlhdr;
uint32 newlen;
size freespace = 0;
relfilenode target_node;
blocknumber blkno;
itempointerdata target_tid;
xlogredoaction action;
xlogrecgetblocktag(record, 0, &target_node, null, &blkno);
itempointersetblocknumber(&target_tid, blkno);
itempointersetoffsetnumber(&target_tid, xlrec->offnum);
/*
* the visibility map may need to be fixed even if the heap page is
* already up-to-date.
*/
if (xlrec->flags & xlh_insert_all_visible_cleared)
{
relation reln = createfakerelcacheentry(target_node);
buffer vmbuffer = invalidbuffer;
visibilitymap_pin(reln, blkno, &vmbuffer);
visibilitymap_clear(reln, blkno, vmbuffer, visibilitymap_valid_bits);
releasebuffer(vmbuffer);
freefakerelcacheentry(reln);
}
/*
* if we inserted the first and only tuple on the page, re-initialize the
* page from scratch.
*/
if (xlogrecgetinfo(record) & xlog_heap_init_page)
{
buffer = xloginitbufferforredo(record, 0);
page = buffergetpage(buffer);
pageinit(page, buffergetpagesize(buffer), 0);
action = blk_needs_redo;
}
else
action = xlogreadbufferforredo(record, 0, &buffer);
if (action == blk_needs_redo)
{
size datalen;
char *data;
page = buffergetpage(buffer);
if (pagegetmaxoffsetnumber(page) 1 < xlrec->offnum)
elog(panic, "invalid max offset number");
data = xlogrecgetblockdata(record, 0, &datalen);
newlen = datalen - sizeofheapheader;
assert(datalen > sizeofheapheader && newlen <= maxheaptuplesize);
memcpy((char *) &xlhdr, data, sizeofheapheader);
data = sizeofheapheader;
htup = &tbuf.hdr;
memset((char *) htup, 0, sizeofheaptupleheader);
/* pg73format: get bitmap [ padding] [ oid] data */
memcpy((char *) htup sizeofheaptupleheader,
data,
newlen);
newlen = sizeofheaptupleheader;
htup->t_infomask2 = xlhdr.t_infomask2;
htup->t_infomask = xlhdr.t_infomask;
htup->t_hoff = xlhdr.t_hoff;
heaptupleheadersetxmin(htup, xlogrecgetxid(record));
heaptupleheadersetcmin(htup, firstcommandid);
htup->t_ctid = target_tid;
if (pageadditem(page, (item) htup, newlen, xlrec->offnum,
true, true) == invalidoffsetnumber)
elog(panic, "failed to add tuple");
freespace = pagegetheapfreespace(page); /* needed to update fsm below */
pagesetlsn(page, lsn);
if (xlrec->flags & xlh_insert_all_visible_cleared)
pageclearallvisible(page);
/* xlh_insert_all_frozen_set implies that all tuples are visible */
if (xlrec->flags & xlh_insert_all_frozen_set)
pagesetallvisible(page);
markbufferdirty(buffer);
}
if (bufferisvalid(buffer))
unlockreleasebuffer(buffer);
/*
* if the page is running low on free space, update the fsm as well.
* arbitrarily, our definition of "low" is less than 20%. we can't do much
* better than that without knowing the fill-factor for the table.
*
* xxx: don't do this if the page was restored from full page image. we
* don't bother to update the fsm in that case, it doesn't need to be
* totally accurate anyway.
*/
if (action == blk_needs_redo && freespace < blcksz / 5)
xlogrecordpagewithfreespace(target_node, blkno, freespace);
}
/*
* handles multi_insert record type.
*/
static void
参考
postgresql技术内幕:事务处理深度探索
阿里云直播—pg-full-page机制与原理
pg技术大讲堂直播—postgresql full-page writes 全页写