上一篇:《第5章-1 优化服务器设置》
一些配置选项会影响MySQL将数据同步到磁盘和执行恢复的方式。这会涉及I/O操作,因此会极大地影响性能。这些选项还代表了性能和数据安全之间的权衡。一般来说,确保数据立即且一致地写入磁盘的代价是很高的。如果愿意冒磁盘写入操作没有真正写入持久存储的风险,是可以增加并发性和/或减少I/O等待的,但你必须自己决定可以承受多大风险。
InnoDB不仅允许你控制其恢复方式,还允许控制其打开和刷新数据的方式,这将极大地影响恢复和总体性能。尽管可以通过配置影响其采取的行动,但InnoDB的恢复过程是自动的,并且总是在InnoDB启动时运行。撇开恢复不谈,假设没有任何崩溃或出错,InnoDB还有很多需要配置的地方。它有复杂的缓冲区和文件的链,旨在提高性能和保证ACID属性,链的每个部分都是可配置的。图5-1显示了这些文件和缓冲区。
图5-1:InnoDB的缓冲区和文件
为了正常使用,需要更改的几个最重要的参数包括InnoDB日志文件大小、InnoDB如何刷新其日志缓冲区,以及InnoDB如何执行I/O。
InnoDB使用日志来降低提交事务的成本。它不会在每个事务提交时将缓冲池刷新到磁盘,而是将事务记录到日志中。事务对数据和索引所做的更改通常映射到表空间中的随机位置,因此将这些更改刷新到磁盘将需要随机I/O。InnoDB假定它使用的是传统的磁盘,随机I/O比顺序I/O的开销要大很多,因为随机I/O需要在磁盘上寻找正确的位置,并等待将所需的磁盘部分旋转到磁头下。
使用日志,InnoDB可以将随机磁盘I/O转换为顺序I/O。一旦日志被安全地保存在磁盘中,即使更改的数据尚未写入数据文件,事务仍将是持久的。如果发生故障(例如停电),InnoDB可以重放日志并恢复已提交的事务。
当然,InnoDB最终必须将更改的数据写入数据文件,因为日志的大小固定,采取的是循环写入的方式:当到达日志的末尾时,它会环绕到日志的开头。如果日志记录中包含的更改尚未应用于数据文件,则无法覆盖日志记录,因为这将删除已提交事务的唯一永久记录。
InnoDB使用后台线程智能地刷新对数据文件的更改。该线程可以将写入分组,并使数据写入顺序化,以提高效率。实际上,事务日志可以将随机数据文件I/O转换为顺序日志文件I/O和顺序数据文件I/O。将刷新移到后台可以更快地完成查询,并有助于缓冲I/O系统的查询负载峰值。
日志文件的总大小由innodb_log_file_size和innodb_log_files_in_group控制,这对写入性能非常重要。如果你采纳了我们之前的建议,使用innodb_dedicated_server,日志文件的大小将根据系统内存量来自动管理。
InnoDB修改数据时会将修改记录写入日志缓冲区,并将其保存在内存中。当缓冲区满了、事务提交时,或者每秒1次(这三个条件以先满足者为准),InnoDB会将缓冲区刷新到磁盘上的日志文件中。如果有大型事务,增加缓冲区大小(默认为1MB)有助于减少I/O。控制缓冲区大小的变量是innodb_log_buffer_size。
通常不需要将缓冲区设置得太大。建议的范围是1~8MB,一般来说足够了,除非写入很多大的BLOB记录。与InnoDB的普通数据相比,日志条目非常紧凑。它们不是基于页面的,所以不会浪费空间一次存储整个页面。InnoDB也会让日志条目尽量短,有时甚至只用几个整数来表示记录的操作类型和该操作所需的任何参数!
当InnoDB将日志缓冲区刷新到磁盘上的日志文件时,会使用互斥锁锁定缓冲区,将其刷新到所需的位置,然后将剩余的条目移动到缓冲区的前面。当释放互斥锁时,可能会有多个事务准备刷新其日志条目。InnoDB使用了一个组提交特性,可以在单次I/O操作中将一组日志全部提交。
日志缓冲区必须被刷新到持久存储中,以确保提交的事务完全持久。如果你更关心性能而不是持久 性,可以更改innodb_flush_log_at_trx_commit来控制日志缓冲区的刷新位置和刷新频率。
可能的设置如下。
0
每秒定时将日志缓冲区写入日志文件,并刷新日志文件,但在事务提交时不做任何操作。
1
每次事务提交时,将日志缓冲区写入日志文件,并将其刷新到持久存储中。这是默认的(也是最安全的)设置;它保证你不会丢失任何已提交的事务,除非磁盘或操作系统“假装”进行刷新操作(没有将数据真正写入磁盘)。
2
每次事务提交时都将日志缓冲区写入日志文件,但不执行刷新。InnoDB按计划每秒刷新1次。与0设置最重要的区别是,如果只是MySQL进程崩溃,设置为2不会丢失任何事务。但是,如果整个服务器崩溃或断电,仍然可能丢失事务。
了解将日志缓冲区写入日志文件和将日志刷新到持久存储之间的区别很重要。在大多数操作系统中,将缓冲区写入日志只是将数据从InnoDB的内存缓冲区移动到操作系统的缓存中,依然还是在内存中。它实际上不会将数据写入持久存储。因此,如果发生崩溃或断电,设置为0和2通常会导致最多1秒的数据丢失,因为数据可能只存在于操作系统的缓存中。我们之所以说“通常”,是因为InnoDB会以每秒1次的速度将日志文件刷新到磁盘上,但在某些情况下,例如刷新暂停时,可能会丢失超过1秒的事务。
有时,硬盘控制器或操作系统通过将数据放入另一个缓存(如硬盘自身的缓存)来“假装”进行刷新。这样做速度会更快,但非常危险,如果驱动器断电,数据仍可能丢失。这甚至比将innodb_flush_log_at_trx_commit设置为1之外的其他值更糟糕,可能导致数据损坏,而不仅仅是事务丢失。
将innodb_flush_log_at_trx_commit设置为1之外的其他值有可能导致事务丢失。然而,如果不担心持久性(ACID中的D),你会发现其他设置也很有用。也许你只是想要InnoDB的一些其他功能,比如聚簇索引、防止数据损坏和行级锁定。
高性能事务需求的最佳配置是将innodb_flush_log_at_trx_commit设置为1,并将日志文件放在具有备用电池的写缓存和SSD的RAID卷上,这既安全又非常快。事实上,我们敢说,任何需要处理重要工作负载的生产数据库服务器都需要这种硬件。
innodb_flush_method选项允许配置InnoDB与文件系统的实际交互方式。这个名字有一点误导,实际上该选项还会影响InnoDB读取数据的方式,而不仅仅是写入数据的方式。
改变InnoDB执行I/O操作的方式会极大地影响性能,所以在改变任何东西之前,一定要理解你在做什么!
这是一个有点令人困惑的选项,因为它同时影响日志文件和数据文件,而且有时对每种文件执行不同的操作。最好为日志文件和数据文件分别提供一个配置选项,但目前是组合在一起的。
如果你使用的是类UNIX操作系统,并且RAID控制器有备用电池的写缓存,我们建议使用O_DIRECT。如果不是,则default或O_DIRECT都可能是最佳选择,具体取决于应用程序。如果你选择使用innodb_dedicated_server,正如我们前面提到的,这个选项会自动设置。
InnoDB将数据保存在表空间中,表空间本质上是一个虚拟文件系统,由磁盘上的一个或多个文件组成。InnoDB将表空间用于多种用途,而不仅仅是存储表和索引。表空间中还包含了Undo日志(重新创建旧行版本所需的信息)、修改缓冲区、双写缓冲区和其他内部结构。
可以使用innodb_data_file_path配置选项指定表空间文件。这些文件都包含在innodb_data_home_dir指定的目录中。下面是一个例子:
innodb_data_home_dir = /var/lib/mysql/ innodb_data_file_path = ibdata1:1G;ibdata2:1G;ibdata3:1G
上述配置将创建一个3GB的表空间,跨越了3个文件。有时人们想知道是否可以使用多个文件来将负载分散到多个驱动器上,例如:
innodb_data_file_path = /disk1/ibdata1:1G;/disk2/ibdata2:1G;...
虽然这确实将文件放在了不同的目录中,并且在这个例子中,这些目录还在不同的磁盘上,但InnoDB会将这些文件端到端串起来使用,因此通常不会获得太多性能提升。
InnoDB会先填满第一个文件,然后当第一个文件填满时再填第二个,以此类推;负载并没有以更高性能所需的方式分散。RAID控制器是分散负载的一种更智能的方式。
如果允许表空间在空间不足时增长,可以按如下方式自动扩展最后一个文件:
...ibdata3: 1G:autoextend
默认行为是创建一个10M B的自动扩展文件。如果让文件自动扩展,最好对表空间的大小设置一个上限,以防止其变得过大,因为一旦增长,就无法再收缩。例如,以下配置会将自动扩展文件限制为2 GB:
.ibdata3:1G:autoextend:max:2G
管理单个表空间可能很麻烦,特别是当它自动扩展而你又想回收空间时(出于这个原因,我们建议禁用自动扩展特性,或者至少为空间设置一个合理的上限)。回收空间的唯一方法是将数据导出,然后关闭MySQL并删除所有文件,再修改配置,重启,让InnoDB创建新的空文件,最后再恢复数据。InnoDB对表空间是完全不宽容的:你不能简单地删除文件或者改变其大小。如果损坏了表空间,InnoDB将无法启动。同样,InnoDB对日志文件也非常严格。如果你习惯像使用MyISAM那样随意地移动文件,请务必注意!
innodb_file_per_table选项允许你将InnoDB配置为每个表使用单独的文件。它将数据存储在数据库目录下的tablename.ibd文件中。这使得删除表时更容易回收空间。然而,将数据放在多个文件中实际上会导致更多的空间浪费,因为跟InnoDB单个表空间中的内部碎片相比,每个.ibd文件中都会有一些浪费的空间。
即使启用innodb_file_per_table选项,你也仍然需要用于Undo日志和其他系统数据的主表空间文件。如果不把数据都存储在里面,主表空间文件会更小。
有些人喜欢使用innodb_file_per_table,因为它提供了额外的可管理性和可视性。例如,通过检查单个文件来查找表的大小要比使用SHOW TABLE STATUS快得多,而SHOW TABLE STATUS必须执行更复杂的工作来确定为一个表分配了空间。
innodb_file_per_table也有不好的一面:会使DROP TABLE性能变差。严重时可能导致服务器范围内明显的停顿,原因有二。
删除表将在文件系统级别解除(删除)文件的链接,这在某些文件系统(ext3)中可能非常慢。可以使用文件系统中的技巧来缩短这个过程的持续时间:先将.ibd文件链接到一个大小为零的文件,然后手动删除该文件,而不是等待MySQL来删除。
当启用该选项时,每个表在InnoDB中都有自己的表空间。事实证明,删除表空间需要InnoDB在查找属于该表空间的页面时锁定和扫描缓冲池,这在服务器的缓冲池很大时是非常慢的。如果使用innodb_buffer_pool_instances将缓冲池分解为多个部分,这一点会有所改善。
MySQL的各种版本对此进行了一些修复。从8.0.23版本开始,这应该不再是一个问题了。
最后的建议是什么?我们建议使用innodb_file_per_table并限制共享表空间的大小,这样会使你的生活更轻松。如前所述,如果你遇到任何使此过程痛苦的情况,请考虑我们建议的修复方法。
InnoDB的表空间在写操作频繁的环境中可能会变得非常大。如果事务长时间保持打开状态(即使没有做任何工作),并且使用默认的可重复读取事务隔离级别,InnoDB将无法删除行的旧版本,因为未提交的事务仍需要能够看到它们。InnoDB将旧版本存储在表空间中,因此随着更多数据的更新,它将继续增长。清除过程是多线程的,但如果遇到清除延迟问题(innodb_purge_threads和innodb_purge_batch_size),则可能需要针对工作负载进行调优。
SHOW INNODB STATUS可以帮助定位问题。可以查看TRANSACTIONS部分中的历史列表长度(History list length),其显示了Undo日志的大小:
TRANSACTIONS Trx id counter 1081043769321 Purge done for trx's n:o < 1081041974531 undo n:o < 0 state: running but idleHistory list length 697068
如果Undo日志很大,并且表空间因此而增长,你可以强制MySQL放慢速度来让InnoDB的清理线程跟上。这听起来可能不太吸引人,但别无选择。否则,InnoDB会不断地写入数据并填充磁盘,直到磁盘空间耗尽或者表空间达到所定义的上限。
要限制写操作,请将innodb_max_purge_lag变量设置为0以外的值。这表示在InnoDB开始延迟更多修改数据的查询之前,可以等待清除的最大事务数。你必须了解你的工作负载,才能决定合适的值。例如,如果平均事务影响1KB的行,并且可以在表空间中容忍100MB未清除的行,那么可以将该值设置为100000。
请记住,未清除的行版本会影响所有查询,因为它们会使表和索引变大。如果清除线程不能跟上进度,性能就会下降。设置innodb_max_purge_lag变量也会降低性能,但这是两害相权取其轻。
sync_binlog选项控制MySQL如何将二进制日志刷新到磁盘,默认值是1,意味着MySQL将执行刷新并保持二进制日志的持久性和安全性。强烈推荐将其设置为1,不建议设置为任何其他值。
如果不将sync_binlog设置为1,发生崩溃时可能会导致二进制日志与事务数据不同步。这很容易破坏复制且不可恢复,尤其是当数据库使用全局事务ID时(更多信息,请参阅第9章)。将其设置为1所提供的安全性远远超过由此产生的I/O性能损失。
我们在第4章中深入地讨论了RAID,在这里值得重复的是,高质量的RAID控制器、使用有备用电池的写缓存、同时使用回写(write backup)策略,可以每秒处理数千次写操作,并仍然能提供持久的存储。数据被写入有电池的快速缓存,即使系统断电,也能被保存下来。当电源恢复时,RAID控制器将数据从缓存写入硬盘后再使硬盘可用。因此,具有足够大备用电池的写缓存的优秀RAID控制器可以显著提高性能,是一项非常好的投资。当然,固态存储也是目前推荐的解决方案,可以极大地提高I/O性能。
当在高并发工作负载下运行MySQL时,可能会遇到其他情况下不会遇到的瓶颈。本节将解释如何检测这些问题,以及如何在高并发工作负载下获得最佳性能。
如果遇到InnoDB并发问题,并且运行的MySQL版本低于5.7,解决方案通常是升级服务器。旧版本仍然面临许多高并发可伸缩性的挑战。所有的东西都在诸如缓冲池互斥锁之类的全局互斥锁上排队,导致服务器几乎停止运行。如果升级到较新版本的MySQL,在大多数情况下不需要限制并发性。
如果你发现自己遇到了并发性瓶颈,最好的选择是对数据进行分片。如果分片不可行,那么可能需要限制并发性。InnoDB有自己的“线程调度器”,它控制线程如何进入内核访问数据,以及进入内核后可以做什么。限制并发性的最基本方法是使用innodb_thread_concurrency变量,该变量限制了内核中同时可以有多少线程。值为0表示对线程的数量没有限制。如果是在老版本的MySQL中遇到InnoDB并发问题,这个变量是最重要的配置变量。
(参见链接20 https://oreil.ly/ThOBP)提供了最佳的配置指南。你必须通过实验来找到适合系统的最佳值,我们建议首先将innodb_thread_concurrency设置为与可用CPU核数相同的值,然后根据需要调整大小。
如果内核中已经有超过允许数量的线程,则新的线程不能进入内核。InnoDB使用一个两阶段的过程来尝试让线程尽可能高效地进入内核。两阶段策略减少了操作系统调度器导致的上下文切换开销。线程首先休眠innodb_thread_sleep_delay指定的微秒数,然后再重试。
如果仍然不能进入,它将进入一个等待线程队列,将控制权交给操作系统。
第一阶段的默认睡眠时间为10000微秒。在高并发性环境中,当CPU未充分利用且许多线程处于“进入队列前的睡眠”状态时,更改此值会有所帮助。如果有很多小查询,默认值也可能太大,因为这会增加查询延迟。
一旦线程进入内核,InnoDB就有一定数量的“门票”,可以“免费”返回内核,而无须任何并发性检查。这限制了它在返回到其他等待的线程队列之前可以完成的工作量。
innodb_concurrency_tickets选项控制“门票”的数量。除非有很多非常长时间运行的查询,否则很少需要更改这个选项。“门票”是根据查询而不是事务授予的。一旦查询完成,未使用的门票将被丢弃。
除了缓冲池和其他结构中的瓶颈之外,在提交阶段还有另一个并发瓶颈,主要是由于刷新操作造成的I/O限制。innodb_commit_concurrency变量控制着可以同时提交的线程数。如果在innodb_thread_concurrency设置为较低的值时仍存在大量线程抖动,配置此选项可能会有所帮助。
上一篇:《第5章-1 优化服务器设置》
下一篇: 《第5章-3 安全设置》