PostgreSQL中的full_page_writes的理解

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: 1. full_page_writes的作用 PostgreSQL中的full_page_writes参数用来防止部分页面写入导致崩溃后无法恢复的问题。手册中的相关描述如下: http://postgres.cn/docs/9.3/runtime-config-wal.html#GUC-FULL-PAGE-WRITES full_page_writes (boolean)  打开这个选项的时候,PostgreSQL服务器在检查点之后对页面的第一次写入时将整个页面写到 WAL 里面。

1. full_page_writes的作用

PostgreSQL中的full_page_writes参数用来防止部分页面写入导致崩溃后无法恢复的问题。手册中的相关描述如下:

http://postgres.cn/docs/9.3/runtime-config-wal.html#GUC-FULL-PAGE-WRITES

full_page_writes (boolean) 
打开这个选项的时候,PostgreSQL服务器在检查点之后对页面的第一次写入时将整个页面写到 WAL 里面。 这么做是因为在操作系统崩溃过程中可能只有部分页面写入磁盘, 从而导致在同一个页面中包含新旧数据的混合。在崩溃后的恢复期间, 由于在WAL里面存储的行变化信息不够完整,因此无法完全恢复该页。 把完整的页面影像保存下来就可以保证正确存储页面, 代价是增加了写入WAL的数据量。因为WAL重放总是从一个检查点开始的, 所以在检查点后每个页面第一次改变的时候做WAL备份就足够了。 因此,一个减小全页面写开销的方法是增加检查点的间隔参数值。

2. 为什么崩溃后无法恢复部分写入的页面

为了理解这个问题,先看看在不考虑部分写入时PostgreSQL的处理逻辑。可以简单概括如下:

  1. 对数据页面的修改操作会引起页面中数据的变化。
  2. 修改操作以XLOG记录的形式被记录到WAL中。
  3. 页面中保存最后一次修改该页面的XLOG记录插入到WAL后的下一个字节位置(PageHeaderData.pd_lsn)。
  4. 必须在最后一次修改该页面的XLOG记录已经刷入磁盘后,数据页面才能刷盘。
  5. 恢复时,跳过数据页面中记录的pd_lsn位置之前的XLOG

如果将修改操作记为Op1,Op2 ...,将数据页面的状态分别记为S1,S2和S3 ...,则如下所示:

S1 ------> S2 ------> S3 ------> ...
    +Op1       +Op2        ... 

当某个数据页面处于S1状态时,这个页面从Op1开始REDO;当数据页面处于S2状态时,从Op2开始REDO;当数据页面处于S3状态时,不需要恢复。

然而,在部分写入时,页面将不再是上面的任何一个状态,而是新旧混合的不一致的状态。如果pd_lsn存的是新值,那么根本就不进行恢复;如果是旧值,由于恢复操作本来是要基于修改前的状态的,在中间状态上执行未必能成功,即使恢复涉及的数据部分恢复了也不能纠正页面其它地方的不一致。为了解决这个问题,PostgreSQL引入了fullpagewrites,checkpoint后的第一次页面修改将完全的页内容记录到WAL,之后从上次的checkpoint点开始恢复时,先取得这个完成的页面内容然后再在其上重放后续的修改操作。

3. 避免部分写入

fullpagewrites会带来很大的IO开销,所以条件许可的话可以使用支持原子块写入的存储设备或文件系统(比如ZFS)避免部分写入。

4. 其它数据库的处理

MySQL中有类似的防止部分写入的机制,叫innodbdoublewrite。原理类似,但实现稍有不同,innodbdoublewrite生效时,在写真正的数据页前,把数据页写到doublewrite buffer中,doublewrite buffer写完并刷新后才往真正的数据页写入数据。

可参考: 
http://dev.mysql.com/doc/refman/5.6/en/glossary.html#glosdoublewritebuffer

5. 参考

可以参考某个XLOG的恢复代码,比如heapxloginsert()。

src/backend/access/heap/heapam.c

static void
heap_xlog_insert(XLogRecPtr lsn, XLogRecord *record)
{
    xl_heap_insert *xlrec = (xl_heap_insert *) XLogRecGetData(record);
    Buffer      buffer;
    Page        page;
    OffsetNumber offnum;
    struct
    {
        HeapTupleHeaderData hdr;
        char        data[MaxHeapTupleSize];
    }           tbuf;
    HeapTupleHeader htup;
    xl_heap_header xlhdr;
    uint32      newlen;
    Size        freespace;
    BlockNumber blkno;

    blkno = ItemPointerGetBlockNumber(&(xlrec->target.tid));

    /*
     * The visibility map may need to be fixed even if the heap page is
     * already up-to-date.
     */
    if (xlrec->flags & XLOG_HEAP_ALL_VISIBLE_CLEARED)
    {
        Relation    reln = CreateFakeRelcacheEntry(xlrec->target.node);
        Buffer      vmbuffer = InvalidBuffer;

        visibilitymap_pin(reln, blkno, &vmbuffer);
        visibilitymap_clear(reln, blkno, vmbuffer);
        ReleaseBuffer(vmbuffer);
        FreeFakeRelcacheEntry(reln);
    }

    /* If we have a full-page image, restore it and we're done */
    if (record->xl_info & XLR_BKP_BLOCK(0))
    {
        (void) RestoreBackupBlock(lsn, record, 0, false, false);
        return;
    }

    if (record->xl_info & XLOG_HEAP_INIT_PAGE)
    {
        buffer = XLogReadBuffer(xlrec->target.node, blkno, true);
        Assert(BufferIsValid(buffer));
        page = (Page) BufferGetPage(buffer);

        PageInit(page, BufferGetPageSize(buffer), 0);
    }
    else
    {
        buffer = XLogReadBuffer(xlrec->target.node, blkno, false);
        if (!BufferIsValid(buffer))
            return;
        page = (Page) BufferGetPage(buffer);

        if (lsn <= PageGetLSN(page))    /* changes are applied */
        {
            UnlockReleaseBuffer(buffer);
            return;
        }
    }

    offnum = ItemPointerGetOffsetNumber(&(xlrec->target.tid));
    if (PageGetMaxOffsetNumber(page) + 1 < offnum)
        elog(PANIC, "heap_insert_redo: invalid max offset number");

    newlen = record->xl_len - SizeOfHeapInsert - SizeOfHeapHeader;
    Assert(newlen <= MaxHeapTupleSize);
    memcpy((char *) &xlhdr,
           (char *) xlrec + SizeOfHeapInsert,
           SizeOfHeapHeader);
    htup = &tbuf.hdr;
    MemSet((char *) htup, 0, sizeof(HeapTupleHeaderData));
    /* PG73FORMAT: get bitmap [+ padding] [+ oid] + data */
    memcpy((char *) htup + offsetof(HeapTupleHeaderData, t_bits),
           (char *) xlrec + SizeOfHeapInsert + SizeOfHeapHeader,
           newlen);
    newlen += offsetof(HeapTupleHeaderData, t_bits);
    htup->t_infomask2 = xlhdr.t_infomask2;
    htup->t_infomask = xlhdr.t_infomask;
    htup->t_hoff = xlhdr.t_hoff;
    HeapTupleHeaderSetXmin(htup, record->xl_xid);
    HeapTupleHeaderSetCmin(htup, FirstCommandId);
    htup->t_ctid = xlrec->target.tid;

    offnum = PageAddItem(page, (Item) htup, newlen, offnum, true, true);
    if (offnum == InvalidOffsetNumber)
        elog(PANIC, "heap_insert_redo: failed to add tuple");

    freespace = PageGetHeapFreeSpace(page);     /* needed to update FSM below */

    PageSetLSN(page, lsn);

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

    MarkBufferDirty(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: We don't get here 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 (freespace < BLCKSZ / 5)
        XLogRecordPageWithFreeSpace(xlrec->target.node, blkno, freespace);
}

 

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
关系型数据库 数据库 PostgreSQL
|
存储 SQL 缓存
PolarDB-MySQL 新特性 - Partial Result Cache
背景查询缓存(Query Cache)是数据库执行层的一个加速查询的特性,用来缓存一条查询语句的结果集,如果后续再有相同的查询,直接从结果集缓存中读取结果,而不用再重新执行而极大提升查询性能。但Query Cache在实际业务使用中存在较多的局限性,首先能够命中Query Cache的规则非常严格,必须是完全相同的SQL语句,并且被查询的表的数据不能有任何的修改,有任意规则不符合要求都会造成cac
196 0
PolarDB-MySQL 新特性 - Partial Result Cache
|
存储 关系型数据库 MySQL
MySQL的innodb_page_size是干什么的?底层原理是什么?
MySQL的innodb_page_size是干什么的?底层原理是什么?
1084 0
|
关系型数据库 MySQL
MySQL - Packet for query is too large (4,544,730 > 4,194,304). You can change this value on the …
MySQL - Packet for query is too large (4,544,730 > 4,194,304). You can change this value on the …
390 0
|
SQL MySQL 关系型数据库
Why You Should Use HybridDB for MySQL for Online and Offline Data Separation
HybridDB for MySQL helps you separate online and offline data in a precise, economical, and secure way.
2375 0
Why You Should Use HybridDB for MySQL for Online and Offline Data Separation
|
关系型数据库 数据库 PostgreSQL
|
关系型数据库 PostgreSQL
PostgreSQL ring buffer策略
PostgreSQL ring buffer策略 When running a query that needs to access a large number of pages just once,such as VACUUM or a large sequential scan, a different strategy is used.
2043 0
|
关系型数据库 MySQL 安全
mysql启用hugepage
hugepage,大页存储,mysql
2114 0
|
数据库 索引 关系型数据库