InnoDB存储引擎优化

Published on 2016 - 06 - 07

InnoDB存储引擎和MyISAM存储引擎最大区别主要有4点,第一点是缓存机制,第二点是事务支持,第三点是锁定实现,最后一点就是数据存储方式的差异。在整体性能表现方面,InnoDB和MyISAM两个存储引擎在不同的场景下差异比较大,主要原因正是这4个主要区别造成的。

InnoDB缓存相关优化

无论对于哪一种数据库,缓存技术都是提高数据库性能的关键,物理磁盘的访问速度与内存的访问速度永远都不是一个数量级的。通过缓存技术无论是在读还是写方面,都可以大大提高数据库整体性能。

Innodb_buffer_pool_size的合理设置

InnoDB存储引擎的缓存机制和MyISAM的最大区别就在于InnoDB不仅仅缓存索引,同时还会缓存实际的数据。所以,完全相同的数据库,InnoDB存储引擎可以使用更多的内存来缓存数据库相关的信息,前提是要有足够的物理内存。这对于内存价格不断降低的时代,无疑是个很吸引人的特性。

innodb_buffer_pool_size参数用来设置InnoDB最主要的Buffer(Innodb Buffer Pool)的大小,也就是缓存用户表及索引数据的最主要缓存空间,对InnoDB整体性能影响也最大。无论是MySQL官方手册还是网络上很多人分享的InnoDB优化建议,都是简单地建议将InnoDB的Buffer Pool设置为整个系统物理内存的50%~80%之间。如此轻率地给出此类建议实在有些不妥。

不管是多么简单的参数,都可能与实际运行场景有很大的关系。完全相同的设置,在不同的场景下表现可能相差很大。从InnoDB的Buffer Pool到底该设置多大这个问题来看,首先须要确定的是这台主机是不是只能提供MySQL服务?MySQL须要提供的最大连接数是多少?MySQL中是否还有MyISAM等其他存储引擎提供服务?如果有,其他存储引擎所需的Cache有要多大?

假设是一台单独给MySQL使用的主机,物理内存总大小为8GB,MySQL最大连接数为500,同时还使用了MyISAM存储引擎,这时候整体内存该如何分配呢?

内存分配为如下几大部分。

  • 系统使用,假设预留800MB。
  • 线程独享,最大约为2GB=500×(1MB+1MB+1MB+512KB+512KB),组成大概如下:

sort_buffer_size:1MB
join_buffer_size:1MB
read_buffer_size:1MB
read_rnd_buffer_size:512KB
thread_statck:512KB

  • MyISAM Key Cache,假设大概为1.5GB。
  • InnoDB Buffer Pool最大可用量:8GB-800MB-2GB-1.5GB=3.7GB。

假设这个时候还按照50%~80%的建议来设置,最小也是4GB。通过估算,最大可用值在3.7GB左右,那么在系统负载很高,线程独享内存差不多到极限时,系统很可能就会出现内存不足的问题了。而且上面还只是列出了一些使用内存较大的地方,如果进一步细化,很可能导致可用内存更少。

上面只是一个简单的示例分析,实际情况并不一定是这样的。这里只是希望大家了解,在设置一些参数的时候千万不要想当然,一定要详细地分析可能出现的情况,然后再通过不断测试和调整来达到所处环境的最优配置。就我个人而言,正式环境上线之初,一般都会采取相对保守的参数配置策略。上线之后,再根据实际情况和收集到的各种性能数据进行针对性调整。

当系统上线时,可以通过InnoDB存储引擎提供的关于Buffer Pool实时状态信息作出进一步分析,来确定系统中InnoDB的Buffer Pool使用情况是否正常高效,如示例代码2所示:

从上面的值可以看出总共有2048 pages,还有1978个是Free状态的,只有70个page有数据,read请求329次,其中有19次请求的数据在buffer pool中没有,也就是说有19次是通过读取物理磁盘来读取数据的,所以很容易得出InnoDB Buffer Pool的Read命中率大概在为:(329-19)/329×100%=94.22%。

当然,通过上面的数据,还可以分析出write命中率,可以得到发生了多少次read_ahead_rnd,多少次read_ahead_seq,发生过多少次latch,多少次因为Buffer空间大小不足而产生wait_free,等等。

单从这里的数据来看,我们设置的Buffer Pool过大,仅仅使用70/2048×100%=3.4%。

在InnoDB Buffer Pool中,还有一个非常重要的概念,叫做“预读”。一般来说,预读概念主要在一些高端存储才会有,简单来说,就是通过分析数据请求的特点来自动判断客户在请求当前数据块之后可能会继续请求的数据块。通过该自动判断,存储引擎可能会一次性将当前请求的数据库和后面可能请求的下一个(或者几个)数据库全部读出,以期望通过这种方式减少磁盘IO次数,提高IO性能。在上面列出的状态参数中就有两个专门针对预读:

  • Innodb_buffer_pool_read_ahead_rnd,记录进行随机读的时候产生的预读次数;
  • Innodb_buffer_pool_read_ahead_seq,记录连续读的时候产生的预读次数;

innodb_log_buffer_size参数的使用

顾名思义,这个参数就是用来设置InnoDB的Log Buffer大小的,系统默认值为1MB。Log Buffer的主要作用就是缓冲Log数据,提高写Log的IO性能。一般来说,如果系统不是写负载非常高且以大事务居多的话,8MB以内的大小就完全足够了。

也可以通过系统状态参数提供的性能统计数据来分析Log的使用情况,如示例代码3所示:

通过这三个状态参数可以很清楚地看到Log Buffer的等待次数和性能状态。

当然,如果完全从Log Buffer本身来说,自然是大一些会减少更多的磁盘IO。但是由于Log本身是为了保护数据安全而产生的,而Log从Buffer到磁盘的刷新频率和控制数据安全一致的事务直接相关,并且也由相关参数来控制(innodb_flush_log_at_trx_commit)。

innodb_additional_mem_pool_size参数理解

innodb_additional_mem_pool_size所设置的是用于存放InnoDB的字典信息和其他内部结构所需要的内存空间。所以,InnoDB表越多,需要的空间自然也就越大,系统默认值仅有1MB。当然,如果InnoDB在实际运行过程中出现了实际需要的内存比设置值更大时,InnoDB也会继续通过OS来申请内存空间,并且会在MySQL的错误日志中记录一条相应的警告信息让我们知晓。

一个常规的几百个InnoDB表的MySQL,如果不是每个表都是上百个字段,20MB内存已经足够了。当然,如果有足够多的内存,完全可以继续增大这个值。实际上,innodb_additional_mem_ pool_size参数对系统整体性能并无太大的影响,所以,只要能存放需要的数据即可,设置超过实际所需的内存并没有太大意义,只是浪费内存而已。

Double Write Buffer

Double Write Buffer是InnoDB所使用的一种较为独特的文件Flush实现技术,主要作用是在减少文件同步次数提高IO性能的情况下,提高系统崩溃(Crash)或断电情况下数据的安全性,避免写入的数据不完整。

一般来说,InnoDB在将数据同步到数据文件进行持久化之前,首先会将须要同步的内容写入存在于表空间中的系统保留的存储空间,也就是被称之为Double Write Buffer的地方,然后再将数据进行文件同步。所以实质上,Double Write Buffer中就是存放了一份需要同步到文件中数据的备份,以便在遇到系统崩溃(Crash)或主机断电时,能够校验最后一次文件同步是否准确地完成了。如果未完成,则可以通过这个备份来继续完成工作,从而保证数据的正确性。

这样,InnoDB不是又一次增加了整体IO量了吗?不是可能会影响系统的性能么?不用太担心,因为Double Write Buffer是一块连续的磁盘空间,所有写入Double Write Buffer的操作都按连续的顺序写入操作,与整个同步过程相比,这点IO消耗所占的比例是非常小的。为了保证数据的准确性,这一点点性能损失是完全可以接受的。

实际上,并不是所有的场景都须要使用Double Write这样的机制来保证数据的安全准确性,比如在使用某些特别文件系统时,如Solaris平台上非常著名的ZFS文件系统,就可以保证文件写入的完整性。而且在Slave端,也可以禁用Double Write机制。

Adaptive Hash Index

在InnoDB中,实现了一个自动监测各表索引变化情况的机制,然后通过一系列的算法来判定如果存在一个Hash Index是否会对索引搜索带来性能改善。如果InnoDB认为可以通过Hash Index来提高检索效率,它就会在内部建立一个基于某个B-Tree索引的Hash Index,而且会根据该B-Tree索引的变化自行调整,这就是常说的Adaptive Hash Index。当然,InnoDB并不一定会将整个B-Tree索引完全转换为Hash Index,可能只是取用该B-Tree索引键一定长度的前缀来构造一个Hash Index。

Adaptive Hash Index并不会进行持久化存放在磁盘,仅仅存在于Buffer Pool中。所以,每次MySQL刚启动时并不存在Adaptive Hash Index,只有在停止服务之后,InnoDB才会根据相应的请求来构建。

Adaptive Hash Index的目的并不是为了改善磁盘IO的性能,而是为了提高Buffer Pool中的数据的访问效率,说得更浅显一点就是给Buffer Pool中的数据做索引。所以,InnoDB在具有大容量内存(可以设置大的Buffer Pool)的主机上,对于其他存储引擎来说,存在一定的性能优势。

事务优化

选择合适的事务隔离级别

InnoDB存储引擎是MySQL中少有支持事务的存储引擎之一,这也是其目前在MySQL环境中使用最广泛的一个重要原因。由于事务隔离的实现本身需要消耗大量的内存和计算资源,而且不同的隔离级别所消耗的资源也不一样,性能表现也各不相同。

首先,大概了解一下InnoDB所支持的各种事务隔离级别。通过InnoDB的参考手册,得到InnoDB在事务隔离级别方面支持的信息如下。

  • READ UNCOMMITTED

常被称为Dirty Reads(脏读),可以说是事务上的最低隔离级别,在普通的非锁定模式下SELECT的执行使我们看到的数据可能并不是查询发起时间点的数据,因而在这个隔离度下是非Consistent Reads(一致性读)的。

  • READ COMMITTED

这个事务隔离级别有些类似Oracle数据库默认的隔离级。属于语句级别的隔离,如通过SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE来执行的请求仅仅锁定索引记录,而不锁定之前的间隙,因而允许在锁定的记录后自由地插入新记录。当然,这与InnoDB的锁定实现机制有关。如果Query可以很准确地通过索引定位到需要锁定的记录,则只须要锁定相关的索引记录,而不须要锁定该索引之前的间隙。但如果Query通过索引检索时无法通过索引准确定位到须要锁定的记录,则只须要锁定相关的索引记录,而不须要锁定该索引之前的间隙。但如果Query通过索引检索时无法通过索引准确定位到须要锁定的记录,或者是一个基于范围的查询,InnoDB就必须设置next-key或gap locks来阻塞其他用户对范围内的空隙插入。

这一隔离级别下,不会出现Dirty Read,但是可能出现Non-Repeatable Reads(不可重复读)和Phantom Reads(幻读)。

  • REPEATABLE READ

REPEATABLE READ隔离级别是InnoDB默认的事务隔离级。SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE和DELETE,这些以唯一条件搜索唯一索引的,只锁定所找到的索引记录,而不锁定该索引之前的间隙。否则这些操作将使用next-key锁定,以next-key和gap locks锁定找到的索引范围,并阻塞其他用户的新建插入。在Consistent Reads中,与前一个隔离级相比这是一个重要的差别:在这一级中,同一事务中所有的Consistent Reads均读取第一次读取时已确定的快照。这个约定就意味着如果在同一事务中发出几个无格式(plain)的SELECT,这些SELECT的相互关系是一致的。

在REPEATABLE READ隔离级别下不会出现Dirty Reads,也不会出现Non-Repeatable Reads,但是仍然存在Phantom Reads的可能。

  • SERIALIZABLE

SERIALIZABLE隔离级别是标准事务隔离级别中的最高级。设置为SERIALIZABLE隔离级别之后,在事务中的任何时候所看到的数据都是事务启动时的状态,不论在这期间是不是有其他事务已经修改了某些数据并提交。所以,在SERIALIZABLE事务隔离级别下,Phantom Reads也不会出现。

以上4种事务隔离级别实际上就是ANSI/ISO SQL92标准所定义的4种隔离级别,InnoDB全部都实现了。对于高并发应用来说,为了尽可能保证数据的一致性,避免并发可能带来的数据不一致,自然是事务隔离级别越高越好。但是,对于InnoDB来说,所使用的事务隔离级别越高,实现复杂度自然就会更高,所要做的事情也会更多,整体性能也就更差。

所以,我们需要分析应用系统的逻辑,选择可以接受的最低事务隔离级别,以保证数据安全一致性,同时达到最高的性能。

虽然InnoDB存储引擎默认的事务隔离级别是REPEATABLE READ,但实际上在大部分应用场景下,只需要READ COMMITED的事务隔离级别就可以满足需求了。

事务与IO的关系及优化

InnoDB存储引擎通过缓存技术,将常用数据和索引缓存到内存中,在读取数据或者索引的时候就可以尽量减少物理IO来提高性能。修改数据的时候InnoDB是如何处理的呢?修改数据的时候InnoDB是不是像常用应用系统中的缓存一样,更改缓存中数据的同时,将更改应用到相应的数据持久化系统中?

可能很多人都会有这个疑问。实际上,InnoDB在修改数据的时候也只是修改Buffer Pool中的数据,并不是在一个事务提交的时候就将BufferPool中被修改的数据同步到磁盘,而是通过另外一种支持事务的数据库系统常用的手段,将修改信息记录到相应的事务日志中。

为什么不是直接将Buffer Pool中被修改的数据同步到磁盘,还要记录一个事务日志呢,这样不是反而增加了整体IO量了么?是的,对于系统的整体IO量而言确实是有所增加。但是,对于系统的整体性能却有很大的帮助。

这里须要理解关于磁盘读写的两个概念:连续读写和随机读写。简单来说,磁盘的顺序读写就是将数据按顺序写入连续的物理位置,而随机读写则相反,数据需要根据各自的特定位置被写入各个位置,也就是被写入了并不连续的物理位置。对于磁盘来说,写入连续的位置最大的好处就是磁头所做的寻址动作很少,而磁盘操作中最耗费时间的就是磁头的寻址。所以,在磁盘操作中,连续读写操作比随机读写操作的性能要好很多。

我们的应用修改Buffer Pool中的数据都是随机的,每次所做的修改都是一个或少数几个数据页,多次修改的数据页也很少会连续。如果每次修改之后都将Buffer Pool中的数据同步到磁盘,那么磁盘就只能一直忙于频繁的随机读写操作。而事务日志在创建之初就申请了连续的物理空间,而且每次写入都是紧接着之前的日志数据顺序地往后写入,基本上都是一个顺序的写入过程。所以,日志的写入操作远比同步Buffer Pool中被修改的数据要更快。

当然,由于事务日志都是通过几个日志文件循环写入,而且每个日志文件大小固定,即使再多的日志也会有旧日志被新产生的日志覆盖的时候。所以,Buffer Pool中的数据还是不可避免地须要被刷新到磁盘上进行持久化,而且这个持久化的动作必须在旧日志被新日志覆盖之前完成。只不过,随着更新数据(Dirty Buffer)的增加,须要刷新的数据连续性就越高,所需的随机读写也就越少,IO性能也就得到了提升。

而且事务日志本身也有Buffer(log buffer),事务日志的每次写入并不是直接在文件中,而是暂时先写入到log buffer中,然后在一定的事件触发下才会同步到文件。当然,为了尽可能地减少事务日志的丢失,可以通过innodb_log_buffer_size参数来控制log buffer的大小。关于事务日志何时同步的说明稍后会做详细分析。

事务日志文件的大小与InnoDB的整体IO性能有非常大的关系。从理论上讲,日志文件越大,Buffer Pool所需的刷新动作也就越少(在Buffer Pool允许足够多的Dirty Buffer的前提下),性能也越高。但是不能忽略系统Crash之后的恢复。

事务日志的作用主要有两个,一个是提高系统整体IO性能,另一个是系统Crash之后的恢复。下面就来简单地分析一下系统崩溃(Crash)之后,InnoDB是如何利用事务日志来进行数据恢复的。

InnoDB中记录了每一次对数据库中的数据及索引所做的修改,以及与修改相关的事务信息。同时还记录了系统每次Checkpoint与log sequence number(日志序列号)。

假设在某一时刻,MySQL Crash了,那么,所有Buffer Pool中的数据都会丢失,包括已经修改且没有来得及刷新到数据文件中的数据。难道就让这些数据丢失么?当然不会,在MySQL从崩溃(Crash)之后再次启动,InnoDB会通过比较事务日志中所记录的Checkpoint信息和各个数据文件中的Checkpoint信息,找到最后一次Checkpoint所对应的Log Sequence Number,然后通过事务日志中所记录的变更记录,将从崩溃(Crash)之前最后一次Checkpoint往后的所有变更重新应用一次,同步所有的数据文件到一致状态,这样就找回了因为系统崩溃(Crash)而造成的所有数据丢失。当然,对于Log Buffer中未来得及同步到日志文件的变更数据就无法再找回了。系统崩溃(Crash)的时间离最后一次Checkpoint的时间越长,所需要的恢复时间也就越长。而日志文件越大,InnoDB所做的Checkpoint频率也越低,自然遇到长时间恢复的可能性也就越大了。

总的来说,InnoDB的事务日志文件设置得越大,系统的IO性能也就越高。但是当遇到MySQL、OS或主机崩溃(Crash)的时候,系统所需要的恢复时间也就越长;反之,日志越小,IO性能自然也就相对会差一些,但是在MySQL、OS或主机崩溃(Crash)之后所需要的恢复时间也越小。所以,到底该将事务日志设置多大其实是一个整体权衡的问题,既要考虑到系统整体的性能,又要兼顾到崩溃(Crash)之后的恢复时间。一般来说,在个人维护的环境中,比较偏向于将事务日志设置为3组,每个日志设置为256MB,整体效果还算不错。

前面所描述的还只是MySQL Crash的场景,丢失的只是Buffer Pool中的数据。实际上InnoDB事务日志也不一定每次事务提交或回滚都保证会同步log buffer中的数据到文件系统,并通知文件系统做文件同步操作。所以在OS崩溃(Crash),或者是主机断点之后,事务日志写入文件系统Buffer中的数据还是可能会丢失。这种情况下,如果事务日志没有及时同步文件系统,刷新缓存中的数据到磁盘文件,就可能会产生因日志数据丢失而造成数据永久性丢失的情况。

其实InnoDB早就考虑到了这种情况,所以在系统中设计了控制InnoDB事务日志刷新方式的参数:innodb_flush_log_at_trx_commit。这个参数的主要功能就是告诉系统,在什么情况下该通知文件系统刷新缓存中的数据到磁盘文件,可设置为如下三种值:

  • innodb_flush_log_at_trx_commit = 0,InnoDB中的Log Thread每隔1秒钟会将log buffer中的数据写入文件,同时还会通知文件系统进行与文件同步的flush操作,保证数据确实已经写入磁盘。但是,每次事务的结束(COMMIT或是ROLLBACK)并不会触发Log Thread将Log Buffer中的数据写入文件。所以,当设置为0时,在MySQL Crash和OS Crash或主机断电之后,最极端的情况是丢失1秒的数据变更。
  • innodb_flush_log_at_trx_commit = 1,这也是InnoDB的默认设置。每次事务的结束都会触发Log Thread将log buffer中的数据写入文件、并通知文件系统同步文件。这个设置是最安全的,能够保证不论是MySQL Crash、OS Crash还是主机断电都不会丢失任何已经提交的数据。
  • innodb_flush_log_at_trx_commit = 2,当设置为2的时候,Log Thread会在每次事务结束的时候将数据写入事务日志,仅仅是调用了文件系统的文件写入操作。而文件系统都是有缓存机制的,所以Log Thread的写入并不能保证内容已经写入到物理磁盘完成持久化的动作。文件系统什么时候会将缓存中的数据同步到物理磁盘、文件,Log Thread就完全不知道了。所以,当设置为2的时候,MySQL Crash并不会造成数据的丢失,但是OS Crash或主机断电后可能丢失的数据量就完全控制在文件系统上了。

从上面的分析可以看出,当innodb_flush_log_at_trx_commit设置为1时是最安全的。由于所做的IO同步操作最多,性能也是三种设置中最差的一种。如果设置为0,则每秒有一次同步,性能相对高一些。如果设置为2,性能可能是这三种中最好的。但也可能是出现故障后丢失数据最多的一种。到底该如何设置,就要根据具体的场景来分析了。一般来说,如果完全不能接受数据的丢失,那么可以通过牺牲一定的性能来换取数据的安全性,选择设置为1。如果可以丢失很少量的数据(比如说1秒之内),那么可以设置为0。当然,如果OS足够稳定,主机硬件设备足够好,而且主机的供电系统也足够安全,可以将innodb_flush_log_at_trx_commit设置为2,让系统的整体性能尽可能地高。

前面还提到了设置Log Buffer大小的参数innodb_log_buffer_size,这里简单介绍一下Log Buffer的设置要领。Log Buffer所存放的数据就是事务日志写入文件之前在内存中的一个缓冲区域。所以,从理论上来讲,Log Buffer越大,系统的性能也会越高。但是,由于触发Log Thread将Log Buffer中的数据写入文件的事件并不仅仅是Log Buffer空间用完的情况,还与innodb_flush_log_at_trx_commit参数的设置有关。如果该参数设置为1或2,那么Log Buffer中只须要保存单个事务的变更量与系统最高并发事务的乘积。也就是说,如果系统同时进行修改的并发事务最高为20,那么Log Buffer就只须要存放20个事务所做的变更。当然,如果设置为0,Log Buffer中须要存放的数据则是1秒内所有的变更量。所以,大家需要根据自己系统的具体环境来针对性地分析innodb_log_buffer_size设置的大小。一般来说,如果不是特别高的事务并发度或系统中都是大事务,8MB的内存空间已经完全够用了。

数据存储优化

理解InnoDB数据及索引文件存储格式

InnoDB存储引擎的数据(包括索引)存放在相同的文件中,这一点和MySQL默认存储引擎MyISAM的区别较大,后者分别存放于独立的文件。除此之外,InnoDB的数据存放格式也比较独特,每个InnoDB表都会将主键以聚簇索引的形式创建。所有的数据都以主键升序排列在物理磁盘上面,所以主键查询并且以主键排序的查询效率也会非常高。

由于主键是聚簇索引的,InnoDB基于主键的查询效率非常高。如果在创建一个InnoDB存储引擎表时并没有创建主键,那么InnoDB会尝试创建于表上的其他索引。如果存在由单个not null属性列的唯一索引,InnoDB则会选择该索引作为聚簇索引。如果没有任何单个not null属性列的唯一索引,InnoDB会自动生成一个隐藏的内部列,该列会在每行数据上占用6个字节的存储长度。所以,实质上每个InnoDB表都至少会有一个索引存在。

在InnoDB上面除了聚簇索引之外的索引,都被称为secondary index,每个secondary index上都含有聚簇索引的索引键信息,方便通过其他索引查找数据的时候能够更快地定位数据位置。

当然,聚簇索引并不是只有优点,没有任何缺点,要不然其他数据库早就大力推广了。聚簇索引的最大问题就是索引键被更新造成的成本并不只是索引数据可能会移动,而是相关的所有记录数据都须要移动。所以,为了性能考虑,尽可能不要更新InnoDB的主键值。

  • Page

InnoDB存储引擎中的所有数据,不论是表还是索引,或是存储引擎自己的各种结构,都以page作为最小物理单位来存放,每个page默认大小为16KB。

  • extent

extent是一个由多个连续page组成的一个物理存储单位。一般来说,每个extent为64个page。

  • segment

segment在InnoDB存储引擎中实际上也代表“files”的意思,每个segment由一个或多个extent组成,而且每个segment都存放同一种数据。一般来说,每个表数据会存放于一个单独的segment中,实际上也就是每个聚簇索引会存放于一个单独的segment中。

  • tablespace

tablespace是InnoDB中最大物理结构单位,由多个segment组成。

当tablespace中的某个segment须要增长的时候,InnoDB最初仅仅分配某一个extent的前32个page,如果继续增长才会分配整个extent来使用。可以通过执行如示例代码4中所示的命令来查看Innodb表空间的使用情况:

虽然每个索引页(index page)大小仅为16KB,但是实际上Innodb在第一次使用该page的时候,如果一个顺序的索引插入,都会预留1KB的空间。而如果随机插入,那么大约会使用(8-15/16)KB的空间。如果一个Index page在进行多次删除之后所占用的空间已经小于8KB(1/2),Innodb会通过一定的收缩机制收缩索引,并释放该index page。此外,每个索引记录中都存放了一个6字节的头信息,主要用于锁定时候的记录及各个索引记录的关联信息。

Innodb在存放数据页的时候不仅仅是存放实际定义的列,同时还会增加两个内部隐藏列,其中一个隐含列的信息主要为事务相关信息,会占用6字节的长度。另外一个则占用7字节长度,主要用来存放一个指向Undo Log中的Undo Segment指针的相关信息,主要用于事务回滚,以及通过Undo Segment中的信息构造多版本数据页。

通过上面的信息,我们至少可以得出对性能有较大影响的4个要点。

  1. 为了尽量减小secondary index的大小,提高访问效率,作为主键的字段所占用的存储空间越小越好,最好是INTEGER类型。当然这并不是绝对的,字符串类型的数据同样可以作为Innodb表的主键。
  2. 创建表的时候尽量自己指定相应的主键,让数据按照预设的顺序排序存放,以提高特定条件下的访问效率。
  3. 尽可能不要在主键上进行更新操作,减少因为主键值的变化带来数据的移动。
  4. 尽可能提供主键条件进行查询。

分散IO提升磁盘响应

由于Innodb和其他非事务存储引擎相比,在记录数据文件的同时还记录相应的事务日志(Transaction Log),相当于增加了整体的IO量。虽然事务日志是以完全顺序的方式写入磁盘,但总会有一定的IO消耗,所以对于没有使用RAID的存储系统来说,建议将数据文件和事务日志文件分别存放于不同的物理磁盘以降低磁盘的相互争用,提高整体IO性能。我们可以通过innodb_log_group_home_dir参数来指定Innodb日志存放位置,再通过设置数据文件位置innodb_data_home_dir参数来告诉Innodb希望将数据文件存放在哪里。

当然,如果使用独享表空间,Innodb会为每个Innodb表创建一个表空间,并且会将该表空间存放在和“.frm”文件相同的路径下。幸运的是,Innodb允许通过软链接的方式来访问数据或日志文件。所以,如果有必要,甚至可以将每个表存放于单独的物理磁盘,然后通过软链接的方式来告诉Innodb实际文件在哪里。

当我们使用共享表空间的时候,最后一个数据文件必须是可以自动扩展的。这样就会带来一个疑问,在每次扩展的时候,到底该扩展多大空间,性能会比较好呢?Innodb设计了innodb_autoextend_increment这个参数,让我们可以自行控制表空间文件每次增加的大小。

Innodb其他优化

除了上面这些可以优化的地方之外,实际上Innodb还有其他一些可能影响到性能的参数设置。

  • Innodb_flush_method

用来设置Innodb打开和同步数据文件及日志文件的方式,不过只在Linux & Unix系统上有效。系统默认值为fdatasync,即Innodb默认通过fsync()来flush数据和日志文件数据。

此外,还可以设置为O_DSYNC和O_DIRECT。当设置为O_DSYNC的时候,系统以O_SYNC方式打开和刷新日志文件,并通过fsync()来打开和刷新数据文件。而设置为O_DIRECT的时候,则通过O_DIRECT(Solaris上为directio())打开数据文件,同时以fsync()来刷新数据和日志文件。

总的来说,innodb_flush_method的不同设置主要影响的是Innodb在不同运行平台下进行IO操作时所调用的操作系统IO接口的区别。而不同的IO操作接口对数据的处理方式会有一定的区别,所以处理性能也会有一定的差异。一般来说,如果磁盘是通过RAID卡做了硬件级别的RAID,建议使用O_DIRECT,可以在一定程度上提高IO性能。如果RAID Cache不够,须要谨慎对待。此外,根据MySQL官方手册的介绍,如果存储环境是SAN环境,使用O_DIRECT有可能会反而使性能降低。对于支持O_DSYNC的平台,也可以尝试设置为O_DSYNC方式,看是否能对写IO性能有所帮助。

  • innodb_thread_concurrency

这个参数主要控制Innodb内部的并发处理线程数量的最大值,系统内部会有相应的检测机制进行检测控制并发线程数量,建议将Innodb设置为CPU个数与磁盘个数之和。但是这个参数一直非常有争议,而且非常著名的BUG(#15815)一直被认为与innodb_thread_concurrency参数所控制的内容相关。从该参数在系统中默认值的变化可以看出,即使是Innodb开发人员也并不是很清楚将innodb_thread_concurrency设置为多少才合适。在MySQL 5.0.8之前,默认值为8,从MySQL 5.0.8开始到MySQL 5.0.18,默认值又被更改为20,然后在MySQL 5.0.19和MySQL 5.0.20两个版本中又默认设置为0。之后,从MySQL 5.0.21开始,默认值再次被更改为8。

innodb_thread_concurrency参数的设置范围是0~1000,但是在MySQL 5.0.19之前的版本,只要该值超过20,Innodb就会认为不需要对并发线程数做任何限制,也就是说Innodb不会再进行并行线程的数目检查。同样,可以通过将其默认值设置为0来禁用并行线程检查,让Innodb根据实际须要创建并行线程,而且在不少场景下设置为0是一个非常不错的选择,尤其是当系统写IO压力较大的时候。

总的来说,innodb_thread_concurrency参数的设置并没有一个很好的规则来判断什么场景该设置多大,而是须要通过不断地调整尝试,寻找出适合应用的设置。

  • autocommit

autocommit的用途大家应该都很清楚,就是当我们将该参数设置为true(1)时,每次执行完一条会修改数据的Query后,系统内部都会自动提交该操作,基本上可以理解为屏蔽了事务的概念。

设置aotucommit为true(1)之后,提交相对于手工控制commit时机来说可能会变得频繁很多。带来的直接影响就是Innodb的事务日志可能须要非常频繁地执行磁盘同步操作,当然还与innodb_flush_log_at_trx_commit参数的设置相关。

一般来说,在通过LOAD ... INFILE ...或其他某种方式向Innodb存储引擎的表加载数据,将autocommit设置为false可以极大地提高加载性能。而在正常的应用中,也最好尽量通过自行控制事务的提交避免过于频繁的日志刷新来保证性能。

Innodb性能监控

可以通过执行“SHOW INNODB STATUS”命令来获取比较详细的系统当前Innodb性能状态,如示例代码5所示:

通过上面的输出,可以看到整个信息被分为7个部分,分别说明如下:

  • SEMAPHORES,这部分主要显示系统中当前的信号等待信息及各种等待信号的统计信息,这部分信息对于调整innodb_thread_concurrency参数有非常大的帮助。当等待信号量非常大的时候,可能就须要禁用并发线程检测设置innodb_thread_concurrency=0;
  • TRANSACTIONS,这里主要展示系统的锁等待信息和当前活动事务信息。通过这部分输出,可以追踪到死锁的详细信息;
  • FILE I/O,文件IO相关的信息,主要是IO等待信息;
  • INSERT BUFFER AND ADAPTIVE HASH INDEX;显示插入缓存当前状态信息及自适应Hash Index的状态;
  • LOG、Innodb事务日志相关信息,包括当前的日志序列号(Log Sequence Number),已经刷新同步到哪个序列号,最近的Check Point到哪个序列号了。除此之外,还显示了系统从启动到现在已经做了多少次Ckeck Point,多少次日志刷新;
  • BUFFER POOL AND MEMORY,这部分主要显示Innodb Buffer Pool相关的各种统计信息,以及其他一些内存使用的信息;
  • ROW OPERATIONS,顾名思义,主要显示的是与客户端的请求Query和Query所影响的记录统计信息。

当然,如果总是要通过不断执行“SHOW INNODB STATUS”命令来获取性能信息实在有些麻烦,所以Innodb存储引擎设计了一个比较奇怪的方式来持续获取该信息并输出到MySQL Error Log中。

实现方式就是通过创建一个名为innodb_monitor,存储引擎为Innodb的表,够奇特吧!如示例代码6所示:

创建这个表之后,Innodb就会每过15秒输出一次Innodb整体状态信息,也就是上面所展示的信息会被记录到Error Log中。可以通过删除该表停止该Monitor功能,如示例代码7所示:

除此之外,还可以通过相同的方式打开和关闭innodb_tablespace_monitor、innodb_lock_monitor、innodb_table_monitor这三种监控功能,各位读者朋友可以自行尝试。

通过各种监控信息的输出,可以比较详细地了解Innodb当前的运行状态,帮助我们及时发现性能问题。

参考文档