MySQL数据库——InnoDB引擎(四种线程,缓存池)

目录

 

1、4种后台线程

1.1、Master Thread

1.2、IO Thread

1.3、Purge Thread

1.4、Page Cleaner Thread

2、InnoDB 引擎缓存

2.1、盘面,磁道,扇区,块,页的概念

2.2、InnoDB缓存的基本原理

2.3、缓存池空间管理——Free列表

2.4、页的管理算法——LRU算法

2.5、脏页的管理——Flush列表

2.6、重做日志缓存 redo log(与 binlog 日志的区别)

2.7、额外内存池

3、Checkpoint技术——刷脏页技术


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日志文件无法继续覆盖地进行存储,而刷脏页。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章