PG中的全页写(full_page_writes)

0    449    2

Tags:

👉 本文共约3211个字,系统预计阅读时间或需13分钟。

1. full_page_writes的作用

PG的full_page_writes和 MYSQL的双写一样,都是为了解决数据库CRASH 时数据的那一刻存在的缺失问题。PG默认每个page的大小为8K,PG数据页写入是以page为单位,但是在断电等情况下,操作系统往往不能保证单个page原子地写入磁盘,这样就极有可能导致部分数据块只写到4K(操作系统是一般以4K为单位),这些“部分写”的页面包含新旧数据的混合。在崩溃后的恢复期间,在 xlog 里面存储的记录变化信息不够完整,无法完全恢复该页。PG为了解决这类问题,full_page_write机制孕育而生。

PostgreSQL 在 checkpoint 之后在对数据页面的第一次写的时候会将整个数据页面写到 xlog(wal)里面。当出现主机断电或者OS崩溃时,redo操作时通过checksum发现“部分写”的数据页,并将xlog(wal)中保存的这个完整数据页覆盖当前损坏的数据页,然后再继续redo就可以恢复整个数据库了。

除了能够解决断电等带来坏数据页问题外,full_page_write还应用在在线备份功能上。PG进行全量备份数据库一般通过pg_basebackup工具实现,pg_basebackup类似于copy操作,在此期间,也会出现部分数据页写到一半时文件被copy走了,正是因为full_page_write存在,备份出来的数据库才可以成功恢复启动。所以即便full_page_write=off,在备份时也会被强制自动打开,保证备份成功。
full_page_write需要在xlog(wal)中记录数据页,会进行更多写操作,不仅数据变,还有数据页的信息,这会增加的IO和磁盘消耗,引起主备延迟变大。

PG以page(大小默认为8K)为基本的存储单元,但OS的存取单元(block)不一定是8k,常见的是4k,而且物理持久化存储块设备扇区大小是512字节,这些不一致的情况会导致PG page的读写不是原子操作,也就是说可能会出现page的部分写问题:
在写一个page的时候,部分写入成功但部分写入失败,这时候的page中的内容是不一致的,也就是说这个page已经被损坏(corrupted page)。为了解决这个问题,PG引入了full-page-write的机制。

文件系统一个块一般是4k,而数据库则一般是一个块8k,当数据库的脏块刷新到磁盘上时,由于底层是两个块组成的,比如刷第一个操作系统块到磁盘上了,而当刷第二个操作系统块的时候发生了停电等突然停机事故,则就发生了块折断(数据块是否折断是根据块的checksum值来检查的)。

当checkpoint后的一个块第一次变脏后就要整块写入到wal日志中,后续继续修改此块则只把修改的信息写入wal日志中,如果在此过程中发生了停电,则实例启动后会从checkpoint检查点,之后开始进行实例恢复,如果有块折断,则在全页写入的块为基础进行恢复,最后覆盖磁盘上的折断块,所以当每次checkpoint后如果数据有修改都会进行全页写入。

PG中的全页写(full_page_writes)

PostgreSQL中的full_page_writes参数用来防止部分页面写入导致崩溃后无法恢复的问题。此参数是为了防止块折断(块损坏)的一种策略。

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

把这个参数关闭会加快正常操作,但是在系统失败后可能导致不可恢复的数据损坏,或者静默的数据损坏。其风险类似于关闭fsync, 但是风险较小。并且只有在可关闭fsync的情况下才应该关闭它。

这个参数只能在postgresql.conf文件中或在服务器命令行上设置。默认值是on

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

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

本人提供Oracle、MySQL、PG等数据库的培训和考证业务,私聊QQ646634621或微信db_bao,谢谢!
  1. 对数据页面的修改操作会引起页面中数据的变化。
  2. 修改操作以XLOG记录的形式被记录到WAL中。
  3. 页面中保存最后一次修改该页面的XLOG记录插入到WAL后的下一个字节位置(PageHeaderData.pd_lsn)。
  4. 必须在最后一次修改该页面的XLOG记录已经刷入磁盘后,数据页面才能刷盘。
  5. 恢复时,跳过数据页面中记录的pd_lsn位置之前的XLOG

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

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

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

full-page-write的机制

考察以下的情况(为方便起见,省略了buffer等相关的信息):

img
img

在T1,数据库成功执行checkpoint;
在T2,执行DML语句,这时候相关的数据会写入到WAL中(此处忽略了WAL buffer);
在T3,提交该事务;
在T4,bgwriter把dirty pages写入到Data file中,但在写入过程中机器出现故障导致Crash(如掉电等),出现了部分写的情况。
为了应对这种情况,PG在T2写入WAL的时候,会把出现变化的page整页写入到WAL中,而不仅仅是tuple data。在数据库重启执行恢复的时候,在Redo point开始回放WAL时,如发现XLOG Record是FPI(full-page-image),则整页替换,通过这种机制解决了部分写的问题。

3. 避免部分写入

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

4. full-page-write的代价

当然这种机制不是免费的,其主要的负面影响是写放大。
由于整页写,不可避免的出现冗余数据;考虑这么一种情况:如果数据库很繁忙,而且数据的热点分散在不同的table上,同时checkpoint执行间隔较短,那非常多的page就会通过full-page-write写入的WAL中,导致日志空间快速膨胀。在极端情况下,page“满载”(基本没有空闲空间)的情况下更新其中一条记录都会导致整页写入WAL。

5. 其它数据库的处理

MySQL中有类似的防止部分写入的机制,叫innodb doublewrite。原理类似,但实现稍有不同,innodb doublewrite生效时,在写真正的数据页前,把数据页写到doublewrite buffer中,doublewrite buffer写完并刷新后才往真正的数据页写入数据。** MySQL 中的Doublewrite buffer是物理上的一块存储,是真实的磁盘文件,而不是类似内存的缓存。

可以参考麦老师的MySQL课程内容:

PG中的全页写(full_page_writes)

Oracle采用了redo+undo机制,其中undo记录了前镜像,而redo则既记录了修改数据又记录了undo块。

参考

http://blog.chinaunix.net/uid-20726500-id-5105905.html

标签:

头像

小麦苗

学习或考证,均可联系麦老师,请加微信db_bao或QQ646634621

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

14 − 1 =

 

嘿,我是小麦,需要帮助随时找我哦
  • 18509239930
  • 个人微信

  • 麦老师QQ聊天
  • 个人邮箱
  • 点击加入QQ群
  • 个人微店

  • 回到顶部
返回顶部