MyISAM存储引擎优化

Published on 2016 - 06 - 07

索引缓存优化

MyISAM存储引擎的缓存策略是它与其他许多数据库或MySQL数据库的其他许多存储引擎不太一样的特性。因为它仅仅缓存索引数据,并不会缓存实际的表数据信息到内存中,它会将这一工作交给了OS级别的文件系统缓存。 所以,在数据库优化中非常重要的优化环节之一——“缓存优化”的工作在使用MyISAM存储引擎的数据库时,就完全集中在对索引缓存的优化上了。

在分析优化索引缓存策略之前,先大概了解一下MyISAM存储引擎的索引实现机制及索引文件的存放格式。

MyISAM存储引擎的索引和数据是分开存放在“.MYI”文件中的,每个“.MYI”文件都有文件头和实际的索引数据。“.MYI”的文件头主要存放4部分信息,分别称为:state(主要是整个索引文件的基本信息)、base(各个索引的相关信息,主要是索引的限制信息)、keydef(每个索引的定义信息)和recinfo(每个索引记录的相关信息)。在文件头后面紧接着的就是实际的索引数据信息了。索引数据以Block(Page)为最小单位,每个Block中只会存在同一个索引的数据,这主要是基于提高索引连续读性能的目的。在MySQL中,索引文件中索引数据的Block被称为Index Block,每个Index Block的大小并不一定相等。

在“.MYI”中,Index Block的组织形式实际上只是逻辑上的,并不是物理意义上的,在物理上,实际是以File Block的形式存放在磁盘的。在Key Cache中缓存的索引信息是以“Cache Block”的形式组织存放的,“Cache Block”是相同大小的,和“.MYI”文件物理存储的Block(File Block)一样。在一条Query通过索引检索表数据的时候,首先会检查索引缓存(key_buffer_cache)中是否已经有需要的索引信息。如果没有,则会读取“.MYI”文件,将相应的索引数据读入Key Cache中的内存空间,同样也以Block形式存放,被称为Cache Block。不过,数据的读入并不是以Index Block的形式,而是以File Block的形式来读入。以File Block形式读入Key Cache之后的Cache Block实际上与File Block完全一样。如图1所示:

从“.MYI”文件中读入File Block到Key Cache中的Cache Block时,如果整个Key Cache中已经没有空闲的Cache Block可以使用,将会通过MySQL实现的LRU相关算法将某些Cache Block清除出去,让新进来的File Block有地方呆。

先来分析一下与MyISAM索引缓存相关的几个系统参数和状态参数。

  • key_buffer_size,索引缓存大小。

这个参数用来设置整个MySQL中常规Key Cache的大小。一般来说,如果MySQL运行在32位平台上,此值建议不要超过2GB。如果运行在64位平台上则不用考虑此限制,但也最好不要超过4GB。

  • key_buffer_block_size,索引缓存中的Cache Block Size。

在前面已经介绍了,在Key Cache中的所有数据都是以Cache Block的形式存在的,而key_buffer_block_size用来设置每个Cache Block的大小,实际上也同时限定了将“.MYI”文件中的Index Block被读入时File Block的大小。

  • key_cache_division_limit,LRU链表中的Hot Area和Warm Area分界值。

实际上,在MySQL的Key Cache中所使用的LRU算法并不像传统的算法,仅仅是访问频率,以及最后访问时间,并通过唯一的链表实现,而是将其分成了两部分。一部分用来存放使用比较频繁的Hot Cache Block(Hot Chain),被称为Hot Area;另外一部分则用来存放使用不太频繁的Warm Cache Block(Warm Chain),被称为Warm Area。这样做的目的主要是为了保护使用比较频繁的Cache Block更不容易被换出。而key_cache_division_limit参数则是告诉MySQL该如何将整个Cache Chain划分为Hot Chain和Warm Chain两部分,参数值为Warm Chain占整个Chain的百分比值。设置范围1~100,系统默认为100,也就是只有Warm Chain。

  • key_cache_age_threshold,控制Cache Block从Hot Area降到Warm Area的限制。

key_cache_age_threshold参数控制Hot Area中的Cache Block何时该被降级到Warm Area中。系统默认值为300,最小可以设置为100。值越小,被降级的可能性越大。

通过以上参数的合理设置,基本上可以完成MyISAM整体优化的70%工作。但是合理设置这些参数却不是很容易的事情。尤其是对key_cache_division_limit和key_cache_age_threshold这两个参数的合理使用。

对于key_buffer_size的设置,一般需要通过三个指标来计算,第一个是系统索引的总大小,第二个是系统可用物理内存,第三个是根据系统当前的Key Cache命中率。对于一个完全从零开始的全新系统,可能除了第二点可以拿到很清楚的数据之外,其他的两个数据都比较难获取(第三点完全没有)。当然,我们可以通过MySQL官方手册给出的计算公式粗略地估算一下系统将来的索引大小,不过前提是要知道创建哪些索引,然后通过各索引估算出索引键的长度,以及表中存放数据的条数,公式如下:

当然,考虑到活跃数据的问题,并不需要将key_buffer_size设置到可以将所有的索引都放下的大小,这时候就需要Key Cache的命中率数据来帮忙了。下面再来看一下系统中记录的与Key Cache相关的性能状态参数变量。

  • Key_blocks_not_flushed,已经更改但还未刷新到磁盘的Dirty Cache Block;
  • Key_blocks_unused,目前未被使用的Cache Block数目;
  • Key_blocks_used,已经使用了的Cache Block数目;
  • Key_read_requests,Cache Block被请求读取的总次数;
  • Key_reads,在Cache Block中找不到须要读取的Key信息后,到“.MYI”文件中读取的次数;
  • Key_write_requests,Cache Block被请求修改的总次数;
  • Key_writes,在Cache Block中找不到须要修改的Key信息后,到“.MYI”文件中读入再修改的次数。

由于上面各个状态参数在MySQL官方文档中都有较为详细的描述,所以仅做基本的说明。在系统上线之后,就可以通过上述状态参数的状态值得到系统当前的Key Cache使用的详细情况和性能状态。

通过上面的三个比率数据,就可以很清楚地知道Key Cache的设置是否合理,尤其是Key_Buffer_Read_HitRatio参数和Key_buffer_UsageRatio这两个比率。一般来说,Key_buffer_UsageRatio应该在99%以上甚至100%。如果该值过低,则说明key_buffer_size设置过大,MySQL根本用不完。Key_Buffer_Read_HitRatio也应该尽可能地高。如果该值较低,则很有可能是key_buffer_size设置过小,须要适当增加key_buffer_size值,也有可能是key_cache_age_threshold和key_cache_division_limit的设置不当,造成Key Cache cache失效太快。一般来说,在实际应用场景中,很少有人调整key_cache_age_threshold和key_cache_division_limit这两个参数的值,大都使用系统的默认值。

多Key Cache的使用

从MySQL 4.1.1版本开始,MyISAM有了支持多个Key Cache并存的功能。也就是说可以根据不同的需要设置多个Key Cache了,如将使用非常频繁而且基本不会被更新的表放入一个Key Cache中,以防止在公共Key Cache中被清除出去,那些使用并不是很频繁而且可能会经常被更新的Key会放入另外一个Key Cache中。这样就可以避免某些场景下大批量的Key被读入Key Cache,因为Key Cache空间问题使本来命中率很高的Key也不得不被清除出去。
MySQL官方建议在比较繁忙的系统上一般可以设置三个Key Cache:

  • 一个Hot Cache使用20%的大小,用来存放使用非常频繁且更新很少的表的索引;
  • 一个Cold Cache使用20%的大小,用来存放更新很频繁的表的索引;
  • 一个Warm Cache使用剩下的60%空间,作为整个系统默认的Key Cache。

Key Cache的Mutex问题

MySQL索引缓存是所有线程共享的全局缓存,当多线程同时并发读取某一个Cache Block的时候并不会有任何问题,每个线程都可以同时读取该Cache Block。但是当某个Cache Block正在被一个线程更新或读入时,该线程就会通过mutex锁定该Cache Block,以达到不允许其他线程同时更新或读取的目的。所以在高并发的环境下,如果Key Cache的大小不够充足非常容易因为Cache Block的Mutex问题造成严重的性能影响。而且在目前正式发行的所有MySQL版本中,Mutex的处理机制存在一定的问题,当Active线程数量稍微高一些的时候,就非常容易出现Cache Block的Mutex问题,甚至有人将此性能问题作为Bug(#31551)报告给了MySQL AB。

Key Cache预加载

在MySQL中,为了让系统刚启动之后不至于因为Cache中没有任何数据而出现短时间的负载过高或响应不够及时,MySQL提供了Key Cache预加载功能,可以通过相关命令(LOAD INDEX INTO CACHE tb_name_list ...),将指定表的所有索引都加载到内存中,而且还可以通过相关参数控制是否只加载(load)根结点和枝节点,还是将页节点也加载进来,主要是为Key Cache的容量考虑。

对于这种启动后立即加载的预热操作,可以利用MySQL的init_file参数来设置相关的命令,如示例代码1所示:

这里init file中首先设置了两个Key Cache(hot cache和cold cache)各为16MB,然后分别将top_message这个变动很少的表的索引缓存(Cache)到Hot Cache,再将event这个变动非常频繁的表的索引缓存(Cache)到了Cold Cache中,最后再通过LOAD INDEX INTO CACHE命令预加载了top_message、groups这两个表所有索引的节点及event和user这两个表索引的非叶子节点数据到Key Cache中,以提高系统启动之初的响应能力。

NULL值对统计信息的影响

虽然都是使用B-Tree索引,但是MyISAM索引和Oracle索引的处理方式不太一样,MyISAM的索引中会记录值为NULL的列信息,只不过NULL值的索引键占用的空间非常少。所以,NULL值的处理方式可能会影响到MySQL的查询优化器对执行计划的选择。于是MySQL提供了myisam_stats_method这个参数让我们可以自行决定对索引中的NULL值的处理方式。

myisam_stats_method参数的作用就是告诉MyISAM在收集统计信息的时候,是认为所有NULL值等同,还是每个NULL值是完全不相等的,所以可设置的值也为nulls_unequal和nulls_equal。

当设置myisam_stats_method = nulls_unequal,MyISAM在搜集统计信息时会认为每个NULL值都不同,则基于该字段索引的Cardinality就会更大,也就是说MyISAM会认为DISTINCT值数量更多,这样就会让查询优化器处理Query的时候使用该索引的倾向性更高。

而在设置myisam_stats_method = nulls_equal之后,MyISAM搜集统计信息的时候则会认为每个NULL值都是一样的,这样Cardinality数值会降低,优化器选择执行计划的时候放弃该索引的倾向性会更高。

当然,上面所说的都是相对于使用等值查询,而且NULL值占比较大的情况。如果NULL值本身就很少,那么不管使用nulls_unequal还是nulls_equal,对优化器选择执行计划的影响是很小的。

表读取缓存优化

在MySQL中有两种读取数据文件的缓冲区,一种是Sequential Scan方式(如全表扫描)扫描表数据的时候使用,另一种则是在Random Scan(如通过索引扫描)的时候使用。虽然这两种文件读取缓冲区并不是MyISAM存储引擎所特有的,但是MyISAM存储引擎并不会Cache数据(.MYD)文件,每次对数据文件的访问都须要通过调用文件系统的相关指令从磁盘上面读取物理文件。所以,每次读取数据文件所需的内存缓冲区的设置就对数据文件访问的性能非常重要了。在MySQL中对应这两种缓冲区的相关参数如下。

  • read_buffer_size,以Sequential Scan方式扫描表数据时候使用的Buffer。

每个Thread进行Sequential Scan的时候都会产生该Buffer,所以在设置的时候尽量不要太高,避免因为并发太大造成内存不够。系统默认为128KB,最大为2GB,设置的值必须是4KB的倍数,否则系统会自动更改成小于设置值的最大的4KB的倍数。一般来说,可以尝试适当调大此参数看是否能够改善全表扫描的性能。在不同的平台上可能会有不同的表现,这主要与OS级别文件系统IO的大小有关。所以,该参数的设置最好是在真实环境中通过多次更改测试调整,这样才能选找到一个最佳值。

  • read_rnd_buffer_size,进行Random Scan的时候使用的Buffer。

read_rnd_buffer_size所设置的Buffer实际上刚好和read_buffer_size所设置的Buffer相反,一个是顺序读的时候使用,一个是随机读的时候使用。但是两者都是针对于线程的设置,每个线程都可能产生两种Buffer中的任何一个。read_rnd_buffer_size的默认值256KB,最大值为4GB。一般来说,将read_rnd_buffer_size的值适当调大,对提高ORDER BY操作的性能有一定的效果,但这并不是说read_rnd_buffer_size就是用在ORDER BY操作所产生的数据读取操作中(实际上很多人都误解为如此)。

这两个读取缓冲区都是线程独享的,每个线程在需要的时候都会创建一个(或者两个)系统中设置大小的缓冲区,所以在设置两个参数的时候一定不能过于激进,而应该根据系统可能的最大连接数和系统可用内存大小,计算出最大可设置值。

并发优化

在查询方面,MyISAM存储引擎的并发并没有太大的问题,而且性能非常高。如果觉得光靠Key Cache来缓存索引还不够快,还可以通过Query Cache功能来直接缓存Query的结果集。

但是,由于MyISAM存储引擎的表级锁定机制,以及读写互斥,其并发写的性能一直是让人比较头疼的问题。一般来说,我们能做的主要也就只有以下几点了。

  1. 打开concurrent_insert的功能,提高INSERT操作和SELECT之间的并发处理,使二者尽可能并行。大部分情况下concurrent_insert的值被设置为1,当表中没有删除记录留下的空余空间时可以在尾部并行插入。这其实也是MyISAM的默认设置。如果系统主要以写为主,尤其是有大量INSERT的时候。为了尽可能地提高INSERT的效率,可以将concurrent_insert设置为2,也就是告诉MyISAM,不管在表中是否有删除行留下的空余空间,都在尾部进行并发插入,使INSERT和SELECT互不干扰。
  2. 控制写入操作的大小,尽量让每次写入操作都能够很快完成,以防止时间过长的阻塞动作。
  3. 通过牺牲读取效率来提高写入效率。为了尽可能地让写入更快,可以适当调整读和写的优先级别,让写入操作的优先级高于读操作的优先级。

对于一个表级锁定的存储引擎来说,除了concurrent_insert比较特殊之外,基本上都只能串行地写。所以,虽然上面给出了三点建议,但是后两点只能算优化建议,并不是真正意义上的并发优化建议。

其他可以优化的地方

除了以上分析的这几个方面之外,MyISAM实际上还存在可以优化的地方和常用的优化技巧。

  1. 通过OPTIMIZE命令来整理MyISAM表的文件。这就像使用Windows操作系统一样会每隔一段时间都会做一次磁盘碎片整理,让系统中的文件尽量使用连续空间,以提高文件的访问速度。MyISAM在通过OPTIMIZE优化整理的时候,主要是将因为数据删除和更新造成的碎片空间清理,使整个文件连续在一起。一般来说,在每次做了较大的数据删除操作之后都须要做一次OPTIMIZE优化整理。而且每个季度都应该有一次OPTIMIZE的维护操作。
  2. 设置myisam_max_[extra]_sort_file_size足够大,对REPAIR TABLE的效率可能会有较大改善。
  3. 在执行CREATE INDEX或REPAIR TABLE等需要大的排序操作之前,可以通过调整session级别的myisam_sort_buffer_size参数值来提高排序操作的效率。
  4. 通过打开delay_key_write功能,减少IO同步的操作,提高写入性能。
  5. 通过调整bulk_insert_buffer_size来提高INSERT...SELECT...这样的bulk insert操作的整体性能,LOAD DATA INFILE...的性能也可以得到改善。当然,在设置此参数的时候,也不应该一味地求大,很多时候追求极端反而会影响系统整体性能,毕竟系统性能是从整体来看的,而不能仅仅针对某一个或某一类操作。

参考文档