InnoDB是事务安全的MySQL存储引擎,设计上采用了类似于Oracle数据库的架构。通常来说,InnoDB 存储引擎是OLTP应用中核心表的首选存储引擎。同时,也正是因为InnoDB的存在,才使MySQL数据库变得更有魅力。
InnoDB存储引擎最早由InnobaseOy公司开发,被包括在MySQL数据库所有的二进制发行版本中,从MySQL 5.5版本开始是默认的表存储引擎(之前的版本InnoDB
存储引擎仅在Windows下为默认的存储引擎)。该存储引擎是第一个完整支持ACID事务的MySQL存储引擎(BDB是第一个支持事务的MySQL存储引擎,现在已经停止开发),其特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读,同时被设计用来最有效地利用以及使用内存和CPU。
InnoDB存储引擎被包含于所有MySQL数据库的二进制发行版本中。早期其版本随着MySQL数据库的更新而更新。从MySQL 5.1版本时,MySQL 数据库允许存储引擎开发商以动态方式加载引擎,这样存储引擎的更新可以不受MySQL数据库版本的限制。
版本 | 功能 |
---|---|
老版本InnoDB | 支持ACID、行锁设计、MVCC |
InnoDB 1.0.x | 继承了上述版本所有功能,增加了compress和dynamic页格式 |
InnoDB 1.1.x | 继承了上述版本所有功能,增加了Linux AIO,多回滚段 |
InnoDB 1.2.x | 继承了上述版本所有功能,增加了全文索引支持、在线索引添加 |
之前很多人使用的MySQL数据库还是停留在MySQL5.1版本,并使用InnoDB Plugin。很多DBA错误地认为InnoDB Plugin 和InnoDB 1.1 版本之间是没有区
别的。但从上表中还是可以发现,虽然都增加了对于compress和dynamic页的支持,但是InnoDB Plugin 是不支持Linux Native AIO功能的。此外,由于不支持多回滚段,InnoDB Plugin支持的最大支持并发事务数量也被限制在1023。而且随着MySQL 5.5版本的发布,InnoDB Plugin也变成了一个历史产品.
InnoDB存储引擎的架构。下图简单显示了InnoDB的存储引擎的体系架构,从图可见,InnoDB 存储引擎有多个内存块,可以认为这些内存块组成了一个大的内存池,负责如下工作: .
后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。此外将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下InnoDB能恢复到正常运行状态。
InnoDB存储引擎是多线程的模型,因此其后台有多个不同的后台线程,负责处理不同的任务。
(1)Master Thread
MasterThread是一个非常核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插人缓冲(INSERT BUFFER)、UNDO页的回收、redo log的刷盘 等。后面会详细地介绍各个版本中Master Thread的工作方式。
(2)IO Thread
在InnoDB存储引擎中大量使用了AIO (Async IO)来处理写IO请求,这样可以极大提高数据库的性能。而IOThread的工作主要是负责这些IO请求的回调(Callback)
处理。
InoDB 1.0 版本之前共有4个IO Thread,分别是write、read、 insert buffer和log IO thread。在Linux平台下,IO Thread的数量不能进行调整,但是在Windows平台下可以通过参数innodb_ fle_ io_threads 来增大IO Thread。
从InnoDB 1.0.x 版本开始,read thread和write thread分别增大到了4个,并且不再使用innodb_ file_ io_threads参数,而是分别使用innodb_ read_io_ threads 和 innodb_write_ io_ threads 参数进行设置,如:
可以通过命令show engine innodb status
来观察InnoDB中的IO Thread:
可以看到IO Thread 0为insert buffer thread,IO Thread 1为log thread。之后就是根据参数innodb_read_ io_threads 及innodb_write_ io_threads 来设置的读写线程,并且读线程的ID总是小于写线程。
(3)Purge Thread
事务被提交后,其所使用的undolog可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页。在InnoDB 1.1版本之前,purge 操作仅在InnoDB存储引擎的Master Thread中完成。而从InnoDB 1.1 版本开始,purge 操作可以独立到单独的线程中进行,以此来减轻Master Thread的工作,从而提高CPU的使用率以及提升存储引擎的性能。用户可以在MySQL数据库的配置文件中添加如下命令来启用独立的Purge:
Thread:
[mysq1d]
innodb_ purge_ threads=1
在InnoDB 1.1 版本中,即使将innodb purge threads 设为大于1, InnoDB 存储引擎启动时也会将其设为1,并在错误文件中出现如下类似的提示:
120529 22:54:16 (Warning] option 'innodb-purge- threads': unsigned value 4 adjusted to 1
从InnoDB 1.2版本开始,InnoDB 支持多个Purge Thread,这样做的目的是为了进步加快undo页的回收。同吋由于PurgeThread需要离散地读取undo页,这祥也能更迸一步利用磁盘的随机湊取性能。如用户可以设置Purge Thread:
(4)Page Cleaner Thread
Page Cleaner Thread是在InnoDB 1.2.x 版本中引人的。其作用是将之前版本中脏页的刷新操作都放人到单独的线程中来完成。而其目的是为了减轻原Master Thread的工作及对于用户查询线程的阻塞,迸一步提高InnoDB存偖引擎的性能。
InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可将其视为基于磁盘的数据库系统(Disk -base Database)。在数据库系统中,由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。
缓冲池简单来说就是一块内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。在数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓冲池中,这个过程称为将页“FIX"在缓冲池中。下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。
对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。这里需要注意的是,页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为Checkpoint的机制刷新回磁盘。同样,这也是为了提高数据库的整体性能。
对于InoDB存储引擎而言,其缓冲池的配置通过参数innodb_bufferpool_size来设置。下面显示一台MySQL数据库服务器,其将InnoDB存储引擎的缓冲池设置为
128MB。
具体来看,缓冲池中缓存的数据页类型有:索引页、数据页、undo 页、插入缓存(insert buffer)、redo log buffer、Change Buffer 、自适应哈希索引、InnoDB 存储的锁信息、数据字典信息等。不能简单地认为,缓冲池只是缓存索引页和数据页,它们只是占缓冲池很大的一部分而已。
通过命令SHOW ENGINE INNODB STATUS
可以观察到buffer的情况:
从InnoDB 1.0.x 版本开始,允许有多个缓冲池实例。每个页根据哈希值平均分配到不同缓冲池实例中。这样做的好处是减少数据库内部的资源竞争,增加数据库的并发处
在配置文件中将innodb_ buffer_ pool_ instances 设置为大于1的值就可以得到多个缓冲池实例。
从MySQL5.6版本开始,还可以通过information_schema架构下的表INNODB_BUFFER_POOL_STATS来观察缓冲的状态,如运行下列命令可以看到各个缓冲池的使用状态:
(1)LRU List
在前一小节中我们知道了缓冲池是一个很大的内存区域,其中存放各种类型的页。那么InnoDB存储引擎是怎么对这么大的内存区域进行管理的呢?
通常来说,数据库中的缓冲池是通过LRU(LatestRecentUsed,最近最少使用)算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放LRU列表中尾端的页。
在InnoDB存储引擎中,缓冲池中页的大小默认为16KB,同样使用LRU算法对缓冲池进行管理。稍有不同的是InnoDB存储引擎对传统的LRU算法做了一些优化——冷热分离。在InnoDB的存储引擎中,LRU列表中还加入了midpoint位置。新读取到的页,虽然是最新访问的页,但并不是直接放人到LRU列表的首部,而是放入到LRU列表的midpoint位置。这个算法在InnoDB存储引擎下称为midpoint insertion strategy。在默认配置下,该位置在LRU列表长度的5/8处。midpoint 位置可由参数innodb_old_blocks_pct 控制,如:
从上面的例子可以看到,参数innodb_old_ blocks_pct默认值为37,表示新读取的页插人到LRU列表尾端的37%的位置(差不多3/8的位置)。在InnoDB存储引擎中,把midpoint之后的列表称为old列表,之前的列表称为new列表。可以简单地理解为new列表中的页都是最为活跃的热点数据。
当从磁盘读取数据页后,会先将数据页存放到 LRU 链表冷数据区的头部,如果这些缓存页在 1 秒之后被访问,那么就将缓存页移动到热数据区的头部;如果是 1 秒之内被访问,则不会移动,缓存页仍然处于冷数据区中。1 秒这个数值,是由参数 innodb_old_blocks_time 控制。
参数innodb_old_ blocks_time用于表示页读取到mid位置后需要等待多久才会被加人到LRU列表的热端。因此当需要执行上述所说的SQL操作时,可以通过下面的方法尽可能使LRU列表中热点数据不被刷出。
set global innodb_old_blocks_time=1000;
如果用户预估自己活跃的热点数据不止63%,那么在执行SQL语句前,还可以通过下面的语句来减少热点页可能被刷出的概率。
set global innodb_old_blocks_pct=20;
那为什么不采用朴素的LRU算法,直接将读取的页放入到LRU列表的首部呢?这是因为若直接将读取到的页放人到LRU的首部,那么某些SQL操作可能会使缓冲池中的页被刷新出,从而影响缓冲池的效率。常见的这类操作为索引或数据的扫描操作。这类操作需要访问表中的许多页,甚至是全部的页,而这些页通常来说又仅在这次查询操作中需要,并不是活跃的热点数据。如果页被放入LRU列表的首部,那么非常可能将所需要的热点数据页从LRU列表中移除,而在下一次需要读取该页时,InnoDB 存储引擎需要再次访问磁盘。而使用了实则这种机制,如果在 1 秒后,因全表扫描和预读机制额外加载进来的缓存页,仍然没有人访问,那么它们会一直待在冷数据区,当再需要淘汰数据时,首先淘汰地就是这一部分数据。
实际上,MySQL 在冷热分离的基础上还做了一层优化。
当一个缓存页处于热数据区域的时候,我们去访问这个缓存页,这个时候我们真的有必要把它移动到热点数据区域的头部吗?
从代码的角度来看,将链表中的数据移动到头部,实际上就是修改元素的指针指向,这个操作是非常快的。但是为了安全起见,在修改链表的时候,我们需要对链表加上锁,否则容易出现并发问题。
当并发量大的时候,因为要加锁,会存在锁竞争,每次移动显然效率就会下降。因此 MySQL 针对这一点又做了优化,如果一个缓存页处于热数据区域,且在热数据区域的前 1/4 区域(注意是热数据区域的 1/4,不是整个链表的 1/4),那么当访问这个缓存页的时候,就不用把它移动到热数据区域的头部;如果缓存页处于热数据的后 3/4 区域,那么当访问这个缓存页的时候,会把它移动到热数据区域的头部。
(2)Free List
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列表的使用情况和运行状态。
通过命令SHOW ENGINE NNODB STATUS可以看到:当前Bufter pool size共有183552个页,即183552*16K大小的缓冲池。Free buffers表示当前Free列表中页的数量,Database pages表示LRU列表中页的数量。可能的情况是Free buffers与Database pages的数量之和不等于Buffer pool size。前面说了,因为缓冲池中的页还可能会被分配给自适应哈希索引、Lock信息、InsertBuffer等页,而这部分页不需要LRU算法进行维护,因此不存在于LRU列表中。
pages made young 显示了LRU列表中页移动到前端的次数,因为该服务器在运行阶段没有改变innodb_old_blocks_time 的值,因此not young为0。youngs/s、 non-youngs/s表示每秒这两类操作的次数。这里还有一个重要的观察变量——Buffer pool hit rate,表示缓冲池的命中率,这个例子中为100%,说明缓冲池运行状态非常良好。通常该值不应该小于95%。若发生Buffer pool hit rate 的值小于95%这种情况,用户需要观察是否是由于全表扫描引起的LRU列表被污染的问题。
注意执行命令 SHOW ENGINE INNODB STATUS显示的不是当前的状态,而是过去某个时间范围内InnoDB存储引擎的状态。从上面的例子可以发现,Persecond averages calculated from the last 24 seconds代表的信息为过去24秒内的数据库状态。
从InnoDB 1.2 版本开始,还可以通过表INNODB_BUFFER_POOL_STATS 来观察缓冲池的运行状态,如:
select pool_id,hit_rate,pages_made_young,pages_not_made_young from information_schema.INNODB_BUFFER_POOL_STATS;
此外,还可以通过表INNODB_BUFFER_PAGE_LRU来观察每个LRU列表中每个页的具体信息,例如通过下面的语句可以看到缓冲池LRU列表中SPACE为1的表的页类型:
select table_name,space,page_number,page_type from information_schema.INNODB_BUFFER_PAGE_LRU where space = 1;
(3)unzip_LRU
InnoDB存储引擎从1.0.x 版本开始支持压缩页的功能,即将原本16KB的页压缩为1KB、2KB、4KB和8KB。而由于页的大小发生了变化,LRU列表也有了些许的改变。对于非16KB的页,是通过unzip_LRU列表进行管理的。通过命令SHOW ENGINE INNODB STATUS
可以观察到如下内容:
可以看到LRU列表中一共有1539个页,而unzip_LRU列表中有156 个页。这里需要注意的是,LRU 中的页包含了unzip_ LRU列表中的页。
对于压缩页的表,每个表的压缩比率可能各不相同。可能存在有的表页大小为8KB,有的表页大小为2KB的情况。unzip_LRU是怎样从缓冲池中分配内存的呢?
首先,在unzip_ LRU列表中对不同压缩页大小的页进行分别管理。其次,通过伙伴算法进行内存的分配。例如对需要从缓冲池中申请页为4KB的大小,其过程如下:
select TABLE_NAME,SPACE,PAGE_NUMBER,COMPRESSED_SIZE from information_schema.innodb_buffer_page_lru where compressed_size!=0;
(4)Flush List
在LRU列表中的页被修改后,称该页为脏页(dirty page),即缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过CHECKPOINT机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表。需要注意的是,脏页既存在于LRU列表中,也存在于Flush列表中。LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷新回磁盘,二者互不影响。
同LRU列表一样,Flush列表也可以通过命令SHOW ENGINE INNODB STATUS
来查看,前面例子中Modified db pages 24673就显示了脏页的数量。information_schema架构下并没有类似INNODB_BUFFER_PAGE_LRU的表来显示脏页的数量及脏页的类型,但正如前面所述的那样,脏页同样存在于LRU列表中,故用户可以通过元数据表INNODB_BUFFERPAGE_LRU来查看,唯一不同的是需要加入 OLDEST_MODIFICATION大于0的SQL查询条件,如:
select TABLE_NAME,SPACE,PAGE_NUMBER,oldest_modification from information_schema.innodb_buffer_page_lru where oldest_modification>0;
可以看到当前共有5个脏页及它们对应的表和页的类型。TABLE_NAME为NULL表示该页属于系统表空间。
InnoDB 存储引擎的内存区域除了有缓冲池外,还有重做日志缓冲(redo log buffer)。InnoDB 存储引擎首先将重做日志信息先放人到这个缓冲区,然后按一定频率将其刷新到重做日志文件。重做日志缓冲一般不需要设置得很大,因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。该值可由配置参数innodb_log_buffer_size 控制,默认为8MB:
SHOW VARIABLES LIKE 'innodb_log_buffer_size'\G;
在通常情况下,8MB的重做日志缓冲池足以满足绝大部分的应用,因为重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中。
额外的内存池通常被DBA忽略,他们认为该值并不十分重要,事实恰恰相反,该值同样十分重要。
在InnoDB存储引擎中,对内存的管理是通过一种称为内存堆(heap)的方式进行的。在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。例如,分配了缓冲池(innodb_buffer_pool),但是每个缓冲池中的帧缓冲(frame buffer)还有对应的缓冲控制对象(buffer control block),这些对象记录了一些诸如LRU、锁、等待等信息,而这个对象的内存需要从额外内存池中申请。因此,在申请了很大的InnoDB缓冲池时,也应考虑相应地增加这个值。
前面已经讲到了,缓冲池的设计目的为了协调CPU速度与磁盘速度的鸿沟。因此页的操作首先都是在缓冲池中完成的。如果一条 DML语句,如Update或Delete 改变了页中的记录,那么此时页是脏的,即缓冲池中的页的版本要比磁盘的新。数据库需要将新版本的页从缓冲池刷新到磁盘。
倘若每次一个页发生变化,就将新页的版本刷新到磁盘,那么这个开销是非常大的。若热点数据集中在某几个页中,那么数据库的性能将变得非常差。同时,如果在从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么数据就不能恢复了。为了避免发生数据丢失的问题,当前事务数据库系统普遍都采用了Write Ahead Log策略,即当事务提交时,先写重做日志,再修改页。当由于发生宕机而导致数据丢失时,通过重做日志来完成数据的恢复。这也是事务ACID中D (Durability 持久性)的要求。
思考下面的场景,如果重做日志可以无限地增大,同时缓冲池也足够大,能够缓冲所有数据库的数据,那么是不需要将缓冲池中页的新版本刷新回磁盘。因为当发生宕机时,完全可以通过重做日志来恢复整个数据库系统中的数据到宕机发生的时刻。但是这需要两个前提条件:
对于第一个前提条件,有经验的用户都知道,当数据库刚开始创建时,表中没有任何数据。缓冲池的确可以缓存所有的数据库文件。然而随着市场的推广,用户的增加,产品越来越受到关注,使用量也越来越大。这时负责后台存储的数据库的容量必定会不断增大。当前3TB的MySQL数据库已并不少见,但是3TB的内存却非常少见。目前Oracle Exadata旗舰数据库一体机也就只有2 TB的内存。因此第一个假设对于生产环境应用中的数据库是很难得到保证的。
再来看第二个前提条件:重做日志可以无限增大。也许是可以的,但是这对成本的要求太高,同时不便于运维。DBA或SA不能知道什么时候重做日志是否已经接近于磁盘可使用空间的阈值,并且要让存储设备支持可动态扩展也是需要一定的技巧和设备支持的。
好的,即使上述两个条件都满足,那么还有一一个情况需要考虑:宕机后数据库的恢复时间。当数据库运行了几个月甚至几年时,这时发生宕机,重新应用重做日志的时间会非常久,此时恢复的代价也会非常大。
因此Checkpoint (检查点)技术的目的是解决以下几个问题:
当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。故数据库只需对Checkpoint后的重做日志进行恢复。这样就大大缩短了恢复的时间。
redo log 包括两部分:
redo log是物理日志,记录事务对数据页做了哪些修改。MySQL 每执行一条 DML 语句,先将记录写入 redo log buffer,后续根据写入策略将多个log block写到 redo log file。策略有:
innodb_flush_log_at_trx_commit 参数默认为 1 ,也就是说当事务提交时会调用 fsync 对 redo log 进行刷盘。另外,InnoDB 存储引擎有一个后台线程,每隔1 秒,就会把 redo log buffer 中的内容写到文件系统缓存(page cache),然后调用 fsync 刷盘。(一个没有提交事务的 redo log 记录,也可能会刷盘)
因此为了保证实物的ACID中的持久性,必须将innodb_flush_log_trx_commit设置为1,也就是每当有事务提交时,就必须确保事务都已经写入重做日志文件。那么当数据库因为意外发生宕机时,可以通过重做日志文件恢复,并保证可以恢复已经提交的事务。而将重做日志文件设置为0或2,都有可能发生恢复时部分事务的丢失。不同之处在于,设置为2时,当MySQL数据库发生宕机而操作系统及服务器并没有发生宕机时,由于此时未写入磁盘的事务日志保存在文件系统缓存中,当恢复时同样能保证数据不丢失。
这种先写先写 redo log buffer,再写 redo log file 的技术就是 MySQL 里经常说到的 WAL(Write-Ahead Logging) 技术。存在的意义主要就是降低对数据页刷盘的要求。redo log记录了数据页上的修改,但是当数据页也刷回到磁盘后,这些记录就失去作用了。
InnoDB中至少有一个redo group,每个redo group至少有两个redo log file(ib_logfile0,ib_logfile1),每个redo log file大小固定,除了保存4个512B块:log file header,checkpoint1,空,checkpoint2,其它用于保存log block。采用循环写入的方式,先写redo log file1,写满后再写redo log file2,然后再写redo log file1这样。
两个CP是因为交替写入,避免因介质失败而导致无法找到可用的CP的情况。
为了得到更高的可靠性,用户可以设置多个日志镜像文件组,将不同的文件组放在不同的磁盘上,以此来提高重做日志高可用性。
InnoDB的存储管理是基于页的,redo log file也是基于页的,并且不同类型的语句日志记录不同,但首部是相同的:
write pos
:表示 redo log 当前记录的日志序列号LSN(log sequence number)check point
:当数据页已经刷盘后,更新check point。write pos 到 check point之间的部分
:是 redo log 空着的部分,用于记录新的操作;check point 到 write pos 之间
:是 redo log 待落盘的数据页更改记录;当 write pos 追上 check point 时
,这时候不能再执行新的更新,得停下来,同步到磁盘,推动 check point 向前移动,空出位置再记录新的日志。LSN是日志序列号,有三个含义:①redo log的日志总量 ②checkpoint的位置 ③页的版本
在前面我们已经提到,写入重做日志文件的操作不是直接写,而是先写入一个重做日志缓冲(redo log buffer)中,然后按照一定的条件顺序地写入日志文件:
从重做日志缓冲往磁盘写入时,是按512个字节,也就是一个扇区的大小进行写入。因为扇区是写入的最小单位,因此可以保证写入必定是成功的,因此在重做日志的写入过程中不需要有doublewrite。
启动innodb的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。恢复时,会先检查数据页中的LSN,如果这个LSN小于write pos,说明在redo log上记录着数据页上尚未完成的操作,接着就会从最近的一个check point出发,开始同步数据。在check point和write pos中已经提交的事务会进行redo,没有提交的事务会进行undo。
redo log
是 InnoDB 引擎所特有的,所以我们如果再使用 InnoDB 引擎创建表时,如果数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe
。
checkpoint的工作是将buffer pool中的脏页刷盘,并在redo log file中记录checkpoint的信息。系统故障后,根据redo来恢复数据时,不需要重做所有日志,只需要重做checkpoint点之后的日志,因为redo log也不能无限大,所以当redo log空间不足时,redo log中那部分被更新到磁盘的日志可以覆盖重用。
在redo log file中保存了两个checkpoint,分别保存在ib_logfile0的512字节和1536字节处,每个checkpoint默认大小为512字节,InnoDB的checkpoint主要有3部分信息组成:
checkpoint no:checkpoint no主要保存的是checkpoint号,因为InnoDB有两个checkpoint,通过checkpoint号来判断哪个checkpoint更新
checkpoint lsn:checkpoint lsn主要记录了产生该checkpoint是flush的LSN,确保在该LSN前面的数据页都已经落盘,不再需要通过redo log进行恢复
checkpoint offset:checkpoint offset主要记录了该checkpoint产生时,redo log在ib_logfile中的偏移量,通过该offset位置就可以找到需要恢复的redo log开始位置。
checkpoint分为两种(checkpoint的时机):
sharp checkpoint:在关闭数据库时,将buffer pool中的脏页全部刷入磁盘。
fuzzy checkpoint:在数据库正常运行时,找到不同时机将脏页写入磁盘,一部分一部分的刷入磁盘,不会因为一次性刷入磁盘造成性能问题。
master thread checkpoint。master thread中,每秒或每10秒一次的频率将脏页按一定的比例从内存刷入磁盘,这个过程是异步的,即用户查询线程不会阻塞。
flush_lru_list checkpoint。flush_lru_list checkpoint是在单独的page cleaner线程(Mysql5.6,即InnoDB1.2.x版本后才是该线程,之前是在master thread线程中)中执行的。lru列表是buffer pool的lru列表,lru空闲列表中保留一定数量的空闲页面,来保证buffer pool中有足够的空间应对新的数据库请求。在lru中空闲列表不足时,Innodb存储引擎会将LRU列表尾端的页移除,如果这些页是脏页,那么就需要checkpoint,空闲数量阈值是可以配置的,由参数innodb_lru_scan_depth
控制LRU列表中可用页的数量,默认是1024。
Dirty Page too much。即脏页的数量太多,导致InoDB存储引擎强制进行Checkpoint。其目的总的来说还是为了保证缓冲池中有足够可用的页。其可由参数innodb_max_dirty_pages_pct
控制,默认是75,即当缓冲池中脏页的数量占据75%时,强制进行checkpoint,刷新一部分的脏页到磁盘。
async/sync flush checkpoint。async/sync flush checkpoint是在单独的page cleaner线程中执行的。当redo log利利用率达到阈值,将buffer pool中的一部分脏数据刷新到磁盘。 通过配置阈值,在redo log空间不足指定阈值时进行刷新。
若将已经写人到重做日志的LSN记为redo_Isn, 将已经刷新回磁盘最新页的LSN记为checkpoint_lsn, 则可定义:
checkpoint_age = redo_lsn - checkpoint_lsn
再定义以下的变量:
async_water_mark = 75% * total_redo_1og_file_size
sync_water_mark = 90% ★ total_redo_1og_file_size
若每个重做日志文件的大小为1GB,并且定义了两个重做日志文件,则重做日志文件的总大小为2GB。那么async_water_mark=1.5GB, sync_water_mark=1.8GB,则:
可见,Async/Sync Flush Checkpoint是为了保证重做日志的循环使用的可用性。在InnoDB 1.2.x 版本之前,Async Flush Checkpoint会阻塞发现问题的用户查询线程,而Sync Flush Checkpoint会阻塞所有的用户查询线程,并且等待脏页刷新完成。从InnoDB1.2.x版本开始一也就是MySQL5.6版本,这部分的刷新操作同样放入到了单独的Page Cleaner Thread中,故不会阻塞用户查询线程。
checkpoint的作用:
Master Thread具有最高的线程优先级别。其内部由多个循环(oop) 组成:主循环(loop)、后台循环(backgroup loop)、刷新循环(flush loop)、暂停循环(suspend loop)。Master Thread会根据数据库运行的状态在loop、background loop、fush loop和suspend loop中进行切换。
void master_thread() {
goto loop;
loop: //------- 主循环
for (int i = 0 ; i < 10; i++) {
thread_sleep(l) // sleep 1 second
do log buffer flush to disk // 将日志缓冲刷新到磁盘
if (last one second ios < 5 )
do merge at most 5 insert buffer // 如果在过去的一秒少于5次io操作,则合并5个插入缓冲
if (buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
do buffer pool flush 100 dirty page // 如果超过了配置文件的阈值,至多刷新100个InnoDB 的缓冲池中的脏页到磁盘
if ( no user activity)
goto backgroud loop // 如果当前没有用户活动,则切换到 background loop
}
if (last_ten_second_ios < 200) // 如果在过去10秒的io操作小于200,则刷新100个脏页到磁盘
do buffer pool flush 100 dirty page
do merge at most 5 insert buffer // 合并5个插入缓冲
do log buffer flush to disk // 将日志缓冲刷新到磁盘
do full purge // 删除无用的 Undo 页
if (buf_get_modified_ratio_pct > 70%)
do buffer pool flush 100 dirty page
else //如果有超过70% 的脏页,则刷新100 个脏页到磁盘,否则刷新10%的脏页
buffer pool flush 10 dirty page
goto loop
background loop: //------- 后台循环
do full purge //删除无用的 Undo 页
do merge 20 insert buffer // 合并20 个插入缓冲
if not idle :
goto loop //跳回到主循环
else :
goto flush loop
flush loop: //------- 刷新循环
do buffer pool flush 100 dirty page //刷新100个脏页
if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
goto flush loop //主要是用来刷新100个脏页直到满足条件
goto suspend loop
suspend loop : //------- 暂停循环
suspend_thread()
waiting event
goto loop;
}
(1)loop(主循环)
Loop 被称为主循环,因为大多数的操作是在这个循环中,其中有两大部分的操作:每秒钟的操作和每10 秒的操作。
loop循环通过 thread sleep 来实现,这意味着所谓的每秒一次或每10 秒一次的操作是 不精确的。在负载很大的清况下可能会有延迟(delay), 只能说大概在这个频率下。当然, InnoDB 源代码中还通过了其他的方法来尽量保证这个频率。
每秒一次的操作:
日志缓冲刷新到磁盘,即使这个事务还没有提交(总是)
即使某个事务还没有提交, InnoDB 存储引擎仍然每秒会将重做日志缓冲中的内容刷新到重做日志文件。这一点是必须要知道的,因为这可以很好地解释为什么再大的事务提交(commit) 的时间也是很短的。
合并插入缓冲( 可能)
合并插入缓冲(Insert Buffer) 并不是每秒都会发生的。InnoDB 存储引擎会判断当前一秒内发生的IO 次数是否小于5 次,如果小于5 次,InnoDB 认为当前的IO 压力很小,可以执行合并插入缓冲的操作。
至多刷新100 个InnoDB 的缓冲池中的脏页到磁盘( 可能)
InnoDB 存储引擎通过判断当前缓冲池中脏页的比例(buf_get_modified_ratio_pct) 是否超过了配置文件中 innodb_max_dirty pages_pct 这个参数(默认为90, 代表90%), 如果超过了这个阙值, InnoDB 存储引擎认为需要做磁盘同步的操作,将100 个脏页写入磁盘中。
如果当前没有用户活动,则切换到background loop( 可能)
每 10 秒一次的操作:
将日志缓冲刷新到磁盘(总是)
和每秒一次的操作一样
合并至多5 个插入缓冲(总是)
不同于每秒一次操作时可能发生的合并插入缓冲操作,这次的合并插入缓冲操作总会在这个阶段进行。
刷新100 个脏页到磁盘(可能的情况下)
InnoDB 存储引擎会先判断过去10 秒之内磁盘的IO 操作是否小于 200 次,如果是, InnoDB 存储引擎认为当前有足够的磁盘IO 操作能力,因此将100个脏页刷新到磁盘。
删除无用的Undo 页(总是)
执行full purge 操作,即删除无用的Undo页。对表进行update 、delete 这类操作时,原先的行被标记为删除,但是因为一致性读(consistent read) 的关系,需要保留这些行版本的信息。但是在full purge 过程中,InnoDB 存储引擎会判断当前事务系统中已被删除的行是否可以删除,比如有时候可能还有查询操作需要读取之前版本的undo 信息,如果可以删除, InnoDB 会立即将其删除。(从源代码中可以发现,每次最多尝试回收 20个undo 页。)
刷新100 个或者10 个脏页到磁盘(总是)
InnoDB 存储引擎会判断缓冲池中脏页的比例( buf_get_modified_ratio pct) ,如果有超过70% 的脏页,则刷新100 个脏页到磁盘,如果脏页的比例小于70%, 则 只需刷新10% 的脏页到磁盘。
(2) background loop (后台循环)
若当前没有用户活动(数据库空闲时)或者数据库关闭(shutdown), 就会切换到这个循环。background loop 会执行以下操作:
(3)flush loop(刷新循环)
刷新100 个脏页,并直到符合条件,也就是直到脏页的数量小于innodb_max_dirty_pages_pct(总是)
(4)suspend loop(悬挂循环)
若flush loop 中没有什么事情可以做了, InnoDB 存储引擎会切换到suspend loop,将Master Thread 挂起,等待事件的发生。若用户启用 (enable) 了InnoDB 存储引擎,却没有使用任何InnoDB 存储引擎的表,那么Master Thread 总是处于挂起的状态。
1.0.x 版本之前的Master Thread的实现方式使 InnoDB 存储引擎对于 IO 其实是有限制的,在缓冲池向磁盘刷新时其实都做了一定的硬编码(hard coding),这种规定在很大程度上限制了 InnoDB 存储引擎对磁盘IO的性能,尤其是写入性能。
void master_thread () {
goto loop ;
loop:
for (int i = O; i innodb_max_dirty_pages_pct)
// 如果超过了配置文件的阈值,至多刷新100个InnoDB 的缓冲池中的脏页到磁盘
do buffer pool flush 100% innodb_io_capacity dirty page
else if enable adaptive flush // innodb_adaptive_flushing (自适应地刷新)
do buffer pool flush desired amount dirty page
if ( no user activity) // 如果当前没有用户活动,则切换到 background loop
goto backgroud loop
}
if(last_ten_second_ios < innodb_io_capacity) //每10秒根据IO的吞吐量更新脏页
do buffer pool flush 100% innodb_io_capacity dirty page
do merge 5% innodb_io_capacity insert buffer //每10秒合并插入缓存为总插入缓存的百分之5
do log buffer flush to disk
do full purge
if(buf_get_modified_ratio_pct > 70%)
do buffer pool flush 100% innodb_io_ capacity dirty page //根据脏页总量来刷新,低于一定值也能刷新
else
dobuffer pool flush 10% innodb_io_capaciy dirty page
goto loop
background loop :
do full purge
do merge 100% innodb_io_ capacity insert buffer //与上面一样
if not idle :
goto loop:
else :
goto flush loop
flush loop :
do buffer pool flush 100% innodb_io_ capacity dirty page //与上面一样
if(buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct)
go to flush loop
goto suspend loop
suspend loop :
suspend_ thread ()
waiting event
goto loop ;
}
问题1:从前面的伪代码来看,无论何时,InnoDB 存储引擎最大只会刷新100 个脏页到磁盘,合并20 个插入缓冲。如果是在写入密集的应用程序中,每秒可能会产生 >100 个的脏页,如果是产生大于20 个插入缓冲的情况, Master Thread 似乎会“忙不过来"'或者说它总是做得很慢。即使磁盘能在1 秒内处理多于100 个页的写入和20 个插入缓冲的合并,但是由于 hard coding, Master Thread 也只会选择刷新100 个脏页和合并20个插入缓冲。同时, 当发生岩机需要恢复时,由于很多数据还没有刷新回磁盘,会导致恢复的时间可能需要很久,尤其是对于insert buffer 来说。
解决方法: 提供了参数 innodb_io_capacity,用来表示磁盘 IO 的吞吐量,默认值为200。对于刷新到磁盘页的数量,会按照 innodb_io_capacity 的百分比来进行控制。规则如下:
若用户使用了SSD 类的磁盘,或者将几块磁盘做了RAID, 当存储设备拥有更高的 IO 速度时,完全可以将innodb_io_capacity 的值调得再高点, 直到符合磁盘IO 的吞吐量为止。
问题2:参数 innodb_max_dirty_pages_pct 默认值的问题,在InnoDB 1.0.x版本之前,该值的默认为90, 意味着脏页占缓冲池的90% 。但是该值“太大”了,因为InnoDB 存储引擎在每秒刷新缓冲池和flush loop 时会判断这个值,如果有很大的内存,或者数据库服务器的压力很大,这时刷新脏页的速度反而会降低。同样,在数据库的恢复阶段可能需要更多的时间。
解决方法: 从 InnoDB 1.0.x 版本开始,innodb_max_dirty_pages_pct 默认值变为了 75, 和 Google 测试的 80 比较接近。这样既可以加快刷新脏页的频率,又能保证了磁盘 IO 的负载。
问题3:对于每秒刷新脏页数量的改进。原来的刷新规则:脏页在缓冲池所占的比例 < innodb max_dirty_pages_pct 时,不刷新脏页;>innodb_max_dirty_pages pct 时,刷新100 个脏页。
解决方法: InnoDB 1.0.x 版本带来的另一个参数是 innodb_adaptive_flushing (自适应地刷新),该值影响每秒刷新脏页的数量。随着innodb_adaptive_flushing 参数的引入, InnoDB 存储引擎会通过一个名为 buf_flush_get_desired_flush_rate 的函数来判断需要刷新脏页最合适的数量。粗略地翻阅源代码后发现 buf_ flush get_desired_ flush_rate 通过判断产生重做日志(redo log) 的速度来决定最合适的刷新脏页数量。因此,当脏页的比例小于innodb_max_dirty_pages_pct 时,也会刷新一定量的脏页。
问题4:之前每次进行full purge 操作时,最多回收20 个Undo 页。
解决方法: 从InnoDB 1.0.x 版本开始引入了参数innodb_purge_batch_size, 该参数可以控制每次full purge 回收的Undo 页的数最。该参数的默认值为20, 并可以动态地对其进行修改。
InnoDB 1.0.x 版本在性能方面取得了极大的提高,其实这和前面提到的Master Thread 的改动是密不可分的,因为InnoDB 存储引擎的核心操作大部分都集中在Master Thread 后台线程中。
命令SHOW ENGINE INNODB STATUS 查看当前 MasterThread 的状态信息:
mysql> show engine innodb status\G;
*************************** 1. row***************************
Type: InnoDB
Name:
Status:
091009 10:14:34 INNODB MONITOR OUTPUT
Per second averages calculated from the last 42 seconds
BACKGROUND THREAD
srv_master_ thread l oops: 2188 1_second, 1537 sleeps, 218 10_second, 2 background, 2 flush
srv_master_thread log flush and writes: 1777 log writes only: 5816
......
信息的意思是:当前主循环运行了2188 次,但是循环中的每秒挂起(sleep) 的操作只运行了1537 次(这是因为InnoDB 对其内部进行了一些优化,当压力大时并不总是等待 1 秒。因此,并不能认为1_second 和 sleeps 的值总是相等的。在某些情况下,可以通过两者之间差值的比较来反映当前数据库的负载压力,差值越大说明数据库的负载压力越大),10 秒一次的活动进行了218 次,符合1 : 10 。background loop 进行了2 次,flush loop 也进行了2次。
在 InnoDB 1.2.x 版本中再次对 Master Thread 进行了优化,由此也可以看出 MasterThread 对性能所起到的关键作用。在 InnoDB 1.2.x 版本中, Master Thread 的伪代码如下:
if InnoDB is idle
srv master do idle tasks( );
else
srv master do active tasks();
其中 srv_master_ do_idle_tasks() 就是之前版本中每10 秒的操作, srv_master_ do_active_tasks() 处理的是之前每秒中的操作。同时对于刷新脏页的操作,从Master Thread线程分离到一个单独的Page Cleaner Thread, 从而减轻了Master Thread 的工作,同时进一步提高了系统的并发性。
Insert Buffer 可能是InnoDB 存储引擎关键特性中最令人激动与兴奋的一个功能。 lnnoDB 缓冲池中有Insert Buffer 信息固然不错,但是 Insert Buffer 和数据页一样,也是物理页的一个组成部分。insert buffer和doublewrite buffer是类似的概念,他实际上属于system tablespace中的一部分,正由于它也是持久化存储,那么在服务器宕机或是重启之后这些信息不会丢失。
在InnoDB 存储引擎中,主键是行唯一的标识符。通常应用程序中行记录的插入顺序是按照主键递增的顺序进行插入的。因此,插入聚集索引(Primary Key) 一般是顺序的,不需要磁盘的随机读取。 对于这类情况下的插入操作,速度是非常快的。
注:并不是所有的主键插入都是顺序的。若主键类是 UUID 这样的类 (即通用唯一识别码 ,让分布式系统中的所有元素,都能有唯一的辨识信息),那么插入和辅助索引一样,同样是随机的。即使主键是自增类型,但是插入的是指定的值,而不是NULL 值,那么同样可能导致插入并非连续的情况。
但是不可能每张表上只有一个聚集索引(主键),更多情况下,一张表上有多个非聚集的辅助索引(secondary index) 。 比如,用户需要按照b 这个字段进行查找,并且b 这个字段不是唯一的:
CREATE TABLE t (
a INT AUTO_ 工NCREMENT,
b VARCHAR(30),
PRIMARY KEY(a),
key(b)
);
在这样的情况下,产生了一个非聚集的且不是唯一的索引。在进行插入操作时,数据页的存放还是按主键a进行顺序存放的,但是对于非聚集索引叶子节点的插入不再是顺序的了,这时就需要离散地访问非聚集索引页,由于随机读取的存在而导致了插入操作性能下降(B+ 树的特性决定了非聚集索引插入的离散性)。
InnoDB 存储引擎开创性地设计了 Insert Buffer (针对非聚集索引)。对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中, 若在,则直接插入; 若不在,则先放入到 Insert Buffer中 。数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个位置。然后再以一定的频率进行Insert Buffer 和辅助索引页子节点的merge (合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中) ,这就大大提高了对于非聚集索引插入的性能。
Insert Buffer 的使用需要同时满足以下两个条件:
insert Buffer带来的缺点:当应用程序进行大量的插入操作,这些都涉及了不唯一的非聚集索引, 也就是使用了Insert Buffer 。若此时MySQL 数据库发生了宕机,这时势必有大量的 Insert Buffer 并没有合并到实际的非聚集索引中去。因此这时恢复可能需要很长的时间,在极端情况下甚至需要几个小时。
用户可以通过命令 SHOW ENGINE INNODB STATUS 来查看插入缓冲的信息:
mysql>SHOW ENGINE INNODB STATUS\G;
*************************** 1. row***************************
Type: InnoDB
Name :
Status:
100727 22 : 21 : 48 INNODB MONITOR OUTPUT
Per second averages calculated from the last 44 seconds
......
---------------------------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
Ibuf: size 7545, free list len 3790, seg size 11336,
8075308 inserts, 7540969 merged recs, 2246304 merges
......
---------------------------------------------------------
END OF INNODB MONI TOR OUTPUT
1 row in set (0.00 sec)
目前 Insert Buffer 存在一个问题是: 在 写密集 的情况下,插入缓冲会占用过多的缓冲池内存 (innodb_buffer _pool), 默认最大可以占用到1/2 的缓冲池内存。(后来可以通过,修改 IBUF_POOL_SIZE_PER_MAX_SIZE 就可以对插入缓冲的大小进行控制,比如值为3,就是1/3)。
InnoDB 从1.0.x 版本开始引入了 Change Buffer, 可将其视为 Insert Buffer 的升级。从这个版本开始, InnoDB 存储引擎可以对 DML 操作(INSERT 、DELETE 、UPDATE )都进行缓冲 :Insert Buffer 、Delete Buffer 、Purge buffer。就是当二级索引页不在内存中时,你对它们的操作会被缓存在change buffer中(目的是省去这次随机的磁盘IO)。等之后再将这部分缓存操作merge到B+Tree中。
当然和之前 Insert Buffer 一样,Change Buffer 适用的对象依然是非唯一的辅助索引。
UPDATE 操作可能分为两个过程:
因此 Delete Buffer 对应 UPDATE 操作的第一个过程, 即将记录标记为删除。然后真正删除后,在Purge Buffer中记录真正的删除标识,然后进行插入,是否需要Insert Buffer取决于索引的类型与是否能命中当前缓冲池中索引页。可见,要缓冲更新操作至少需要Insert Buffer、Delete Buffer。
同时,InnoDB 存储引擎提供了参数 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_buffer_max_size 来控制 Change Buffer 最大使用内存的数量。该参数值默认为25, 表示最多使用 1/4 的缓冲池内存空间。最大有效值为50 。
在MySQL5.5 版本中通过命令 SHOW ENGINE INNODB STATUS,
可以观察到类似如下的内容:
mysql> SHOW ENGINE I NNODB STATUS\G;
*************************** 1. row***************************
Type: InnoDB
----------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
----------------------------------------
Ibuf: size 1 , free list len 3 4397 , seg size 34399 , 10875 merges
merged operations :
insert 20462 , delete mark 20158 , delete 4215
discarded operat i ons :
insert 0 , delete mark 0 , delete 0
Insert Buffer 的数据结构是一棵 B+ 树。在MySQL 4.1 之前的版本中每张表有一棵 Insert Buffer B+ 树。而在现在的版本中, 全局只有一棵 Insert Buffer B+ 树,负责对所有的表的辅助索引进行 Insert Buffer 。而这棵 B+ 树存 放在共享表空间中,默认也就是 ibdatal 中。因此,试图通过独立表空间 ibd 文件恢复表中数据时, 往往会导致 CHECK TABLE 失败。这是因为表的辅助索引中的数据可能还在 Insert Buffer 中,也就是共享表空间中,所以通过 ibd 文件进行恢复后,还需要进行 REPAIR TABLE 操作来重建表上所有的辅助索引。
Insert Buffer 是一棵 B+ 树,因此其也由叶节点和非叶节点组成。非叶节点存放的是查询的 search key (键值)。
search key 一共占用9 个字节,其中:
当一个辅助索引要插入到页(space, offset) 时,如果这个页不在缓冲池中,那么 InnoDB 存储引擎首先根据上述规则构造一个search key, 接下来查询Insert Buffer 这棵 B+ 树,然后再将这条记录插入到Insert Buffer B+ 树的叶子节点中。
对于插入到 Insert Buffer B+ 树叶子节点的记录,并不是直接将待插入的记录插入,而是需要根据如下的规则进行构造:
space 、marker 、offset 字段和之前非叶节点中的含义相同,一共占用9 字节。第4 个字段 metadata 占用4 字节,其存储的内容如表:
名称 | 字节 |
---|---|
IBUF_REC_OFFSET_COUNT | 2 |
IBUF_REC_OFFSET_TYPE | 1 |
IBUF_REC_OFFSET_FLAGS | 1 |
IBUF_REC_OFFSET_COUNT 是保存两个字节的整数,用来排序每个记录进入Insert Buffer 的顺序。因为从InnoDB l.0.x 开始支持 Change Buffer, 所以这个值同样记录进入Change Buffer 的顺序。通过这个 顺序回放(replay) 才能得到记录的正确值。
从Insert Buffer 叶子节点的第5 列开始,就是实际插入记录的各个字段了。因此较之原插入记录, Insert Buffer B+ 树的叶子节点记录需要额外13 字节的开销。
因为启用Insert Buffer 索引后,辅助索引页(space, offect) 中的记录可能被插入到 Insert Buffer B+ 树中,所以为了保证每次 Merge Insert Buffer 页必须成功,还需要有一个特殊的页用来标记每个辅助索引页 (space, page_no) 的可用空间。这个页的类型为 Insert Buffer Bitmap 。
每个 Insert Buffer Bitmap 页用来追踪 16384 个辅助索引页,也就是256 个区(Extent)(一个区64个辅助索引页)。每个 Insert Buffer Bitmap 页都在16384 个页的第二个页中。
每个辅助索引页在 Insert Buffer Bitmap 页中占用 4 位:
名称 | 大小(bit) | 说明 |
---|---|---|
IBUF_BITMAP_FREE | 2 | 表示该辅助索引页中的可用空间数量,可取值为: ① 0表示无可用剩余空间 ② 1表示剩余空间大于1/32页(512 字节) ③ 2表示剩余空间大于1/16页 ④ 3表示剩余空间大于1/8页 |
IBUF_BITMAP_BUFFERED | 1 | 1表示该辅助索引页有记录被缓存在Insert Buffer B+树中 |
IBUF_BITMAP_IBUF | 1 | 1表示该页为Insert Buffer B+树的索引页 |
Insert/Change Buffer 是一棵 B + 树。若需要实现插入记录的辅助索引页不在缓冲池中,那么需要将辅助索引记录首先插入到这棵B+树中。但是Insert Buffer 中的记录何时合并(merge)到真正的辅助索引中呢?
Merge Insert Buffer 的操作可能发生在以下几种情况下:
第一种情况为当辅助索引页被读取到缓冲池中时,例如:在执行正常的SELECT 查询操作,这时需要检查Insert Buffer Bitmap 页,然后确认该辅助索引页是否有记录存放于Insert Buffer B+ 树中。若有,则将Insert Buffer B+ 树中该页的记录插入到该辅助索引页中。可以看到对该页多次的记录操作通过一次操作合并到了原有的辅助索引页中,因此性能会有大幅提高。
Insert Buffer Bitmap 页用来追踪每个辅助索引页的可用空间,并至少有 1/32 页的空间。若插入辅助索引记录时检测到插入记录后可用空间会小于1/32 页,则会强制进行一个合并操作,即强制读取辅助索引页,将Insert Buffer B+ 树中该页的记录及待插入的记录插入到辅助索引页中。
之前在分析Master Thread 时曾讲到,在Master Thread 线程中每秒或每10 秒会进行一次Merge Insert Buffer 的操作,不同之处在于每次进行merge 操作的页的数量不同。
在 Master Thread 中,执行 merge 操作的不止是一个页,而是根据 srv_innodb_io_capactiy 的百分比来决定真正要合并多少个辅助索引页 。但 InnoDB 存储引擎又是根据怎样的算法来得知需要合并的辅助索引页呢?
在Insert Buffer B+ 树中,辅助索引页根据(space, offset) 都已排序好,故可以根据(space, offset) 的排序顺序进行页的选择。然而,对于Insert Buffer 页的选择,InnoDB 存储引擎并非采用这个方式,它随机地选择 Insert Buffer B+ 树的一个页,读取该页中的space 及之后所需要数量的页。 该算法在复杂情况下应有更好的公平性。同时,若进行merge 时,要进行merge 的表已经被删除,此时可以直接丢弃已经被 Insert/Change Buffer 的数据记录。
如果说 Insert Buffer 带给 InnoDB 存储引擎的是性能上的提升,那么double write (两次写)带给 InnoDB 存储引擎的是 数据页的可靠性。
当发生数据库宕机时,可能InnoDB 存储引擎正在写入某个页到表中,而这个页只写了一部分,比如16KB 的页,只写了前4KB, 之后就发生了宕机,这种情况被称为 部分写失效。
有一个办法,那就是通过重做日志进行恢复,但重做日志中记录的是对页的物理操作,那么如果这个页本身已经发生了损坏,再对其进行重做是没有意义的。
进行改进,在应用(apply) 重做日志前,用户需要一个页的副本,当写入失效发生时,先通过页的副本来还原该页,再进行重做,这就是 doublewrite。
doublewrite 由两部分组成:
内存中的 doublewrite buffer, 大小为 2MB
物理磁盘上共享表空间中连续的128 个页,即2个区(extent), 大小同样为2MB 。
工作流程:
可以通过以下命令观察到 doublewrite 运行的情况:
mysql>SHOW GLOBAL STATUS LIKE'innodb_dblwr%' \ G ;
*************************** 1 . row***************************
Variable_narne : Innodb_dblwr_pages_written
Value : 6325194
*************************** 2. row***************************
Variable name: Innodb_dblwr_writes
Value : 100399
2 rows in set (0.00 sec )
doublewrite 一共写了 6325194 个页,但实际的写入次数为 100399, 基本上符合64: 1 。如果发现系统在高峰时的Innodb_dblwr_pages_written : Innodb_dblwr _writes 远小于64: 1, 那么可以说明系统写入压力并不是很高(因为都通过二次写完成,并且没有类似宕机的事件发生)。如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中, InnoDB 存储引擎可以从共享表空间中的 doublewrite 中找到该页的一个副本,将其复制到表空间文件,再应用重做日志。
若查看MySQL 官方手册,会发现在命令SHOW GLOBAL STATUS 中 Innodb_buffer_pool_pages_flushed 变星表示当前从缓冲池中刷新到磁盘页的数量。根据之前的介绍,用户应该了解到,在默认情况下所有页的刷新首先都需要放入到doublewrite 中,因此该变量应该和Innodb_dblwr_pages_written 一致。然而在MySQL 5 . 5.24 版本之前,Innodb_buffer pool_pages_flushed 总是为Innodb_dblw _pages_written 的2倍,而此Bug直到MySQL5.5.24 才被修复。因此用户若需要统计数据库在生产环境中写人的量,最安全的方法还是根据Innodb_dblwr_pages_witten 来进行统计,这在所有版本的MySQL数据库中都是正确的。
参数 skip_innodb_doublewrite 可以禁止使用doublewrite 功能,这时可能会发生前面提及的写失效问题。不过如果用户有多个从服务器 (slave server), 需要提供较快的性能(如在slaves erver 上做的是RAIDO), 也许启用这个参数是一个办法。不过对于需要提供数据高可靠性的主服务器(master server), 任何时候用户都应确保开启doublewrite 功能。
哈希(hash) 是一种非常快的查找方法,在一般情况下这种查找的时间复杂度为O(1), 即一般仅需要一次查找就能定位数据。而B+ 树的查找次数,取决于 B+ 树的高度,在生产环境中, B+ 树的高度一般为3 ~ 4 层,故需要3~4 次的查询。
InnoDB 存储引擎会监控对表上各索引页的查询。 如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index,AHI) 。 AHI 是通过缓冲池的B+ 树页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引。InnoDB 存储引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引。
使用 AHI 的要求:
对这个页的连续访问模式必须是一样的(指的是查询的条件一样)。
例如对于(a,b)这样的联合索引页,其访问模式可以是以下情况:
若交替进行上述两种查询,那么InnoDB存储引擎不会对该页构造AHI
以该模式至少访问了100 次
页通过该模式访问了N 次,其中N= 页中记录*1/16
哈希索引只能用来搜索等值的查询
根据InnoDB 存储引擎官方的文档显示,启用AHI 后,读取和写入速度可以提高2倍,辅助索引的连接操作性能可以提高5 倍。毫无疑问, AHi 是非常好的优化模式,其设计思想是数据库自优化的(self-tuning), 即无需OBA 对数据库进行人为调整。
通过命令 SHOW ENGINE INNODB STATUS 可以看到当前AHI的使用状况。可以通过参数 innodb_adaptive_hash_index 禁用或启动此特性,默认 AHI 为 开启状态。
为了提高磁盘操作性能,当前的数据库系统都采用异步 IO(Asynchronous IO, AIO) 的方式来处理磁盘操作。InnoDB 存储引擎亦是如此。
与 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的页
在InnoDB l.l.x 之前, AIO 的实现通过InnoDB 存储引擎中的代码来模拟实现。而从 InnoDB 1.1.x 开始(InnoDB Plugin 不支持),提供了内核级别AIO 的支持,称为Native AIO 。因此在编译或者运行该版本MySQL 时,需要libaio 库的支持。若没有则会出现如下的提示:
/usr/local/mysql /bin/mysqld: error while l oading shared libraries: libaio.so.l:
cannot open shared object file: No such file or directory
Windows 系统和Linux 系统都提供 Native AIO 支持,而Mac OSX 系统则未提供。因此在这些系统下,依旧只能使用原模拟的方式。参数innodb_use_native_aio 用来控制是否启用Native AIO, 在Linux 操作系统下,默认值为ON。用户可以通过开启和关闭Native AIO 功能来比较InnoDB 性能的提升。官方的测试显示,启用NativeAIO, 恢复速度可以提高75% 。
在InnoDB 存储引擎中, read ahead 方式的读取都是通过AIO 完成,脏页的刷新,即磁盘的写入操作则全部由AIO 完成。
预读是当发起一个 I/O 请求时候,异步的预取磁盘多个页面放入缓冲池,也就是预料这些页会马上被读取到,所以预先把接下来的部分数据页读取出来。
(1)Linear read-ahead(线性预读)
线性预读以 extent 作为预定范围单位,预测在buffer pool中被访问到的数据它临近的页也会很快被访问到。能够通过调整被连续访问的页的数量来控制InnoDB的预读操作,使用参数 innodb_read_ahead_threshold
配置:
show variables like 'innodb_read_ahead_threshold';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| innodb_read_ahead_threshold | 56 |
+-----------------------------+-------+
innodb_read_ahead_threshold
这个参数控制InnoDB在检测顺序页面访问模式时的灵敏度。如果在一个区块顺序读取的页数大于或者等于 innodb_read_ahead_threshold 这个参数,InnoDB启动预读操作来读取下一个区块。innodb_read_ahead_threshold参数值的范围是 0-64,默认值为56. 这个值越高则访问默认越严格。比如,如果设置为48,在当前区块中当有48个页被顺序访问时,InnoDB就会启动异步的预读操作,如果设置为8,则仅仅有8个页被顺序访问就会启动异步预读操作。
若没有设置该参数,InnoDB会在读取到当前区段最后一页时才会发起异步预读请求。
(2)Random read-ahead(随机预读)
随机预读 以 页 作为预定范围单位,也就是一个 extent 中的 64个页为预读范围,通过buffer pool中已有的页来预测哪些页可能很快会被访问,而不考虑这些页的读取顺序。如果发现buffer pool中存中一个区段的13个连续的页,InnoDB会异步发起预读请求这个区段剩余的页。通过设置 innodb_random_read_ahead 为 ON开启随机预读特性。
通过 SHOW INNODB ENGINE STATUS 命令输出的统计信息可以帮助你评估预读算法的效果,统计信息包含了下面几个值:
由于随机预读相对复杂性,同时在性能也存在不稳定性,在5.5中已经将这种预读方式废弃。
InnoDB 存储引擎还提供了 Flush Neighbor Page ( 刷新邻接页) 的特性。
工作原理为: 当刷新一个脏页时, InnoDB 存储引擎会检测该页所在区(extent) 的所有页,如果是脏页,那么一起进行刷新。
好处:通过 AIO 可以将多个IO 写入操作合并为一个IO 操作,故该工作机制在传统机械磁盘下有着显著的优势。
刷新邻接页 需要考虑的问题:
为此, InnoDB 存储引擎从 1.2 .x 版本开始提供了参数 innodb_flush_neighbors
, 用来控制是否启用该特性。对于传统机械硬盘建议启用该特性,而对于固态硬盘有着超高 IOPS(磁盘每秒的读写次数,用来衡量磁盘性能的) 性能的磁盘,则建议将该参数设置为 0, 即关闭此特性。
InnoDB 是MySQL 数据库的存储引擎之一,因此InnoDB 存储引擎的启动和关闭,更准确的是指在MySQL 实例的启动过程中对InnoDB 存储引擎的处理过程。
在关闭时,参数 innodb_fast_shutdown 影响着表的存储引擎为 InnoDB 的行为。该参数可取值为0 、1 、2, 默认值为1 。
这里需要注意的是,insert buffer和doublewrite buffer是类似的概念,他实际上属于system tablespace中的一部分,正由于它也是持久化存储,那么在服务器宕机或是重启之后这些信息不会丢失。
当正常关闭 MySQL 数据库时,下次的启动应该会非常“正常"。但是如果没有正常地关闭数据库,如用 kill 命令关闭数据库,在 MySQL 数据库运行中重启了服务器,或者在关闭数据库时,将参数 innodb_fast_shutdown 设为了2 时,下次MySQL 数据库启动时都会对InnoDB 存储引擎的表进行恢复操作。
参数 innodb_force_recovery 影响了整个 InnoDB 存储引擎恢复的状况。该参数值默认为0,代表当发生需要恢复时,进行所有的恢复操作, 当不能进行有效恢复时,如数据页发生了corruption,MySQL 数据库可能发生宕机(crash), 并把错误写入错误日志中去。
但是,在某些情况下,可能并不需要进行完整的恢复操作,因为用户自已知道怎么进行恢复。比如在对一个表进行 alter table 操作时发生意外了,数据库重启时会对 InnoDB 表进行 回滚操作,对于一个大表来说这需要很长时间,可能是几个小时。这时用户可以自行进行恢复,如可以把表删除,从备份中重新导入数据到表,可能这些操作的速度要远远快于回滚操作。
参数 innodb_force_recovery 可以设置为6 个非零值: 1~6 。
在设置了参数 innodb_force_recovery 大于0 后,用户可以对表进行select 、create 和drop 操作,但insert 、update 和delete 这类DML 操作是不允许的。
现在来做一个实验,模拟故障的发生。
(1)在第一个会话中(session), 对一张接近1000万行的InnoDB存储引擎表进行更新操作,但是完成后不要马上提交:
mysql>START TRANSACTION;
Query oK, 0 rows affected (0.00 sec)
mysql>UPDATE Profile SET password='';
Query oK,9587770 rows affected (7 min 55.73 sec)
Rows matched: 9999248 Changed: 9587770 Warnings: 0
(2)这时,人为通过kill命令杀掉MySQL数据库服务器:
[ root@nineyou0-43 ~]# ps -ef 1 grep mysqld
root 28007 1 0 13:40 pts/1 00:00:00 /bin/sh. /bin/mysqld_safe --datadir=/usr/local/mysql/dạta --pid-file=/usr/local/mysql/data/nineyou0-43.pid
mysql 28045 28007 42 13:40 pts/l 00:04:23 /usr/local/mysql/bin/mysqld --basedir=/usr/local /mysq1 --datadir=/usr/local/mysql/data --user=mysq1 --pid-file=/usr/local/mysql/data/nineyou0-43.pid --skip-external-locking --port=3306 --socket=/ tmp/mysql.sock
root 28110 26963 0 13:50 pts/11 00:00:00 grep mysqld
(root@nineyou0-43 ~]# kill -9 28007
[root@nineyou0-43 ~)# kill -9 28045
(3)下次MySQL启动时会对之前的update事务进行回滚操作,而这些信息都会记录在错误日志文件(默认后缀名为err)中。如果查看错误日志文件,可以看到如下结果:
可以看到,采用默认的策略(即innodb_force_recovery设为0),InnoDB会在每次启动后对发生问题的表进行恢复操作,通过错误日志文件,可知这次回滚操作需要回滚8867280行记录,差不多总共进行了9分钟。
重新做一次实验,此次在启动MySQL之前,将参数innodb_force_recovery设为3,然后观察InnoDB是否还会进行回滚操作,查看错误日志文件,内容如下:
这里出现了“!!!”,InnoDB警告已经将innodb_force_recovery设置为3,不会进行回滚操作了,因此数据库很快启动完成。