时间:2023-03-04 10:51:17 | 栏目:Mysql | 点击:次
咱们在使用mysql的时候,比如很简单的select * from table;这条语句,具体查询数据其实是在存储引擎中实现的,数据库中的数据实际上最终都是要存放在磁盘文件上的,如果每次查询都直接从磁盘里面查询,这样势必会很影响性能,所以一定是先把数据从磁盘中取出,然后放在内存中,下次查询直接从内存中来取。
但是一台机器中往往不是只有mysql一个进程在运行的,很多个进程都需要使用内存,所以mysql中会有一个专门的内存区域来处理这些数据,这个专门为mysql准备的区域,就叫buffer pool。
buffer pool是mysql一个非常关键的核心组件。
如下图所示:
在对数据库执行增删改操作的时候,不可能直接更新磁盘上的数据的,因为如果你对磁盘进行随机读写操作,那速度是相当的慢,随便一个大磁盘文件的随机读写操作,可能都要几百毫秒。如果要是那么搞的话,可能你的数据库每秒也就只能处理几百个请求了! 在对数据库执行增删改操作的时候,实际上主要都是针对内存里的Buffer Pool中的数据进行的,也就是实际上主要是对**数据库的内存(Buffer Pool)**里的数据结构进行了增删改。
如下图所示:
操作内存的主要问题就是在数据库的内存里执行了一堆增删改的操作,内存数据是更新了,但是这个时候如果数据库突然崩溃了,那么内存里更新好的数据不是都没了吗? MySQL就怕这个问题,所以引入了一个redo log机制,你在对内存里的数据进行增删改的时候,他同时会把增删改对应的日志写入redo log中。
如下图:
万一数据库突然崩溃了,没关系,只要从redo log日志文件里读取出来你之前做过哪些增删改操作,瞬间就可以重新把这些增删改操作在你的内存里执行一遍,这就可以恢复出来你之前做过哪些增删改操作了。
当然对于数据更新的过程,它是有一套严密的步骤的,还涉及到undo log、binlog、提交事务、buffer pool脏数据刷回磁盘等等。
小结:
Buffer Pool就是数据库的一个内存组件,里面缓存了磁盘上的真实数据,然后我们的系统对数据库执行的增删改操作,其实主要就是对这个内存数据结构中的缓存数据执行的。通过这种方式,保证每个更新请求,尽量就是只更新内存,然后往磁盘顺序写日志文件。
更新内存的性能是极高的,然后顺序写磁盘上的日志文件的性能也是比较高的,因为顺序写磁盘文件,他的性能要远高于随机读写磁盘文件。
以查询语句为例
在正式讲解buffer pool 之前,我们先搞清楚buffer pool缓冲池和查询缓存(query cache)简称Qcache的区别。
如果将Mysql分为Server层和存储引擎层两大部分,那么Qcache位于Server层,Buffer Pool位于存储引擎层。
如果Mysql 查询缓存功能是打开的,那么当一个sql进入Mysql Server之后,Mysql Server首先会从查询缓存中查看是否曾经执行过这个SQL,如果曾经执行过的话,曾经执行的查询结果之前会以key-value的形式保存在查询缓存中。key是sql语句,value是查询结果。我们将这个过程称为查询缓存。查询缓存会被所有的session共享。
如果查询缓存中没有你要找的数据的话,MySQL才会执行后续的逻辑,通过存储引擎将数据检索出来。
MySQL查询缓存是查询结果缓存。它将以SEL开头的查询与哈希表进行比较,如果匹配,则返回上一次查询的结果。进行匹配时,查询必须逐字节匹配,例如 SELECT * FROM t1; 不等于select * from t1;,此外,一些不确定的查询结果无法被缓存,任何对表的修改都会导致这些表的所有缓存无效(只要有一个sql update了该表,那么表的查询缓存就会失效)。因此,适用于查询缓存的最理想的方案是只读,特别是需要检查数百万行后仅返回数行的复杂查询。如果你的查询符合这样一个特点,开启查询缓存会提升你的查询性能。
MySQL查询缓存的目的是为了提升查询性能,但它本身也是有性能开销的。需要在合适的业务场景下(读写压力模型)使用,不合适的业务场景不但不能提升查询性能,查询缓存反而会变成MySQL的瓶颈。
查询缓存的开销主要有:
查询缓存的缺点:
首先,查询缓存的效果取决于缓存的命中率,只有命中缓存的查询效果才能有改善,因此无法预测其性能。只要有一个sql update了该表,那么表的查询缓存就会失效,所以当你的业务对表CRUD的比例不相上下,那么查询缓存会影响应用的吞吐效率。
其次,查询缓存的另一个大问题是它受到单个互斥锁的保护。在具有多个内核的服务器上,大量查询会导致大量的互斥锁争用。
注意:在mysql8.0的版本中,已经将查询缓存模块删除了。
我们先了解一下数据页这个概念。它是 MySQL 抽象出来的数据单位,磁盘文件中就是存放了很多数据页,每个数据页里存放了很多行数据。
默认情况下,数据页的大小是 16kb。
所以对应的,在 Buffer Pool 中,也是以数据页为数据单位,存放着很多数据。但是我们通常叫做缓存页,因为 Buffer Pool 毕竟是一个缓冲池,并且里面的数据都是从磁盘文件中缓存到内存中。它和磁盘文件中数据页是一一对应的。
假设我们要更新一行数据,此时数据库会找到这行数据所在的数据页,然后从磁盘文件里把这行数据所在的数据页直接给加载到Buffer Pool里去。如下图。
每个缓存页都会对应着一个描述数据块,里面包含数据页所属的表空间、数据页的编号,缓存页在 Buffer Pool 中的地址等等。
描述数据块本身也是一块数据,它的大小大概是缓存页大小的5%左右。假设你设置的buffer pool大小是128MB,实际上Buffer Pool真正的最终大小会超出一些,可能有个130多MB的样子,因为他里面还要存放每个缓存页的描述数据。
在Buffer Pool中,每个缓存页的描述数据放在最前面,然后各个缓存页放在后面。
所以Buffer Pool实际看起来大概如下:
buffer pool通常由数个内存块加上一组控制结构体对象组成。内存块的个数取决于buffer pool instance的个数,不过在5.7版本中开始默认以128M(可配置)的chunk单位分配内存块,这样做的目的是为了支持buffer pool的在线动态调整大小。
Buffer Pool默认情况下是128MB,还是有一点偏小了,我们实际生产环境下完全可以对Buffer Pool进行调整。 比如我们的数据库如果是16核32G的机器,那么你就可以给Buffer Pool分配个2GB的内存。
主要配置参数如下:
这里面有个关系要确定一下,最好按照这个设置 innodb_buffer_pool_size=innodb_buffer_pool_chunk_size * innodb_buffer_pool_instancesN(N>=1);
当buffer pool比较大的时候(超过1G),innodb会把buffer pool划分成几个instances,这样可以提高读写操作的并发,减少竞争。读写page都使用hash函数分配给一个instances。
当增加或者减少buffer pool大小的时候,实际上是操作的chunk。buffer pool的大小必须是innodb_buffer_pool_chunk_sizeinnodb_buffer_pool_instances的整数倍,如果不是的话,innodb会自动调整的。
比如:
如果指定的buffer pool size大小是9G,instances的个数是16,chunk默认的大小是128M,那么buffer会自动调整为10G。因为9216MB/16/128MB = 4.5不是整数倍,会自动调整为10240MB/16/128MB=5整数倍
理想情况下,在给服务器的其他进程留下足够的内存空间的情况下,Buffer Pool Size 应该设置的尽可能大。当 Buffer Pool Size 设置的足够大时,整个数据库就相当于存储在内存当中,当读取一次数据到 Buffer Pool Size 以后,后续的读操作就不用再访问磁盘。
设置方式:
当数据库已经启动的情况下,我们可以通过在线调整的方式修改 Buffer Pool Size 的大小。
通过以下语句:SET GLOBAL innodb_buffer_pool_size=402653184;
当执行这个语句以后,并不会立即生效,而是要等所有的事务全部执行成功以后才会生效;新的连接和事务必须等其他事务完全执行成功以后,Buffer Pool Size 设置生效以后才能够连接成功,不然会一直处于等待状态。
期间,Buffer Pool Size 要完成碎片整理,去除缓存 page 等等操作。在执行增加或者减少 Buffer Pool Size 的操作时,操作会作为一个执行块执行,innodb_buffer_pool_chunk_size 的大小会定义一个执行块的大小,默认的情况下,这个值是128M。
Buffer Pool Size 的大小最好设置为 innodb_buffer_pool_chunk_size/ innodb_buffer_pool_instances 的整数倍,而且是大于等于1。
如果我们要查 Buffer Pool 的状态的话,可以使用一下sql:SHOW STATUS WHERE Variable_name='InnoDB_buffer_pool_resize_status';
可以帮我们查看到状态。我们可以看一下增加 Buffer Pool 的时候的一个过程,再看一下减少的时候的日志,其实还是很好理解的,我们可以看成每次增大或者减少 Buffer Pool 的时候就是进行 innodb_buffer_pool_chunk 的增加或者释放,按照 innodb_buffer_pool_chunk_size 设定值的大小增加或者释放执行块。
在64位操作系统的情况下,可以拆分缓冲池成多个部分,这样可以在高并发的情况下最大可能的减少争用。
配置多个 Buffer Pool Instances 能在很大程度上能够提高 MySQL 在高并发的情况下处理事物的性能,优化不同连接读取缓冲页的争用。
我们可以通过设置 innodb_buffer_pool_instances 来设置 Buffer Pool Instances。当 InnoDB Buffer Pool 足够大的时候,你能够从内存中读取时候能有一个较好的性能,但是也有可能碰到多个线程同时请求缓冲池的瓶颈。这个时候设置多个 Buffer Pool Instances 能够尽量减少连接的争用。
这能够保证每次从内存读取的页都对应一个 Buffer Pool Instances,而且这种对应关系是一个随机的关系。并不是热数据存放在一个 Buffer Pool Instances下,内部也是通过 hash 算法来实现这个随机数的。每一个 Buffer Pool Instances 都有自己的 free lists,LRU 和其他的一些 Buffer Pool 的数据结构,各个 Buffer Pool Instances 是相对独立的。
innodb_buffer_pool_instances 的设置必须大于1才算得上是多配置,但是这个功能起作用的前提是innodb_buffer_pool_size 的大小必须大于1G,理想情况下 innodb_buffer_pool_instances 的每一个 instance 都保证在1G以上。
当你的数据库启动之后,你随时可以通过上述命令,去查看当前innodb里的一些具体情况,执行:SHOW ENGINE INNODB STATUS
就可以了。此时你可能会看到如下一系列的东西:
下面解释一下这里的东西,主要讲解这里跟buffer pool相关的一些东西。
缓冲池也是有大小限制的,那么既然缓冲池有大小限制的,每次都读入的数据页怎么来管理呢?这里我们来聊聊缓冲池的空间管理,其实对缓冲池进行管理的关键部分是如何安排进池的数据并且按照一定的策略淘汰池中的数据,保证池中的数据不溢出,同时还能保证常用数据留在池子中。
缓冲池是基于传统的 LRU 方法来进行缓存页管理的,我们先来看下如果使用 LRU 是如何管理的。
LRU,全称是 Least Recently Used,中文名字叫作「最近最少使用」。从名字上就很容易理解了。
这里分两种情况:
这种情况下会将对应的缓存页放到 LRU 链表的头部,无需从磁盘再进行读取,也无需淘汰其它缓存页。
如下图所示,如果要访问的数据在 6 号页中,则将 6 号页放到链表头部即可,这种情况下没有缓存页被淘汰。
缓存页不在缓冲中,这时候就需要从磁盘中读入对应的数据页,将其放置在链表头部,同时淘汰掉末尾的缓存页
如下图所示,如果要访问的数据在 60 号页中,60 号页不在缓冲池中,此时加载进来放到链表的头部,同时淘汰掉末尾的 17 号缓存页。
简单的LRU算法会带来几个问题:
传统的 LRU 方法并不能满足缓冲池的空间管理。因此,Msyql 基于 LRU 设计了冷热数据分离的处理方案。
也就是将 LRU 链表分为两部分,一部分为热数据区域,一部分为冷数据区域。
当数据页第一次被加载到缓冲池中的时候,先将其放到冷数据区域的链表头部,1s(由 innodb_old_blocks_time 参数控制) 后该缓存页被访问了再将其移至热数据区域的链表头部。
为什么要等 1s 后才将其移至热数据区域呢?
如果数据页刚被加载到冷数据区就被访问了,之后再也不访问它了呢?这不就造成热数据区的浪费了吗?要是 1s 后不访问了,说明之后可能也不会去频繁访问它,也就没有移至热缓冲区的必要了。当缓存页不够的时候,从冷数据区淘汰它们就行了。
另一种情况,当我的数据页已经在热缓冲区了,是不是缓存页只要被访问了就将其插到链表头部呢?不用我说你肯定也觉得不合理。热数据区域里的缓存页是会被经常访问的,如果每访问一个缓存页就插入一次链表头,那整个热缓冲区里就异常骚动了,所以,Mysql 中优化为热数据区的后 3/4 部分被访问后才将其移动到链表头部去,对于前 1/4 部分的缓存页被访问了不会进行移动。
预读是mysql提高性能的一个重要的特性。预读就是 IO 异步读取多个页数据读入 Buffer Pool 的一个过程,并且这些页被认为是很快就会被读取到的。InnoDB使用两种预读算法来提高I/O性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)
为了区分这两种预读的方式,我们可以把线性预读放到以extent为单位,而随机预读放到以extent中的page为单位。线性预读着眼于将下一个extent提前读取到buffer pool中,而随机预读着眼于将当前extent中的剩余的page提前读取到buffer pool中。
Linear线性预读
线性预读的单位是extend,一个extend中有64个page。线性预读的一个重要参数innodb_read_ahead_threshold,是指在连续访问多少个页面之后,把下一个extend读入到buffer pool中,不过预读是一个异步的操作。当然这个参数不能超过64,因为一个extend最多只有64个页面。
例如,innodb_read_ahead_threshold = 56,就是指在连续访问了一个extend的56个页面之后把下一个extend读入到buffer pool中。在添加此参数之前,InnoDB仅计算当它在当前范围的最后一页中读取时是否为整个下一个范围发出异步预取请求。
Random随机预读
随机预读方式则是表示当同一个extent中的一些page在buffer pool中发现时,Innodb会将该extent中的剩余page一并读到buffer pool中。由于随机预读方式给innodb code带来了一些不必要的复杂性,同时在性能也存在不稳定性,在5.5中已经将这种预读方式废弃,默认是OFF。若要启用此功能,即将配置变量设置innodb_random_read_ahead为ON。
Buffer Pool 是Innodb 内存中的的一块占比较大的区域,用来缓存表和索引数据。众所周知,从内存访问会比从磁盘访问快很多。为了提高数据的读取速度,Buffer Pool 会通过三种Page 和链表来管理这些经常访问的数据,保证热数据不被置换出Buffer Pool。
如上图所示,是Buffer Pool里面的LRU(least recently used)链表。LRU链表是被一种叫做最近最少使用的算法管理。
LRU链表被分成两部分:
默认情况下
如果一个数据页已经处于Young 链表,当它再次被访问的时候,只有当其处于Young 链表长度的1/4(大约值)之后,才会被移动到Young 链表的头部。这样做的目的是减少对LRU 链表的修改,因为LRU 链表的目标是保证经常被访问的数据页不会被驱逐出去。
移动到young链表的时间配置:
innodb_old_blocks_time 控制的Old 链表头部页面的转移策略。该Page需要在Old 链表停留超过innodb_old_blocks_time 时间,之后再次被访问,才会移动到Young 链表。这么操作是避免Young 链表被那些只在innodb_old_blocks_time时间间隔内频繁访问,之后就不被访问的页面塞满,从而有效的保护Young 链表。
在全表扫描或者全索引扫描的时候,Innodb会将大量的页面写入LRU 链表的Mid Point位置,并且只在短时间内访问几次之后就不再访问了。设置innodb_old_blocks_time的时间窗口可以有效的保护Young List,保证了真正的频繁访问的页面不被驱逐。
innodb_old_blocks_time 单位是毫秒,默认值是1000。调大该值提高了从Old链表移动到Young链表的难度,会促使更多页面被移动到Old 链表,老化,从而被驱逐。
当扫描的表很大,Buffer Pool都放不下时,可以将innodb_old_blocks_pct设置为较小的值,这样只读取一次的数据页就不会占据大部分的Buffer Pool。例如,设置innodb_old_blocks_pct = 5,会将仅读取一次的数据页在Buffer Pool的占用限制为5%。
当经常扫描一些小表时,这些页面在Buffer Pool移动的开销较小,我们可以适当的调大innodb_old_blocks_pct,例如设置innodb_old_blocks_pct = 50。
在SHOW ENGINE INNODB STATUS
里面提供了Buffer Pool一些监控指标,有几个我们需要关注一下:
脏页清除线程:
SQL 的增删改查都在 Buffer Pool 中执行,慢慢地,Buffer Pool 中的缓存页因为不断被修改而导致和磁盘文件中的数据不一致了,也就是 Buffer Pool 中会有很多个脏页,脏页里面很多脏数据。
所以,MySQL 会有一条后台线程,定时地将 Buffer Pool 中的脏页刷回到磁盘文件中。但是,后台线程怎么知道哪些缓存页是脏页呢,不可能将全部的缓存页都往磁盘中刷吧,这会导致 MySQL 暂停一段时间。
MySQL 是怎么判断脏页的
我们引入一个和 free 链表类似的 flush 链表。他的本质也是通过缓存页的描述数据块中的两个指针,让修改过的缓存页的描述数据块能串成一个双向链表,这两指针大家可以认为是 flush_pre 指针和 flush_next 指针。
下面我用伪代码来描述一下:
DescriptionDataBlock{ block_id = block1; // free 链表的 free_pre = null; free_next = null; // flush 链表的 flush_pre = null; flush_next = block2; }
flush 链表也有对应的基础节点,也是包含链表的头节点和尾节点,还有就是修改过的缓存页的数量。
FlushListBaseNode{ start = block1; end = block2; count = 2; }
到这里,我们都知道,SQL 的增删改都会使得缓存页变为脏页,此时会修改脏页对应的描述数据块的 flush_pre 指针和 flush_next 指针,使得描述数据块加入到 flush 链表中,之后 MySQL 的后台线程就可以将这个脏页刷回到磁盘中。
使用原理
free 链表,它是一个双向链表,链表的每个节点就是一个个空闲的缓存页对应的描述数据块。他本身其实就是由 Buffer Pool 里的描述数据块组成的,你可以认为是每个描述数据块里都有两个指针,一个是 free_pre 指针,一个是 free_next 指针,分别指向自己的上一个 free 链表的节点,以及下一个 free 链表的节点。
通过 Buffer Pool 中的描述数据块的 free_pre 和 free_next 两个指针,就可以把所有的描述数据块串成一个 free 链表。
下面我们可以用伪代码来描述一下 free 链表中描述数据块节点的数据结构:
DescriptionDataBlock{ block_id = block1; free_pre = null; free_next = block2; }
free 链表有一个基础节点,他会引用链表的头节点和尾节点,里面还存储了链表中有多少个描述数据块的节点,也就是有多少个空闲的缓存页。
下面我们也用伪代码来描述一下基础节点的数据结构:
FreeListBaseNode{ start = block01; end = block03; count = 2; }
到此,free 链表就介绍完了。上面我们也介绍了 MySQL 启动时 Buffer Pool 的初始流程,接下来,我会将结合刚介绍完的 free 链表,讲解一下 SQL 进来时,磁盘数据页读取到 Buffer Pool 的缓存页的过程。
但是,我们先要了解一下一个新概念:数据页缓存哈希表,它的 key 是表空间+数据页号,而 value 是对应缓存页的地址。
描述如图所示:
磁盘数据页读取到 Buffer Pool 的缓存页的过程
LRU链表:
Flush链表:
Innodb 的策略是在运行过程中尽可能的多占用内存,因此未被使用的页面会很少。当我们读取的数据不在Buffer Pool里面时,就需要申请一个空闲页来存放。如果没有足够的空闲页时,就必须从LRU 链表的尾部淘汰页面。如果该页面是干净的,可以直接拿来用,如果是脏页,就需要进行刷脏操作,将内存数据Flush到磁盘。
所以,如果出现以下情况,是很容易影响MySQL实例的性能:
innodb_io_capacity 参数定义了Innodb 后台任务的IO能力,例如刷脏操作还有Change Buffer的merge操作等。
Innodb 的三种Page和链表的设计,保证了我们需要的热数据常驻在内存,及时淘汰不需要的数据,提升了我们的查询速度,同时不同的刷脏策略也提高了我们的恢复速度,保证了数据安全。