这种现象出现的原因是什么呢?在前面更新语句的执行过程中,我们提到过Mysql在执行更新语句的时候并没有每次都将数据直接写到磁盘上,而是在更新内存中的数据行并将更新操作记录到redo log文件中之后就向客户端返回写入成功的回复。如果符合条件的数据行不在内存中该怎么办呢?
这时就会出现一个现象,内存中的数据页和磁盘上的数据页的内容是不一致的,内存中的数据页和磁盘上的数据页的内容是不一致的是什么意思呢?如果内存数据页上的数据行和磁盘数据页上的数据行中的数据有不一致的地方,我们就说,这个内存数据页和磁盘数据页的内容不一致。
当内存中的数据页和磁盘上的数据页的内容不一致时,我们就称此时的内存数据页是脏页,如果内存中的数据页和磁盘上的数据页的内容是一致的,那么我们就称这个内存数据页是干净页。
把脏页中的数据写入到磁盘上的过程被称为flush,那么我们就可以解释最开始所提现象出现的原因了,有的语句在个别时候会执行的很慢,很有可能是因为刚好在执行这些语句的时候Mysql也在向磁盘上刷脏页,由于刷脏页的过程会占用cpu的使用时间和磁盘的读写能力,所以会影响到sql语句的执行时间。
那么什么时候会发生向磁盘刷入脏页数据的过程呢?当redo log文件被写满时会发生向磁盘刷入脏页数据的过程,我们都知道redo log日志文件是一个环形写入的文件,所以当redo log文件被写满时,Mysql会停止执行所有更新语句,Mysql会先向磁盘中刷入脏页数据,从而将redo log文件中的checkpoint指针向前移动,从而清理出足够的空间可以继续记录新的更新操作,从write pos指针到checkpoint指针之间的位置就是记录新的更新操作的地方。
如果我们需要将新的数据页读取到内存中,但是此时内存中的空间不足以放下新的数据页时,也会发生向磁盘刷入脏页数据的过程,当内存不足以放下新的数据页时,Mysql就会从内存中淘汰掉一些数据页,给新的数据页腾出空间,如果在淘汰的数据页中存在脏页,就要先把脏页中的数据刷入到磁盘上再把这个数据页淘汰掉。怎么从内存中淘汰掉数据页?
那么为什么不能直接把脏页淘汰掉,而是要先把脏页中的数据刷入到磁盘后再淘汰掉呢?我们先来看一下直接把脏页淘汰掉可不可行?如果我们直接把脏页淘汰掉,而不是先把脏页中的数据刷入到磁盘后再淘汰,会发生什么呢?假如现在我们要执行一个查询语句,那么我们在把磁盘中的数据页读取到内存后不能直接把其中的数据行返回给客户端,每次都要判断一下在redo log文件中有没有需要应用到这个数据页的更新操作,这个判断操作显然是冗余的,如果我们保证在每次淘汰脏页时都会把脏页中的数据写入到磁盘上,那么就可以保证从磁盘中读取到的数据页一定是最新的,可以把其中的数据行直接返回给客户端,而不需要去判断在redo log文件中有没有需要应用到这个数据页的更新操作。
那么Mysql还会在什么时候向磁盘刷入脏页数据呢?Mysql还会在空闲的时候向磁盘刷入脏页数据,这里指的空闲并不是指在完全没有语句执行的时候,而是在同时执行的sql语句没那么多的时候,Mysql会使用剩余的cpu性能和磁盘的读写能力向磁盘中刷入脏页数据。
当Mysql正常关闭的时候,Mysql也会向磁盘刷入脏页数据,为什么要这么设计呢?和淘汰脏页时先把脏页数据刷到磁盘上的理由一致,为了保证从磁盘上读取到数据总是最新的。
很显然,由于redo log文件被写满所以向磁盘刷入脏页数据的情况是Mysql尽量避免的,那么Mysql是怎么避免的呢?因为如果redo log文件被写满了,那么所有的更新语句都没办法再执行,而业务中总是存在大量更新数据库的sql语句,所以业务也会因此受到影响。
向磁盘刷入脏页数据最常见的原因是在内存不够用时淘汰的数据页中存在脏页,也就是说这种情况是很经常出现的,既然sql语句只是在个别时候会变的很慢,那么就不可能是每次淘汰脏页都会导致语句的执行变慢。那么淘汰脏页在什么时候才会导致sql语句变慢呢?当一个查询语句在执行过程中需要淘汰很多个脏页时,这个查询语句的执行时间就会明显变长。为什么一个查询语句在执行过程中会淘汰很多个脏页呢?
那么Innodb是怎么来避免这两种情况的出现的呢?Innodb通过控制内存中脏页的比例来避免上面的两种情况。那么Innodb是怎么控制内存中脏页的比例的呢?控制内存中脏页的比例是什么意思?就是指确保在内存中脏页的比例不超过多少。既然内存中有脏页,还有什么数据页?内存中不仅有脏页,还有干净页,还有没有被使用过的数据页。???
想要控制内存中脏页的比例,就要先告诉Innodb它所在主机的磁盘读写能力,Innodb会根据它所在主机的磁盘读写能力向磁盘中刷入不同的脏页数量,那么我们怎么告诉Innodb它所在主机的磁盘读写能力呢?我们又怎么知道它所在主机的磁盘读写能力呢?我们可以通过给Mysql的innodb_io_capacity参数设置值来告诉Innodb它所在主机的磁盘读写能力。那么我们怎么知道Innodb所在主机的磁盘读写能力呢?我们可以使用fio工具来测试出磁盘的每秒读写次数。那么innodb_io_capacity参数具体设置为什么值呢?通常来说建议设置为磁盘的每秒读写次数。为什么设置为这个值呢?因为磁盘的每秒读写次数反映的就是主机的磁盘读写能力,Innodb在设计这个参数时希望的就是用户可以根据不同磁盘的不同读写能力给这个参数设置不同的值,读写能力强的磁盘设置一个较大的值,读写能力弱的磁盘设置一个较小的值。那么为什么把这个参数的值设置为磁盘的每秒读写次数,而不是同比增大或减小为磁盘的每秒读写次数乘上某个比例系数呢?可能Innodb设计人员在调试这个参数的值时就是用磁盘的每秒读写次数来调试的,也和设计人员采用的决定向磁盘中刷入脏页速度的策略具体实现有关,还有一个原因就是在实际使用Mysql时把innodb_io_capacity参数的值设置为磁盘的每秒读写次数Mysql的表现最好。
如果没能正确的设置innodb_io_capacity参数的值,也会出现性能上的问题。如果你使用的是固态硬盘,但是innodb_io_capacity参数的值设置的很小,那么Innodb就会认为这台主机的磁盘读写能力很差,Innodb就会控制向磁盘中刷新脏页的速度,如果向磁盘中刷新脏页的速度还没有生成脏页的速度快,那么内存中就会存在大量的脏页,那么在内存不足淘汰数据页时就会淘汰很多脏页,就会发生将脏页刷入磁盘的过程,就会导致查询语句和更新语句的执行过程变慢。没有正确的设置Innodb_io_capacity参数的值不仅会影响查询语句的执行过程,也会影响更新语句的执行过程吗?我们只需要回答这两个问题,在执行查询语句的过程中会向内存中读取数据页吗?会,那么就可能会发生内存不足刷新脏页的过程,而没有正确设置innodb_io_capacity参数的值则会大大增加在查询语句执行过程中发生刷入脏页的概率,所以没有正确设置innodb_io_capacity参数的值可能会影响查询语句的执行速度。在执行更新语句的过程中会向内存中读取数据页吗?会,如果需要更改的数据行不在内存中,会先把数据行读取到内存中,所以这个参数的值没有被正确设置也可能会影响更新语句的执行速度。
现在Innodb已经知道了每秒最多能向磁盘中刷入多少个脏页了,那么Innodb把向磁盘中刷入脏页的速度设置为多少合适呢?总不能把所有的磁盘读写能力都用在刷入脏页上,这样做的话在Innodb向磁盘刷入脏页的过程中就不能执行sql语句了,因为执行sql语句也需要消耗磁盘的读写能力,这显然不是我们想要的,但是我们也不能刷入的太慢,刷入的太慢就会导致内存中的脏页过多,脏页一直不写入到磁盘上,redo log文件中的空间也就一直不能重复使用,redo log就会很容易被写满,从而导致所有的更新语句都会停止执行。
Innodb向磁盘刷入脏页的速度取决于两个因素,脏页在所有数据页中所占的比例和redo log日志文件写入日志的速度。
Innodb设置的脏页在所有数据页中所占比例的上限是75%,Innodb会根据当前的脏页比例,假设为M,计算出一个0到100之间的数字,计算策略如下所示。
F1(M)
{
if M>=innodb_max_dirty_pages_pct then
return 100;
return 100*M/innodb_max_dirty_pages_pct;
}
如果当前的脏页比例超过了所设置的脏页比例上限,那么就返回100,如果当前的脏页比例没有超过所设置的脏页比例上限,那么就用100乘上当前脏页比例和脏页比例上限的比值。
Innodb存储引擎向redo log日志文件写入的每条日志中都有一个序号,Mysql会去计算最新写入的日志的序号和checkpoint指针所指向的日志的序号的差值,我们假设这个值为N,Innodb也会根据这个N值计算出一个0到100之间的数字,根据N值计算数字的策略比较复杂,我们只需要知道N值越大,计算出来的数值就会越大。
现在我们根据M和N分别计算出了一个数值,取其中比较大的数值记为R,然后Innodb存储引擎将刷入脏页的速度设置为Innodb_io_capacity*R%。
现在了解了这么多关于向磁盘中刷入脏页的知识,那么为什么有的语句在个别时候会执行的很慢呢?
发生这种现象可能是因为在这些时刻Mysql向磁盘上刷入脏页的行为占用了过多的IO资源,而磁盘的读写能力是有限的,所以只有少量的IO资源被用来执行sql语句,当只有被用于执行sql语句的IO资源不同时不同数据库执行同一条sql语句所需要读取或者写入的数据页是不变的,那么能够使用的IO资源越少,在读取或者写入时消耗的时间就越长,从而导致sql语句的执行时间变长。
那么什么时候Mysql会占用过多的IO资源来执行刷脏过程呢?当Innodb_io_capacity参数的值设置的太大时,和内存中的当前脏页比例接近脏页比例上限时,都会导致Mysql占用过多的IO资源进行刷脏。
所以我们要给Innodb_io_capacity参数的值设置为一个合适的值,通常来说都是设置为磁盘的每秒读写次数,还有就是控制内存中的当前脏页比例不要经常接近75%。那么怎么控制内存中的当前脏页比例不经常接近75%呢?
如果在某个查询语句的执行过程中只会刷入一个脏页,那么这个查询语句的执行时间会明显变长吗?会延长多长时间主要取决于向磁盘中刷入一个脏页需要多长时间,因为向磁盘中刷入一个脏页所需要的时间远远大于占用cpu判断是否需要淘汰数据页,需要淘汰多少个数据页,会淘汰哪些数据页,在这些需要淘汰的数据页中有多少脏页,哪些数据页是脏页的时间。那么向磁盘中刷入一个脏页需要多长时间呢?Mysql实现的机制应该不会是在内存不足时读入一个数据页只会从内存中淘汰掉一个数据页吧。
在Mysql中存在这么一个有趣的机制,现在Mysql想要向磁盘上刷入一个脏页,而这个脏页旁边的数据页也是脏页,那么Mysql就会把这两个脏页都刷到磁盘上,如果在第二个脏页旁边的数据页还是脏页,那么就会把这三个脏页都刷到磁盘上,以此类推,直到最后一个旁边不是脏页的数据页时停止。
所以即使在执行查询语句的过程中由于内存不足只需要向磁盘上刷入一个脏页,也有可能会因为这个机制向磁盘上刷入很多个脏页。
这个机制在我们使用的硬盘是机械硬盘时是很有用的,因为机械硬盘的每秒随机读写次数很小,一般只有几百,假设在内存中有100个脏页,并且这100个脏页是连续的,而且我们没有开启一次性刷入连续脏页的机制,每次查询过程都只会向磁盘中写入一个脏页,在这个场景中把所有脏页写入到磁盘上的总时间远远大于使用一次性刷入连续脏页机制将100个脏页写入到磁盘上的时间。这是因为每次向磁盘中写入一个脏页的数据时磁盘的磁头都需要经过在不同磁道之间移动,中轴带动盘面旋转到合适的位置,盘面继续转动的过程,盘面继续转动的阶段就是从磁盘上读写具体数据的过程,这实际上就是向磁盘进行随机读写会经过的过程。如果我们采用一次性刷入连续脏页机制将100个脏页写入到磁盘上,那么只会在写入第一个脏页时会经历随机读写的过程,剩下的脏页会直接写在磁盘上紧跟在第一个脏页所写数据页后面的数据页上,也就是说从写入第二个脏页开始,就不需要经过在不同磁道之间移动,中轴带动盘面旋转到合适的位置这两个阶段了,只需要进行第三个阶段就可以完成脏页的写入,这样做显然会节省大量的时间,这实际上就是向磁盘进行连续读写所经过的过程。
如果使用的是固态硬盘这种每秒随机读写次数比较高的硬盘,就不需要开启这个机制了,因为固态硬盘的每秒随机读写次数很高,把所有脏页写入到磁盘上的总时间和使用一次性刷入连续脏页机制将100个脏页写入到磁盘上的总时间相差不大,如果我们不开启这个机制的话,每次刷入脏页时只刷入必需的脏页,反而更难出现某个时候有的语句执行的很慢的现象。