InnoDB引擎之-两次写(Double Write)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了InnoDB引擎之-两次写(Double Write)相关的知识,希望对你有一定的参考价值。

参考技术A InnoDB引擎有几个重点特性,为其带来了更好的性能和可靠性:

介绍double write之前我们有必要了解 partial page write (部分页失效)。

InnoDB的Page Size一般是16KB,其数据校验也是针对这16KB来计算的,将数据写入到磁盘是以Page为单位进行操作的。我们知道,由于文件系统对一次大数据页(例如InnoDB的16KB)大多数情况下不是原子操作,这意味着如果服务器宕机了,可能只做了部分写入。16K的数据,写入4K时,发生了系统断电/os crash ,只有一部分写是成功的,这种情况下就是 partial page write 问题。

我们一般可能会想到,如果发生写失效,mysql可以根据redo log进行恢复。这是一个办法,但是必须清楚地认识到,redo log中记录的是对页的物理修改,如偏移量 800 ,写 aaaa 记录。如果这个页本身已经发生了损坏,再对其进行重做是没有意义的。MySQL在恢复的过程中检查page的checksum,checksum就是检查page的最后事务号,发生 partial page write 问题时,page已经损坏,找不到该page中的事务号。在InnoDB看来,这样的数据页是无法通过checksum验证的,就无法恢复。即时我们强制让其通过验证,也无法从崩溃中恢复,因为当前InnoDB存在的一些日志类型,有些是逻辑操作,并不能做到幂等。

为了解决这个问题,InnoDB实现了 double write buffer ,简单来说,就是在写数据页之前,先把这个数据页写到一块独立的物理文件位置(ibdata),然后再写到数据页。这样在宕机重启时,如果出现数据页损坏,那么在应用redo log之前,需要通过该页的副本来还原该页,然后再进行redo log重做,这就是 double write 。 double write 技术带给innodb存储引擎的是数据页的可靠性,下面对 double write 技术进行解析,让大家充分理解 double write 是如何做到保障数据页的可靠性。

double write 由两部分组成,一部分是InnoDB内存中的 double write buffer ,大小为2M,另一部分是物理磁盘上 ibdata 系统表空间中大小为2MB,共128个连续的Page,既2个分区。其中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来做预刷,避免用户线程陷入其中。

如果发生了极端情况(断电),InnoDB再次启动后,发现了一个Page数据已经损坏,那么此时就可以从double write buffer中进行数据恢复了。

当一系列机制(main函数触发、checkpoint等)触发数据缓冲池中的脏页进行刷新到data file的时候,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的double write buffer,之后通过double write buffer再分两次、每次1MB顺序写入共享表空间的物理磁盘上。然后马上调用fsync函数,同步脏页进磁盘上。由于在这个过程中,double write页的存储时连续的,因此写入磁盘为顺序写,性能很高;完成 double write 后,再将脏页写入实际的各个表空间文件,这时写入就是离散的了。各模块协作情况如下图(第一步应为脏页产生的redo记录log buffer,然后log buffer写入redo log file,为简化次要步骤直接连线表示):

查看double write工作情况,可以执行命令:

以上数据显示,double write一共写了 61932183个页,一共写了15237891次,从这组数据我们可以分析,之前讲过在开启double write后,每次脏页刷新必须要先写double write,而double write存在于磁盘上的是两个连续的区,每个区由连续的页组成,一般情况下一个区最多有64个页,所以一次IO写入应该可以最多写64个页。而根据以上我这个系统Innodb_dblwr_pages_written与Innodb_dblwr_writes的比例来看,一次大概在4个页左右,远远还没到64,所以从这个角度也可以看出,系统写入压力并不高。

如果操作系统在将页写入磁盘的过程中发送了崩溃,在恢复过程中,InnoDB存储引擎可以从工序表空间中的double write中找到该页的副本,将其复制到表空间文件,再应用redo log。下面显示了一个由double write进行恢复的过程:

dblwr位于共享表空间上的double write buffer实际上也是一个文件,引入了一次额外写的开销,每个数据页都被要求写两次。由于需要大量的fsync操作,所以它会降低MySQL的整体性能,但是并不会降低到原来的50%。这主要是因为:

double write默认开启,参数skip_innodb_double_write虽然可以禁止使用double write功能,但还是强烈建议大家使用double write。避免部分写失效问题,当然,如果你的数据表空间放在本身就提供了部分写失效防范机制的文件系统上,如ZFS/FusionIO/DirectFS文件系统,在这种情况下,就可以不开启double write了。

如果是写double write buffer本身失败,那么这些数据不会被写到磁盘,InnoDB此时会从磁盘载入原始的数据,然后通过InnoDB的事务日志来计算出正确的数据,重新写入到double write buffer。

如果double write buffer写成功的话,但是写磁盘失败,InnoDB就不用通过事务日志来计算了,而是直接用buffer的数据再写一遍。如上图中显示,在恢复的时候,InnoDB直接比较页面的checksum,如果不对的话,Innodb存储引擎可以从共享表空间的double write中找到该页的一个最近的副本,将其复制到表空间文件,再应用redo log,就完成了恢复过程。因为有副本所以也不担心表空间中数据页是否损坏,但InnoDB的恢复通常需要较长的时间。

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。

实际上这不能算是改进,只是提供了一个新的选项。在现实场景中,宕机是非常低概率的事件。大部分情况下dblwr都是用不上的。但如果我们直接关闭dblwr,如果真的发生例如掉电宕机了,我们需要知道哪些page可能损坏了。

因此Facebook MySQL提供了一个选项,可以写page之前,只将对应的page number写到dblwr中(而不是写全page),在崩溃恢复时,先读出记录在dblwr中的page号,检查对应的数据页是否损坏,如果损坏了,那就需要从备库重新恢复该实例。

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线程的繁忙程度可能是不一样的。

在解决上述问题后,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时会被删除掉,并在重启后重建。

详解MySQL存储引擎Innodb

设置存储引擎SQL语句

查看当前MySQL支持的存储引擎列表

show engines

在创建表时指定存储引擎

create table 表名 engine = innodb;

修改已有表的存储引擎

alter table 表名 engine = innodb;


innodb存储引擎

MySQL版本>=5.5 默认的存储引擎,MySQL推荐使用的存储引擎。支持事务,行级锁定,外键约束。事务安全型存储引擎。更加注重数据的完整性和安全性。

存储格式

数据与索引集中存储在同一个表空间文件中。

  • 例如:创建一个test数据,新建一张student表,选择存储引擎为innodb, 然后打开mysql的data下的test目录,发现有以下3个文件。
create table student(
id int(10),
name varchar(10)
)
engine=INNODB

其中db.opt存放了数据库的配置信息,比如数据库的字符集还有编码格式。student.frm是表结构文件,仅存储了表的结构、元数据(meta),包括表结构定义信息等。不论是哪个表引擎都会有一个frm文件。student.ibd是表索引文件,包括了单独一个表的数据及索引内容。

如果往表里插入了新的数据,则在mysql的data目录下会生成ibdata1文件,这个文件是存储了所有innodb表的数据。

insert into student(id,name)VALUES('1','张三')


了解表空间相关内容可以访问:https://chonglian.blog.csdn.net/article/details/122044721


体系架构


InnoDB存储引擎有多个内存块,这些内存块组成了一个大的内存池。后台线程主要负责刷新内存池中的数据、将已修改的数据刷新到磁盘等等。

一.后台线程

  1. Master Thread:主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲(INSERT BUFFER)、UNDO页的回收等。

  2. IO THread:在InnoDB存储引擎中大量使用了AIO (Async IO)来处理写IO请求,这样可以极大提高数据库的性能。而IO Thread的工作主要是负责这些IO请求的回调(call back)处理。

  3. Purge Thread:事务被提交后,其所使用的undolog可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页。(在InnoDB 1.1版本之前,purge操作仅在InnoDB存储引擎的Master Thread中完成。而从InnoDB 1.1版本开始,purge操作可以独立到单独的线程中进行,以此来减轻Master Thread 的工作,从而提高CPU的使用率以及提升存储引擎的性能。)

  4. Page Cleaner Thread:Page Cleaner Thread是在InnoDB 1.2.x版本中引入的。其作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成。而其目的是为了减轻原Master Thread 的工作及对于用户查询线程的阻塞,进一步提高InnoDB存储引擎的性能。

二.内存

缓冲池

InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可将其视为基于磁盘的数据库系统(Disk-base Database)。在数据库系统中,由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。

简单的来说缓冲池就是内存中的一块区域,在对数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓冲池中,这个过程称为将页FIX在缓冲池中。下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。对于修改操作,首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。这里需要注意的是,页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为 Checkpoint 的机制刷新回磁盘。同样,这也是为了提高数据库的整体性能。

具体来看,缓冲池中缓存的数据页类型有索引页数据页undo页插入缓冲( insert buffer),自适应哈希索引(adaptive hash index)InnoDB存储的锁信息(lockinfo)数据字典信息(data dictionary)等。不能简单地认为,缓冲池只是缓存索引页和数据页,它们只是占缓冲池很大的一部分而已。从InnoDB 1.0.x版本开始,允许有多个缓冲池实例。每个页根据哈希值平均分配到不同缓冲池实例中。这样做的好处是减少数据库内部的资源竞争,增加数据库的并发处理能力。

重做日志缓冲

InnoDB存储引擎首先将重做日志信息先放入到这个缓冲区,然后按-定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置得很大,因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。默认为8MB。


在通常情况下,8MB的重做日志缓冲池足以满足绝大部分的应用,因为重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中。

  1. Master Thread每一秒将重做日志缓冲刷新到重做日志文件;
  2. 每个事务提交时会将重做日志缓冲刷新到重做日志文件;
  3. 当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件。

额外内存池

在InnoDB存储引擎中,对内存的管理是通过一种称为内存堆(heap)的方式进行的。在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。

例如,分配了缓冲池( innodb_buffer_pool),但是每个缓冲池中的帧缓冲(frame buffer)还有对应的缓冲控制对象(buffer control block),这些对象记录了一些诸如LRU,锁,等待等信息,而这个对象的内存需要从额外内存池中申请。因此,在申请了很大的InnoDB缓冲池时,也应考虑相应地增加这个值。


关键特性

一.插入缓冲

插入聚集索引一般是顺序的,不需要磁盘的随机读取但插入非聚集索引叶子节点不是顺序的,需要离散访问非聚集索引页,速度较慢对于非聚集索引的插入或更新,先判断插入的非聚集索引页是否在缓存池中,若在,直接插入,或不在,先放到一个Inser Buffer对象中,然后根据一些算法将Insert Buffer缓存的记录通过后台线程慢慢合并刷新回辅助索引。插入缓冲将多次插入合并为一次操作,减少磁盘的离散操作。

使用Insert Buffer需满足两个条件:

  • 索引是辅助索引
  • 索引不是唯一的(不需要查找索引页判断唯一性)

InnoDB从1.0.x引入Change Buffer,对INSERT,DELETE,UPDATE都进行缓冲。

二.两次写

当发生数据库宕机时,可能InnoDB存储引擎正在写入某个页到表中,而这个页只写了一部分,比如16KB的页,只写了前4KB,之后就发生了宕机,这种情况被称为部分写失效(partial page write)。

因此在使用重做日志恢复数据库,需要有一个页的副本,当发生部分写失效时,先通过页的副本还原该页,再进行重做。于是InnoDB实现了doublewrite技术。

doublewrite有两部分,一部分是内存中的doublewrite buffer,大小为2MB,另一部分是磁盘共享表空间连续的128个页,也是2MB。

doublewrite要求刷新缓冲池的脏页时执行以下步骤:

  1. 通过memcpy函数将脏页复制到内存的doublewrite buffer
  2. doublewrite buffer分两次,每次1MB顺序写入共享表空间
  3. 调用fsync函数同步磁盘,避免缓冲写带来问题,确保数据刷新到共享表空间(顺序写,开销小)
  4. 将上述的脏页数据写入各个表空间文件(离散写)

三.自适应哈希索引

哈希(hash)是一种非常快的查找方法,在一般情况下这种查找的时间复杂度为o(1),即一般仅需要一次查找就能定位数据。而B+树的查找次数,取决于B+树的高度,在生产环境中,B+树的高度一般为3~4层,故需要3~4次的查询。

InnoDB会监控对表上各索引页的查询执行情况,如发现建立哈希索引可以提升速度,则建立哈希索引,这是过程不需要用户干预。

四.异步 IO

InnoDB使用异步IO操作磁盘,避免同步IO导致阻塞,也可以进行IO Merge操作,将多个IO操作合并为一个IO操作。

五.刷新邻接页

当刷新一个脏页时,InnoDB会检测该页所在区的所有页,如果是脏页,一起刷新,这是可以通过AIO将多个IO写入操作合并为一个IO操作。

考虑到下面两个问题:

  1. 是不是可能将不怎么脏的页进行了写人,而该页之后又会很快变成脏页?
  2. 固态硬盘有着较高的IOPS,是否还需要这个特性?

为此,InnoDB存储引擎从1.2.x版本开始提供了参数innodb_flush_neighbors,用来控制是否启用该特性。对于传统机械硬盘建议启用该特性,而对于固态硬盘有着超高IOPS性能的磁盘,则建议将该参数设置为0,即关闭此特性。


更多精彩文章还请访问:https://blog.csdn.net/weixin_45692705?spm=1011.2124.3001.5343

以上是关于InnoDB引擎之-两次写(Double Write)的主要内容,如果未能解决你的问题,请参考以下文章

InnoDB插入缓存,两次写,自适应hash索引

Innodb Double Write

InnoDB关键特性之double write

MySQL学习double write 介绍 (半原创)

详解MySQL存储引擎Innodb

详解MySQL存储引擎Innodb