Mysql探索(四)之InnoDB的内存buffer和I/O线程

mysql执行sql都是先数据到内存中,然后再将内存中的修改过的数据刷到磁盘中,Innodb的内存结构是映射磁盘的页结构的,那都会有哪些划分呢?有内存数据,则需要保证缓存一致性问题,Innodb是怎么做到的呢?mysql的IO线程,Innodb的三大特性啊这些等等,都来探究下。

内存结构

一个程序要想高速,那么必不可少的要用到内存,mysql也不例外,不然用磁盘的速度就好比乌龟的速度,都不知要被淘汰多少次了。既然用到了内存,那么必然要对内存进行管理。
Innodb先向我们电脑申请一块连续的运行内存,把它命名为Buffer Pool(缓存池),然后把后续需要到的mysql数据加载到Buffer Pool这块内存中,那么Innodb就面临第一个问题,该申请多大的内存空间呢?
可以使用下面命令查看Innodb默认分配的大小:

mysql> show variables like '%innodb_buffer_pool_size%';

buffer_pool_size
可以看到Innodb分配的内存其实是很小的,128M能存储的数据并不多,特别是当数据量非常大的时候。
Innodb的一个页内存是16kb,134217728/1024/16 = 8192个,总共能够存储8千多个页。

q : 建议分配多少的内存给buffer pool呢?
官网buffer pool内存给了建议: 建议分配物理机内存的80%大小,但是我觉得太大了,给个70%就好了,下图也有说,系统给分配的空间会比buffer pool的大小大10%,所以我们还得留下空间预防。
官方建议值
图中也说了:当buffer pool的大小大于1G,设置innodb_buffer_pool_instances的参数大于1会提高服务器的伸缩性,这个参数是指buffer pool的实例,即个数。
系统分配buffer pool内存时是可以按块分配的,默认每块是128M,系统参数是innodb_buffer_pool_chunk_size,在扩容的时候也是按着这个大小来的。

这时候头脑里应该有这样的一个架构:
初始架构1
下方是官方的Innodb架构图:
Innodb架构图
架构图中的左边就是buffer pool内存。

其实内存划分不止是只有buffer pool,只是buffer pool是最主要的核心,简单列一下其他的内存区域用途:
1. Innodb buffer pool
用来缓存表数据,索引,插入缓存,数据字典等信息。
2. Innodb log buffer
用来缓存事务的数据,即redo log buffer数据
3. sort buffer
用于sql语句在内存中的临时排序
4. join buffer
用于表连接,用于BKA

BKA (Batched Key Access):是一种用于表连接的技术,收集第一张表需要进行join的列id,然后排序,将这些id集合作为key发送给MRR接口,使得随机查询变为顺序查询,提高join连接效率。

5. read_rnd_buffer
用于Innodb随机读的,做MRR

MRR (Multi-Range Read Optimization):是一种将随机I/O变为顺序I/O的技术,将普通索引上的需要回表的主键id先存储在read_rnd_buffer缓存中,然后排序,最后将排好序的id集合去访问表中数据,这样就把随机I/O变为了顺序I/O。

6. tmp_table_size
如果sql排序或者分组时没有用到索引,则会使用该临时表空间

接下来就好好填补下buffer pool。

Buffer Pool

在磁盘上存储的就是页,将页加载到内存中就是buffer,即buffer pool是由一个个buffer组成的。
buffer-页
刚开始这些buffer都是空闲的,使用久了就会演变成3种类型:
free buffer:空闲buffer,就是未被使用的。
clean buffer:和磁盘上页的数据一样的buffer。
dirty buffer:buffer上的数据和磁盘上页的不一样。

buffer的3大管理链表

当我们的mysql启动的时候呢,buffer pool中就只有一堆free buffer,用链表将这些buffer连接起来进行管理,这个链表就叫做free list,可以思考下,一个链表,会将buffer作为链表元素吗?
一个buffer占16kb,如果使用buffer作为链表元素,那么这个链表得多占内存啊,所以Innodb给每个buffer派发一个身份证,叫做控制块,控制块里边会存着表空间号,页号,缓存页在buffer pool中的地址等等能标识buffer的信息,控制块也是在buffer pool中,而且是在最前面的。所以我们这个架构图要改动下:
内存布局1
链表就是存储着这些控制块,控制块大概是buffer的5%大小,这部分空间不在buffer pool中,所以我们申请的内存实际上比配置参数的要大些。
freelist
有sql进来时,如果是select语句,则通过线程I/O将查询到的页加载到free buffer中,此时的free buffer就会变成clean buffer,
clean buffer也有链表进行管理,这个链表叫做lru list,该链表是依据最近最少使用淘汰策略进行管理的,同时也是起着缓存的作用。
buffer移动过程1
看上图:

  1. 先从磁盘中加载页到内存中的free buffer中,此时free buffer就不再空了,而是与磁盘中的数据一样,就上边说的clean buffer。
  2. 这时候就需要将free buffer对应的控制块从free list中删除,链接到lru list链表中。

既然lru list是有缓存的作用,查询sql的时候会将来这边查找,但是Innodb需要将链表遍历一边吗?
很显然不能这么干

缓存页的哈希检索:
Innodb通过将表空间号+页号作为key,缓存页作为value,建立一个哈希表,可以通过哈希表来判断一个页是否已经被加载进内存中

q:lru list做缓存,肯定是缓存热点数据,经常要访问的,那么相信大家都有这样的疑惑:你是不是经常写select * from table;这样的语句,打比方lru list有2000个页,那条语句却能查出1900甚至2900个页的数据,热点数据不就全没了吗?怎么办,怎么办,急,在线等!

innodb会将数据分为冷热数据,热数据会放在lru list链表的前面,冷数据会放在链表的后面,

mysql> show variables like '%innodb_old_block%';

old_block
可以看到innodb_old_blocks_pct的参数是37,表示分配给冷数据的页占lru list的37%,整个链表的3/8,通过select *语句查询出来的页都存放在lru list链表的尾部。如下图分布
冷热数据
我们做全表扫描的时候,很多数据其实是不需要的,基本都是短时间内访问完就不访问的,但是在mysql里,有一个这样的约定,每访问一次页就算一次访问数据,那么一整个页的数据都扫描完,那岂不是一条数据短时间内被频繁访问,但是这样的数据我们不能把它们当初热数据,所以mysql针对此有一个策略:在某个时间范围内多次访问数据也只当是访问一次,这个时间间隔是由innodb_old_blocks_time参数决定的,即上边查询出来的参数,值是1000毫秒。这样就能有效的预防全表扫描造成的缓存失效。

进一步优化缓存:
想想,因为buffer入链表是用头插法的,如果一个尾巴的热点数据被访问到就要将它插入到头部是不是不太合理(为哈需要插入头部?在末尾的数据是有可能被淘汰的,所以尾部的数据被访问到就要保持数据的热度)?所以mysql将热数据分为2部分,前部分占3/4,后部分占1/4,只有访问到后部分的数据才会将数据查到头节点,这样就避免了频繁调整链表,提升性能。

当有update、insert、delete语句过来的时候,那必然会导致buffer中的数据与磁盘页中的数据不一致,这时的clean buffer就会变成dirty buffer,关于dirty buffer,Innodb也使用一个链表来管理它,它叫做flush list
这些脏数据既然是我们的需要更新到DB的,那必然不能长时间停留在内存中。

flush list链表脏数据刷新机制

mysql后台会有专门的IO线程去刷数据到磁盘中,这个下文再说,在我们修改数据的时候,是将clean buffer变为dirty buffer的,所以在lru list链表中是有可能存储脏数据的:

  • 从lru list的冷数据中刷新一部分页面到磁盘中:后台线程会去扫描lru list列表里的页面,如果有脏页,则将它刷新到磁盘中,可以通过参数innodb_lru_scan_depth来指定扫描的页数量,这种刷新方式称为BUF_FLUSH_LRU。
    lru_scan_depth
  • 从flush list中刷新一部分页到磁盘中:当内存被用满了或者到预定刷盘时间时,会将数据刷到磁盘中,这种刷盘方式称为BUF_FLUSH_LIST。

在多线程的情况下呢,mysql修改链表数据是需要上锁的,这时候可以考虑使用多个buffer pool实例,这样就互不干扰了,可以通过参数innodb_buffer_pool_instances来设置,实例的大小就是将之前的一个实例进行均分。
innodb_buffer_pool_instances

log buffer 与 刷新机制

可能大家都注意到了mysql给的架构图里有个change buffer和log buffer,那么这两个是干嘛用的呢?
先不管change buffer,我们来看看log buffer,其实我们应该叫它为redo log buffer,就是用来记录redo log文件刷盘前的事务改动的数据,可以使用redo log来做数据恢复,这里就不讨论redo log了,在我们执行完update,insert,delete语句后,数据改动在redo log这,我们需要怎么刷新数据到磁盘中才能保证数据的不丢失呢?
redo log的刷盘条件有3种:

  1. 通过master thread线程来每秒进行刷新
  2. 当redo log buffer使用超过一半时进行刷盘
  3. 可以通过设置参数innodb_flush_log_at_trx_commit来主动设置,主要有0,1,2三个值:
    • 0:通过redo log thread线程每1秒就刷新redo log buffer的数据到redo log中,同时会进行刷盘操作,但是该设置下,如果有事务提交动作也不会主动的进行刷盘操作,如果宕机了可能会导致1s的数据丢失,性能却是最好
    • 1:在事务提交的情况下将数据写入redo log中,并进行刷盘操作,该模式下是最安全的操作,不会丢失事务提交的数据
    • 2:在事务提交的情况下将数据写入redo log中,但是不会同时刷新数据到磁盘。介于0和1之间。
Innodb的三大特性

插入缓存(change buffer),两次写(double write),自适应哈希索引(adaptive hash index)是Innodb的三大特性,这些特性能够让Innodb存储引擎有更好的性能和可靠性。

插入缓存(change buffer)

最影响数据库性能的就是IO问题了,而插入缓存就是将随机IO变为顺序IO,提高I/O效率。它的原理就是判断在普通索引上的DML语句是否在缓存buffer中,如果在则直接插入,否则就保留在change buffer中,将多个DML语句合并在一起,然后一起插入,提高了普通索引的插入性能。
对于change buffer有两个参数:
innodb_change_buffer
innodb_change_buffer_max_size 是指占buffer pool的比例,默认是25%。
innodb_change_buffering 是指change buffer缓存的数据类型,有以下几种:

  • all :缓存全部的insert、delete标记操作和purges操作
  • none:不做缓存操作
  • inserts:缓存insert操作
  • deletes:缓存delete操作
  • changes:缓存insert和delete标记操作
  • purges:缓存后台的物理删除操作
两次写(double write)

插入缓存是提高普通索引的插入性能,而两次写则是提高数据写入的安全性。 所谓的double write,是分为两次写操作,而double write也有个专门的存储区域,在 MySQL 8.0.20 之前,双写缓冲存储区位于InnoDB系统表空间中。从 MySQL 8.0.20 开始,双写缓冲区存储区位于双写文件中。
在buffer中的数据要刷数据入磁盘中时,会先将数据写到双写缓冲区中,然后才从双写缓冲区中将数据写入磁盘数据文件中。虽然写了两次,但是并不需要双倍的I/O消耗,因为在第一次写的时候,会在双写缓冲区中形成一个大顺序写的块,所以在第二次写的时候只需要一次操作系统的fsync()调用,即两次写需要的是 一倍的I/O + 一次fsync()调用。

之所以有双写缓冲区的存在,是为了保证写入操作系统中的页数据不完整时,这时候mysql实例宕机了,则会造成页数据的不完整,这时候就会丢失掉这个页的数据,虽然redo log可以恢复数据,但是恢复不了缺失页的数据,所以就需要双写缓冲区中该页的完整数据,先使用双写缓冲区的数据恢复该页的数据,然后使用redo log恢复全部的数据。

自适应哈希索引(adaptive hash index)

自适应哈希索引是Innodb中的一种自发性行为机制,Innodb会检测到如果某查询能够通过哈希索引来提高查询效率的话,则会自动建立哈希索引,可以通过一下参数innodb_adaptive_hash_index来控制:
Innodb_adaptive_hash
我们还会看到innodb_adaptive_hash_index_parts参数,因为自适应哈希索引是分区的,这个参数就是分区数,这是为了减少索引竞争带来的性能消耗。

慢慢的丰富了架构图如下:
内存架构图

Innodb的各大线程以及作用

任何程序都少不了我们的光荣劳动人民 - 线程,Innodb存储引擎也是多线程工作的,下面就来介绍在这些幕后默默付出的工作线程:

  • master thread线程:是后台线程中的主线程,优先级别最高,在它内部有4个循环:主循环loop,后台循环background loop,刷新循环flush loop,暂停循环suspend loop。根据数据的运行状态会在4个循环中切换,在主线程中又包含了两种频率的操作模式,每1s和每10s的操作:
    • 每1s的操作:
      • 1 - 日志缓存刷新到磁盘,即使这个事务没有提交
      • 2 - 刷新脏页到磁盘
      • 3 - 执行合并插入缓存的操作
      • 4 - 产生checkpoint
      • 5 - 清除无用的table cache
      • 6 - 如果当前没有用户活动,则切换到background loop
    • 每10s的操作
      • 1 - 日志缓存刷新到磁盘,即使这个事务没有提交
      • 2 - 刷新脏页到磁盘
      • 3 - 执行合并插入缓存的操作
      • 4 - 删除无用的undo页
      • 5 - 产生checkpoint
  • 四大I/O线程
    • read thread线程:负责数据库的读请求线程
    • write thread线程:负责数据库的写请求线程
    • redo log buffer线程:负责把redo log中的数据刷新到redo log文件中
    • change buffer线程:负责把change buffer中的数据刷新到磁盘中
      read/write的线程默认都是4个:
      io_threads
  • page cleaner thread线程:负责刷新脏页的
  • purge thread:负责删除无用的undo页,由于DML语句都会产生undo数据,系统需要定期处理这些数据,这时就需要purge操作,可以通过参数innodb_purge_threads设定
    在这里插入图片描述
  • checkpoint线程:作用是在redo log发生切换时,执行checkpoint。redo log发生切换或者文件快满时,会触发把脏页刷新到磁盘中,可以确保redo log刷新到磁盘中,保证数据不丢失。
  • error monitor thread:负责数据库报错的监控
  • lock monitor thread:负责锁的监控

就先到吧。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值