PG中文社区 /

PostgreSQL · 源码分析 · 回放分析(一)

原作者:阿里云PG内核组  创作时间:2020-07-31 16:03:54+08
wangliyun 发布于2020-08-01 08:00:00           评论: 2   浏览: 6619   顶: 634  踩: 658 

基本原理

在数据库的运行过程中,难免会遇到各种非预期的问题,例如:

硬件错误,例如突然断电、磁盘错误、有人拔了你的内存条 :P

软件问题,例如操作系统崩溃、数据库内部存在bug等等

操作错误,例如误删数据、插入了不符合预期的数据、应用程序异常等等

… …

在这些情况下,我们不希望我们的数据异常甚至丢失,有的情况下我们不能进行修复,例如火灾(这类问题依赖于备份存储介质的方式解决,需要异地容灾),但有的情况下我们可以进行解决,例如断电、崩溃。我们希望当数据库重新启动时,能够恢复其崩溃的那一瞬间的状态,能够恢复出“一致的”、“完整的”数据。

由于内存是易失性的,当数据库发生断电、崩溃等情况时,存储在内存中的数据会丢失,因此不能寄希望于存储在内存中的数据,我们希望找到一种方式,能够帮助数据库系统完成崩溃恢复,同时不那么影响性能。

CENTER_PostgreSQL_Community

表1 REDO和UNDO的对比

WAL(Write-Ahead Logging,预写式日志),就是完成这一工作的重要方式,数据库在执行事务的过程中,会将对数据的操作过程记录在WAL中,当数据库发生崩溃的时候,能够使用这个操作记录,将数据库恢复到崩溃前的状态。日志有几种记录方式,一是记录REDO,二是UNDO,还有一种是REDO/UNDO日志,REDO允许我们重新进行对数据的修改,UNDO允许我们撤销对数据的修改,REDO/UNDO日志是以上两种日志的结合。

除了WAL以外,还有Shadow Pagging的技术,是System R和sqlite所使用到的技术,看上去有点像COW(Copy On Write,写时复制)技术;此外还存在WBL(Write-Behind Logging,结合NVM所产生的技术)等技术出现。

CENTER_PostgreSQL_Community

图1 数据库基本组件的联系,I/O是围绕着缓冲区管理器进行的《数据库系统实现》

在数据库系统的内部,存在一个叫做 日志管理器 的基本组件,当数据库在正常运行的时候,事务管理器将对数据的操作发送到日志管理器中,日志管理器会将日志顺序写入到缓冲区管理器中,缓冲区管理器将日志刷入到磁盘中,事务管理器只有在确认这条事务的最后一条日志被刷入到磁盘后,才会向客户端返回事务提交的信息。

当崩溃发生时,在重启的时候,恢复管理器就会开始工作,它会读取事务的状态,将已经提交的数据重新回放,将已经放弃或者中断的事务进行回滚,将数据库内不一致的数据恢复到一致的状态。在恢复的时候,恢复管理器有一套算法逻辑在其中,决定如何进行回放,大名鼎鼎的ARIES就是这方面的一个算法。

ARIES的算法,是IBM提出的一整套关于日志记录和恢复处理的算法,后续的数据库管理系统都多少参考了该算法。

可以预见的是,如果数据库长时间运行了很久,突然崩溃了,在重启的时候可能需要从数天前开始进行恢复,需要花费数个小时甚至上天的时间。这时候需要使用到检查点技术,将脏数据刷入到磁盘中,记录检查点刷下的最旧的数据页的,可以保证我们在恢复的时候从相对较新的位置开始。同时让我们可以清理掉旧的日志文件(或者复用),让日志不会无限制地增长。

日志所提供的功能不仅于崩溃恢复,它还能提供复制(包括主备复制、外部订阅复制等)、主备状态同步、按时间点还原等功能。

实现简述

在记录日志时

每个数据页面 (堆或索引) 都标有影响页面的最新XLog记录的LSN

在缓冲区管理器能够写出一个脏页面之前,它必须确保XLog已经被刷新到磁盘,至少达到页面的LSN

在写XLog、写数据页面的时候,都只写入到缓冲区中,而不等待写入到磁盘中,以提供很快的写入速度,只在事务提交时会进行等待(当打开同步提交时)。

LSN检查仅存在于共享缓冲区管理器中,不存在于临时表使用的本地缓冲区管理器中,因此,对临时表的操作不能被 WAL记录。

XLog:Transaction log,事务的日志,通常指的是记录时的在内存中的事务日志,WAL指的是持久化后的日志 LSN:Log sequence number,日志序列号,这是WAL日志唯一的、全局的标识 bgwriter:PostgreSQL负责将脏页面刷入磁盘的进程 walwriter:PostgreSQL负责将WAL刷入磁盘的进程

在崩溃恢复时

从检查点开始,回放WAL日志,如果数据页面的LSN小于WAL记录的LSN,则说明数据页面比较旧,需要进行回放,反之则不需要回放,就会跳过回放过程。

在回放的过程中,checkpointer会持续地做检查点,让数据页面向前更新,这样万一又重启了,能更快地恢复。

checkpointer:PostgreSQL中的检查点进程

日志内容

PostgreSQL的WAL是REDO类型的。我们看一下PostgreSQL的日志的格式和包含的信息。

PG社区还在实现Zheap的特性,这是PG的新的日志格式,是一种REDO/UNDO日志,届时将能够很好地解决PG数据库的膨胀问题,我们将在后续的文章中介绍这一特性。

WAL文件

PostgreSQL的WAL文件存放于数据目录下的pg_wal目录里,ls一下可以看到以下文件:

-rw------- 1 postgres users 1073741824 Apr 17 08:41 000000010000000000000001
-rw------- 1 postgres users 1073741824 Apr 14 11:09 000000010000000000000002
-rw------- 1 postgres users 1073741824 Apr 14 11:09 000000010000000000000003

drwx------ 2 postgres users 4096 Apr 14 11:09 archive_status #和备份有关,表示日志文件的备份状态,这里不做介绍

可以看到这里每个WAL文件大小为1GB(这和我们configure、initdb时的参数有关),命名为一串16进制的串,这个串和时间线以及LSN紧密相关,每个WAL文件都包含了特定时间线内,从某个LSN开始到某个LSN结束的WAL日志。根据一个特定的LSN,可以知道对应的WAL日志的文件名,以及在文件中所处的位置。

CENTER_PostgreSQL_Community

事务日志与WAL段文件 《PostgreSQL指南:内幕探索》

WAL日志

使用pg_waldump工具我们可以看到PostgreSQL的日志,每一条日志可以理解为一次对数据库的操作记录:

rmgr: Standby     len (rec/tot):     42/    42, tx:        699, lsn: 0/410E21B8, prev 0/410E2180, desc: LOCK xid 699 db 13933 rel 221196
rmgr: Heap        len (rec/tot):     59/    59, tx:        699, lsn: 0/410E21E8, prev 0/410E21B8, desc: INSERT off 4, blkref #0: rel 1663/13933/221196 blk 0
rmgr: Transaction len (rec/tot):     38/    38, tx:        699, lsn: 0/410E2228, prev 0/410E21E8, desc: COMMIT 2020-04-17 08:38:04.881890 UTC

这是一条id为699的事务所产生的三条日志,做了锁表、插入数据、提交的操作,让我们对照着SQL看一下这条日志是怎么生成的:

postgres=# begin;
BEGIN      --开启一个新的事务,此时不会分配事务ID,也不会生成WAL
postgres=# lock table t;
LOCK TABLE --锁住表t,生成事务ID 699,生成锁表的日志0/410E21B8
                     --锁住了(db:13933, rel:221196)的表(我们后续会聊这条日志如何在热备模式下发挥作用)
postgres=# insert into t select 1; 
INSERT 0 1 --向表t插入一条数据,生产插入数据的日志0/410E21E8
                     --向表(1663,13933,221196),BlockNumber为0的page,offset为4的tupe的位置,写入了一条数据,该页面的LSN会被更新为这条日志的LSN
postgres=# end;
COMMIT     --提交,生成提交日志0/410E2228(数据库会等待这条日志刷盘再返回给客户端,这是保证持久化的关键,当然得设置同步提交为on)
                     --这条日志包含了事务的提交状态,以及提交的时间(我们后续会聊这个时间如何在时间点还原下发挥作用)

在上面产生了三条不同类型的日志,有Standby,Heap,Transaction三种类型,这里的类型指的是资源管理器的类型。在PostgreSQL中,对数据不同的操作被进行了分类,例如对序列号的操作、对BTree索引的操作,每一类操作类型会使用对应的资源管理器进行管理,包括进行记录和回放。

下图展示了在PostgreSQL 10中所包含的资源管理器的类型,共计有22种(在最新的PostgreSQL 12中,资源管理器的类型未增加),涉及到了堆元组操作、索引操作、序列号操作等。

CENTER_PostgreSQL_Community

PostgreSQL 10的资源管理器 《PostgreSQL指南:内幕探索》

记录流程

在数据库的运行过程中,很多操作需要记录WAL日志,一个标准的记录流程是这样的:

对需要修改的页面进行PIN和LOCK操作

STARTCRITSECTION() 开启临界区,此时不允许任何错误,若发生错误,直接报PANIC错误

将需要的修改应用到页面上

将页面标记为脏,这必须发生在WAL日志插入前

如果该表需要进行插入WAL记录的操作,初始化一条XLOG并插入,然后设置页面的LSN

ENDCRITSECTION() 结束临界区。

对需要修改的页面进行UNPIN和UNLOCK操作

buffer和page的区别在于buffer是内存中的,page是在存储中的,buffer中有块区域叫做frame(页框), page会被读取到frame中以供读写 PIN buffer表示从磁盘中置换入page到frame中,并且不能被置换出去 LOCK > buffer表示锁定住buffer,使其他进程无法读写frame(page)

我们可以结合插入数据的代码看一下插入数据是WAL是如何记录的:

调用顺序:PostgresMain->exec_simple_query->PortalRun->PortalRunMulti->ProcessQuery->
        standard_ExecutorRun->ExecutePlan->ExecModifyTable->ExecInsert->
        heapam_tuple_insert->heap_insert

heap_insert(Relation relation, HeapTuple tup, CommandId cid,
            int options, BulkInsertState bistate)
{
    // 获取将要插入的heaptup
    heaptup = heap_prepare_insert(relation, tup, xid, cid, options);

    // 读取buffer,在内部会自动PIN buffer,LOCK buffer
    buffer = RelationGetBufferForTuple(relation, heaptup->t_len,
                                       InvalidBuffer, options, bistate,
                                       &vmbuffer, NULL);

    // 开始临界区
    START_CRIT_SECTION();

    // 插入数据
    RelationPutHeapTuple(relation, buffer, heaptup,
                         (options & HEAP_INSERT_SPECULATIVE) != 0);

    // 将页面标记为脏页
    MarkBufferDirty(buffer);

    // 开始记录WAL日志,RelationNeedsWAL,如果是临时表,就不需要WAL日志了
    if (!(options & HEAP_INSERT_SKIP_WAL) && RelationNeedsWAL(relation))
    {
        // info信息,标记记录为XLOG_HEAP_INSERT类型的,将来将会使用heap_xlog_insert回放
        // 如果是新页,还会标记这个为XLOG_HEAP_INIT_PAGE,就表示回放时需要先初始化新页
        uint8       info = XLOG_HEAP_INSERT;
        if (ItemPointerGetOffsetNumber(&(heaptup->t_self)) == FirstOffsetNumber &&
            PageGetMaxOffsetNumber(page) == FirstOffsetNumber)
        {
            info |= XLOG_HEAP_INIT_PAGE;
            bufflags |= REGBUF_WILL_INIT;
        }

        // 初始化一条XLog记录,并插入
        XLogBeginInsert();
        XLogRegisterData((char *) &xlrec, SizeOfHeapInsert);
        ...
        // 这是一条RM_HEAP_ID类型的日志,将来回放的时候,将会根据这个ID使用heap_redo进行回放
        recptr = XLogInsert(RM_HEAP_ID, info);

        // 设置页面的LSN,值得注意的是这里的LSN用的是EndRecPtr,为什么要在最后设置?
        PageSetLSN(page, recptr);
    }

    //结束临界区
    END_CRIT_SECTION();

    //UNLOCK buffer,UNPIN buffer,之后buffer可以被其他事务使用,或者置换出去
    UnlockReleaseBuffer(buffer);
    if (vmbuffer != InvalidBuffer)
        ReleaseBuffer(vmbuffer);
}

上述代码是一个典型的插入数据、写WAL的一个流程,但关于这个流程还是有不少疑问:

1.先修改buffer里的数据,再写WAL,会不会导致数据落盘而写WAL不成功

回到前面的 缓冲区管理器能够写出一个脏页面 的前提,这个是数据库需要确保不能发生的。需要这个前提的原因在于,PostgreSQL的日志类型时REDO的,数据只能往前回放,无法向后恢复,因此数据页面不能比WAL“新”

2.为什么将buffer标记为脏要发生在WAL日志插入前 如果在WAL日志插入后将buffer标记为脏,有可能做检查点时,使用了新的LSN,但是由于该页不是脏页导致跳过刷脏,导致该页数据在磁盘中的是旧的,但是检查点已经超前了,后续崩溃恢复时,该页面就会存在这条WAL日志未回放的情况

3.为什么要使用EndRecPtr,可以使用RecPtr吗

不只是页面的LSN,包括检查点的LSN、刷数据的LSN(flushPtr)等也是使用的EndRecPtr,以刷数据的LSN为例,使用EndRecPtr就能表示已经刷完了到哪个LSN结束的WAL日志对应的数据,要是使用RecPtr就很费解了;检查点的LSN使用EndRecPtr,就能方便地在下次回放时,找到下一条需要回放的日志的LSN。在页面的LSN中,使用就EndRecPtr可以和上述逻辑维持一致了;而且RecPtr在影响完页面后,对这个页面来说已经不重要了,我们关心的是下一条影响这个页面的WAL记录

另外,这里仅仅展示了最简单的插入数据的流程,生成的WAL日志也比较简单,有一些比较复杂的对数据库的修改,比如涉及到索引的分裂,需要创建一个新页面,再写入新key,这需要至少记录两个WAL(涉及到连续分裂会更多),当回放处于这两个WAL日志之间时,数据库处于一个“中间状态”,这就需要一些技巧来隐藏这种状态。

恢复流程

数据库从崩溃中重启,从控制文件中,获知上一次没有正常停库,进入崩溃恢复状态,从控制文件中读取到上一次检查点的位置,从检查点开始进行严格的串行回放。

读取到新的日志,解析日志头部,根据日志的类型,将日志交由对应资源管理器回放

解析该WAL日志,根据具体的操作类型,交由具体的函数进行回放

解析WAL日志内容

XLogReadBufferForRedo,读取需要修改的页面,进行PIN和LOCK操作,并根据LSN确认是否需要REDO

如果需要REDO,则将日志应用到页面上,更新页面的LSN,标记页面为脏页

对需要修改的页面进行UNPIN和UNLOCK操作,其他进程可以使用该页面,bgwriter可以向下刷该页面

我们可以结合插入数据的代码看一下redo是如何工作的:

调用顺序:StartupXLOG->heap_redo->heap_xlog_insert

heap_xlog_insert(XLogReaderState *record)
{
    // 如果xl_info中存在XLOG_HEAP_INIT_PAGE,则说明需要初始化页
    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是根据page LSN和record LSN计算得到的
        // 如果page LSN<record LSN,说明页面比较旧,需要进行redo
        action = XLogReadBufferForRedo(record, 0, &buffer);
    if (action == BLK_NEEDS_REDO)
    {
        ...

        // 构建htup (HeapTuple),这个就是新插入的数据
        htup = &tbuf.hdr;
        ...

        // 向page中插入这条htup
        if (PageAddItem(page, (Item) htup, newlen, xlrec->offnum,
                        true, true) == InvalidOffsetNumber)
            elog(PANIC, "failed to add tuple");

        // 将该page的LSN设置为这条记录的LSN
        PageSetLSN(page, lsn);

        if (xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED)
            PageClearAllVisible(page);

        // 将该buffer标记为脏
        MarkBufferDirty(buffer);
    }

    // UNLOCK buffer,UNPIN buffer
    if (BufferIsValid(buffer))
        UnlockReleaseBuffer(buffer);
}

这是一条插入数据的WAL日志的回放流程,我们可以看到,记录WAL日志的代码和回放部分的代码是高度一致的,这也该过程被叫做回放的原因。

在崩溃恢复的过程中,数据库已经看不到具体的SQL语句了,只有一条条操作记录,恢复管理器只负责机械地将这些记录应用到数据上,将数据库还原到崩溃前的状态。

部分写问题

现在的磁盘/文件系统大多是4KB对齐的(部分老的磁盘甚至是512字节的扇区),这样就只能保证4KB的原子读写。这就导致了当写入一个较大页面时,会在文件系统、磁盘驱动里被拆分为几次I/O,当写入到一半时,就会发生部分写问题,导致数据页面或者WAL文件损坏。

MySQL也存在类似的问题,它采用了一个叫做double write buffer技术解决了这个问题,但也带来了额外的开销。

PostgreSQL有自己的一套解决的方法:

当数据页面损坏时,有一个叫做FullPageWrite(FPW)的特性来保证数据的完整性

数据文件可以打开checksum用于校验,由于较为影响性能,所以需要在初始化数据库时手动指定开启

每条WAL记录都包含crc校验码,来检查WAL记录是否正确

每个WAL页面,都包含magic,来检查页面的有效性

FullPageWrite(FPW)的原理是,当做了checkpoint后,如果某个数据页面是第一次被修改,那么就会记录完整的数据页面到WAL文件中,当恢复时,就能够获取完整数据页面重新进行修复,因此哪怕数据页面被写坏了,也能够修复出来。当然这也会带来写放大的开销,尤其是当checkpoint十分频繁时,写放大会十分地严重。

该特性需要手动开启,如果数据页大小大于文件系统所提供的原子写粒度的话,就不需要这个特性了。

当WAL也出现错误时,又不巧碰上了崩溃恢复,需要这段WAL日志,很不幸就不能进行恢复了,数据库会及时地崩溃并告诉你无能为力。

但是WAL日志是预分配且一直是顺序写入的,因此也最多由于部分写会丢失尾部的部分WAL日志,且这部分WAL文件没落盘成功,数据库也不会返回事务成功(当同步提交为on时),因此WAL文件遇到部分写问题也没啥影响,直接丢弃这段不完整的WAL日志就行了。

至于更加麻烦的磁盘静默错误和内存错误的话,就很难在数据库层面解决了,一般会通过冗余校验的方式进行解决,例如磁盘的RAID技术(部分RAID级别),ECC内存等。

总结

本文简单描述了数据库崩溃恢复的基本原理,以及PostgreSQL是如何记录日志、进行崩溃恢复的。

本文严重参考了PG源码中的src/backend/access/transam/README,README的原理部分讲的十分清晰,以至于该文在这部分的原理只做了翻译,以及结合源码进行了分析,该README中还包含更多的细节,如果对这部分原理感兴趣,强烈建议去阅读这篇文档。

在下一篇文章中,我将会详细描述在热备的情况下备库如何进行恢复,以及如何做到按时间点还原(PITR),这部分README没有进行描述,希望能将这部分原理清晰地带给大家。

参考

《Intro to Database Systems》CMU Database Group

《数据库系统实现》机械工业出版社

https://github.com/postgres/postgres/blob/master/src/backend/access/transam/README

https://www.pgcon.org/2012/schedule/track/Hacking/408.en.html

https://www.enterprisedb.com/blog/zheap-storage-engine-provide-better-control-over-bloat

http://www.vldb.org/pvldb/vol10/p337-arulraj.pdf

https://chenhuajun.github.io/2017/09/02/PostgreSQL如何保障数据的一致性.html

CENTER_PostgreSQL_Community


评论:2   浏览: 6619                   顶: 634  踩: 658 

请在登录后发表评论,否则无法保存。

1# __ xcvxcvsdf 回答于 2024-10-14 10:33:16+08
http://nalei.zjtcbmw.cn/dalian/ http://js.sytcxxw.cn/scnj/ https://doumen.tiancebbs.cn/ http://wogao.ahtcbmw.cn/chenzhou/ http://ouyu.hftcbmw.cn/hbhs/ http://tuiguang.hntcxxw.cn/tangshan/ http://taiying.njtcbmw.cn/zjls/ http://ruanwen.xztcxxw.cn/xinyang/ http://taiying.njtcbmw.cn/hljhh/ http://km.lstcxxw.cn/xinjiang/ https://renmindaxue.tiancebbs.cn/ http://huilong.sctcbmw.cn/dgqz/ https://meilie.tiancebbs.cn/ http://fs.shtcxxw.cn/xinyu/ http://huaguang.jxtcbmw.cn/jjbc/ https://binhengzhen.tiancebbs.cn/ http://ruanwen.xztcxxw.cn/fang/

2# __ xiaowu 回答于 2024-04-24 10:48:11+08
石墨烯取暖器的优缺点:https://www.nanss.com/jiaju/20421.html 摆摊卖什么赚钱成本又低:https://www.nanss.com/wenti/20706.html 电热毯一晚上耗几度电:https://www.nanss.com/wenti/20425.html 关于风景的作文:https://www.nanss.com/xuexi/20613.html 北回归线穿过我国的省区是:https://www.nanss.com/shenghuo/18959.html 隔热材料:https://www.nanss.com/shenghuo/20391.html 用那么那么造句:https://www.nanss.com/xuexi/20632.html 恐怖片排行榜前十名电影:https://www.nanss.com/shenghuo/20560.html 高情商生日句子:https://www.nanss.com/wenan/20365.html 2024对照6个方面查摆问题:https://www.nanss.com/gongzuo/20581.html 廉洁从教心得体会:https://www.nanss.com/gongzuo/19633.html 古尔邦节:https://www.nanss.com/shenghuo/18349.html 周末祝福:https://www.nanss.com/yulu/20452.html 国五和国六有什么区别:https://www.nanss.com/wenti/19949.html 一磅是多少斤:https://www.nanss.com/wenti/19873.html 评价领导的话简短精辟:https://www.nanss.com/gongzuo/20496.html 四大名著读书笔记:https://www.nanss.com/xuexi/20544.html 大班幼儿年龄特点:https://www.nanss.com/yuer/20372.html 兵马俑是什么:https://www.nanss.com/shenghuo/19998.html 类地行星:https://www.nanss.com/shenghuo/20565.html 手机信号不好:https://www.nanss.com/shenghuo/19806.html 安全阀作用:https://www.nanss.com/jiaju/20414.html 精忠报国串词:https://www.nanss.com/shenghuo/18822.html 村晚改写成小短文:https://www.nanss.com/xuexi/xiezuo/20752.html 转基因和非转基因的区别:https://www.nanss.com/shenghuo/18170.html 最新思想汇报:https://www.nanss.com/xuexi/20493.html 素质拓展训练心得:https://www.nanss.com/gongzuo/20319.html 梦见亲人去世:https://www.nanss.com/xingzuo/20571.html 砌体结构:https://www.nanss.com/shenghuo/20428.html 退学申请书:https://www.nanss.com/xuexi/20511.html



发表评论:
加入我们
QQ群1:5276420
QQ群2:3336901
QQ群3:254622631
文档群:150657323
文档翻译平台:按此访问
社区邮件列表:按此订阅
扫码关注
© PostgreSQL中文社区 ... (自2010年起)