分享人:吕海波(VAGE) 杭州美创科技内核专家
正文:本文从八方面介绍了FPW的开发实例。
一、为什么要对FPW动手:性能测试
1)性能影响对比,PostgreSQL Full Page Writes性能影响:
先对比下性能,左侧是打开FPW,右边是关掉FPW,这个跟IO是强相关的,如果IO很快速,FPW的影响就越小,如果使用的是土豪级NVM1设备,FPW的影响可能就没有了,甚至观察不到有任何的影响。平均FPW性能的影响可以达到百分之值30以上。
2)性能影响对比,MySQL Double Write性能影响
对比下MySQL,FPW的功能主要针对在页分裂或者不分写,页破碎等,对应到MySQL里面针对页分裂不分写的特性是Double Right双写。在同一台机器上测试一边打开双写,另一边关闭双写,同一台机器上使用同样的压测软件的,可以看到MySQL双写对性能的影响是微乎其微的。
二、为什么要对FPW动手
PG的FPW影响也很巨大,所以对它动手可以最大程度提升性能,代码改动比较少,实现逻辑也很简单。
三、了解FPW
1)页裂
那要在对它动手之前,特性是针对页裂的,针对不分写或页破碎,数据库的页一般是4k的倍数,8k 、16k等,数据库下发一次写,可能操作系统前4k写成功后4k写失败,这就导致不分写或者页分裂,在对它动手之前要先了解一下赠分裂,了解它对数据库的影响,数据库是怎么解决的。
2)页裂的模拟
我们去制造一个页分裂,走两步看看数据库是怎么处理页分裂的,然后再对它动手。
制造一个叶分裂也简单,突然中断电源,内存里就页分裂,不分写、页分裂、破碎页等情况,这种方式可以实现但有点粗暴。直接中断电源不能针对某一个固定的页,比如想把某个页制造成页分裂是做不到的。如果脏页多又突然断电,这种情况可控性就比较差。如要去针对某个表、普通表、原数据表,针对某个固定的页去制造一个页分裂,可以拦截系统调用,再去修改传入OS内核参数拦截系统调用。比如在Linux里面这个systemtap就能做到,我之前有过关于这个systemtae的很多分享,就是用它去观察PG、,MySQL等,也比较简单。
3)从MySQL开始
主要思想是用它拦截IO,然后把IO的大小改一下,比如让操作系统写8k,把8k拦截后改成4k,就让前4k写成功,后4k不写,这就是页分裂。这种数据一致性让性能提升很诱人,但是做不好数据就不一致就没用了,那就没有任何意义了。
我们先从MySQL数据库开始,可以都比一遍,多借鉴各个主流数据库的经验,再决定如何对FPW动手。制作MySQL的页分裂,就要先去做IO函数,如果打开的话,它使用异步IO,如果关掉的话,使用同步IO,我们使用同步IO做测试,因为今天时间比较紧张,异步测试会比较复杂,讲起来又耗时多,但是总体思路是一样的。
4)从MySQL开始
因为要拦截系统,所以要先找到IO函数。这是测试表就不详细说了。
我让目标行MySQL的块是16k,让行呢处在靠下的位置,第三个4k的地方MySQL空间,它是从上往下使用的。我插入200行,以第200行作为目标,到时我去更新这个第200行。当我更新这个第200行的时候,MySQL更新这个4k外,第一个4k也会变,因为第一个4k前面管理性信息,这样第一个4k也变了,第三4k也变了。
我让目标行MySQL的块是16k,让行处在靠下的位置,第三个4k的地方。MySQL空间是从上往下使用的。我插入200行,以第200行作为目标。
到时我去更新这个第200行。当我更新这个第200行的时候,MySQL更新这个4k外,第一个4k也会变,因为第一个4k前面管理性信息,这样第一个4k也变了,第三4k也变了。
这是测试语句,是一条update更新第200行,中间还有个脚本,脚本上面是一个探针,探针就像数据库的触发器一样,先去做输出,输出是把函数的一些信息接过来,上图是输出的这个结果,那我做一个update,触发了MySQL的写操作,触发把内存改小一点,如果内存太大, update知道脏页要先修改一些参数。
制造了脏页后,让MySQL尽快去写这个页,在这儿就能看到输出的结果了,这个28就是写目标表的IO,这个文件号28十进制就是40。在目录下就能找到表对应的文件号,在这里就不详细说了。
它还写了0*b号文件,0*b号文件是第一次写18000的六个页,也是双写,待会儿我要让双写、四号文、日志log redo都成功,后面几个写有UNDO、系统表、28号写都失败。
观察过IO后,生成一个脚本进行破坏,就是对指定的进行破坏,让redo、双写都成功。因为双写失败了,还要靠再去恢复,看双写是如何恢复的。
这是最终运行效果。先运行脚本,再去执行update,执行完之后,IO被触发了,查询后发现MySQL已经宕机了,因为发现IO报错了,所以就宕库了,就启动不了,因为它检测到有页被写坏了。因为涉及到数据一致性的特性,我做过大量测试。
动手之前要做大量准备工作,如果这个制造的坏页,是对用户表的话大部分都能启动成功的,如果涉及到UNDO、系统表,用双写去恢复成功的概率那就很低了。
总体看双写不能对所有的页分裂和不分写发挥想象中的作用,它只能针对部分页分裂。如果出现这样的问题怎么办?就是做备份恢复,就能解决Partial Writes。对MySQL来说,就是you may have to recover from a backup,就是要靠备份恢复。
五、业内标杆,Oracle的页分裂解决方案
虽然说在国内Oracle的形势有点日落西山了,但是技术上仍然有它的先进性。下面观察Oracle,看它的页分裂的解决方式,跟MySQL步骤一样。首先找到IO的函数做拦截,Oracle的IO函数打开IO的时候是submit,跟MySQL是一样的。
准备测试数据的时候就简单了,建个表插一行就行,因为的空间是倒着来的,你插一行它就在第二个4k,然后你更新着Oracle第二个4k目标行,第一个4k也会改,因为块头有管理性信息,修改完目标行后,需要手动触发检查点。Oracle写是比较延迟的,PG也是一样。手动触发检查点,再触发它的写操作,再拦截系统调用,把8k改成前4k成功后4k失败。
再去看Oracle的处理过程,先观察Oracle的操作有哪些。
然后观察Oracle的结果,除了对我的目标表外,它还写了 UNDO等,我把这些都破坏掉变成坏页。
脚本是这样的,最主要就是中间这一行,把它改成改成1000,因为2000是16进制的,16进制2000就892是,把它变成1000,只要一改Oracle,IO就会报错了。MySQL脚本还比较复杂,最终要让IO报错,不能让IO成功。
先让脚本跑起来, update是成功的,cmmit也是成功的,执行检查点时报错了,因为这个写是Oracle的核心进程,核心进程出错之后它就直接down了。
Down后看如何去恢复。启动到这个地方,报错没有启动成功。
从日志里可以看到很多信息,在Oracle的告警日志里,发现数据库上次是异常宕机了。
找到了一个恢复点,从日志redo的地方去恢复,叫7112号redo文件的1736号块,它要从这个地方去恢复。
PG里面也有类似的东西,它如何恢复起始点呢?对所有数据库来说,只要保证数据一致性,比如不能因为断电数据就完了,只要能做到这一点的数据库都要有redo,redo对它来说是日志流。Record跟脏块是一一对应的,而且Record的顺序就代表了脏块的顺序, Record1:在Record2前,那Record1对应的脏块比Record2对应的脏块更早变脏。Redo从某种意义上来说相当于一种时间,这所有数据库都差不多,MySQL和Oracle都以redo为准。
检查点位置对开发很有意义的,它定期去执行检查点,检查点就是写脏块,数据库都有这样类似的机制,比如说这次检查点开始时写了四个脏块,还剩四个脏块,其中四个块已经落盘了,还有四个没有落盘。
第五脏块在redo里对应的位置就叫检查点位置,这个检查点位置就是我们在告警日志里面看到的恢复的起始点,7112号块的1736,因为已经落盘成功了,在这个位置之前的脏块都是不需要恢复的。
它落盘成功后才会把检查点位置寄到控制文件里面,Oracle和PG都有这样的机制,也是在检查点完成后把位置记到控制文件里面,这个位置之前的脏块儿都已经落盘成功了,恢复时就从这个地方开始往后去redo恢复就可以了。
Oracle检测到这个日这个检查点位置之后,检测到有74k的redo,74k的redo对应24个块需要恢复。
在恢复的时候没恢复成功,数据库不对坏页修复。
页坏了后有redo,页坏的可能性千奇百怪,数据库不会以有涯随无涯,它要在以前完好的数据的基础上做恢复。MySQL的双写和PG的FPW都是需要时不时的做个备份,把完好的一致的数据做个备份,以备将来在这个基础上做恢复,数据库不去做修复只去做恢复。
Oracle遇到坏页、页分裂、不分写、页破碎,它是如何做的。Oracle依赖检查,只要能检查出来页有问题就可以了,检查出来让DBA做恢复,你不恢复成功数据库我不启动,如果数据库启动了,就代表你已经恢复成功了,它的数据是一致的。
为了尽快完成恢复,它提供了快恢复的功能,比如说32G的文件夹有8k坏了,MySQL怎么做呢?如果双写没有搞定,那把以前备份的文件就一个表一个文件把这个表给恢复过来, 32G数据全部来一遍,就会比较慢。
Oracle有块恢复功能,如果只有一个块或者部分块坏了,只需要恢复问题块,而不需要32G全部来一遍,这是Oracle应对页分裂的方式、检查和介质恢复。
六、PG如何处理页分裂
PG的测试过程,PG的IO函数pwrite,测试过程跟oracle差不多,PG机制跟Oracle机制是更类似一些,行也是倒着用,MySQL的行是正着来的。
所以我要插200行,把这个目标行撑到第二个或第三个4k,Oracle和PG是插入一行就行了,间隔表插一行在最末尾,在这个页的第二个4k,更新第二个4k,第一个4k也会变,也要手动触发检查点拦截系统调用,改8k为4k,让前4k写成功,再让IO报错。
在测试之前我先用一个简单的小脚本看一下。
它的写没有Oracle那么多,比MySQL的也少,没有UNDO,所以写会少一点,第一次这个写是针对WAL日志的,第二次写也是针对WAL日志的。
这些都让它成功,日志不成功没法做恢复。
第三次写是针对目标表的,要让四号文件写是失败的,其它的都是成功的。
上图是脚本。针对某个固定的进程,16045就是checkpoint的进程,让四号文件满足条件后才去破坏,最终这个条件也是为了让IO报错的,不能让io成功。前4k的IO是成功的,后4k是失败的。
上图是脚本。针对某个固定的进程,16045就是checkpoint的进程,让四号文件满足条件后才去破坏,最终这个条件也是为了让IO报错的,不能让io成功。前4k的IO是成功的,后4k是失败的。
update执行是成功的,commit也是成功的,执行checkpoint是失败的。
目标就是让它是失败的。这个数据库并没有宕掉,PG还是比较坚强的,如果把IO破坏之后MySQL就直接down了,Oracle也直接down了。
为了模拟,把所有的PG进程一起宕掉,就像页分裂是断电分裂,其实只kill第一个主进程也是可以的,kill多点保险点,这样就能看到日志里面有大量的IO报错,再重新启动的时候,PG找到了恢复的位置,这个恢复位置跟Oracle机制一样,这个位置就是检查点位置。
从日志的这个地方恢复成功后,数据库最后的启动过程也是成功的,连上后查我目标表,这个目标表数据查出来后,触发语句大写转小写。最终看到数据就是小写,页分裂问题就搞定了,它是以巨大的性能的代价解决了这个问题,,最终问题解决了,就是性能下降确实有点大。
这个页分裂是偶尔出现的一个问题,偶尔出现荡机,还不是当库,所以操作系统宕机和断电这种情况是极其偶尔出现的,为了一个极其偶尔出现的情况,引入性能损耗比较大的特性值得吗?
七、PG如何处理页分裂 --- 关闭FPW时的测试
对比各个主流的数据库后,我们要对FPW动手了,今天先讲第一步,刚才是把FPW打开时的测试,那把FPW关掉再来一遍,看下页分裂是什么情况。
整个测试过程是一模一样的,唯一区别就是把FPW关掉了,关掉后update是成功的,提交也是成功的,因为是隐含提交的。
Checkpoint也是失败的。
整个测试过程是一模一样的,唯一区别就是把FPW关掉了,关掉后update是成功的,提交也是成功的,因为是隐含提交,Checkpoint失败,
重启数据库时候发现这个数据没报错,就是因为前面把它改成小写了,但是这个地方可以看到是大写的。如果关掉了FPW后,数据可能是不一致的,但是这是在没有打开Checksum的情况,如果打开了通常还是会报错的。这样测试我做了很多遍,这个测试结果我还不确定,因为时间比较紧,所以这个例子我没放上来。
八、Non-FPW的思路
MySQL双写不能解决所有的问题,那我就摸着Oracle过河,Oracle的解决方案是报出错误,能够检测块的问题,检测不出来数据就不一致了,我提供让DB恢复,为了更快的恢复,也可以提供一个像Oracle一样的块恢复功能。MySQL不支持这样的功能,它是逻辑日志,只能恢复数据,不能恢复某一个固定的页。PG和Oracle都是物理日志,他们可以有black recovery的功能。
最终我们的方式是摸着Oracle过河,因为Oracle跟PG备份恢复体系差不多,检查点位置、特性都差不多,唯一不同的是Oracle是增量检查点,PG没有增量检查点,这点对我们影响不大。
目标是让PG实现跟Oracle一样检查加块恢复的功能,实现快速的不分写的解决方式。实现之后就可以安全的关掉FPW,不用担心出现断电后数据不一致的问题。
MySQL的恢复功能在StartupXLOG函数里面,这个函数在这个Xlog.c里面,在此if之后,就是它的恢复流程。
下面是要对这一部分代码去精读熟读,搞清楚每一行的意义,每个变量的作用等。在这个基础上去做修改调整,把它复制出来再自己做一个模块,用它做检查块和级恢复。