mysql double write (二次写)是mysql innodb存储引擎的一个重要特性,本人这两天翻阅了相关的资料,结合自己已有的知识,说说自己对double write的理解,供各位看官参考。
页断裂(partial write)
double write技术innodb为解决页断裂(partial write)问题而生,所谓页断裂是数据库宕机时(OS重启,或主机掉电重启),数据库页面只有部分写入磁盘,导致页面出现不一致的情况。数据库,OS和磁盘读写的基本单位是块,也可以称之为(page size)block size。我们知道数据库的块一般为8K,16K;而OS的块则一般为4K;IO块则更小,linux内核要求IO block size<=OS block size。磁盘IO除了IO block size,还有一个概念是扇区(IO sector),扇区是磁盘物理操作的基本单位,而IO 块是磁盘操作的逻辑单位,一个IO块对应一个或多个扇区,扇区大小一般为512个字节。所以各个块大小的关系可以梳理如下:
DB block > OS block >= IO block > 磁盘 sector,而且他们之间保持了整数倍的关系。比如我的系统各个块的大小如下,DB以mysql为例,OS以linux为例。
DB block size
root@(none) 09:13:02>show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
OS block size
[1 Single:CHECK: mysql ~ ]
$ getconf PAGESIZE
4096
IO block size
比如查看sdb1分区的IO block size
[root@chuckkkk]# blockdev --getbsz /dev/sdb1
4096
sector size
[root@chuckkkk]# fdisk -l | grep Sector
Sector size (logical/physical): 512 bytes / 512 bytes
从上面的结果可以看到DB page=4*OS page=IO page=8*sector size。
由于任何DB page的写入,最终都会转为sector的写入,如果在写磁盘的过程中,出现异常重启,就可能会发生一个DB页只写了部分sector到磁盘,进而出现页断裂的情况。
页断裂与数据库一致性
前面我们分析了异常重启一定会导致页断裂,而页断裂就意味着数据库页面不完整,那么数据库页面不完整是否就意味着数据库不一致呢???我们知道,数据库异常重启时,自身有异常恢复机制,我这里不打算展开讲异常恢复机制,因为不同数据库的异常恢复流程不同。主流数据库基本原理类似:第一阶段重做redo日志,恢复数据页和undo页到异常crash时的状态;第二阶段,根据undo页的内容,回滚没有提交事务的修改。通过两个阶段保证了数据库的一致性。对于mysql而言,在第一阶段,若出现页断裂问题,则无法通过重做redo日志恢复,进而导致恢复中断,数据库不一致。这里大家可能会有疑问,数据库的redo不是记录了所有的变更,并且是物理的吗?理论上来说,无论页面是否断裂,从上一个检查点对应的redo位置开始,一直重做redo,页面自然能恢复到正常状态。对吗?讲清楚这个问题,先讲讲重做日志(redo)格式。
重做日志(redo)格式
数据库系统实现日志主要有三种格式,逻辑日志(logical logging),物理日志(physical logging),物理逻辑日志(physiological logging),而对于redo日志,则主要采用物理日志和物理逻辑日志两类。逻辑日志,记录一个个逻辑操作,不涉及物理存储位置信息,比如mysql的binlog;物理日志,则是记录一个个具体物理位置的操作,比如在2号表空间,1号文件,48页的233这个offset地方写入了8个字节的数据,通过(group_id,file_id,page_no,offset)4元组,就能唯一确定数据存储在磁盘的物理位置;物理逻辑日志是物理日志和逻辑日志的混合,如果一个数据库操作(DDL,DML,DCL)产生的日志跨越了多个页面,那么会产生多个物理页面的日志,但对于每个物理页面日志,里面记录则是逻辑信息。这里我举一个简单的INSERT操作来说明几种日志形式。
比如innodb表T(c1,c2, key key_c1(c1)),插入记录row1(1,’abc’)
逻辑日志:
<insert OP, T, 1,’abc’>
逻辑物理日志:
因为表T含有索引key_c1, 一次插入操作至少涉及两次B树操作,二次B树必然涉及至少两个物理页面,因此至少有两条日志
<insert OP, page_no_1, log_body>
<insert OP, page_no_2, log_body>
物理日志:
由于一次INSERT操作,物理上来说要修改页头信息(如,页内的记录数要加1),要修改相邻记录里的链表指针,要修改Slot属性等,因此对应逻辑物理日志的每一条日志,都会有N条物理日志产生。
< group_id,file_id,page_no,offset1, value1>
< group_id,file_id,page_no,offset2, value2>
……
< group_id,file_id,page_no,offsetN, valueN>
因此对于上述一个INSERT操作,会产生一条逻辑日志,二条逻辑物理日志,2*N条物理日志。从上面简单的分析可以看出,逻辑日志的日志量最小,而物理日志的日志量最大;物理日志是纯物理的;而逻辑物理日志则页间物理,页内逻辑,所谓physical-to-a-page, logical-within-a-page。
redo格式与数据一致性
回到“发生页断裂后,是否会影响数据库一致性”的问题,发生页断裂后,对于利用纯物理日志实现redo的数据库不受影响,因为每一条redo日志完全不依赖物理页的状态,并且是幂等的(执行一次与N次,结果是一样的)。另外要说明一点,redo日志的页大小一般设计为512个字节,因此redo日志页本身不会发生页断裂。而逻辑物理日志则不行,比如修改页头信息,页内记录数加1,slot信息修改等都依赖于页面处于一个一致状态,否则就无法正确重做redo。而mysql正是采用这种日志类型,所以发生页面断裂时,异常恢复就会出现问题,需要借助于double write技术来辅助处理。
double write处理页断裂
doublewrite是Innodb表空间内部分配的一片缓冲区,一般double write包含128个页,对于pagesize为16k的页,总共2MB,doublewrite页与数据页一样有物理存储空间,存在于共享表空间中。Innodb在写出缓冲区中的数据页时采用的是一次写多个页的方式,这样多个页就可以先顺序写入到doublewrite缓冲区,并调用fsync()保证这些数据被写出到磁盘,然后数据页才被写出到它们实际的存储位置并再次调用fsync()。故障恢复时Innodb检查doublewrite缓冲区与数据页原存储位置的内容,若doublewrite页处于页断裂状态,则简单的丢弃;若数据页不一致,则会从doublewrite页还原。由于doublewrite页落盘与数据页落盘在不同的时间点,不会出现doublewrite页和数据页同时发生断裂的情况,因此doublewrite技术可以解决页断裂问题,进而保证了重做日志能顺利进行,数据库能恢复到一致的状态。
oracle如何处理页断裂
oracle与mysql一样,采用的redo日志也是逻辑物理格式,但没有doublewrite技术。我一直就想着oracle这么牛逼的数据库,一定有它自己的方式来应对这种问题。也许这就是所谓的蛮目崇拜,找了N久资料,中文的英文的,包括咨询何登成大神,基本能得出一个结论oracle遇到页断裂问题,一样会挂,重启不来。但是oracle有一个相对简单的策略来恢复数据库到一致状态,备份+归档日志。备份保证了数据页不发生页断裂,加上归档日志增量可以恢复到某个时间点。为什么不做呢?我想一般使用oracle都会使用DataGuard做容灾,主库发生故障时,备库会承担起主库的责任,然后异常主库通过备份+归档日志的方式来恢复,虽然不如doublewrite技术快,但也是能恢复出来的。
其他方式解决页断裂
前面讨论的都是基于一个假设,异常重启后,一定会导致页断裂,其实在底层设施在一定程度上是可以解决这个问题的,比如文件系统层面,采用ZFS文件系统,ZFS通过日志的方式保证了OS页面的完整性,从底层防止了页断裂问题;在磁盘层面,一般RAID卡都会有带电缓存,即使OS异常重启,缓存数据也不会立即丢失,因而也能保证不发生partial write问题。但是我在想,OS的pagesize比DB的pagesize要小,即使能保证OS page不发生页断裂,也不能保证DB page 不断裂,个人感觉不能彻底解决。当然了,如果将DB pagesize设置成和OS pagesize一样大,就没问题了。