lnnoDB是事务安全的MySQL存储引擎, 设计上采用了类似于Oracle数据库的架构。 通常来说,InnoDB存储引擎是OLTP应用中核心表的首选存储引擎 。同时, 也正是因为InnoDB的存在, 才使MySQL数据库变得更有魅力。 本章将详细介绍lnnoDB存储引擎的体系架构及其不同于其他存储引擎的特性。
2.1 InnoDB存储引擎概述
lnnoDB存储引擎特点是行锁设计、 支持MVCC、 支持外键、 提供一致性非锁定读, 同时被设计用来最有效地利用以及使用内存和CPU。
2.3 lnnoDB体系架构
图2-1简单显示了InnoDB的存储引擎的体系架构, 从图可见,lnnoDB存储引擎有多个内存块, 可以认为这些内存块组成了一个大的内存池, 负责如下工作:
1,维护所有进程/线程需要访问的多个内部数据结构。
2,缓存磁盘上的数据, 方便快速地读取, 同时在对磁盘文件的数据修改之前在这里缓存。
3,重做日志(redo log)缓冲。
后台线程的主要作用是负责刷新内存池中的数据, 保证缓冲池中的内存缓存的是最近的数据。此外将已修改的数据文件刷新到磁盘文件, 同时保证在数据库发生异常的情况下InnoDB能恢复到正常运行状态。
2.3.1 后台线程
InnoDB存储引擎是多线程的模型, 因此其后台有多个不同的后台线程, 负责处理不同的任务。
1 . Master Thread
Master Thread是一个非常核心的后台线程, 主要负责将缓冲池中的数据异步刷新到磁盘, 保证数据的一致性, 包括脏页的刷新、 合并插入缓冲(INSERT BUFFER)、 UNDO页的回收等。2.5节会详细地介绍各个版本中Master Thread的工作方式。
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的使用率以及提升存储引擎的性能。
从InnoDB 1.2版本开始,InnoDB支持多个Purge Thread, 这样做的目的是为了进一步步加快undo页的回收。同时由于Purge Thread需要离散地读取undo页, 这样也能更进一步利用磁盘的随机读取性能。
#undo log有两个作用:提供回滚和多个行版本控制(MVCC)。
在数据修改的时候,不仅记录了redo,还记录了相对应的undo,如果因为某些原因导致事务失败或回滚了,可以借助该undo进行回滚。
4. Page Cleaner Thread
Page Cleaner Thread是在InnoDB 1.2.x版本中引入的。其作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成。而其目的是为了减轻原Master Thread的工作及对于用户查询线程的阻塞, 进一步提高InnoDB存储引擎的性能。
2.3.2 内存
1. 缓冲池
lnnoDB存储引擎是基于磁盘存储的, 并将其中的记录按照页的方式进行管理。因此可将其视为基于磁盘的数据库系统(Disk-base Database)。在数据库系统中, 由于CPU速度与磁盘速度之间的鸿沟, 基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。
缓冲池简单来说就是一块内存区域, 通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。在数据库中进行读取页的操作, 首先将从磁盘读到的页存放在缓冲池中, 这个过程称为将页"FIX"在缓冲池中。下一次再读相同的页时,首先判断该页是否在 缓冲池中。若在缓冲池中, 称该页在缓冲池中被命中, 直接读取该页。否则, 读取磁盘上的页。
对于数据库中页的修改操作, 则首先修改在缓冲池中的页, 然后再以一定的频率刷新到磁盘上。这里需要注意的是, 页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为Checkpoint的机制刷新回磁盘。同样,这也是为了提高数据库的整体性能。
综上所述,缓冲池的大小直接影响指数据库的整体性能。由于32位操作系统的限制,在该系统下最多将该值设置为3G。此外用户可以打开操作系统的PAE选项来获得 32位操作系统下最大64GB内存的 支持。随着内存技术的不断成熟,其成本也在不断下降。单条8GB的内存变得非常普遍,而PC服务器已经能支持512GB的内存 。因此为了让数据库使用更多的内存,强烈建议数据库服务器都采用64位的操作系统。
对于InnoDB存储引擎而言,其缓冲池的配置通过参数innodb_ buffer _pool_ size来设置。
具体来看,缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB存储的锁信息(lockinfo)、数据字典信息(data dictionary)等。不能简单地认为,缓冲池只是缓存索引页和数据页,它们只是占缓冲池很大的一部分而已。图2-2很好地显示了InnoDB存储引擎中内存的结构情况。
从InnoDB 1.0.x版本开始,允许有多个缓冲池实例。每个页根据哈希值平均分配到不同缓冲池实例中 。这样做的好处是减少数据库内部的资源竞争,增加数据库的并发处理能力。可以通过参数innodb_ buffer _pool_ instances来进行配置, 该值默认为1。
2. LRU List、Free List和Flush List
通常来说, 数据库中的缓冲池是通过LRU (Latest Recent Used, 最近最少使用)算法来进行管理的。 即最频繁使用的页在LRU列表的 前端, 而最少使用的页在LRU列表的尾端。 当缓冲池不能存放新读取到的页时, 将首先释放LRU列表中尾端的页。
在InnoDB存储引擎中, 缓冲池中页的大小默认为16KB, 同样使用LRU算法对缓冲池进行管理。稍有不同的是InnoDB存储引擎对传统的LRU算法做了一些优化,在InnoDB的存储引擎中,LRU列表中还加入了midpoint位置。新读取到的页,虽然是最新访问的页,但并不是直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置。这个算法在 lnnoDB存储引擎下称为midpoint insertion strategy。在默认配置下, 该位置在LRU列表长度的5/8处。midpoint位置可由参数innodb _old_ blocks _pct控制
那为什么不采用朴素的LRU算法 ,直接将读取的页放入到LRU列表的首部呢?这是因为若直接将读取到的页放入到LRU的首部, 那么某些SQL操作可能会使缓冲池中的页被刷新出,从而影响缓冲池的效率。常见的这类操作为索引或数据的扫描操作。这类操作需要访问表中的许多页,甚至是全部的页,而这些页通常来说又仅在这次查询操作中需要,并不是活跃的热点数据。 如果页被放入LRU列表的首部,那么非常可能将所需要的热点数据页从 LRU列表中移除,而在 下 次需要读取该页时,InnoDB存储引擎需要再次访问磁盘。
为了解决这个问题,lnnoDB存储引擎引入了另一个参数来进一步管理LRU列表,这个参数是innodb _old_ bloks_time,用于表示页读取到Mid位置后需要等待多久才会被加入到LRU列表的热端。
LRU列表用来管理已经读取的页, 但当数据库刚启动时,LRU列表是空的, 即没有任何的页。 这时页都存放在Free列表中。 当需要从缓冲池中分页时, 首先从Free列表中查找是否有可用的空闲页, 若有则将该页从Free列表中删除, 放入到LRU列表中。否则, 根据LRU算法, 淘汰LRU列表末尾的页,将该内存空间分配给新的页。 当页从 LRU列表的old部分加人到new部分 时,称此时发生的操作为page made young, 而因为innodb_old_ blocks_ time的设置而导致页没有从old部分移动到new部分的操作称为page not made young。 可以通过命令SHOW ENGINE INNODB STATUS来观察LRU列表及Free列表的使用情况和运行状态。
从InnoDB 1.2版本开始, 还可以通过表 INNODB_BUFFER_POOL_STATS来观察缓冲池的运行状态。此外, 还可以通过表INNODB_BUFFER_PAGE_LRU来观察每个LRU列表中每个页的具体信息。
在LRU列表中的页被修改后, 称该页为脏页(dirty page) , 即缓冲池中的页和磁盘上的页的数据产生了不一致。 这时数据库会通过CHECKPOINT机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表。需要注意的是,脏页既存在于LRU列表中, 也存在于Flush列表中。LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷新回磁盘,二者互不影响。
同 LRU列表一样, Flush列表也可以通过命令SHOW ENGINE INNODB STATUS 来查看。
3. 重做日志缓冲
InnoDB 存储引擎的内存区域除了有缓冲池外, 还有重做日志缓冲 (redologbuffer)。 InnoDB 存储引擎首先将重做日志信息先放入到这个缓冲区, 然后按一定频率将其刷新到重做日志文件。 重做日志缓冲一般不需要设置得很大, 因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件, 因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。该值可由配置参数innodb_log_ buffer_ size 控制, 默认为8MB。
在通常悄况下, 8MB 的重做日志缓冲池足以满足绝大部分的应用, 因为重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中。
1.Master Thread 每一秒将重做日志缓冲刷新到重做日志文件;
2.每个事务提交时会将重做日志缓冲刷新到重做日志文件。
3.当重做日志缓冲池剩余空间小于1/2 时, 重做日志缓冲刷新到重做日志文件。
4. 额外的内存池
额外的内存池通常被DBA 忽略, 他们认为该值并不十分重要, 事实恰恰相反, 该值同样十分重要。在InnoDB 存储引擎中, 对内存的管理是通过一种称为内存堆Cheap)的方式进行的。在对一些数据结构本身的内存进行分配时, 需要从额外的内存池中进行申请, 当该区域的内存不够时, 会从缓冲池中进行申请。例如, 分配了缓冲池(innodb_buffer_ pool), 但是每个缓冲池中的帧缓冲 (framebuffer)还有对应的缓冲控制对象(buffer control block), 这些对象记录了一些诸如LRU、锁、等待等信息, 而这个对象的内存需要从额外内存池中申请。因此, 在申请了很大的InnoDB 缓冲池时, 也应考虑相应地增加这个值。
2.4 Checkpoint 技术
前面已经讲到了, 缓冲池的设计目的为了协调CPU速度与磁盘速度的鸿沟。 因此页的操作首先都是在缓冲池中完成的。 如果一条DML语句, 如Update或Deiete改变了页 中的记录, 那么此时页是脏的, 即缓冲池中的页的版本要比磁盘的新。 数据库需要将新版本的页从缓冲池刷新到磁盘。
倘若每次一个页发生变化, 就将新页的版本刷新到磁盘, 那么这个开销是非常大的。 若热点数据集中在某几个页中, 那么数据库的性能将变得非常差。 同时, 如果在从缓冲池将页的新版本刷新到磁盘时发生了宥机, 那么数据就不能恢复了。 为了避免发生数据丢失的问题, 当前事务数据库系统普遍都采用了Write Ahead Log策略, 即当事务提交时, 先写重做日志, 再修改页。 当由于发生岩机而导致数据丢失时, 通过重做日志来完成数据的恢复。 这也是事务 ACID中D (Durability持久性)的要求。
Checkpoint(检查点)技术的目的是解决以下几个问题:
1.缩短数据库的恢复时间;
2.缓冲池不够用时,将脏页刷新到磁盘;
3.重做日志不可用时,刷新脏页。
当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。故数据库只需对Checkpoint后的重做日志进行恢复。这样就大大缩短了恢复的时间。
此外,当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷回磁盘。
重做日志出现不可用的情况是因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大的,这从成本及管理上都是比较困难的。重做日志可以被重用的部分是指这些重做日志已经不再需要,即当数据库发生宕机时,数据库恢复操作不需要这部分的重做日志,因此这部分就可以被覆盖重用。若此时重做日志还需要使用,那么必须强制产生Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。
对于lnnoDB存储引擎而言,其是通过LSN(Log Sequence Number)来标记版本的。而LSN是8字节的数字,其单位是字节。每个页有LSN,重做日志中也有LSN, Checkpoint也有LSN。可以通过命令SHOWENGINE INNODB STATUS来观察。
Checkpoint所做的事情无外乎是将缓冲池中的脏页刷回到磁盘。不同之处在于每次刷新多少页到磁盘,每次从哪里取脏页,以及什么时间触发Checkpoint。在InnoDB存储 引擎内部,有两种Checkpoint,分别为:
1.Sharp Checkpoint
2. Fuzzy Checkpoint
Sharp Checkpoint发生在数据库关闭时将所有的脏页都刷新回磁盘,这是默认的工作方式,即参数innodb_fast_ shutdown=1。
是若数据库在运行时也使用 SharpCheckpoint, 那么数据库的可用性就会受到很大的影响。故在InnoDB存储引擎内部使用FuzzyCheckpoint进行页的刷新,即只刷新一 部分脏页,而不是刷新所有的脏页回磁盘。在InnoDB存储引擎中可能发生如下几种情况的Fuzzy Checkpoint:
1 Master Thread Checkpoint
2 FLUSH_LRU_LIST Checkpoint
3 Async/Sync Flush Checkpoint
4 Dirty Page too much Checkpoint
对于Master Thread (2.5 节会详细介绍各个版本中Master Thread的实现)中发生的Checkpoint, 差不多以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回 磁盘。这个过程是异步的,即此时InnoDB存储引擎可以进行其他的操作,用户查询线程不会阻塞。
FLUSH_ LRU _LIST Checkpoint是因为InnoDB存储引擎需要保证LRU列表中需要有差不多100个空闲页可供使用。在InnoDB1.1.x版本之前,需要检查LRU列表中是否有足够的可用空间操作发生在用户查询线程中,显然这会阻塞用户的查询操作。倘若没有100个可用空闲页,那么InnoDB存储引擎会将 LRU列表尾端的页移除。如果这些页中有脏页,那么需要进行Checkpoint, 而这些页是来自LRU列表的,因此称为FLUSH_ LRU _LIST Checkpoint。
而从MySQL 5.6版本,也就是lnnoDB1.2.x版本开始,这个检查被放在了一个单独的PageCleaner线程中进行,并且用户可以通过参数innodb _ lru _scan_ depth控制LRU列表中可用页的数量,该值默认为1024。
Async/Sync Flush Checkpoint指的是重做日志文件不可用的情况, 这时需要强制将一些页刷新回磁盘, 而此时脏页是从脏页列表中选取的 。Async/SyncFlush Checkpoint是为了保证重做日志的循环使用的可用性。 从lnnoDB1.2.x版本开始 也就是MySQL 5.6版本, 这部分的刷新操作同样放入到了单独的PageCleaner线程中进行。
最后一种Checkpoint的情况是Dirty Page too much, 即脏页的数量太多, 导致lnnoDB存储引擎强制进行Checkpoint。 其目的总的来说还是为了保证缓冲池中有足够可用的页 。 其可由参数innodb_max_ dirty _pages _pct控制。
innodb _max_ dirty _pages _pct值为75表示, 当缓冲池中脏页的数量占据75%时,强制进行Checkpoint, 刷新一部分的脏页到磁盘。 在lnnoDB 1.0.x版本之前, 该参数默认值为90, 之后的版本都为75。
2.5 Master Thread 工作方式
在2.3节中我们知道了,InnoDB存储引擎的主要工作都是在一个单独的后台线程Master Thread中完成的,这一节将具体解释该线程的具体实现及该线程可能存在的问题。
2.5.1 lnnoDB 1.0.x版本之前的Master Thread
Master Thread具有最高的线程优先级别。 其内部由多个循环(loop)组成: 主循环(loop)、 后台循环(backgroup loop)、 刷新循环(flush loop)、 暂停循环(suspend loop)。 Master Thread会根据数据库运行的状态在loop、 background loop、 flush loop和suspendloop中进行切换。
Loop被称为主循环,因为大多数的操作是在这个循环中,其中有两大部分的操作-每秒钟的操作和每 10秒的操作。
可以看到,loop循环通过thread sleep来实现,这意味着所谓的每秒一次或每10秒一次的操作是不精确的。在负载很大的情况下可能会有延迟(delay), 只能说大概在这个频率下。当然,InnoDB源代码 中还通过了其他的方法来尽量保证这个频率每秒一次的操作包括:
1.日志缓冲刷新到磁盘,即使这个事务还没有提交(总是);
2.合并插入缓冲(可能);
3.至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能);
4.如果当前没有用户活动,则切换到backgro und loop (可能)。
即使某个事务还没有提交,InnoDB存储引擎仍然每秒会将重做日志缓冲中的内容刷新到重做日志文件。这一点是必须要知道的,因为这可以很好地解释为什么再大的事务提交(commit)的时间也是很短的。
合并插入缓冲(InsertBuffer)并不是每秒都会发生的。InnoDB存储引擎会判断当前一秒内发生的IO次数是否小于5次,如果小于5次,InnoDB认为当前的IO压力很小,可以执行合并插入缓冲的操作。
同样, 刷新100个脏页也不是每秒都会发生的。InnoDB存储引擎通过判断当前缓冲池中脏页的比例(buf_get_ modified_ ratio _pct)是否超过了配置文件中innodb _max_ dirty _pages _pct 这个参数(默认为90, 代表90%), 如果超过了这个阔值,InnoDB存储引擎认为需要做磁盘同步的操作, 将100个脏页写人磁盘中。
接着来看每10秒的操作, 包括如下内容:
1.刷新100个脏页到磁盘(可能的情况下);
2. 合并至多5个插入缓冲(总是);
3. 将日志缓冲刷新到磁盘(总是);
4.删除无用的Undo页(总是);
5. 刷新100个或者10个脏页到磁盘(总是)。
在以上的过程中,InnoDB存储引擎会先判断过去10秒之内磁盘的IO操作是否小于200次, 如果是,InnoDB存储引擎认为当前有足够的磁盘IO操作能力, 因此将100个脏页刷新到磁盘。 接着,InnoDB存储引擎会合并插入缓冲。 不同于每秒一次操作时可能发生的合并插人缓冲操作, 这次的合并插人缓冲操作总会在这个阶段进行。 之后, InnoDB存储引擎会再进行一次将日志缓冲刷新到磁盘的操作。 这和每秒一次时发生的操作是一样的。
接着InnoDB存储 引擎会进行 一步执行full purge操 作, 即删除 无用的Undo 页。 对表进行update、 delete这类操作时, 原先的行被标记为删除, 但是因为一致性读(consistentread)的关系, 需要保留这些行版本的信息。 但是在full purge过程中, InnoDB存储引擎会判断当前事务系统中已被删除的行是否可以删除, 比如有时候可能还有查询操作需要读取之前版本的undo信息, 如果可以删除,lnnoDB会立即将其删除。
从源代码中可以发现,InnoDB存储引擎在执行fullpurge操作时,每次最多尝试回收6个undo页。
然后,InnoDB存储引擎会判断缓冲池中脏页的比例Cbuf_get_modified_ratio_pct),如果有超过70%的脏页,则刷新100个脏页到磁盘,如果脏页的比例小于70%,则只需刷新10%的脏页到磁盘。
接着来看backgroundloop, 若当前没有用户活动(数据库空闲时)或者数据库关闭(shutdown), 就会切换到这个循环。backgroundloop会执行以下操作:
1 删除无用的Undo页(总是);
2 合并20个插人缓冲(总是);
3 跳回到主循环(总是);
4.不断刷新100个页直到符合条件(可能,跳转到flushloop中完成)。
若flushloop中也没有什么事情可以做了,InnoDB存储引擎会切换到suspendloop, 将MasterThread挂起,等待事件的发生。若用户启用(enable)了InnoDB存储 引擎,却没有使用任何InnoDB存储引擎的表,那么MasterThread总是处于挂起的状态。
2.5.2 lnnoDB1 .2.x版本之前的Master Thread
无论何时,InnoDB存储引擎最大只会刷新100个脏页到磁盘, 合并20个插入缓冲。 如果是在写入密集的应用 程序中,每秒可能会产生大于100个的脏页 ,如果是产生大于20个插入缓冲的情况,Master Thread似乎会 “ 忙不过来"'或者说它总是做得很慢。即使磁盘能 在1秒内处理多于100个页的写入和20个插入缓冲的合并,但是由于hard coding, Master Thread也只会选择刷新100个脏页 和合并20个插入缓冲。同时,当发生右机需要恢复时,由于很多数据还没有刷新回磁盘,会导致恢复的时间可能需要很久,尤其是对于insert buff er来说。
InnoDB存储引擎的开发团队参考了Google的patch, 提 供了类似的方法来修正该问题。因此lnnoDB Plugin (从lnnoDB1.0.x版本开始)提供了参数innodb_ io _capacity, 用来表示磁盘IO的吞吐量, 默认值 为200。 对于刷新到磁盘页的数量,会按照innodb_io _ capacity 的百分比来进行控制。 规则如下:
1.在合并插入缓冲时, 合并插入缓冲的数世为innodb_ io _ capacity值的5%;
2.在从缓冲区刷新脏页时,刷新脏页的数最为innodb_ io _ capacity。
另一个问题是,参数innodb_max_ dirty pages _pct 默认值的问题, 在InnoDB 1.0. x版本之前, 该值的默认为90, 意味着脏页占缓冲池的90%。但是该值“ 太大” 了,因为InnoDB存储引擎在每秒刷新缓冲池和flush loop时会判断这个值,如果该值大于innodb _max_ dirty _pages _pct, 才刷新100个脏页,如果有很大的内存, 或者数据库服务器的压力很大,这时刷新脏页的速度反而会降低。 同样, 在数据库的恢复阶段可能需要更多的时间。
而从InnoDB1.0.x版本开始,innodb_max_ dirty _pages _pct默认值变为了75, 和Google测试的80比较接近。 这样既可以加快刷新脏页的频率, 又能保证了磁盘IO的负载。
InnoDB 1.0.x版本带来的另一个参数是 innodb_adaptive_ flushing (自适应地刷新), 该值影响每秒刷新脏页的数批。 原来的刷新规则是: 脏页在缓冲池所占的比例小于 innodb _max_ dirty _pages _pct时, 不刷新脏页; 大于innodb_max_ dirty _pages _pct时, 刷新100个脏页。 随着innodb_adaptive_ flushing参数的引入,InnoDB存储引擎会通过一个名为buf_ flush _get_ desired_ flush _rate的函数来判断需要刷新脏页最合适的数量。 粗略地翻阅源代码后发现buf_ flush _get_ desired_ flush _rate通过判断产生重做日志Credo log)的速度来决定最合适的刷新脏页数拯。 因此, 当脏页的比例小于innodb_max_ dirty _pages_ pct时, 也会刷新一定量的脏页。
还有一个改变是: 之前每次进行full purge操作时,最多回收20个Undo页, 从 InnoDB 1.0.x版本开始引入了参数 innodb_purge_ batch_ size, 该参数可以控制每次full purge回收的Undo页的数妞。 该参数的默认值为20, 并可以动态地对其进行修改。
2.5.3 lnnoDB1 .2.x版本的Master Thread
在InnoDB1.2.x版本中再次对MasterThread进行了优化,由此也可以看出MasterThread对性能所起到的关键作用。对于刷新脏页的操作,从MasterThread 线程分离到一个单独的PageCleaner Thread, 从而减轻了MasterThread的工作,同时进一步提高了系统的并发性。
2.6 lnnoDB关键特性
InnoDB存储引擎的关键特性包括:
1.插入缓冲(InsertBuffer)
2.两次写(DoubleWrite)
3.自适应哈希索引(AdaptiveHash Index)
4.异步IO(Async IO)
5.刷新邻接页(FlushNeighbor Page)
2.6.1 插入缓冲
1. Insert Buffer
Insert Buffer 可能是 InnoDB 存储引擎关键特性中最令人激动与兴奋的一个功能。 不过这个名字可能会让人认为插入缓冲是缓冲池中的一个组成部分。 其实不然, lnnoDB 缓 冲池中有 Insert Buffer 信息固然不错, 但是 Insert Buffer 和数据页一样, 也是物理页的一个组成部分。
在 InnoDB 存储引擎中, 主键是行唯一的标识符。 通常应用程序中行记录的插入顺序是按照主键递增的顺序进行。因此,插入聚集索引一般是顺序的,不需要磁盘的随机读取。
但是不可能每张表上只有一个聚集索引, 更多情况下, 一张表上有多个非聚集的辅助索引 (secondary index)。 比如, 用户需要按照 b 这个字段进行查找, 并且 b 这个字段不是唯一的, 即表是按如下的 SQL 语句定义的:
在这样的情况下产生了了一个非聚集的且不是唯一的索引。在进行插入操作时, 数据页的存放还是按主键a进行顺序存放的,但是对于非聚集索引叶子节点的插入不再是有序的了,这时就需要离散地访问非聚集索引页,由于随机读取的存在而导致了插入操作性能下降。当然这并不是索引这个B字段上索引的错误,而是因为B+树的特性决定了非聚集索引插入的离散性。
需要注意的是,在某些情况下,辅助索引的插人依然是顺序的,或者说是比较顺序的,比如用户购买表中的时间字段。在通常情况下,用户购买时间是一个辅助索引,用来根据时间条件 进行查询。但是在插入时根据时间的递增而插入的,因此插入也是“较为”顺序的。
InnoDB 存储引擎开创性地设计了 Insert Buffer, 对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个 Insert Buffer 对象中,好似欺骗数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另 一个位置。然后再以一定的频率和悄况进行Insert Buffer 和辅助索引页子节点的merge (合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。
然而Insert Buffer 的使用需要同时满足以下两个条件:
1.索引是辅助索引(secondary index);
2.索引不是唯一(unique) 的。
当满足以上两个条件时,InnoDB 存储引擎会使用Insert Buffer, 这样就能提高插入操作的性能了。不过考虑这样一种情况:应用程序进行大量的插入操作,这些都涉及了不唯一的非聚集索引,也就是使用了Insert Buffer。若此时MySQL 数据库发生了宕机,这时势必有大掀的Insert Buffer 并没有合并到实际的非聚集索引中去。因此这时恢复可能需要很长的时间,在极端情况下甚至需要几个小时。
辅助索引不能是唯一的,因为在插入缓冲时,数据库并不去查找索引页来判断插入的记录的唯一性。如果去查找肯定又会有离散读取的情况发生,从而导致Insert Buffer失去了意义。
用户可以通过命令SHOW ENGINE INNODB STATUS 来查看插入缓冲的信息
2. Change Buffer
InnoDB从1.0.x版本开始引入了 Change Buffer, 可将其视为Insert Buffer的升级。从这个版本开始,lnnoDB存储引擎可以对DML操作——INSERT、DELETE、UPDATE都进行缓冲,他们分别是: Insert Buffer、Delete Buffer、Purge buffer。
当然和之前Insert Buffer 一样,Change Buffer适用的对象依然是非唯一的辅助索引。
对一条记录进行 UPDATE操作可能分为两个过程:
1.将记录标记为己删除;
2.真正将记录删除。
因此Delete Buffer对应UPDATE操作的第一个过程,即将记录标记为删除。PurgeBuffer对应UPDATE操作的第二个过程,即将记录真正的 删除。同时,lnnoDB存储引 擎提供了参数innodb_change_ buffering, 用来开启各种Buffer的选项。该参数可选的值为: inserts、deletes、purges、changes、all、none。inserts、deletes、purges就是前面讨论过的三种情况。changes表示启用 inserts和deletes, all表示启用所有,none表示都不启用 。该参数默认值为all。
从InnoDB 1.2.x版本开始,可以通过参数innodb_change_ buff er_ max_ size来控制 Change Buffer最大使用内存的数量。
innodb _change_ buffer_ max_ size值 默认为25, 表示最多使用1/4的缓冲池内存空间。而需要注意的是,该参数的最大有效值为50。
3. Insert Buffer 的内部实现
通过前一个小节读者应该已经知道了 Insert Buffer 的使用场景, 即非唯一辅助索引的插入操作。 但是对于 Insert Buffer 具体是什么, 以及内部怎么实现可能依然模糊, 这 正是本节所要阐述的内容。
可能令绝大部分用户感到吃惊的是, Insert Buffer 的数据结构是一棵 B+ 树。 在MySQL 4.1 之前的版本中每张表有一棵 InsertBuffer B+ 树。 而在现在的版本中, 全局只有一棵 Insert Buffer B+ 树, 负责对所有的表的辅助索引进行 Insert Buffer。 而这棵 B+ 树存放在共享表空间中, 默认也就是 ibdatal 中。 因此, 试图通过独立表空间 ibd 文件恢复表中数据时, 往往会导致 CHECK TABLE 失败。 这是因为表的辅助索引中的数据可能还在 Insert Buffer 中, 也就是共享表空间中, 所以通过 ibd 文件进行恢复后, 还需要进行REPAIR TABLE操作来重建表上所有的辅助索引。
Insert Buffer是一棵 B+ 树, 因此其也由叶节点和非叶节点组成。 非叶节点存放的是查询的 search key (键值), 其构造如图 2-3 所示。
search key 一共占用9 个字节, 其中 space 表示待插入记录所在表的表空间 id, 在 lnnoDB 存储引擎中, 每个表有一个唯一的 space id, 可以通过 space id 查询得知是哪张表。 space 占用 4 字节。 marker 占用1 字节, 它是用来兼容老版本的 Insert Buffer。 offset 表示页所在的偏移抵, 占用4 字节。
当一个辅助索引要插入到页(space,offset)时,如果这个页不在缓存池中,那lnnoDB存储引擎首先根据上述规则构造一个searchkey, 接下来查询InsertBuffer这棵B+树,然后再将这条记录插人到InsertBuffer B+树的叶子节点中。
对于插入到 Insert Buffer B+ 树叶子节点的记录(如图 2-4 所示),并不是直接将待插入的记录插入,而是需要根据如下的规则进行构造:
space、 marker、 page_no 字段和之前非叶节点中的含义相同,一共占用9字节。第4 个字段 metadata 占用4 字节,其存储的内容如表 2-2 所示。
IBUF _REC_OFFSET_COUNT是保存两个字节的整数,用来排序每个记录进入 Insert Buffer的顺序。因为从InnoDBl.0.x开始支持ChangeBuffer, 所以这个值同样记录进入InsertBuffer的顺序。通过这个顺序回放(replay)才能得到记录的正确值。
从InsertBuffer叶子节点的第5列开始,就是实际插入记录的各个字段了。因此较之原插入记录,InsertBuffer B+树的叶子节点记录需要额外13字节的开销。
因为启用 Insert Buffer 索引后,辅助索引页C space, page_ no) 中的记录可能被插入到 Insert Buffer B+ 树中,所以为了保证每次 Merge Insert Buffer 页必须成功,还需要有一个特殊的页用来标记每个辅助索引页的可用空间。这个页的类型为InsertBufferBitmap.
每个InsertBuffer Bitmap页用来追踪16384个辅助索引页,也就是256个区 (Extent)。每个InsertBuffer Bitmap页都在16384个页的第二个页中。关于InsertBuff er Bitmap页的作用会在下一小节中详细介绍。
每个辅助索引页在InsertBuffer Bitmap页中占用4位(bit),由表2-3中的三个部分组成。
4. Merge Insert Buffer
通过前面的小节读者应该已经知道了Insert/Change Buffer是一棵B+树。 若需要实现插入记录的辅助索引页不在缓冲池中, 那么需要将辅助索引记录首先插入到这棵B+ 树中。 但是Insert Buffer中的记录何时合并(merge)到真正的辅助索引中呢?这是本小节需要关注的重点。
概括地说, MergeInsert Buffer的操作可能发生在以下几种情况下:
1 辅助索引页被读取到缓冲池时:
2 Insert Buffer Bitmap页追踪到该辅助索引页已无可用空间时;
3 Master Thread。
第一种情况为当辅助索引页被读取到缓冲池中时, 例如这在执行正常的SELECT查询操作, 这时需要检查InsertBuffer Bitmap页, 然后确认该辅助索引页是否有记录存放于InsertBuffer B+树中。 若有, 则将Insert Buffer B+树中该页的记录插入到该辅助索引 页中。 可以看到对该页多次的记录操作通过一次操作合并到了原有的辅助索引页中, 因此性能会有大幅提高。
Insert Buffer Bitmap页用来追踪每个辅助索引页的可用空间, 并至少有1/32页的空间。 若插入辅助索引记录时检测到插人记录后可用空间会小于1/32页, 则会强制进行一个合并操作, 即强制读取辅助索引页, 将Insert Buffer B+树中该页的记录及待插入的记录插人到辅助索引页中。 这就是上述所说的第二种情况。
还有一种情况, 之前在分析Master Thread 时曾讲到, 在Master Thread线程中每秒或每10秒会进行一次MergeInsert Buffer的操作, 不同之处在千每次进行merge操作的页的数量不同。
在MasterThread中,执行merge操作的不止是一个页,而是根据srv_innodb_io_capacity的百分比来决定真正要合并多少个附注索引页。但InnoDB存储引擎又是根据怎样的算法来得知需要合并的辅助索引页呢?
在Insert Buffer B+树中, 辅助索引页根据(space, offset)都已排序好, 故可以根据(space, offset) 的排序顺序进行页的选择。 然而, 对于Insert Buffer页的选择, lnnoDB存储引擎并非采用这个方式, 它随机地选择Insert Buffer B+树的一个页, 读取该页中的 space及之后所需要数扯的页。 该算法在复杂情况下应有更好的公平性。 同时, 若进行merge时, 要进行merge的表已经被删除, 此时可以直接丢弃已经被Insert/Change Buffer的数据记录。
2.6.2 两次写
如果说Insert Buffer带给lnnoDB存储引擎的是性能上的提升, 那么doublewrite(两 次写)带给InnoDB存储引擎的是数据页的可靠性。
当发生数据库宕机时, 可能lnnoDB存储引擎正在写入某个页到表中, 而这个页只写了一部分, 比如16KB 的页, 只写了前4KB, 之后就发生了岩机, 这种情况被称为部分写失效(partial page write)。 在InnoDB存储引擎未使用doublewrite技术前, 曾经出现过因为部分写失效而导致数据丢失的情况。
doublewrite由两部分组成, 一部分是内存中的doublewrite buffer, 大小为2MB , 另一部分是物理磁盘上共享表空间中连续的128个页, 即2个区(extent) , 大小同样为 2MB。 在对缓冲池的脏页进行刷新时, 并不直接写磁盘, 而是会通过 memcpy函数将脏页先复制到内存中的doublewritebuffer, 之后通过doublewrite buffer再分两次, 每次 1MB 顺序地写入共享表空间的物理磁盘上, 然后马上调用fsync函数, 同步磁盘, 避免缓冲写带来的问题。 在这个过程中, 因为doublewrite页是连续的, 因此这个过程是顺序写的,开销并不是很大。在完成 doublewrite 页的写入后,再将 doublewrite buffer 中的页写入各个表空间文件中,此时的写入则是离散的。可以通过以下命令观察到 doublewrite 运行的情况
2.6.3 自适应哈希索引
哈希(hash) 是一种非常快的查找方法, 在一般情况下这种查找的时间复杂度为O(1), 即一般仅需要一次查找就能定位数据。而B+树的查找次数, 取决于B+树的高度, 在生产环境中, B+树的高度一般为3-4层, 故需要3-4次的查询。
InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升, 则建立哈希索引, 称之为自适应哈希索引(A daptiveH ash Index, AHi)。AHI是通过缓冲池的B+树页构造而来, 因此建立的速度很快, 而且不需要对整张表构建哈希索引。InnoDB存储引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引。
AHI有一个要求, 即对这个页的连续访问模式必须是一样的。例如对于(a, b) 这样的联合索引页,其访问模式可以是以下情况:
WHERE a=xxx
WHERE a=xxx and b=xxx
访问模式一样指的是查询的条件一样,若交替进行上述两种查询,那么InonDB存 储引擎不会对该页构造AHi。此外AHi还有如下的要求:
1.以该模式访问了100次
2.页通过该模式访问了N次,其中N=页中记录*1/16
根据lnnoDB存储引擎官方的文档显示,启用AHi后,读取和写入速度可以提高2倍,辅助索引的连接操作性能可以提高5倍。毫无疑问,AHi是非常好的优化模式,其设计思想是数据库自优化的(self-tuning),即无需OBA对数据库进行人为调整。通过命令SHOWENGINE INNODB STATUS可以看到当前AHi的使用状况。
2.6.4 异步IO
为了提高磁盘操作性能, 当前的数据库 系统都采用异步IO (Asynchronous IO, AIO) 的方式来处理磁盘操作。lnnoDB存储引擎亦是如此 。
与AIO对应的是 Sync IO, 即每进行一次IO操作, 需要等待此次操作结束才能继续接下来的操作。 但是如果用户 发出的是一条索引扫描的查询, 那么这条SQL 查询语句可能需要扫描多个索引页 ,也就是需要进行多次的IO操作。 在每扫描一个页并等待其完成后再进行下一次的扫描 ,这是没有必要的 。用户可以在 发出一个IO请求后立即再 发出另一个IO请求, 当全部IO请求发送完毕后, 等待所有IO操作的完成, 这就是AIO。
AIO的另一个优势是可以进行IO Merge操作, 也就是将多个IO合并为1个IO, 这样可以提高IOPS的性能。例如用户需要访问页的(space, page_ no)为:(8, 6)、(8, 7), (8, 8)每个页的大小为 16KB, 那么同步IO需要进行3次IO操作。 而AIO会判断到这三个页是连续的(显然可以通过(space, page_ no)得知)。 因此AIO底层会发送一个IO请求, 从(8, 6) 开始 , 读取48KB的页 。
2.6.5 刷新邻接页
InnoDB存储引擎还提供了Flush Neighbor Page (刷新邻接页)的特性。其工作原理为:当刷新一个脏页时,InnoDB存储引擎会检测该页所在区( extent)的所有页,如果是脏页,那么一起进行刷新。这样做的好处显而易见,通过AIO可以将多个IO写入操作合并为一个IO操作,故该工作机制在传统机械磁盘下有着显著的优势。但是需要考 虑到下面两个问题:
1.是不是可能将不怎么脏的页进行 了写入,而 该页之后又会很快变成脏页?
2.固态硬盘有着较高的IOPS, 是否还需要这个特性?
2.7 启动、 关闭与恢复
InnoDB是MySQL数据库的存储引擎之一,因此InnoDB存储引擎的启动和关闭, 更准确的是指在MySQL实例的启动过程中对lnnoDB存储引擎的处理过程。
在关闭时,参数innodb_ fast_ shutdown影响着表的存储引擎为lnnoDB的4行为,该参数可取值为0、1、2,默认值为1。
1.0表示在MySQL数据库关闭时,InnoDB需要完成所有的fullpurge和mergeinsert buffer, 并且将所有的脏页刷新回磁盘。这需要一些时间,有时甚至需要几个小时来完成。如果在进行InnoDB升级时,必须将这个参数调为0,然后再关闭数据库。
2.1是参数innodb_fast_ shutdown的默认值,表示不需要完成上述的fullpurge和merge insert buff er操作,但是在缓冲池中的一些数据脏页还是会刷新回磁盘。
3.2表示不完成fullpurge和mergeinsert buffer操作,也不将缓冲池中的数据脏页写回磁盘,而是将日志都写入日志文件。这样不会有任何事务的丢失,但是下次MySQL数据库启动时,会进行恢复操作(recovery)。
当正常关闭MySQL数据库时,下次的启动应该会非常“正常”。但是如果没有正常地关闭数据库,如用kill命令关闭数据库,在MySQL数据库运行中重启了服务器,或者在关闭数据库时,将参数innodb_ fast_ shutdown设为了2时,下次MySQL数据库启动时都会对InnoDB存储引擎的表进行恢复操作。
参数innodb_force_ recovery影响了整个InnoDB存储引擎恢复的状况。该参数值默认为0,代表当发生需要恢复时,进行所有的恢复操作,当不能进行有效恢复时,如数据页发生了corruption,MySQL数据库可能发生宥机(crash),并把错误写入错误日志中去。
但是,在某些情况下,可能并不需要进行完整的恢复操作,因为用户自已知道怎么进行恢复。比如在对一个表进行altertable操作时发生意外了,数据库重启时会对 InnoDB表进行回滚操作,对于一个大表来说这需要很长时间,可能是几个小时。这时用户可以自行进行恢复,如可以把表删除,从备份中重新导入数据到表,可能这些操作的速度要远远快千回滚操作。
参数innodb_ force _recovery还可以设置为6个非零值:1~6。大的数字表示包含了 前面所有小数字表示的影响。具体情况如下:
1.(SRV _FORCE_IGNORE_CORRUPT): 忽略检查到的corrupt页。
2. 2(SRV _FORCE_NO _BACKGROUND): 阻止MasterThread线程的运行,如MasterThread线程需要进行fullpurge操作,而这会导致crash。
3. 3(SRV _FORCE_NO_TRX_UNDO): 不进行事务的回滚操作。
4.(SRV FORCE_NO_IBUF MERGE):不进行插入缓冲的合并操作。
5.5(SRV FORCE_NO_UNDO_LOG_SCAN): 不查看撤销日志(Undo Log), lnnoDB存储引擎会将未提交的事务视为已提交。
6.6(SRV FORCE_NO_LOG_REDO): 不进行前滚的操作。
需要注意的是, 在设置了参数innodb_force_ recovery 大于0 后, 用户可以对表进行select 、create 和drop 操作, 但insert 、update 和delete 这类DML 操作是不允许的。
2.8 小结
本章对 InnoDB 存储引擎及其体系结构进行了概述, 先给出了 InnoDB 存储引擎的历史、 InnoDB 存储引擎的体系结构(包括后台线程和内存结构) ; 之后又详细介绍了 InnoDB 存储引擎的关键特性, 这些特性使 InnoDB 存储引擎变得更具 “魅力 ”; 最后介绍了启动和关闭 MySQL 时一些配置文件参数对 InnoDB 存储引擎的影响。
通过本章的铺垫, 读者在学习后面的内容时就会对 InnoDB 引擎理解得更深入和更全面。 第 3 章开始介绍 MySQL 的文件, 包括 MyS'QL 本身的文件和与 InnoDB 存储引擎本身有关的文件。 之后本书将介绍基千 InnoDB 存储引擎的表, 并揭示内部的存储构造。