我们知道,由于文件系统对一次大数据页(例如InnoDB的16KB)大多数情况下不是原子操作,这意味着如果服务器宕机了,可能只做了部分写入。在InnoDB看来,这样的数据页是无法通过checksum验证的。即时我们强制让其通过验证,也无法从崩溃中恢复,因为当前InnoDB存在的一些日志类型,有些是逻辑操作,并不能做到幂等。
为了解决这个问题,InnoDB实现了double write buffer,简单来说,就是在写数据页之前,先把这个page写到一块独立的物理文件位置,然后再写到数据页。这样在宕机重启的时候,损坏的数据页可以从其中提取出来。
我之前的这篇博客有对dblwr的介绍,这里简单的再介绍一下。
在官方版本中,InnoDB的double write buffer存储在ibdata系统表空间中,大小为2MB,共128个Page,其中120个用于批量写脏,另外8个用于Single Page Flush。做区分的原因是批发刷脏是后台线程做的,不影响前台线程。而Single page flush是用户线程发起的,需要尽快的刷脏并替换出一个空闲页出来。
对于批量刷脏,每次找到一个可做flush的page,对其持有S lock,然后将该page拷贝到dblwr中,当dblwr满后者一次批量刷脏结束时,将dblwr中的page全部刷到ibdata中,注意这是同步写操作;然后再唤醒后台IO线程去写数据页。当后台IO线程完成写操作后,会去更新dblwr中的计数以腾出空间,释放block上的S锁,完成写入。
对于Single Page Flush,则做的是同步写操作,在挑出一个可以刷脏的page后,先加入到dblwr中,刷到ibdata,然后写到用户表空间,完成后,会对该用户表空间做一次fsync操作。
Single Page Flush在buffer pool中free page不够时触发,通常由前台线程发起,由于每次single page flush都会导致一次fsync操作,在大并发负载下,如果大量线程去做flush,很显然会产生严重的性能下降。Percona在5.6版本中做了优化,可以选择由后台线程lru manager来做预刷,避免用户线程陷入其中。
Mariadb/MySQL的改进
dblwr引入了一次额外写的开销,每个数据页都被要求写两次,究其原因是因为写入不是原子的。但如果你的数据表空间放在FusionIO/DirectFS文件系统上,就可以获得原子写特性。
MariaDB使用参数innodb_use_atomic_writes来控制原子写行为,当打开该选项时,会使用O_DIRECT模式打表空间,通过posix_fallocate来扩展文件(而不是写0扩展),当在启动时检查到支持atomic write时,即使开启了innodb_doublewrite,也会关闭掉。
Oracle MySQL同样支持FusionIO的Atomic Write特性(Fusion-io Non-Volatile Memory (NVM) file system),对于支持原子写的文件系统,也会自动关闭double write buffer,具体见该commit(Bug#18069105 - ADD FUSIONIO ATOMIC WRITE SUPPORT FOR LINUX)以及commit2
Facebook改进
实际上这不能算是改进,只是提供了一个新的选项。在现实场景中,宕机是非常低概率的事件。大部分情况下dblwr都是用不上的。但如果我们直接关闭dblwr,如果真的发生例如掉电宕机了,我们需要知道哪些page可能损坏了。
因此Facebook MySQL提供了一个选项,可以写page之前,只将对应的page number写到dblwr中(而不是写全page),在崩溃恢复时,先读出记录在dblwr中的page号,检查对应的数据页是否损坏,如果损坏了,那就需要从备库重新恢复该实例。
Percona 5.7改进
Percona Server的每个版本都对InnoDB的刷脏逻辑做了不少的优化,进入5.7版本也不例外。在官方5.7中已经实现了多个Page Cleaner,我们可以把Page Cleaner配置成和buffer pool instance的个数相同,可以更好的实现并行刷脏。
但是官方版本中,Page cleaner既要负责刷FLUSH LIST,同时也要做LRU FLUSH(但每个bp instance不超过innodb_lru_scan_depth)。而这两部分任务是可以独立进行的。
因此Percona Server增加了多个LRU FLUSH线程,可以更高效的进行lru flush,避免用户线程陷入single page flush状态。每个buffer pool instance拥有自己的lru flush线程和page cleaner线程。lru flush基于当前free list的长度进行自适应计算。 每个lru线程负责自己的那个Buffer pool。因此不同lru flush线程的繁忙程度可能是不一样的。
MULTI-LRU-FLUSH代码见github,以及Percona的博客
在解决上述问题后,bp flush的并行效率大大的提升了。但是对于所有的刷脏操作,都需要走到double write buffer。这意味着dblwr成为了新的瓶颈。为了解决这个问题,dblwr进行了拆分,每个bp instance都有自己的dblwr区域。这样各个Lru flush线程及Page cleaner线程在做page flush时就不会相互间产生锁冲突,从而提升了系统的扩展性。
你可以通过参数来配置一个独立于ibdata之外的文件来存储dblwr,文件被划分成多个区域,分区数为bp instance的个数,每个分区的大小为2 * srv_doublewrite_batch_size,每个batch size默认配置为120个page,其中一个用于刷FLUSH LIST,一个用于刷LRU。
如果fast shutdown设置为2,dblwr文件在正常shutdown时会被删除掉,并在重启后重建。
dblwr-split代码见github: pull#377以及Percona的博客
Aurora
Aurora是一个闭源的MySQL数据库引擎,具体的实现我们不得而知,根据其公开的信息表明,其采用了存储和数据库服务器分离的方式来实现架构。基于这种架构,提供服务的服务器无需去写数据文件,只需要存储日志即可,存储层单独做日志apply到数据页。基于这种实现,也就无需开启double write buffer。
Aurora相关链接:
Youtube视频,需翻墙
AWS Aurora Benchmark - Choose the right tool for the job
Amazon Aurora: Amazon’s New Relational Database Engine
Amazon Aurora: the Benefits and the Drawbacks
Introducing Amazon Aurora: A New MySQL-Compatible Database