MYSQL双写总结
一:为啥会有两次写?必要了解partial page write 问题 :
InnoDB 的Page Size一般是16KB,其数据校验也是针对这16KB来计算的,将数据写入到磁盘是以Page为单位进行操作的。而计算机硬件和操作系统,写文件是以4KB作为单位的,那么每写一个innodb的page到磁盘上,在os级别上需要写4个块.通过以下命令可以查看文件系统的块大小.
dumpe2fs /dev/vda1 |grep "Block size" dumpe2fs 1.41.12 (17-May-2010) Block size: 4096
在极端情况下(比如断电)往往并不能保证这一操作的原子性,16K的数据,写入4K 时,发生了系统断电/os crash ,只有一部分写是成功的,这种情况下就是 partial page write 问题。有人会想到系统恢复后MySQL可以根据redolog 进行恢复,而mysql在恢复的过程中是检查page的checksum,checksum就是pgae的最后事务号,发生partial page write 问题时,page已经损坏,找不到该page中的事务号,就无法恢复。
二 doublewrite 原理
书上这里没有画图,直接介绍单页面刷盘跟批量刷盘。
为了解决 partial page write 问题 ,
- 当mysql将脏数据flush到data file的时候, 先使用memcopy 将脏数据复制到内存中的double write buffer ,
- 通过double write buffer再分2次,每次写入1MB到共享表空间,
- 然后马上调用fsync函数,同步到磁盘上,避免缓冲带来的问题。
在这个过程中,doublewrite是顺序写,开销并不大,在完成doublewrite写入后,在将double write buffer写入各表空间文件,这时是离散写入。如果发生了极端情况(断电),InnoDB再次启动后,发现了一个Page数据已经损坏,那么此时就可以从doublewrite buffer中进行数据恢复了。
两次写需要额外添加两个部分:
- 内存中的两次写缓冲(doublewrite buffer),大小为2MB
- 磁盘上共享表空间中连续的128页,大小也为2MB。其中120个用于批量写脏页,另外8个用于Single Page Flush。做区分的原因是批量刷脏是后台线程做的,不影响前台线程。而Single page flush是用户线程发起的,需要尽快的刷脏页并替换出一个空闲页出来。
show status like "%InnoDB_dblwr%"; +----------------------------+------------+ | Variable_name | Value | +----------------------------+------------+ | Innodb_dblwr_pages_written | 2212378738 | | Innodb_dblwr_writes | 133881618 | +----------------------------+------------+
InnoDB_dblwr_pages_written:从bp flush 到 DBWB的个数
InnoDB_dblwr_writes:写文件的次数
从这个数据来看,系统数据变更的频率不是特别高。
三 单页面刷盘
单一页面刷盘,实际是mysql5.5版本中实现方式,MYSQL会在系统页面,也就是ibdata的page5页面(同时也是存储事务信息的一个页面中存储两次写的信息),偏移位置就是页面结束位置的200字节处,内容如下:
两次写总共包括2M(默认值)的数据,有2个BLOCK,那么每一个BLOCK是1M,每个页面是16K,那么一个BLOCK包括64个页面,正是一个簇的大小。,所以其实两次写页面的空间是2个簇的空间。
除上面的信息需要持久化到文件中之外,还会有一个空间用来存储这128个页面的页面信息,这是在内存中的,每次在刷盘前,都会把要刷盘的页面信息临时保存到数组中,这是一个长度为128的数组。这个缓存被称为两次写缓存数组。
原理上面已经介绍过了。需要注意的是,buffer pool中的页面,刷到真实文件的时是异步IO的,只有当自己刷到自己表空间的刷盘操作完成以后,两次写缓存数组的数据才能被覆盖。
四 批量页面刷盘
单一页面的两次写,导致IO增多性能下降,mysql 5.7还有批量页面刷盘方式。当buffer pool中的free list不足时,为了获取一个空闲block,通常会触发page驱逐操作.刷盘包括两种方式:LRU方式和LIST方式。LRU就是系统把LRU列表找到最老的页面,进行批量刷盘,讲空间还原到空闲空间去。而当空间不足或者主线程在定时刷盘时,不需要区分页面新旧状态,只需要选择LSN最小的页面,从前到后刷一批到文件,就是LIST方式。
书上没有展开讨论,补充下相关知识点:
4.1 LRUlist
这里用到了顺序表list来作为缓冲池,每个数据节点称为block。LRU-list维护着最近不常用的页面列表。该算法采用“中点插入法”:当插入一个新block时,移除表尾最近最少使用的block,在中点插入新block。这个中点将链表分为两部分:
靠近表头的一部分,为young区,这里的block是最近使用的节点 MRU
靠近表尾的一部分,为old区,这里的block是最近少使用的 LRU
MRU位于LRU-list的5/8之前,其余部分为LRU。也就是5/8之前存储着常用的页面,其余3/8为不常用页面(可以修改innodb_old_blocks_pct系统变量值控制MRU与LRU之间的分割点,默认值为5/8)。删除内容会在最不常用链表的末端删除几个页面。删除之前会进行刷脏。
当无法从LRU上获得一个可替换的Page时,说明当前Buffer pool可能存在大量脏页,这时候会触发single page flush(buf_flush_single_page_from_LRU),即用户线程主动去刷一个脏页并替换掉。这是个慢操作,尤其是如果并发很高的时候,可能观察到系统的性能急剧下降。除了single page flush外,在MySQL 5.7版本里还引入了多个page cleaner线程,根据一定的启发式算法,可以定期且高效的的做page flush操作。
4.2 两次写组织结构
在两次写中,两种刷盘算法对应的两次写空间互不影响,同时INNODB 自身的整个buffer pool分为多个Instance,每一个Instance管理属于自己的一套两次写空间。书上给出了图如下:
图中每一个shard,其实是一个batch,参数是INNODB_double_write_batch_size。一个shard有一个数组,长度就是INNODB_double_write_batch_size。
批量刷脏过程
举例为LRU方式,系统将当前页加入到两次写缓存中,根据当前页面所在的instance号及刷盘类型就找到对应的shard缓存,照后判断shard缓存是否满了(是否达到INNODB_double_write_batch_size),不满将当前页面追加到shard缓存中即可。不需要像单页面那样双写。
如果当前shard缓存已经满了,则不得不把shard缓存的页面写入两次写文件中,再把两次写文件flush到磁盘中。最后将对应的真实页面刷盘,但此时可能就是随机写入了。上面写入连续的INNODB_double_write_batch_size个页面,所以性能比连续多次,每次写一个页面也好很多。
五 总结
性能损耗
表面看上去,它是每个页面都写了2遍,会非常影响性能。但实际上,由于所写的页面会先缓存到内存中,因此每一部分缓存空间在满了之后才会真正地写入文件。并且doublewrite是一个连接的存储空间,所以硬盘在写数据的时候是顺序写,而不是随机写,这样性能很高。doublewrite有效利用这个特地那,所以降低并不会相差1倍,经过测试,大概5-10%左右。当然,这是针对普通磁盘。对于目前比较流行的SSD来说,随机写已经不是问题,性能影响可能更小。
doublewrite默认开启,参数skip_innodb_doublewrite虽然可以禁止使用doublewrite功能,但还是强烈建议大家使用doublewrite。避免部分写失效问题,当然,如果你的数据表空间放在本身就提供了部分写失效防范机制的文件系统上,如ZFS/FusionIO/DirectFS文件系统,在这种情况下,就可以不开启doublewrite了。
其实两次写并不是什么特性或优点,它只是一个被动解决方案而已。这个问题的本质就是磁盘在写入时,都是以512字节为单位,不能保证MySQL数据页面16KB的一次性原子写,所以才有可能产生页面断裂的问题。而目前有些厂商从硬件驱动层面做了优化,可以保证16KB(或其他配置)数据的原子性写入。如果真是这样,那么两次写就完全没有必要了,取消两次写,才是最终级优化,值得期待。
两次写的作用
在数据库启动时(异常关闭的情况下),都会做数据库恢复(redo)操作,恢复的过程中,数据库都会检查页面是不是合法(校验等等),如果发现一个页面校验结果不一致,则此时会用到两次写这个功能,这个特点也正是为了处理这样的错误而设计的。
此时的操作很明白了,将两次写的2个BLOCK(簇)都读出来,然后将所有这些页面写回到对应的页面中去,那么这时可以保证这些页面是正确的,并且是在写入前已经更新过的(最新数据)。在写回对应页面中去之后,那么就可以在这基础上继续做数据库恢复了,之后则不会再遇到这样的问题了,因为已经将最后有可能产生写断裂的数据页面都恢复了。
如果是写doublewrite buffer本身失败,那么这些数据不会被写到磁盘,InnoDB此时会从磁盘载入原始的数据,然后通过InnoDB的事务日志来计算出正确的数据,重新写入到doublewrite buffer。