目录
2.6、重做日志缓存 redo log(与 binlog 日志的区别)
1、4种后台线程
MySQL数据库实例是一个进程,包含了多个线程,这篇文章将介绍一下InnoDB引擎的4种线程。
1.1、Master Thread
主线程。是数据库功能的核心提供者,如果主线程崩溃了,那数据库就没法正常使用了,除非重启主线程。
1.2、IO Thread
这是负责数据库读取和写入的线程,默认读取线程有4个,写入线程有4个,当然可以通过设置下图的两个参数来增加或者减少线程数。
1.3、Purge Thread
在提交了一个事务给数据库时,为了保证事务能够回滚,会在内存中缓存下该事务未执行时的状态(undo 日志),如果事务执行不成功,那么就恢复之前的状态,如果执行成功,undo日志就用不着了,要把它删除以腾出空间。Purge 线程就是做这个删除工作的。
默认有4个线程。
1.4、Page Cleaner Thread
这个线程专门用于将内存中的“脏页”刷入磁盘,使得磁盘内的数据和内存中的数据一致。内存就相当于数据库的缓存,同一个数据存在两个地方,就会出现数据不一致的情况。
默认是一个刷脏页线程,查看 performance_schema 库里面的 global_variables 表格,里面存的全局变量,包含了刷脏页线程的线程数量。
脏页线程的线程号是13,后台运行,线程名为 thread/innodb/page_cleaner_thread 。
MySQL 5.7 版本以后,支持设置多个刷脏页线程,提高脏页处理性能。设置命令,比如:SET GLOBAL innodb_page_cleaner = 3
2、InnoDB 引擎缓存
2.1、盘面,磁道,扇区,块,页的概念
先弄明白几个概念:磁盘(盘面,磁道,扇区),内存(块,页)。
下面是磁盘的结构图(网上搜的),左图是一个盘面,右图是磁盘结构,磁盘有很多个盘,每个盘有2面,2面都能存储数据,在每一个面里,有很多同心圆圈,这些圈就是一条条磁道,然后每条磁道又被分为均匀地分为很多扇区,扇区是磁盘读取数据的最小单元(通常是512B),最小单元的意思就是不管你存1B 还是 512B,都会占用一个扇区,没有说我只用半个扇区,另外半个留给其它的数据用。
还有,为什么很多地方都讲:对连续的物理存储数据进行读取的时候,速度也和内存随机读取差不多。因为在同一条磁道上,如果数据内容是连续的,那直接旋转读取就OK,之所以说磁盘的IO速度慢,其中一部分原因就在于数据存储的位置不连续的话,磁针就要多次去寻道(找磁道,并定位起始扇区),磁盘又是机械的,所以速度相对于内存来说,是慢很多的。
块:操作系统是软件层面的,它不认识什么扇区,它将磁盘空间进行了抽象(换成自己认识的概念),块就是操作系统对磁盘读取数据的最小单元,一个块通常对应于磁盘的一个扇区或者几个连续的扇区(这就看操作系统的粗细粒度)。
页:这个概念涉及操作系统寻址的知识了,64位的寻址总线,要能够管理更大的内存,就需要使用分页技术,每一页代表一段连续的内存空间,一个地址对应一页,先用地址去找到对应页,至于里面的内容,就用偏移量去找。所以页是内存层面的概念。
到此,就比较明了了,比如,我们的软件要读取一页的数据,会将指令交给操作系统,操作系统会按照块为单位进行读取,底层的驱动则会驱动磁盘读取出对应数量的扇区的数据。一页对应若干块,一块通常对应若干扇区。
InnoDB引擎是软件,用缓存技术在内存中缓存一小部分数据库里的数据,提高性能,所以InnoDB的脑子里只有页,并且它也是将数据库按照页的方式进行管理的,页的大小默认是16k = 16384 bytes。
2.2、InnoDB缓存的基本原理
由于InnoDB是基于磁盘的,但是磁盘的IO速度慢,为了提升数据库性能,InnoDB会在内存中缓存一部分数据,当用户添加更新数据时,先看是否命中缓存,如果是,直接更新缓存中的数据,等到刷脏页时机触发时,再将缓存中的新数据写入磁盘;当用户读取数据时,也是先看是否命中缓存,如果是,直接从缓存里读取数据即可。
InnoDB缓存在很多教材里被叫作缓存池或者缓冲池,buffer_pool。在performance_schema库的global_variables表里,存放着全局变量,下图是关于缓存池的配置参数。
innodb_buffer_poop_chunk_size:缓存池大小可以动态修改,但是要以innodb_buffer_poop_chunk_size为单位,进行多少倍增加或者减少。
innodb_buffer_pool_instances:缓存池实例数量,默认为1。建议设置为CPU个数。
innodb_buffer_pool_size:所有缓存池的总大小,我这个版本的mysql默认为 128M = 134217728 Bytes。如果要扩大,建议不要超过物理内存的80%,一般就设置在50% ~ 80%。
缓存池中存储的数据类型:索引页、数据页、undo日志页、插入缓存、自适应哈希索引、锁信息、数据字典信息等等,其中,索引页和数据页占了大部分,InnoDB里的索引是聚集索引,因此索引里面既存了索引值,又存储了记录;而这里的数据页不是指用户的数据,而是指 undo数据,系统数据,blob数据等等。
缓存池可以有多个,每个缓存池被称为缓存池实例,InnoDB可以有多个缓存池实例,每个页根据哈希值取模来决定要存储到那个缓存池实例(跟redis数据存储于哪个槽有点像哦~~)。innodb_buffer_pool_instances 这个全局变量就是缓存池的数量,每个缓存池的大小 = innodb_buffer_pool_size / innodb_buffer_pool_instances。为什么要分为多个缓存池呢?因为要提升数据库的并发性能,如果是一个缓存池的话,多个线程会竞争缓存池,影响性能,如果分为多个缓存池实例的话,减小竞争,这也正好理解了“为什么建议缓存池实例数量设置为CPU个数”。
每一个缓存池实例的状态信息够存储在information_schema库的 INNODB_BUFFER_POOL_STATS 里,每个实例都有个Pool_id,用于区分缓存池实例。可以查看这个表。
一个缓存池实例的结构如下图(借用的,好几处文章都看见这个图了,也不知道谁是原作者,勿喷),它不是单纯地存储每一个页,试想,每一页都存进去了,怎么管理呢?哪一页在哪个位置?这一页数据代表什么?因此,为了方便管理,每一页都有对应的一个控制块,控制块在缓存池的一端,缓存页在另一端,块里的内容就是该页所属的表空间编号(space id)、页号(page number)、页在Buffer Pool中的地址,一些锁信息以及LSN信息等等,每一个控制块的大小都是相同的,每一页的大小也是相同的,所以我们要好好计算 innodb_buffer_pool_instances 和 innodb_buffer_pool_size,以避免出现下图的内存碎片。
缓存池看起来有点像一个列表。
2.3、缓存池空间管理——Free列表
当需要缓存一页数据时,我们怎么指定缓存池中哪些页(缓存池的空间是按页划分管理的)是空白的,不然万一存在某个有数据的页空间去,覆盖掉原先的数据就惨了。
所以,InnoDB用了一个Free列表去存储缓存池中所有空白的页空间情况, 比如每一页的起始地址等等。每次需要在缓存池存储页的时候,都会看看Free列表是否有空闲的页,如果没有,那就只有清空一个页,腾出一个位置给新页。
一个缓存池实例对应一个Free表。
2.4、页的管理算法——LRU算法
一个缓存池实例就那么大点,存储的页有限,因此,有一个算法LRU(最近最久未被使用算法)被用来管理缓存池,决定哪些页被清出缓存池,哪些页被加入缓存池。InnoDB用了一个LRU列表来存储缓存页标识,列表里对应的缓存页代表目前还在缓存池中,越是靠近列表头,说明该页越是最近被频繁使用,越在列表尾部,说明越是不怎么被使用。并且,还定义了一个midpoint位置,这个位置之前代表new页(频繁被用),这个位置之后(包括midpoint本身)代表old页(不怎么被用),old页容易被清出缓存池。
这个midpoint位置默认处于列表尾部的37%处,全局变量 innodb_old_blocks_pct 就代表这个位置。每当有一个页需要存储到缓存池时,不是放在列表头,而是放在midpoint位置,如果此时缓存池已满,就将列表末端页删除腾出一个位置给新页,如果此时缓存还没有满,那就直接放在midpoint位置。
old页就永远是old页吗?不是,它转为new页的时机需要根据innodb_old_blocking_time(单位毫秒),默认是1秒,当用户读取数据库数据时,假如就读取一页,这一页数据被缓存在缓存池中midpoint位置,从此刻开始的innodb_old_blocking_time时间内,不管用户读取它多少次,都不会变为new页放到列表前面去,只有当innodb_old_blocking_time之后,一旦用户再次读取它,它就会变为new页了。
我们可以调整下面2个参数来减小热点数据被清出的概率。
另外,Free表大小 + LRU表大小 = 缓存池页数量。 一个缓存池实例对应一个LRU表,在information_schema库里的INNODB_BUFFER_PAGE_LRU表里可以查看LRU表格情况。
2.5、脏页的管理——Flush列表
如果用户要修改某数据,先是看数据是否命中缓存池,如果命中,会将新值更新到缓存池,此时,缓存池里的数据和磁盘里的数据不一致了,就需要刷脏页,将缓存池里的新数据写入磁盘,使其一致。但是我们知道磁盘IO速度慢,如果用户频繁地更改数据,那岂不是影响性能。
因此InnoDB用了一个Flush列表存储脏页信息,每隔一段时间,或者时机到了,就会触发一次刷脏页,将Flush表的脏页一起处理,Flush表的信息在information_schema库的INNODB_BUFFER_PAGE_LRU里,和LRU表信息放在一起的,只是Flush表的信息需要增加查询条件:OLDEST_MODIFICATION > 0。
在刷脏页的时候,是有专门的线程去执行的,我们考虑一下按什么样的顺序去刷脏页,我个人想法有2种:1、某一页被修改的次数,次数越多,说明与磁盘的数据越不一致,越是急迫需要刷新(第一种好像是InnoDB的刷新策略);2、某一页第一次被修改的时间,越早被修改过,说明磁盘里的数据越不实时,越应该被优先刷新。
2.6、重做日志缓存 redo log(与 binlog 日志的区别)
先讲讲一个事务的执行过程吧,首先,事务开始,对要修改的数据上锁,如果数据没有在缓存池内,就需要将数据从磁盘里读到缓存池内,如果数据已经在缓存池就更好了,然后将事务对页的修改进行redo日志记录,存放在redo日志缓存中,缓存好了之后,才开始修改缓存池中的数据,修改完毕后,事务被提交,表明事务执行完了,事务的提交会触发将redo日志缓存刷新到磁盘中的redo日志文件中。
redo日志缓存的作用:当用户将要执行一个事务时,会在事务执行之前,将事务对页的修改存入redo日志缓存中,等事务修改了数据后,并提交事务,然后才触发将redo日志缓存里的数据写入磁盘中的redo日志文件中,以保存起来。
为什么要redo日志呢?因为事务执行完毕并提交了事务后,修改的数据还在缓存池里,磁盘还没有刷新,如果此时系统崩溃了或者断电了,缓存池里的数据都没了,该事务就等于没有执行。如果有了redo日志文件(磁盘里的),我们就可以恢复事务执行的结果了。因此,一旦缓存池的页被刷新到磁盘后,其对应的redo日志数据就没用了,因为最新的数据已经持久化到磁盘了,不会丢失了,自然就用不着redo日志了,这种redo日志被叫作无用redo日志。
那redo日志缓存区到底有多大呢?我这个版本的Mysql给的默认值是16K = 16777216 bytes。其它资料里说,8M基本上就够存储大部分应用的事务redo日志了。
redo日志缓存刷入redo日志文件的时机:1、定期(master线程做); 2、每个事务提交时,就会触发redo日志缓存刷入,及时将该事务的redo日志存入磁盘;3、当redo日志缓存空间剩余不到1/2时,也会触发。
关于第二个时机,再具体讲一点,redo 缓存内容会先写入系统文件缓存中,在fsysn操作后,才会同步到磁盘中,下面有个全局参数,默认为1,表示每个事务提交时,都会触发执行一个 fsysn 操作,即将 redo 日志刷入磁盘,但是我们还可以将参数设置为 0, 表示提交事务不触发redo 日志刷新, 设置为 2, 表示事务提交时,仅会把 redo 日志缓存写入 系统文件缓存,不会执行 fsysn 操作,我想什么时候 fsysn 操作会被执行呢? 可能这就是操作系统的策略了吧,如果数据库崩了,那系统文件缓存里还有redo数据,如果操作系统崩了或者重启,那就没了。
另外,还有一个binlog日志,这个日志是应用层面的,和数据库引擎没有关系,binlog 日志的作用目的和redo log差不多,但是也有区别,binlog 用于存储对数据库的所有改动sql,存的sql级别的,而且是所有的,而 redo 日志记录的是物理内存级别上的修改,更加底层。
为什么要有2种呢,因为 redo 日志记录的是底层级别的修改记录,一旦用redo 日志恢复起来,redo日志里的修改操作被执行起来的并发性更好,而 binlog 是应用层面的修改操作,执行起来效率更低。
2.7、额外内存池
缓存池用于存储页,对于每一页,还需要存储它的信息,比如这一页对应磁盘里的是那一部分的空间,这一页在缓存池中的地址等等,另外还有LRU表,Free表,Flush表,这些内容都存放在哪里呢?不是存在缓存池,而是存在额外内存池中。
因此,不要忽略额外内存池,因为如果缓存池越大,额外内存池需要记录的数据也越多,应该相应的扩大额外内存池的空间。
在MySQL 5.7.4 版本之前,全局参数 innodb_additional_mem_pool_size 表示额外内存池大小, innodb_use_sys_malloc 表示是否让操作系统替它分配内存,ON表示要让,OFF表示不让,InnoDB自己分配。
在MySQL 5.7.4 版本之后, innodb_additional_mem_pool_size 和 innodb_use_sys_malloc 被取消了,直接交由操作系统来分配额外内存池,你要多大,操作系统就给你分配多大,不用我们去关心了。
3、Checkpoint技术——刷脏页技术
上面已经简单的介绍了刷脏页,这一节详细地说一说。
1、缓存某个页的时候,如果某个缓冲池已经满了,此时会根据LRU算法将LRU列表尾的页清除掉,如果该页是脏页,在清除之前,要进行一次刷脏页操作。 (换一种专业说法,首先查看Free列表是否有空闲页,如果没有,那就根据LRU算法将LRU列表末尾的页删除以腾出空间,在删除之前,看看该末尾页是否在Flush列表中,如果是,说明该末尾页是在脏页,触发一次刷脏页,刷新后,再删除该末尾页腾出空间)。
2、磁盘里的redo日志文件大小有限,因此redo日志数据会循环地进行覆盖存储,但是只能覆盖那些无用redo日志,如果此时没有无用redo日志给新的日志覆盖,怎么办呢?会立即触发一次刷脏页,部分脏页被刷后,这些脏页对应的redo日志就没用了,那新日志就可以覆盖了。
刷脏页并不是每次都把所有的脏页刷新,而是刷新一部分脏页,保证数据库的性能,如果每次都全部刷新的话,岂不是用户要等数据库刷完了才能进行访问和操作。
InnoDB 有两种刷脏页方式(不是二选一,而是并存的):Sharp Checkpoint 和 Fuzzy CheckPoint。
Sharp Checkpoint 是在数据库关闭的时候执行,默认把所有的脏页刷新。 全局参数 innodb_fast_shutdown 代表了 Sharp Checkpoint执行的方式,有3个值(0,1,2)。0表示在innodb关闭的时候,需要purge all, merge insert buffer,flush dirty pages。这是最慢的一种关闭方式,但是restart的时候也是最快的。1表示在innodb关闭的时候,它不需要purge all,merge insert buffer,只需要flush dirty page。 2表示在innodb关闭的时候,它不需要purge all,merge insert buffer,也不进行flush dirty page,只将log buffer里面的日志flush到log files。因此等下进行恢复的时候它是最耗时的。
Fuzzy CheckPoint 是数据库运行期间刷脏页机制,前文基本上讲得差不多了,总结为:1、master thread 定期刷脏页; 2、缓存池中的脏页刷新腾出空间; 3、redo日志文件无法继续覆盖地进行存储,而刷脏页。