一条SQL进入MySQL服务器,会依次经过连接池模块(进行鉴权,生成线程),查询缓存模块(是否被缓存过),SQL接口模块(简单的语法校验),查询解析模块,优化器模块(生成语法树),然后在进入InnoDB存储引擎模块。 进入InnoDB后,首先会判断该SQL涉及到的页是否存在于缓存中,注意MySQL是每次从硬盘中加载相应的页到内存中,进行相关操作的。如果不存在,则加载数据到缓存中。 其次,如果是select语句,则读取相关语句,并将查询结果返回值服务器。如果是update,insert,delete语句,则读取相关的页面,先试图给该SQL涉及的记录枷锁。 接着,加锁成功后,先写undo页,逻辑记录这些记录修改前的状态,然后在修改相关记录,这些操作会同步到redo log buffer,继而生成重新日志。
一、后台线程
1)Master Thread
Master Thread 是一个非常核心的线程,主要负责将缓存池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页数据、合并插入缓存、undo页的回收等。
2)IO Thread
InnoDB 存储引擎大量使用了AIO来处理IO请求,这样可以提高数据库的性能。IO Thread主要负责这些IO请求的回调处理。
涉及参数:innodb_read_id_thread 、innodb_write_io_thread
可以通过命令来观察 InnoDB 中的 IO Thread:
mysql> SHOW ENGINE INNODB STATUS\G
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2016-03-23 20:19:53 0x700000d51000 INNODB MONITOR OUTPUT
....
...
--------
FILE I/O
--------
I/O thread 0 state: waiting for i/o request (insert buffer thread)
I/O thread 1 state: waiting for i/o request (log thread)
I/O thread 2 state: waiting for i/o request (read thread)
I/O thread 3 state: waiting for i/o request (read thread)
I/O thread 4 state: waiting for i/o request (read thread)
I/O thread 5 state: waiting for i/o request (read thread)
I/O thread 6 state: waiting for i/o request (write thread)
I/O thread 7 state: waiting for i/o request (write thread)
I/O thread 8 state: waiting for i/o request (write thread)
I/O thread 9 state: waiting for i/o request (write thread)
......
----------------------------
END OF INNODB MONITOR OUTPUT
可以看到, InnoDB 共有10个 IO Thread, 分别是 4个 write、4个 read、1个 insert buffer和1个 log thread.
3)purge Thread
事物被提交之后, undo log 可能不再需要,因此需要 Purge Thread 来回收已经使用比分配的 undo页. InnoDB 支持多个 Purge Thread, 这样做可以加快 undo 页的回收
InnoDB 引擎默认设置为4个 Purge Thread:
4)page thread cleaner
Page Cleaner Thread 是新引入的,其作用是将之前版本中脏页的刷新操作都放入单独的线程中来完成,这样减轻了 Master Thread 的工作及对于用户查询线程的阻塞
二、内存
1) 缓冲池 innodb_buffer_pool
索引页index page、数据页datat page、undo页、插入缓存 insert buffer、自适应哈希索引、innoDB存储的锁信息 lock info、数据字典
重做日志缓冲池 redo_log_buffer
额外缓冲池 innodb_addition_mem_pool_size
2)LRU(Lastt Recent Used,近少使用)
mysql对LRU进行了一些优化,加入了midpoint位置。新读到的页虽然是新访问的页,但不直接放入LRU列表的首部,而是放在midpoint位置。
正常情况下位置在LRU的列表长度的5/8处。 参数:innodb_old_blocks_pct,该参数设置新页插入LRU列表的百分比。
主要是为了防止访问全部的页把活跃度高的页挤走。
InnoDB 存储引擎支持事务,其设计目标主要面向在线事务处理(OLTP)的应用。其特点是行锁设计,支持外键,并支持非锁定读,即默认读操作不会产生锁。
InnoDB通过使用多版本并发控制(MVCC)来获取高并发性,并且实现了SQL标准的4中隔离级别,默认为REPEATABLE级别。同时,使用一种被称为next-key-locking的策略来避免幻读现象的产生。除此之外,InnoDB存储引擎还提供了插入缓冲(insert buffer)、二次写(double write)、自适应哈希索引(adaptive hash index)、预读(read ahead)等高性能和高可用的功能。
Innodb 存储引擎的体系架构由三部分组成,分别为内存池,后台线程和磁盘文件三大部分组成。
InnoDB内存池
内存池大概包括三类:
1 缓冲池(buffer pool)
2 重做日志缓冲(redo log buffer)
3 额外的缓冲池(additional buffer pool)
内存池主要工作
维护所有进程/线程需要访问的多个内部数据结构
缓存磁盘上的数据,方便快速读取,同时在对磁盘文件修改之前进行缓存
缓存重做日志(redo log)
缓冲池
InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。但是由于CPU速度和磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池记录来提高数据库的的整体性能。
在数据库中进行读取操作,首先将从磁盘中读到的页放在缓冲池中,下次再读相同的页中时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。一页16KB,这跟mmu的内存页不同,mmu是4kB。
对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为CheckPoint的机制刷新回磁盘。
所以,缓冲池的大小直接影响着数据库的整体性能,可以通过配置参数innodb_buffer_pool_size来设置。
缓冲池中缓存的数据页类型:
索引页: 缓存数据表索引
数据页: 缓存数据页,占缓冲池的绝大部分
undo页: undo页是保存事务,为回滚做准备的
插入缓冲(Insert Buffer): 插入数据时要先插入到缓存池中
insert buffer是一种特殊的数据结构(B+ tree)并不是缓存的一部分,而是物理页。
在InnoDB引擎上进行插入操作时,一般需要按照主键顺序进行插入,这样才能获得较高的插入性能。当一张表中存在非聚簇的且不的索引时,在插入时,数据页的存放还是按照主键进行顺序存放,但是对于非聚簇索引叶节点的插入不再是顺序的了,这时就需要离散的访问非聚簇索引页,由于随机读取的存在导致插入操作性能下降。
primary key 是按照递增的顺序进行插入的,异常插入聚族索引一般也顺序的,非随机IO。
写索引要检查记录是不是存在,所以在修改索引之前,必须把修改的记录相关的索引页读出来才知道是不是、这样Insert buffer就没意义了,要读出来(随机IO),所以只对非索引有效。
InnoDB为此设计了Insert Buffer来进行插入优化。对于非聚簇索引的插入或者更新操作,不是每一次都直接插入到索引页中,而是先判断插入的非聚集索引是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个Insert Buffer中。看似数据库这个非聚集的索引已经查到叶节点,而实际没有,这时存放在另外一个位置。然后再以一定的频率和情况进行Insert Buffer和非聚簇索引页子节点的合并操作。这时通常能够将多个插入合并到一个操作中,这样就大大提高了对于非聚簇索引的插入性能。
自适应哈希索引(adaptive Hash Index)
除了B+ Tree索引外,在缓冲池还会维护一个哈希索引,以便在缓冲池中快速找到数据页。InnoDB会根据访问的频率和模式,为热点页建立哈希索引,来提高查询效率。InnoDB存储引擎会监控对表上各个索引页的查询,如果观察到建立哈希索引可以带来速度上的提升,则建立哈希索引,所以叫做自适应哈希索引
innoDB 存储的锁信息(lock info)
每个事务锁的信息是在trx_lock_t对象的lock_heap变量中进行分配的,当申请空间大于8KB时,就会从缓冲池中进行分配,所以有部分内存空间保存的锁信息。这种情况发生在一个事务对大量记录进行了上锁操作
数据字典信息(data dictionary):
InnoDB有自己的表缓存,可以称为表定义缓存或者数据字典。当InnoDB打开一张表,就增加一个对应的对象到数据字典.
数据字典是对数据库中的数据、库对象、表对象等的元信息的集合。在MySQL中,数据字典信息内容就包括表结构、数据库名或表名、字段的数据类型、视图、索引、表字段信息、存储过程、触发器等内容。MySQL INFORMATION_SCHEMA库提供了对数据局元数据、统计信息、以及有关MySQL server的访问信息(例如:数据库名或表名,字段的数据类型和访问权限等)。该库中保存的信息也可以称为MySQL的数据字典。
重做日志缓冲
buffer pool是数据库页面的缓存,对InnoDB的任何修改操作都会首先在bp的page上进行,然后这样的页面将被标记为dirty并被放到专门的flush list上,后续将由master thread或专门的刷脏数据的线程阶段性的将这些页面写入磁盘(disk or ssd)。这样的好处是避免每次写操作都操作磁盘导致大量的随机IO,阶段性的刷脏可以将多次对页面的修改merge成一次IO操作,同时异步写入也降低了访问的时延。
上图中可以看出在每次Buffer Pool将新数据先写到redo log buffer,然后再写入redo log file后,后还需要调用一次fsync操作,因为重做日志缓冲只是把内容先写入操作系统的缓冲系统中,并没有确保直接写入到磁盘上,所以必须进行一次fsync操作。因此,磁盘的性能在一定程度上也决定了事务提交的性能。
然而,如果在脏页 dirty page(数据已经被修改的页)还未刷入磁盘时,server非正常关闭,这些修改操作将会丢失,如果写入操作正在进行,甚至会由于损坏数据文件导致数据库不可用。
解决方案为: Innodb将所有对页面的修改操作写入重做日志文件(redo log file),并在数据库启动时从此文件进行恢复操作。这样就推迟了buffer pool 页面的刷新,从而提升了数据库的吞吐,有效的降低了访问时延。
缺点: 是额外的写redo log操作的开销(顺序IO,当然很快),以及数据库启动时恢复操作所需的时间。
重做日志由两部分构成:redo log buffer、redo log file。
重做日志缓冲(redo log Buffer)一般不需要设置得很大,因为一般情况每一秒钟都会将重做日志缓冲刷新到日志文件中。可通过配置参数innodb_log_buffer_size控制日志缓冲的大小默认为8MB;
重做日志文件(redo log file)
其物理表现形式为 ib_logfile0和ib_logfile1
innodb_log_file_size 默认是5M
innodb_log_files_in_group 默认是2
redo log file的大小就是 5M*2=10M 大小。
在日志组中的每个重做日志文件的大小一致,并以循环的方式写入。innodb存储引擎先写重做日志文件0,当达到文件的后时,会切换到重做日志1,并checkpoint。以此循环
innodb事务的持久性是用 Force Log at Commit机制实现的。
一个事务可以同时修改了多个页,Write-AheadLog单个数据页的一致性,无法保证事务的持久性。
Force -log-at-commit要求当一个事务提交时,其产生所有的mini-transaction日志必须刷到持久设备中。这样即使在页数据刷盘的时候宕机,也可以通过日志进行redo恢复。
Force Log at Commit机制就是靠InnoDB存储引擎提供的参数 innodb_flush_log_at_trx_commit来控制的。
innodb_flush_log_at_trx_commit 参数可以设置的有3个:
0 只把日志缓冲写到日志文件,并且每秒刷新一次。在事务提交时不进行写入重做日志操作,该操作只在主线程中完成,
1 (这个是默认值),事务每次提交都刷新到持久化存储(必须进行一次fsync操作);
2 表示事务每次提交时把日志缓冲(redo log buffer)写入重做日志(redo log file),但是只写入文件系统缓存,不进行fsync操作。InnoDb 每秒钟做一次刷新。
额外的缓冲池
‘innodb_additional_mem_pool_size
在InnoDB存储引擎中,对内存的管理是通过一种称为内存堆的方式进行的。在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。
是 InnoDB 用来保存数据字典信息和其他内部数据结构的内存池的大小,单位是 byte,参数默认值为8M。数据库中的表数量越多,参数值应该越大,如果 InnoDB 用完了内存池中的内存,就会从操作系统中分配内存,同时在 error log 中打入报警信息。
innodb_use_sys_malloc 配置为 ON 时,innodb_additional_mem_pool_size 失效(直接从操作系统分配内存)。
innodb_additional_mem_pool_size 和 innodb_use_sys_malloc 在 MySQL 5.7.4 中移除。
后台线程:
后台线程主要有:Master Thread 、IO Thread、Purge Thread、Page Cleaner Thread。
Master Thread
核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲、undo页的回收等。
Master thread在主循环中,分两大部分操作,每秒钟的操作和每10秒钟的操作:
每秒一次的操作
日志缓冲刷新到磁盘: 即使这个事务还没有提交(总是),这点解释了为什么再大的事务commit时都很快;
合并插入缓冲(可能): 合并插入并不是每秒都发生,InnoDB会判断当前一秒内发生的IO次数是否小于5,如果是,则系统认为当前的IO压力很小,可以执行合并插入缓冲的操作。
至多刷新100个InnoDB的缓冲池的脏页到磁盘(可能) : 这个刷新100个脏页也不是每秒都在做,InnoDB引擎通过判断当前缓冲池中脏页的比例(buf_get_modified_ratio_pct)是否超过了配置文件中innodb_max_drity_pages_pct参数(默认是90,即90%),如果超过了这个阈值,InnoDB引擎认为需要做磁盘同步操作,将100个脏页写入磁盘。
每10秒一次的操作
刷新100个脏页到磁盘(可能): InnoDB引擎先判断过去10秒内磁盘的IO操作是否小于200次,如果是,认为当前磁盘有足够的IO操作能力,即将100个脏页刷新到磁盘。
合并至多5个插入缓冲(总是): 此次的合并插入缓冲操作总会执行,不同于每秒操作时可能发生的合并操作。
将日志缓冲刷新到磁盘(总是): InnoDB引擎会再次执行日志缓冲刷新到磁盘的操作,与每秒发生的操作一样。
删除无用的undo页(总是): 当对表执行update,delete操作时,原先的行会被标记为删除,但是为了一致性读的关系,需保留这些行版本的信息,在进行10S一次的删除操作时,InnoDB引擎会判断当前事务系统中已被删除的行是否可以删除,如果可以,InnoDB会立即将其删除。InnoDB每次多删除20个Undo页。
产生一个检查点(checkpoing);
IO Thread
在 InnoDB 存储引擎中大量使用了异步 IO 来处理写 IO 请求,IO Thread 的工作主要是负责这些 IO 请求的回调.。分别为write、read、insert buffer和log IO thread。线程数量可以通过参数进行调整。5.6以后的版本可以通过innodb_write_io_threads和innodb_read_io_threads来限制读写线程,而在5.6版本以前,只有一个参数innodb_file_io_threads来控制读写总线程数。
Purge threads
负责回收已经使用并分配的undo页,purge操作默认是由master thread中完成的,为了减轻master thread的工作,提高cpu使用率以及提升存储引擎的性能。用户可以在参数文件中添加如下命令来启动独立的purge thread。
innodb_purge_threads=1
从innodb1.2版本开始,可以指定多个innodb_purge_threads来进一步加快和提高undo回收速度。
page cleaner threads
Page Cleaner Thread是在InnoDB1.2.X版本中引入的。其作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成。 其目的是减轻master thread的工作以及对于用户查询线程的阻塞,进一步提高InnoDB存储引擎的性能。
InnoDB的特性:
双写(Doublewrite ) :又叫两次写
InnoDB 用双写缓冲来避免页没写完整所致的数据损坏,当一个磁盘写操作不能完整的完成时,不完整的页写入就可能发生,16KB的页可能只有一部分被写到磁盘上。这种情况被称为部分写失效(partial page write).双写缓冲在这种情况发生时可以保证数据完整性。
双写缓冲区是一个位于系统表空间的存储区域,在一些连续的块中足够保存100个页。
在写入时,InnoDB先把从缓冲池中的得到的page写入系统表空间的双写缓冲区。之后,再把page写到.ibd数据文件中相应的位置。
如果在写page的过程中发生意外崩溃,InnoDB在稍后的恢复过程中在doublewrite buffer中找到完好的page副本用于恢复
有人会认为系统恢复后,MySQL可以根据redo log进行恢复,而MySQL在恢复的过程中是检查page的checksum,checksum就是page的后事务号,发生partial page write问题时,page已经损坏,找不到该page中的事务号,就无法恢复。
innodb只能通过在doublewrite buffer中找到完好的page副本用于恢复。
double write 由两部分组成,一部分是内存中的 doublewrite buffer,大小为 2MB,另一部分是物理磁盘上共享表空间中连续的 128 个页,即 2 个区,大小同样为 2MB(128*16KB/1024=2MB)
在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过 memcpy 函数将脏页(t图中的两个page)先复制到内存中的 doublewrite buffer,之后通过 doublewrite buffer 再分两次,每次 1MB 顺序地写入共享表空间的物理磁盘上,然后马上调用 fsync 函数,同步磁盘。在这个过程中,因为 doublewrite 页是连续的,因此这个过程是顺序写的,开销不是很大
当宕机发生时,有那么几种情况:
1、磁盘还未写,此时可以通过 redo log 恢复;
2、磁盘正在进行从内存到共享表空间的写,此时数据文件中的页还没开始被写入,因此也同样可以通过 redo log 恢复;
3、磁盘正在写数据文件,此时共享表空间已经写完,可以从共享表空间拷贝页的副本到数据文件实现恢复
设置InnoDB_doublewrite=0即可关闭doublewrite buffer。