PG中的全页写(full_page_writes)
Tags: full_page_writesPG全页写参数介绍
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后如果数据有修改都会进行全页写入。
PostgreSQL中的full_page_writes参数用来防止部分页面写入导致崩溃后无法恢复的问题。此参数是为了防止块折断(块损坏)的一种策略。
full_page_writes (boolean)
打开这个选项的时候(默认为打开),PostgreSQL服务器在检查点之后对页面的第一次写入时将整个页面写到 WAL 里面。 这么做是因为在操作系统崩溃过程中可能只有部分页面写入磁盘, 从而导致在同一个页面中包含新旧数据的混合。在崩溃后的恢复期间, 由于在WAL里面存储的行变化信息不够完整,因此无法完全恢复该页。 把完整的页面影像保存下来就可以保证正确存储页面, 但是代价是增加了写入WAL的数据量。因为WAL重放总是从一个检查点开始的, 所以在检查点后每个页面第一次改变的时候做WAL备份就足够了。 因此,一个减小全页面写开销的方法是增加检查点的间隔参数值。
把这个参数关闭会加快正常操作,但是在系统失败后可能导致不可恢复的数据损坏,或者静默的数据损坏。其风险类似于关闭fsync
, 但是风险较小。并且只有在可关闭fsync
的情况下才应该关闭它。
这个参数只能在postgresql.conf
文件中或在服务器命令行上设置。默认值是on
。
1 2 3 4 5 | postgres=# show full_page_writes; full_page_writes ------------------ on (1 row) |
2. 为什么崩溃后无法恢复部分写入的页面
为了理解这个问题,先看看在不考虑部分写入时PostgreSQL的处理逻辑。可以简单概括如下:
- 对数据页面的修改操作会引起页面中数据的变化。
- 修改操作以XLOG记录的形式被记录到WAL中。
- 页面中保存最后一次修改该页面的XLOG记录插入到WAL后的下一个字节位置(PageHeaderData.pd_lsn)。
- 必须在最后一次修改该页面的XLOG记录已经刷入磁盘后,数据页面才能刷盘。
- 恢复时,跳过数据页面中记录的pd_lsn位置之前的XLOG
如果将修改操作记为Op1,Op2 ...,将数据页面的状态分别记为S1,S2和S3 ...,则如下所示:
1 2 | S1 ------> S2 ------> S3 ------> ... +Op1 +Op2 ... |
当某个数据页面处于S1状态时,这个页面从Op1开始REDO;当数据页面处于S2状态时,从Op2开始REDO;当数据页面处于S3状态时,不需要恢复。
然而,在部分写入时,页面将不再是上面的任何一个状态,而是新旧混合的不一致的状态。如果pd_lsn存的是新值,那么根本就不进行恢复;如果是旧值,由于恢复操作本来是要基于修改前的状态的,在中间状态上执行未必能成功,即使恢复涉及的数据部分恢复了也不能纠正页面其它地方的不一致。为了解决这个问题,PostgreSQL引入了fullpagewrites,checkpoint后的第一次页面修改将完全的页内容记录到WAL,之后从上次的checkpoint点开始恢复时,先取得这个完成的页面内容然后再在其上重放后续的修改操作。
full-page-write的机制
考察以下的情况(为方便起见,省略了buffer等相关的信息):
在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课程内容:
Oracle采用了redo+undo机制,其中undo记录了前镜像,而redo则既记录了修改数据又记录了undo块。