概叙
操作系统文件预读(Prefetching)
科普文:软件架构Linux系列之【Linux的文件预读readahead】-CSDN博客
前面文章我们从操作系统角度解释了文件预读readahead,指Linux系统内核将指定文件的某区域预读进页(OS page cache)缓存起来,便于接下来对该区域进行读取时,不会因缺页(page fault)而阻塞。因为从内存读取比从磁盘读取要快很多。预读可以有效的减少磁盘的寻道次数和应用程序的I/O等待时间,是改进磁盘读I/O性能的重要优化手段之一。
预读(Prefetching)是一种优化技术,用于提前加载将在未来需要的数据或文件。在Linux中,预读通常是通过文件的预读守护进程(Prefetch Daemon)实现的,它可以在系统启动时自动运行,也可以手动启动。
Linux 系统中的预读机制目的:为了提高文件读取的性能,通过预先读取相邻的数据块到内存中以便后续的读操作能够更快地进行。预读通常是由文件系统自动进行的,但是用户可以通过一些工具或编程接口来影响预读的行为。这可以帮助提高后续对同一文件区域的实际读取操作性能。
注意:文件预读readahead这只是一个建议,内核可能会忽略这个请求,或者根据自己的预读策略来进行实际的预读。
预读(Prefetching)应用场景:
-
数据库:数据库管理系统(如PostgreSQL, MySQL)经常使用预读来加速查询过程。
-
高性能文件服务器:服务器应用程序预读大文件,以便客户端请求可以快速响应。
MySQL的MRR(Multi-Range Read Optimization)
MRR(Multi-Range Read Optimization)是一种预读机制。MRR通过改变数据检索的顺序,并利用操作系统缓存进行预读,从而显著减少I/O操作数量,提高查询速度。
科普文:软件架构数据库系列之【MySQL查询优化器中的优化策略optimizer_switch--MRR 优化器】-CSDN博客
https://dev.mysql.com/doc/refman/8.4/en/mrr-optimization.html
MRR的工作原理
MRR通过将磁盘随机读取转换为磁盘顺序读取,利用磁盘预读机制(Prefetching)来优化索引的查询性能。
具体来说,MRR会按照一定的顺序读取索引范围,而不是随机读取,这样可以减少磁盘I/O操作,提高查询效率。MRR 【Multi-Range Read】将ID或键值读到buffer排序,通过把「随机磁盘读」,转化为「顺序磁盘读」,减少磁盘IO,从而提高了索引查询的性能。
MRR的应用场景
MRR特别适用于包含范围条件(如BETWEEN、<、>等)的查询,以及需要通过辅助索引访问表数据的场景。在这些情况下,MRR能够显著提高查询性能。
InnoDB的Buffer Pool具有以下四大特性:
-
Change Buffer: Change Buffer是InnoDB存储引擎中的一个特殊数据结构,用于缓存对不在buffer pool中的二级索引页所做的修改。这些修改可能来自INSERT、UPDATE或DELETE操作(DML)。当这些页被其他读操作加载到buffer pool时,Change Buffer中的修改会被合并到这些页中,从而减少对磁盘的随机I/O操作。
-
Double Write:Double Write机制用于确保InnoDB在写操作时的数据完整性。在写入磁盘之前,数据首先被写入到两个独立的缓冲区中,然后再从这两个缓冲区写入到最终的磁盘位置。如果其中一个缓冲区写入失败,另一个缓冲区中的数据仍然可以保证数据的一致性,从而防止数据损坏。
-
Adaptive Hash Index:Adaptive Hash Index(AHI)是InnoDB自动为Buffer Pool中的热点页面构建的哈希索引。这可以加速等值查找操作,提高查询性能。AHI会根据访问模式动态调整,以反映热点页面的变化,从而提高查询效率。(MySQL 8.4版本开始,默认关闭自适应哈希索引(Adaptive Hash Index,AHI)特性。)
-
Read Ahead:Read Ahead机制预测未来的I/O请求,预先读取数据页到buffer pool中。这样可以减少实际的磁盘I/O操作,提高读取性能。Read Ahead通过预测未来的访问模式,提前加载数据页,从而减少查询时的等待时间。
科普文:软件架构数据库系列之【MySQL 8.4 LTS版本20个 InnoDB系统变量默认值修改说明】_mysql 8.4对于8.0-CSDN博客
Innodb的Read Ahead预读机制
从官方文档目录“Configuring InnoDB Buffer Pool Prefetching”可以看出Prefetching是用来配置Innodb缓冲池的(O(∩_∩)O)。
主要是:Innodb_buffer_pool_read_ahead,Innodb_buffer_pool_read_ahead_evicted, Innodb_buffer_pool_read_ahead_rnd这三个参数,用来优化innodb 磁盘io。
设计目的:
Read-Ahead用于异步预取buffer pool中的多个page的一个预测行为。
InnoDB使用两种提前预读Read-Ahead算法来提高I/O性能。
Linear read-ahead:线性预读
选择是否预读下一个 Extent 的数据。有一个重要的参数 innodb_read_ahead_threshold,如果当前 Extent 中连续读取的数据页超过规定值,就会将下一个 Extent 的数据也读到缓冲池中。innodb_read_ahead_threshold 的范围是 0-64(因为一个 Extent 也就64页)。
Random read-ahead:随机预读(默认关闭)
用来设置是否将当前 Extent 的剩余页也预读到缓冲池中,由于这种预读性能不稳定,所以MySQL 5.5开始默认关闭。
评估预读算法的有效性
The SHOW ENGINE INNODB STATUS
command displays statistics to help you evaluate the effectiveness of the read-ahead algorithm. Statistics include counter information for the following global status variables:
-
Innodb_buffer_pool_read_ahead 通过预读读入buffer pool中数据page数
-
Innodb_buffer_pool_read_ahead_evicted 通过预读没有被访问就被驱逐的page
-
Innodb_buffer_pool_read_ahead_rnd
通过随机预读的次数
Innodb预读详解:Configuring InnoDB Buffer Pool Prefetching
预读从字面意义就能理解,就是通过一系列指标,判断先读取某些数据 加载到内存中,从而减少实时IO请求。
预读(Read-Ahead)是InnoDB预估执行当前的请求可能之后会读取某些数据页,就预先把它们加载到 Buffer Pool中。预读和数据页访问机制、缓冲池刷新策略息息相关。
数据库发展到目前,IO交互的代价还是最高的,特别是传统的机械硬盘下预读能力确实能提供性能。但前提是内存充裕,比如刚预读,数据还没有读取,内存容量不够,就得立即从缓存中淘汰掉,那就等于做了无用的事情。
Innodb预读前提:InnoDB Buffer Pool足够大(不可描述,反正小了肯定没有效果;重点参考InnoDB Buffer Pool的命中率>99%)。
关于InnoDB Buffer Pool的优化,后面再单独说明。
InnoDB Buffer Pool的命中率
mysql> SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';
+----------------------------------+--------+
| Variable_name | Value |
+----------------------------------+--------+
| Innodb_buffer_pool_read_requests | 153125 |
+----------------------------------+--------+
1 row in set (0.03 sec)
mysql> SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| Innodb_buffer_pool_reads | 916 |
+--------------------------+-------+
1 row in set (0.01 sec)
mysql> select (1- 916/153125)*100;
+---------------------+
| (1- 916/153125)*100 |
+---------------------+
| 99.4018 | -- InnoDB Buffer Pool的命中率 > 99%
+---------------------+
1 row in set (0.01 sec)
mysql>
Innodb_buffer_pool_reads
The number of logical reads that InnoDB could not satisfy from the buffer pool, and had to read directly from disk.
InnoDB是MySQL数据库使用的一种存储引擎。它将数据存储在内存中的缓冲池中,称为InnoDB缓冲池。
当从数据库请求数据时,首先搜索InnoDB缓冲池。如果数据未在缓冲池中找到,则必须将其从磁盘读入缓冲池。这也就是官网上说的Cound not satisfy from the buffer pool,缓冲池中无法中找到满足条件的数据,而必须从磁盘中读入缓冲池,这个操作称为Innodb_buffer_pool_reads(InnoDB缓冲池读取)。
Innodb_buffer_pool_reads读衡量了需要从磁盘读取到缓冲池的次数。
通常情况下,我们希望Innodb_buffer_pool_reads的值越小越好,因为它表示从磁盘中读取数据页到缓冲池中的次数越少,缓冲池的命中率越高,查询性能和系统响应时间也会更好。
如果Innodb_buffer_pool_reads的值较大,则可能表示缓冲池的大小不足或者热数据没有被缓存到缓冲池中,需要增加缓冲池的大小或者优化MySQL的查询语句,以提高缓存命中率和减少磁盘I/O的次数。
Innodb_buffer_pool_read_requests
The number of logical read requests.
当从数据库请求数据时,首先搜索Innodb缓冲池,如果数据已经在缓冲池中存在,则可以从磁盘读取,此操作称为Innodb_buffer_pool_read_requests InnoDB(缓冲池读取请求)。
InnoDB缓冲池读请求指标衡量了InnoDB能够直接从缓冲池中读取满足数据请求的次数,而无需从磁盘中读取数据。
与Innodb_buffer_pool_reads不同的是,Innodb_buffer_pool_reads衡量了需要从磁盘读取数据到缓冲池的次数,而Innodb_buffer_pool_read_requests衡量了数据已经在缓冲池中而无需从磁盘读取数据。
Innodb_buffer_pool_reads涉及到 IOPS 资源的消耗,Innodb_buffer_pool_read_requests涉及到 CPU 资源的消耗。
上图是InnoDB逻辑存储单元,主要分为:表空间、段、区和页(行)。
层级关系为:表空间tablespace -> 段segment -> 区extent(64个page,1M) -> 页page(默认16kb)。
innodb引擎对于数据页变更的操作是异步进行的,但对于读请求来说可以使read-time读之外,通过预读方式进行先加载策略。
MySQL 内部一般都会使用缓冲池,而如果多次语句操作的是相邻的记录,那么就会多次进行磁盘读取,导致速度降低,所以 MySQL 一般在读取数据时都是采用预读方式,读取指定数据周围的多条数据。
而在 InnoDB 引擎中的数据是以页为单位进行存储的,并且提出了“数据页”概念。
数据页的结构如上图,大小默认为 16K,关于数据页这里就不过多阐述。对硬盘上的数据读取最小单位就是数据页。
InnoDB 引擎在预读时, 有两种预读算法。线性预读和随机预读。
InnoDB在I/O的优化上有个比较重要的特性为预读,预读请求是一个i/o请求,它会异步地在缓冲池中预先回迁多个页面,预计很快就会需要这些页面,这些请求在一个范围内引入所有页面。
InnoDB以64个page为一个extent,那么InnoDB的预读是以page为单位还是以extent?
InnoDB的预读机制是以page为单位进行的。InnoDB在内存与磁盘之间是以page页为单位进行数据交互的,因为使用page页可以减少磁盘I/O操作,提高内存利用率和性能。
数据库请求数据的时候,会将读请求交给文件系统,放入请求队列中;相关进程从请求队列中将读请求取出,根据需求到相关数据区(内存、磁盘)读取数据;取出的数据,放入响应队列中,最后数据库就会从响应队列中将数据取走,完成一次数据读操作过程。
接着进程继续处理请求队列,(如果数据库是全表扫描的话,数据读请求将会占满请求队列),判断后面几个数据读请求的数据是否相邻,再根据自身系统IO带宽处理量,进行预读,进行读请求的合并处理,一次性读取多块数据放入响应队列中,再被数据库取走。(如此,一次物理读操作,实现多页数据读取,rrqm>0(# iostat -x),假设是4个读请求合并,则rrqm参数显示的就是4)
InnoDB预读机制的工作原理
InnoDB使用两种预读算法:线性预读(Linear read-ahead)和随机预读(Random read-ahead)。
- 线性预读(Linear read-ahead):这种预读方式会预测在查询过程中会使用到的数据页,并将下一个extent提前读取到buffer pool中。线性预读通过参数
innodb_read_ahead_threshold
控制,当顺序读取的页数达到或超过该参数值时,InnoDB会异步地将下一个extent读取到buffer pool中。 - 随机预读(Random read-ahead):这种预读方式通过buffer pool中已有的页来预测哪些页可能很快会被访问,并在发现连续13个页时异步读取该区段剩余的页。随机预读通过参数
innodb_random_read_ahead
控制。
InnoDB预读配置参数
-
innodb_read_ahead_threshold
:控制线性预读的触发条件,默认值为56,范围为0-64。 -
innodb_random_read_ahead
:控制随机预读的开关,默认关闭。
通过这些配置参数,可以优化InnoDB的预读行为,从而提高数据库的性能。
1、线性预读(innodb_read_ahead_threshold)
如果一个extent中的被顺序读取的page超过或者等于
参数变量时,Innodb将会异步的将下一个extent读取到buffer pool中,innodb_read_ahead_threshold可以设置为0-64的任何值(注:innodb中每个extent就只有64个page),默认为56。值越大,访问模式检查就越严格。innodb_read_ahead_threshold
例如,如果将值设置为48,则InnoDB只有在顺序访问当前extent中的48个pages时才触发线性预读请求,将下一个extent读到内存中。如果值为8,InnoDB触发异步预读,即使程序段中只有8页被顺序访问。你可以在MySQL配置文件中设置此参数的值,或者使用SET GLOBAL需要该SUPER权限的命令动态更改该参数。
在没有该变量之前,当访问到extent的最后一个page的时候,Innodb会决定是否将下一个extent放入到buffer pool中。
2、随机预读(innodb_random_read_ahead)
如果当同一个extent中连续的13个page在buffer pool中发现时,Innodb会将该extent中的剩余page读到buffer pool中。控制参数 innodb_random_read_ahead 默认没有开启。
随机预读方式则是表示当同一个extent中的一些page在buffer pool中发现时,Innodb会将该extent中的剩余page一并读到buffer pool中,由于随机预读方式给Innodb code带来了一些不必要的复杂性,同时在性能也存在不稳定性。
备注:MySQL5.5中已经将Random read-ahead:随机预读这种预读方式废弃。MySQL8.4也是默认关闭了Random read-ahead:随机预读
3、异步IO:AIO的影响
科普文:软件架构Linux系列之【五种IO模型小结】-CSDN博客
预读操作是通过aio来进行:
InnoDB使用Linux上的异步I/O子系统(native AIO)对数据文件页执行预读和写请求。该行为由innodb_use_native_aio(默认开启)配置选项控制。
对于AIO机制下, 磁盘I/O调度算法一般建议使用noop和deadline I/O调度器。
mysql> show variables like '%aio%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_use_native_aio | ON |
+-----------------------+-------+
InnoDB在特性提高了大量I/o限制系统的可伸缩性,这些系统通常在show ENGINE INNODB STATUS\G输出中显示许多挂起的读/写。
mysql> SHOW ENGINE INNODB STATUS\G
--------
FILE I/O
--------
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
ibuf aio reads:, log i/o's:, sync i/o's:
Pending flushes (fsync) log: 0; buffer pool: 0
1501 OS file reads, 345 OS file writes, 46 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s
aio 线程读取文件是受系统参数控制。特别是运行大量InnoDB I/O线程,比如同一台服务器机器上运行多个这样的实例,可能会超过Linux系统的容量限制。
在这种情况下,可能会收到以下错误:
EAGAIN: The specified maxevents exceeds the user's limit of available events.
可以通过在/proc/sys/fs/aio-max-nr中写入更高的限制来解决这个错误。
shell> cat /proc/sys/fs/aio-max-nr
65536
如果操作系统中的异步I/O子系统出现问题导致InnoDB无法启动,或
InnoDB检测到tmpdir位置、tmpfs文件系统和Linux内核不支持tmpfs上的AIO等潜在问题,也可以在启动过程中自动禁用该选项innodb_use_native_aio=0启动服务。
4、InnoDB Buffer Pool缓冲池的LRU算法和LRU链表
InnoDB 的缓冲池数据的存储算法是改进版的 LRU 算法,以此来避免了传统 LRU 算法的两个问题,预读失效和缓冲池污染。
LRU 算法简单来说,如果用链表来实现,将最近命中(加载)的数据页移在头部,未使用的向后偏移,直至移除链表。这样的淘汰算法就叫做 LRU 算法。但是其会含有前面说得两个问题。
LRU算法有以下的标准算法:
- 1)3/8的list信息是作为old list,这些信息是被驱逐的对象。
- 2)list的中点就是我们所谓的old list头部和new list尾部的连接点,相当于一个界限。
- 3)新数据的读入首先会插入到old list的头部。
- 4)如果是old list的数据被访问到了,这个页信息就会变成new list,变成young page,就会将数据页信息移动到new sublist的头部。
- 5)在数据库的buffer pool里面,不管是new sublist还是old sublist的数据如果不会被访问到,最后都会被移动到list的尾部作为牺牲者。
Flush list
Flush 链表中的所有节点都是脏页(Dirty page),脏页就是这些数据页被修改过,但是还没来得及被刷新到磁盘上。如果频繁的将修改过的数据立即刷新到磁盘将会严重影响性能,所以有了脏页的存在。那这些脏页要放到哪里呢?所以就多了Flush链表来管理这些脏页。Flush 链表上的数据都是需要被刷新到磁盘中,所以叫Flush 链表。
LRU list
LRU 链表是缓冲池最重要的链表,所有读取的数据页都会放到LRU链表上。缓冲池使用的LRU(least recently used,最近最少使用)算法是LRU算法的一种变体,当需要向缓冲池添加新数据页时,最近最少使用的数据页将被排除,新数据页将被添加到列表的中间。
这种中点插入策略将一个列表分为两个子列表:
- 头部列表:存放最新的最近访问的数据页
- 尾部列表:存放旧的最近访问的数据页
1、预读失效(预读数据被挤出buffer pool)
在磁盘上读取数据时,可能会因为操作不当导致多个用不到的数据页加载到缓冲池。
从而导致之前经常被使用的数据页缓存被无用的数据页挤到尾部,甚至被移出缓存,那么就会降低性能。
而 InnoDB 的解决方案是将缓冲池分为两部分,新生代和老年代,比例默认为5:3,分别存储常用的数据页以及不常用的数据页,新生代位于头部,新生代位于尾部,这两部分都有头部和尾部。
当从磁盘的数据页移入缓冲池中时,首先是放入老年代的头部,然后进行筛选,使用到的数据页会移入新生代的头部,未使用的数据页会随着时间流逝而慢慢移入老年代的尾部,直至淘汰。
2、缓冲池污染(预读数据被挤出buffer pool)
在处理数据页时,如果需要对大量数据页进行筛选(但是没有用到),那么还是会使大量的热点数据页被挤出。
如 select * from student where name like '张%';name字段包含索引,那么在执行时虽然会先加载到老年代的头部,但是因为每条数据都需要筛选,所以都会移入新生代头部,导致新生代热点数据页被挤到老年代甚至移除。
InnoDB 为了解决这个问题,使用了 "老年代停留时间窗口" 机制,这个机制是设置一个时间,如果在老年代的数据页被调用后还需要去检查它在老年代的停留时间是否达到了这个规定时间,达到了才能移入新生代头部,否则只会移到老年代头部。
MySQL InnoDB数据参数设置:
参数:innodb_buffer_pool_size
介绍:配置缓冲池的大小,在内存允许的情况下,DBA往往会建议调大这个参数,越多数据和索引放到内存里,数据库的性能会越好。
参数:innodb_old_blocks_pct
介绍:老生代占整个LRU链长度的比例,默认是37,即整个LRU中新生代与老生代长度比例是63:37。
画外音:如果把这个参数设为100,就退化为普通LRU了。
参数:innodb_old_blocks_time
介绍:老生代停留时间窗口,单位是毫秒,默认是1000,即同时满足“被访问”与“在老生代停留时间超过1秒”两个条件,才会被插入到新生代头部。
总结:
缓冲池(buffer pool)是一种常见的降低磁盘访问的机制;
缓冲池通常以页(page)为单位缓存数据;
缓冲池的常见管理算法是LRU,memcache,OS,InnoDB都使用了这种算法;
InnoDB对普通LRU进行了优化:
将缓冲池分为老生代和新生代,入缓冲池的页,优先进入老生代,页被访问,才进入新生代,以解决预读失效的问题
页被访问,且在老生代停留时间超过配置阈值的,才进入新生代,以解决批量数据访问,大量热数据淘汰的问题
5、监控Innodb的预读
1、可以通过show engine innodb status显示统计信息
mysql> show engine innodb status\G
----------------------
BUFFER POOL AND MEMORY
----------------------
……
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
……
1、Pages read ahead:表示每秒读入的pages;
2、evicted without access:表示每秒读出的pages;
3、Random read ahead 一般随机预读都是关闭的,也就是0。
2、通过两个状态值,评估预读算法的有效性
mysql> show global status like '%read_ahead%';
+---------------------------------------+-------+
| Variable_name | Value |
+---------------------------------------+-------+
| Innodb_buffer_pool_read_ahead_rnd | 0 |
| Innodb_buffer_pool_read_ahead | 2303 |
| Innodb_buffer_pool_read_ahead_evicted | 0 |
+---------------------------------------+-------+
3 rows in set (0.01 sec)
1、Innodb_buffer_pool_read_ahead:通过预读(后台线程)读入innodb buffer pool中数据页数
2、Innodb_buffer_pool_read_ahead_evicted:通过预读来的数据页没有被查询访问就被清理的pages,无效预读页数
3、Innodb_buffer_pool_read_ahead_rnd 随机预读取的页数:默认关闭随机预读,该值是0
innodb预读和LRU缓冲队列
Mysql的预读 mysql预读机制_mob6454cc7aec82的技术博客_51CTO博客
实战一:innodb线性预读对磁盘io性能的提升
线程预读主要是在进行物理page读取的时候,如果满足一定规则,则采取预读。线性预读触发条件:buf_read_ahead_linear函数,逻辑比较简单,大概有如下一些原则:
- 必须设置了innodb_read_ahead_threshold参数
- 当访问到某个extent的边界值比如有如下4个extent
ext1 ext2 ext3 ext4
0-63 64-127 128-191 192-255
如果访问到128这个page或者191这个page,则进行ext3所有page的检查,检查方式为访问ext3中64个page的最后访问时间,分2种方式
- 如果访问是128这个 low limit,则进行降序检查也就是需要满足如下
page 128的访问时间 > page 129的访问时间 > page 130的访问时间...
这可能是一种反向的全索引扫描。比如DESC 反向索引全扫描
- 如果访问是191这个 high limit,则进行升序检查也就是需要满足如下
page 128的访问时间 < page 129的访问时间 < page 130的访问时间...
正向全表扫描或者索引扫描使用这种检查,因为page在叶子节点有序。如果检查出来不符合访问顺序的page大于了 64 - innodb_read_ahead_threshold(默认56) = 8 个那么,则不进行线性预读。
这里包含一个关键问题就是一个extent的block是否能够按照索引(或者主键)的顺序进行排列,如果不能满足这个要求那么这个判定算法就不能成立,因为如果extent的block是乱序的则访问时间不能按顺序排列。
从索引的分裂来看,如果一个extent已经满了,往中间插入数据肯定会分配到新的extent,那么这种乱序插入数据,在全表扫描的时候也不一定会完全用到线性预读,因为默认的情况下要扫描56个page满足规则才可以。
线性预读方式分2种
-
如果是反向扫描,则直接访问前一个extent
-
如果是顺序扫描,则直接访问后一个extent
读取方式为循环这64个page,大概如下:
for (i = low; i < high; i++) {buf_read_page_low(i)}
这里的low和high分别对应预读extent的开始和结束page no,也就是进行分别读取每个page,但是使用的方式是异步AIO,最终每次调用io_submit将IO需求发送给操作系统。而异步IO线程则通过调用:
io_handler_thread
->fil_aio_wait
->os_aio_handler
->os_aio_linux_handler
->LinuxAIOHandler::poll
->LinuxAIOHandler::collect
->io_getevents
进行io完成情况的收集,然后进行IO完成后调用buf_page_io_complete完成IO操作,比如解除IO fixed状态,
buf_page_set_io_fix(bpage, BUF_IO_NONE);
预读使用异步IO也是可以理解的,当我们再次访问page查看是否在innodb buffer中的时候会发现page已经存在于buffer中也就不需要再次进行物理IO了,而将读取IO的任务交给异步IO来完成,这实际上做到了前台session和后台IO线程同步并发进行操作的原则,提高的性能。
当我们将异步IO线程加入等待sleep后,发现实际上前台session在查询page的时候会发现这个page还没有完成buffer的读取,也就是发现这个page已经被fixed住了,则进行等待。并且show engine出现大量的pending read。
从连续数据的线性预读来看,效果还是比较明显,连续数据只的是顺序插入的表且主键是自增,表大小1G左右下图:
#0 0x00007ffff7bcdf90 in pread64 () from /lib64/libpthread.so.0
#1 0x00000000050a066b in SyncFileIO::execute (this=0x7fffc8561c10, request=...) at /newdata/mysql-8.0.23/storage/innobase/os/os0file.cc:2083
#2 0x00000000050a47bc in os_file_io (in_type=..., file=39, buf=0x7fffd5478000, n=16384, offset=162430976, err=0x7fffc856209c, e_block=0x0)
at /newdata/mysql-8.0.23/storage/innobase/os/os0file.cc:5067
#3 0x00000000050a50c5 in os_file_pread (type=..., file=39, buf=0x7fffd5478000, n=16384, offset=162430976, err=0x7fffc856209c) at /newdata/mysql-8.0.23/storage/innobase/os/os0file.cc:5246
#4 0x00000000050a51fa in os_file_read_page (type=..., file_name=0x7fffe26c1db0 "./t10/testbig.ibd", file=39, buf=0x7fffd5478000, offset=162430976, n=16384, o=0x0, exit_on_err=true)
at /newdata/mysql-8.0.23/storage/innobase/os/os0file.cc:5287
#5 0x00000000050a6357 in os_file_read_func (type=..., file_name=0x7fffe26c1db0 "./t10/testbig.ibd", file=39, buf=0x7fffd5478000, offset=162430976, n=16384)
at /newdata/mysql-8.0.23/storage/innobase/os/os0file.cc:5723
#6 0x00000000050a9477 in os_aio_func (type=..., aio_mode=SYNC, name=0x7fffe26c1db0 "./t10/testbig.ibd", file=..., buf=0x7fffd5478000, offset=162430976, n=16384, read_only=false,
m1=0x7fffe26c1e90, m2=0x7fffd1a2ffb0) at /newdata/mysql-8.0.23/storage/innobase/os/os0file.cc:7263
#7 0x0000000005432194 in pfs_os_aio_func (type=..., aio_mode=SYNC, name=0x7fffe26c1db0 "./t10/testbig.ibd", file=..., buf=0x7fffd5478000, offset=162430976, n=16384, read_only=false,
m1=0x7fffe26c1e90, m2=0x7fffd1a2ffb0, src_file=0x706fb90 "/newdata/mysql-8.0.23/storage/innobase/fil/fil0fil.cc", src_line=8116)
at /newdata/mysql-8.0.23/storage/innobase/include/os0file.ic:226
#8 0x00000000054443e7 in Fil_shard::do_io (this=0x7fffe02e2840, type=..., sync=true, page_id=..., page_size=..., byte_offset=0, len=16384, buf=0x7fffd5478000, message=0x7fffd1a2ffb0)
at /newdata/mysql-8.0.23/storage/innobase/fil/fil0fil.cc:8116
#9 0x0000000005444909 in fil_io (type=..., sync=true, page_id=..., page_size=..., byte_offset=0, len=16384, buf=0x7fffd5478000, message=0x7fffd1a2ffb0)
at /newdata/mysql-8.0.23/storage/innobase/fil/fil0fil.cc:8264
#10 0x0000000005367758 in buf_read_page_low (err=0x7fffc856301c, sync=true, type=0, mode=132, page_id=..., page_size=..., unzip=false)
at /newdata/mysql-8.0.23/storage/innobase/buf/buf0rea.cc:123
#11 0x0000000005367de7 in buf_read_page (page_id=..., page_size=...) at /newdata/mysql-8.0.23/storage/innobase/buf/buf0rea.cc:287
#12 0x000000000532ab34 in Buf_fetch<Buf_fetch_normal>::read_page (this=0x7fffc8563470) at /newdata/mysql-8.0.23/storage/innobase/buf/buf0buf.cc:3895
#13 0x000000000531b669 in Buf_fetch_normal::get (this=0x7fffc8563470, block=@0x7fffc8563418: 0x0) at /newdata/mysql-8.0.23/storage/innobase/buf/buf0buf.cc:3522
#14 0x000000000532b761 in Buf_fetch<Buf_fetch_normal>::single_page (this=0x7fffc8563470) at /newdata/mysql-8.0.23/storage/innobase/buf/buf0buf.cc:4089
#15 0x000000000531bc9a in buf_page_get_gen (page_id=..., page_size=..., rw_latch=1, guess=0x0, mode=NORMAL, file=0x701bb88 "/newdata/mysql-8.0.23/storage/innobase/btr/btr0pcur.cc",
line=312, mtr=0x7fffc8563ee0, dirty_with_no_latch=false) at /newdata/mysql-8.0.23/storage/innobase/buf/buf0buf.cc:4283
#16 0x00000000052ef88e in btr_block_get_func (page_id=..., page_size=..., mode=1, file=0x701bb88 "/newdata/mysql-8.0.23/storage/innobase/btr/btr0pcur.cc", line=312, index=0xb6086d0,
mtr=0x7fffc8563ee0) at /newdata/mysql-8.0.23/storage/innobase/include/btr0btr.ic:76
#17 0x00000000052f0c1d in btr_pcur_t::move_to_next_page (this=0xb659c10, mtr=0x7fffc8563ee0) at /newdata/mysql-8.0.23/storage/innobase/btr/btr0pcur.cc:311
#18 0x0000000004fea918 in btr_pcur_t::move_to_next (this=0xb659c10, mtr=0x7fffc8563ee0) at /newdata/mysql-8.0.23/storage/innobase/include/btr0pcur.h:973
#19 0x00000000051a3a3c in row_search_mvcc (buf=0xb5c9468 "", mode=PAGE_CUR_G, prebuilt=0xb6599a0, match_mode=0, direction=1) at /newdata/mysql-8.0.23/storage/innobase/row/row0sel.cc:5912
#20 0x0000000004f26b95 in ha_innobase::general_fetch (this=0xb61b798, buf=0xb5c9468 "", direction=1, match_mode=0) at /newdata/mysql-8.0.23/storage/innobase/handler/ha_innodb.cc:10052
#21 0x0000000004f27661 in ha_innobase::rnd_next (this=0xb61b798, buf=0xb5c9468 "") at /newdata/mysql-8.0.23/storage/innobase/handler/ha_innodb.cc:10329
#22 0x0000000003b717dc in handler::ha_rnd_next (this=0xb61b798, buf=0xb5c9468 "") at /newdata/mysql-8.0.23/sql/handler.cc:2980
#23 0x00000000036eba58 in TableScanIterator::Read (this=0xb66f140) at /newdata/mysql-8.0.23/sql/records.cc:361
#24 0x0000000003ed6add in AggregateIterator::Read (this=0xb66f170) at /newdata/mysql-8.0.23/sql/composite_iterators.cc:295
#25 0x0000000003919704 in SELECT_LEX_UNIT::ExecuteIteratorQuery (this=0xb66be88, thd=0xaabeb10) at /newdata/mysql-8.0.23/sql/sql_union.cc:1228
#26 0x0000000003919a2e in SELECT_LEX_UNIT::execute (this=0xb66be88, thd=0xaabeb10) at /newdata/mysql-8.0.23/sql/sql_union.cc:1281
#27 0x0000000003871ef4 in Sql_cmd_dml::execute_inner (this=0xb66e288, thd=0xaabeb10) at /newdata/mysql-8.0.23/sql/sql_select.cc:827
#28 0x000000000387145a in Sql_cmd_dml::execute (this=0xb66e288, thd=0xaabeb10) at /newdata/mysql-8.0.23/sql/sql_select.cc:612
#29 0x00000000037fa060 in mysql_execute_command (thd=0xaabeb10, first_level=true) at /newdata/mysql-8.0.23/sql/sql_parse.cc:4407
#30 0x00000000037fbf41 in dispatch_sql_command (thd=0xaabeb10, parser_state=0x7fffc8566a50) at /newdata/mysql-8.0.23/sql/sql_parse.cc:4988
#31 0x00000000037f2543 in dispatch_command (thd=0xaabeb10, com_data=0x7fffc8567b00, command=COM_QUERY) at /newdata/mysql-8.0.23/sql/sql_parse.cc:1836
#32 0x00000000037f095e in do_command (thd=0xaabeb10) at /newdata/mysql-8.0.23/sql/sql_parse.cc:1320
#33 0x00000000039c5c91 in handle_connection (arg=0x9bd6950) at /newdata/mysql-8.0.23/sql/conn_handler/connection_handler_per_thread.cc:301
#34 0x000000000562cd84 in pfs_spawn_thread (arg=0xad3b3a0) at /newdata/mysql-8.0.23/storage/perfschema/pfs.cc:2900
#35 0x00007ffff7bc6ea5 in start_thread () from /lib64/libpthread.so.0
#36 0x00007ffff5e388dd in clone () from /lib64/libc.so.6
实战二:innodb随机预读对磁盘io性能的提升
mysql> show variables like '%read_ahead%';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| innodb_random_read_ahead | OFF |
| innodb_read_ahead_threshold | 56 |
+-----------------------------+-------+
- innodb_random_read_ahead:开启随机预读技术,优化InnoDB I/O。默认关闭
- innodb_read_ahead_threshold:控制InnoDB用于将页面预取到缓冲池中的线性预读的灵敏度
CREATE TABLE `sbtest1` (
`id` int NOT NULL AUTO_INCREMENT,
`k` int NOT NULL DEFAULT '0',
`c` char(120) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
`pad` char(60) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB
通过sysbench创建一个 1个page 基本包含16kb*1024=16384字节,
INT 4字节 ,char按照长度
Id(4字节)+k(4字节)+c(120字节)+ pad(60字节)=188字节
16384/188=88, 一个页平均下来大致88行的数据。
Extent=64个页 * 88 =5632行的数据。
模式数据:
shell> ./sysbench ./lua/oltp_common.lua --mysql-user=root --mysql-password=123456 --mysql-socket=/opt/data8.0/mysql/mysql.sock --mysql-db=sbtest --tables=1 --table-size=6000 --db-driver=mysql --report-interval=10 --threads=10 --time=120 prapare
Initializing worker threads...
Creating table 'sbtest1'...
Inserting 6000 records into 'sbtest1'
Creating a secondary index on 'sbtest1'...
通过status指标:
#初次记录ahead指标
mysql> show global status like '%ahead%';
+---------------------------------------+-------+
| Variable_name | Value |
+---------------------------------------+-------+
| Innodb_buffer_pool_read_ahead_rnd | 387 |
| Innodb_buffer_pool_read_ahead | 0 |
| Innodb_buffer_pool_read_ahead_evicted | 0 |
+---------------------------------------+-------+
#不输出数据
mysql> pager cat > /dev/null
mysql>select * from sbtest1 where id <7000;
mysql> nopager
#记录结果
mysql> show status like '%ahead%';
+---------------------------------------+-------+
| Variable_name | Value |
+---------------------------------------+-------+
| Innodb_buffer_pool_read_ahead_rnd | 487 |
| Innodb_buffer_pool_read_ahead | 0 |
| Innodb_buffer_pool_read_ahead_evicted | 0 |
+---------------------------------------+-------+
Random read-ahead下通过上面6000行的数据大致需要100页的读取。
Linear read-ahead下指标没有变化,统计信息村子问题。
- Innodb_buffer_pool_read_ahead
由预读后台线程读入InnoDB缓冲池的页数。 - Innodb_buffer_pool_read_ahead_rnd:
由InnoDB发起的“随机”预读次数。当查询以随机顺序扫描表的大部分时,就会发生这种情况。 - Innodb_buffer_pool_read_ahead_evicted
由预读后台线程读入InnoDB缓冲池的页面数,这些页面随后在没有被查询访问的情况下被逐出。
SHOW ENGINE INNODB STATUS还显示了预读页面的读取速率,以及这些页面在不被访问的情况下被驱逐的速率。每秒平均数据是基于上次调用SHOW ENGINE INNODB STATUS以来收集的统计数据,显示在SHOW ENGINE INNODB STATUS输出的BUFFER POOL and MEMORY部分。
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137039872
Dictionary memory allocated 375221
Buffer pool size 8192
Free buffers 6850
Database pages 1338
Old database pages 513
。。。
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 1338, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
标签:ahead,read,buffer,预读,mysql,page,pool
From: https://blog.csdn.net/Rookie_CEO/article/details/143378939