解决PostgreSQL“WAL写放大”问题

2017-11-07 18:38:36 济南网站建设 4

PostgreSQL在写入频繁的场景中,可能会产生大量的WAL日志,而且WAL日志量远远超过实际更新的数据量。 我们可以把这种现象起个名字,叫做“WAL写放大”,造成WAL写放大的主要原因有2点。

  1. 在checkpoint之后第一次修改页面,需要在WAL中输出整个page,即全页写(full page writes)。全页写的目的是防止在意外宕机时出现的数据块部分写导致数据库无法恢复。

  2. 更新记录时如果新记录位置(ctid)发生变更,索引记录也要相应变更,这个变更也要记入WAL。更严重的是索引记录的变更又有可能导致索引页的全页写,进一步加剧了WAL写放大。

过量的WAL输出会对系统资源造成很大的消耗,因此需要进行适当的优化。

  1. 磁盘IO
    WAL写入是顺序写,通常情况下硬盘对付WAL的顺序写入是绰绰有余的。所以一般可以忽略。

  2. 网络IO
    对局域网内的复制估计还不算问题,远程复制就难说了。

  3. 磁盘空间
    如果做WAL归档,需要的磁盘空间也是巨大的。

WAL记录的构成

每条WAL记录的构成大致如下:

src/include/access/xlogrecord.h:

主要占空间是上面的”block data”,再往上的XLogRecordBlockHeader是”block data”的元数据。 一条WAL记录可能不涉及数据块,也可能涉及多个数据块,因此WAL记录中可能没有”block data”也可能有多个”block data”。

“block data”的内容可能是下面几种情况之一

  • full page image
    如果是checkpoint之后第一次修改页面,则输出整个page的内容(即full page image,简称FPI)。但是page中没有数据的hole部分会被排除,如果设置了wal_compression = on还会对这page上的数据进行压缩。

  • buffer data
    不需要输出FPI时,就只输出page中指定的数据。

  • full page image + buffer data
    逻辑复制时,即使输出了FPI,也要输出指定的数据。

究竟”block data”中存的是什么内容,通过前面的XLogRecordBlockHeader中的fork_flags进行描述。这里的XLogRecordBlockHeader其实也只是个概括的说法,实际上后面还跟了一些其它的Header。完整的结构如下:

下面以insert作为例子说明。

src/backend/access/heap/heapam.c:


WAL的解析

PostgreSQL的安装目录下有个叫做pg_xlogdump的命令可以解析WAL文件,下面看一个例子。

这条WAL记录的解释如下:

  • rmgr: Heap
    PostgreSQL内部将WAL日志归类到20多种不同的资源管理器。这条WAL记录所属资源管理器为Heap,即堆表。除了Heap还有Btree,Transaction等。

  • len (rec/tot): 14/ 171
    WAL记录的总长度是171字节,其中main data部分是14字节(只计数main data可能并不合理,本文的后面会有说明)。

  • tx: 301170263
    事务号

  • lsn: 555/D5005080
    本WAL记录的LSN

  • prev 555/D50030A0
    上条WAL记录的LSN

  • desc: UPDATE off 30 xmax 301170263 ; new off 20 xmax 0
    这是一条UPDATE类型的记录(每个资源管理器最多包含16种不同的WAL记录类型,),旧tuple在page中的位置为30(即ctid的后半部分),新tuple在page中的位置为20。

  • blkref #0: rel 1663/13269/54349226 fork main blk 1640350
    引用的第一个page(新tuple所在page)所属的堆表文件为1663/13269/54349226,块号为1640350(即ctid的前半部分)。通过oid2name可以查到是哪个堆表。

  • blkref #1: rel 1663/13269/54349226 fork main blk 1174199
    引用的第二个page(旧tuple所在page)所属的堆表文件及块号

UPDATE语句除了产生UPDATE类型的WAL记录,实际上还会在前面产生一条LOCK记录,可选的还可能在后面产生若干索引更新的WAL记录。

上面的LOCK记录的例子中,第一个引用page里有PFW标识,表示包含FPI,这也是这条WAL记录长度很大的原因。 后面的hole: offset: 268, length: 116表示page中包含hole,以及这个hole的偏移位置和长度。 可以算出FPI的大小为8196-116=8080, WAL记录中除FPI以外的数据长度8135-8080=55。

WAL的统计

PostgreSQL 9.5以后的pg_xlogdump都带有统计功能,可以查看不同类型的WAL记录的数量,大小以及FPI的比例。例子如下:

postgres.conf配置

下面是一个未经特别优化的配置


测试

先手动执行checkpoint,再利用pgbench做一个10秒钟的压测


日志统计

统计压测期间产生的WAL

这个统计结果显示FPI的比例占到了98.10%。但是这个数据并不准确,因为上面的Record size只包含了WAL记录中”main data”的大小,Combined size则是”main data”与FPI的合计,漏掉了FPI以外的”block data”。 这是一个Bug,社区正在进行修复,参考BUG #14687

作为临时对策,可以在pg_xlogdump.c中新增了一行代码,重新计算Record size使之等于WAL总记录长度减去FPI的大小。为便于区分,修改后编译的二进制文件改名为pg_xlogdump_ex。

src/bin/pg_xlogdump/pg_xlogdump.c:

修改后,重新统计WAL的结果如下:

这上面可以看出,有95.62%的WAL空间都被FPI占据了(也就是说WAL至少被放大了20倍),这个比例是相当高的。

如果不修改pg_xlogdump的代码,也可以通过计算WAL距离的方式,算出准确的FPI比例。


WAL的优化

在应用的写负载不变的情况下,减少WAL生成量主要有下面几种办法。

  1. 延长checkpoint时间间隔
    FPI产生于checkpoint之后第一次变脏的page,在下次checkpoint到来之前,已经输出过PFI的page是不需要再次输出FPI的。因此checkpoint时间间隔越长,FPI产生的频度会越低。增大checkpoint_timeout和max_wal_size可以延长checkpoint时间间隔。

  2. 增加HOT_UPDATE比例
    普通的UPDATE经常需要更新2个数据块,并且可能还要更新索引page,这些又都有可能产生FPI。而HOT_UPDATE只修改1个数据块,需要写的WAL量也会相应减少。

  3. 压缩
    PostgreSQL9.5新增加了一个wal_compression参数,设为on可以对FPI进行压缩,削减WAL的大小。另外还可以在外部通过SSL/SSH的压缩功能减少主备间的通信流量,以及自定义归档脚本对归档的WAL进行压缩。

  4. 关闭全页写
    这是一个立竿见影但也很危险的办法,如果底层的文件系统或储存支持原子写可以考虑。因为很多部署环境都不具备安全的关闭全页写的条件,下文不对该方法做展开。

延长checkpoint时间

首先优化checkpoint相关参数

postgres.conf:

然后,手工发起一次checkpoint

再压测10w个事务,并连续测试10次

测试结果如下

第1次执行

第5次执行