##
数据库和文件系统的工具#
数据目录的结构#
表在文件系统中的表示#
kjskfjksdf
s ksfd
InnoDB是如何存储表数据的1#
系统表空间1#
撒旦发就
系统表空间2#
撒旦发就
系统表空间3#
撒旦发就

内容简介
本书是MySQL领域的经典之作,拥有广泛的影响力。第3版更新了大量的内容,不但涵盖了最新MySQL 5.5版本的新特性,也讲述了关于固态盘、高可扩展性设计和云计算环境下的数据库相关的新内容,原有的基准测试和性能优化部分也做了大量的扩展和补充。全书共分为16章和6个附录,内容涵盖MySQL架构和历史,基准测试和性能剖析,数据库软硬件性能优化,复制、备份和恢复,高可用与高可扩展性,以及云端的MySQL和MySQL相关工具等方面的内容。每一章都是相对独立的主题,读者可以有选择性地单独阅读。
本书不但适合数据库管理员(DBA)阅读,也适合开发人员参考学习。不管是数据库新手还是专家,相信都能从本书有所收获。
©2012 by Baron Schwartz,Peter Zaitsev,Vadim Tkachenko.
Simplified Chinese Edition,jointly published by O’Reilly Media,Inc. and Publishing House of Electronics Industry,2013. Authorized translation of the English edition,2012 O’Reilly Media,Inc.,the owner of all rights to publish and sell the same.
All rights reserved including the rights of reproduction in whole or in part in any form.
本书简体中文版专有出版权由O’Reilly Media,Inc.授予电子工业出版社。未经许可,不得以任何方式复制或抄袭本书的任何部分。专有出版权受法律保护。
版权贸易合同登记号图字:01-2013-1661
图书在版编目(CIP)数据
高性能MySQL:第3版/(美)施瓦茨(Schwartz,B.),(美)扎伊采夫(Zaitsev,P.),(美)特卡琴科(Tkachenko,V.)著;宁海元等译.—北京:电子工业出版社,2013.5
书名原文:High Performance MySQL,Third Edition
ISBN 978-7-121-19885-4
第9章 操作系统和硬件优化
MySQL服务器性能受制于整个系统最薄弱的环节,承载它的操作系统和硬件往往是限制因素。磁盘大小、可用内存和CPU资源、网络,以及所有连接它们的组件,都会限制系统的最终容量。因此,需要小心地选择硬件,并对硬件和操作系统进行合适的配置。例如,若工作负载是I/O密集型的,一种方法是设计应用程序使得最大限度地减少MySQL的I/O操作。然而,更聪明的方式通常是升级I/O子系统,安装更多的内存,或重新配置现有的磁盘。
硬件的更新换代非常迅速,所以本章有关特定产品或组件的内容可能将很快变得过时。像往常一样,我们的目标是帮助提升对这些概念的理解,这样对于即使没有直接覆盖到的知识也可以举一反三。这里我们将通过现有的硬件来阐明我们的观点。
许多不同的硬件都可以影响MySQL的性能,但我们认为最常见的两个瓶颈是CPU和I/O资源。当数据可以放在内存中或者可以从磁盘中以足够快的速度读取时,CPU可能出现瓶颈。把大量的数据集完全放到大容量的内存中,以现在的硬件条件完全是可行的(1)。
另一方面,I/O瓶颈,一般发生在工作所需的数据远远超过有效内存容量的时候。如果应用程序是分布在网络上的,或者如果有大量的查询和低延迟的要求,瓶颈可能转移到网络上,而不再是磁盘I/O(2)。
第3章中提及的技巧可以帮助找到系统的限制因素,但即使你认为已经找到了瓶颈,也应该透过表象去看更深层次的问题。某一方面的缺陷常常会将压力施加在另一个子系统,导致这个子系统出问题。例如,若没有足够的内存,MySQL可能必须刷出缓存来腾出空间给需要的数据——然后,过了一小会,再读回刚刚刷新的数据(读取和写入操作都可能发生这个问题)。本来是内存不足,却导致出现了I/O容量不足。当找到一个限制系统性能的因素时,应该问问自己,“是这个部分本身的问题,还是系统中其他不合理的压力转移到这里所导致的?”在第3章的诊断案例中也有讨论到这个问题。
还有另外一个例子:内存总线的瓶颈也可能表现为CPU问题。事实上,我们说一个应用程序有“CPU瓶颈”或者是“CPU密集型”,真正的意思应该是计算的瓶颈。接下来将深入探讨这个问题。
在升级当前硬件或购买新的硬件时,应该考虑下工作负载是不是CPU密集型。
可以通过检查CPU利用率来判断是否是CPU密集型的工作负载,但是仅看CPU整体的负载是不合理的,还需要看看CPU使用率和大多数重要的查询的I/O之间的平衡,并注意CPU负载是否分配均匀。本章稍后讨论的工具可以用来弄清楚是什么限制了服务器的性能。
当遇到CPU密集型的工作时,MySQL通常可以从更快的CPU中获益(相对更多的CPU)。
但这不是绝对的,因为还依赖于负载情况和CPU数量。更古老的MySQL版本在多CPU上有扩展性问题,即使新版本也不能对单个查询并发利用多个CPU。因此,CPU速度限制了每个CPU密集型查询的响应时间。
当我们讨论CPU的时候,为保证本文易于阅读,对某些术语将不会做严格的定义。现在一般的服务器通常都有多个插槽(Socket),每个插槽上都可以插一个有多个核心的CPU(有独立的执行单元),并且每个核心可能有多个“硬件线程”。这些复杂的架构需要有点耐心去了解,并且我们不会总是明确地区分它们。不过,在一般情况下,当谈到CPU速度的时候,谈论的其实是执行单元的速度,当提到的CPU数量时,指的通常是在操作系统上看到的数量,尽管这可能是独立的执行单元数量的多倍(3)。
这几年CPU在各个方面都有了很大的提升。例如,今天的Intel CPU速度远远超过前几代,这得益于像直接内存连接(directly attached memory)技术以及PCIe卡之类的设备互联上的改善等。这些改进对于存储设备尤其有效,例如Fusion-io和Virident的PCIe闪存驱动器。
超线程的效果相比以前也要好得多,现在操作系统也更了解如何更好地使用超线程。而以前版本的操作系统无法识别两个虚拟处理器实际上是在同一芯片上,认为它们是独立的,于是会把任务安排在两个实际上是相同物理执行单元上的虚拟处理器。实际上单个执行单元并不是真的可以在同一时间运行两个进程,所以这样做会发生冲突和争夺资源。而同时其他CPU却可能在闲置,从而浪费资源。操作系统需要能感知超线程,因为它必须知道什么时候执行单元实际上是闲置的,然后切换相应的任务去执行。这个问题之前常见的原因是在等待内存总线,可能花费需要高达一百个CPU周期,这已经类似于一个轻量级的I/O等待。新的操作系统在这方面有了很大的改善。超线程现在已经工作得很好。过去,我们时常提醒人们禁用它,但现在已经不需要这样做了。
这就是说,现在可以得到大量的快速的CPU——比本书的第2版出版的时候要多得多。所以多和快哪个更重要?一般来说两个都想要。从广义上来说,调优服务器可能有如下两个目标:
低延时(快速响应)
要做到这一点,需要高速CPU,因为每个查询只能使用一个CPU。
高吞吐
如果能同时运行很多查询语句,则可以从多个CPU处理查询中受益。然而,在实践中,还要取决于具体情况。因为MySQL还不能在多个CPU中完美地扩展,能用多少个CPU还是有极限的。在旧版本的MySQL中(MySQL 5.1以后的版本已经有一些提升),这个限制非常严重。在新的版本中,则可以放心地扩展到16或24个CPU,或者更多,取决于使用的是哪个版本(Percona往往在这方面略占优势)。
如果有多路CPU,并且没有并发执行查询语句,MySQL依然可以利用额外的CPU为后台任务(例如清理InnoDB缓冲、网络操作,等等)服务。然而,这些任务通常比执行查询语句更加轻量化。
MySQL复制(将在下一章中讨论)也能在高速CPU下工作得非常好,而多CPU对复制的帮助却不大。如果工作负载是CPU密集型,主库上的并发任务传递到备库以后会被简化为串行任务,这样即使备库硬件比主库好,也可能无法保持跟主库之间的同步。也就是说,备库的瓶颈通常是I/O子系统,而不是CPU。
如果有一个CPU密集型的工作负载,考虑是需要更快的CPU还是更多CPU的另外一个因素是查询语句实际在做什么。在硬件层面,一个查询可以在执行或等待。处于等待状态常见的原因是在运行队列中等待(进程已经是可运行状态,但所有的CPU都忙)、等待闩锁(Latch)或锁(Lock)、等待磁盘或网络。那么你期望查询是等待什么呢?如果等待闩锁或锁,通常需要更快的CPU;如果在运行队列中等待,那么更多或者更快的CPU都可能有帮助。(也可能有例外,例如,查询等待InnoDB日志缓冲区的Mutex,直到I/O完成前都不会释放——这可能表明需要更多的I/O容量)。
这就是说,MySQL在某些工作负载下可以有效地利用很多CPU。例如,假设有很多连接查询的是不同表(假设这些查询不会造成表锁的竞争,实际上对MyISAM和MEMORY表可能会有问题),并且服务器的总吞吐量比任何单个查询的响应时间都更重要。吞吐量在这种情况下可以非常高,因为线程可以同时运行而互不争用。
再次说明,在理论上这可能更好地工作:不管查询是读取不同的表还是相同的表, InnoDB都会有一些全局共享的数据结构,而MyISAM在每个缓冲区都有全局锁。而且不仅仅是存储引擎,服务器层也有全局锁。以前InnoDB承担了所有的骂名,但最近做了一些改进后,暴露了服务器层中的其他瓶颈。例如臭名昭著的LOCK_open互斥量(Mutex),在MySQL 5.1和更早版本中可能就是个大问题,另外还有其他一些服务器级别的互斥量(例如查询缓存)。
通常可以通过堆栈跟踪来诊断这些类型的竞争问题,例如Percona Toolkit中的pt-pmp工具。如果遇到这样的问题,可能需要改变服务器的配置,禁用或改变引起问题的组件,进行数据分片(Sharding),或者通过某种方式改变做事的方法。这里无法列举所有的问题和相应的解决方案,但是一旦有一个确定的诊断,答案通常是显而易见的。大部分不幸遇到的问题都是边缘场景,最常见的问题随着时间的推移都在服务器上被修复了。
可能99%以上的MySQL实例(不含嵌入式使用)都运行在Intel或者AMD芯片的x86架构下。本书中我们基本都是针对这种情况。
64位架构现在都是默认的了,32位CPU已经很难买到了。MySQL在64位架构上工作良好,尽管有些事暂时不能利用64位架构来做。因此,如果使用的是较老旧版本的MySQL,在64位服务器上可能要小心。例如,在MySQL 5.0发布的早期时候,每个MyISAM键缓冲区被限制为4 GB,由一个32位整数负责寻址。(可以创建多个键缓冲区来解决这个问题。)
确保在64位硬件上使用64位操作系统!最近这种情况已经不太常见了,但以前经常可以遇到,大多数主机托管提供商暂时还是在服务器上安装32位操作系统,即使是64位CPU。32位操作系统意味着不能使用大量的内存:尽管某些32位系统可以支持大量的内存,但不能像64位系统一样有效地利用,并且在32位系统上,任何一个单独的进程都不能寻址4 GB以上的内存。
多CPU在联机事务处理(OLTP)系统的场景中非常有用。这些系统通常执行许多小的操作,并且是从多个连接发起请求,因此可以在多个CPU上运行。在这样的环境中,并发可能成为瓶颈。大多数Web应用程序都属于这一类。
OLTP服务器一般使用InnoDB,尽管它在多CPU的环境中还存在一些未解决的并发问题。然而,不只是InnoDB可能成为瓶颈:任何共享资源都是潜在的竞争点。InnoDB之所以获得大量关注是因为它是高并发环境下最常见的存储引擎,但MyISAM在大压力时的表现也不好,即使不修改任何数据只是读取数据也是如此。许多并发瓶颈,如InnoDB的行级锁和MyISAM的表锁,没有办法优化——除了尽可能快地处理任务之外,没有别的办法解决,这样,锁就可以尽快分配给等待的任务。如果一个锁是造成它们(其他任务)都在等待的原因,那么不管有多少CPU都一样。因此,即使是一些高并发工作负载,也可以从更快的CPU中受益。
实际上有两种类型的数据库并发问题,需要不同的方法来解决,如下所示。
逻辑并发问题
应用程序可以看到资源的竞争,如表或行锁争用。这些问题通常需要好的策略来解决,如改变应用程序、使用不同的存储引擎、改变服务器的配置,或使用不同的锁定提示或事务隔离级别。
内部并发问题
比如信号量、访问InnoDB缓冲池页面的资源争用,等等。可以尝试通过改变服务器的设置、改变操作系统,或使用不同的硬件解决这些问题,但通常只能缓解而无法彻底消灭。在某些情况下,使用不同的存储引擎或给存储引擎打补丁,可以帮助缓解这些问题。
MySQL的“扩展模式”是指它可以有效利用的CPU数量,以及在压力不断增长的情况下如何扩展,这同时取决于工作负载和系统架构。通过“系统架构”的手段是指通过调整操作系统和硬件,而不是通过优化使用MySQL的应用程序。CPU架构(RISC、CISC、流水线深度等)、CPU型号和操作系统都影响MySQL的扩展模式。这也是为什么说基准测试是非常重要的:一些系统可以在不断增加的并发下依然运行得很好,而另一些的表现则糟糕得多。
有些系统在更多的处理器下甚至可能降低整体性能。这是相当普遍的情况,我们了解到许多人试图升级到有多个CPU的系统,最后只能被迫恢复到旧系统(或绑定MySQL进程到其中某些核心),因为这种升级反而降低了性能。在MySQL 5.0时代,Google的补丁和Percona Server出现之前,能有效利用的CPU核数是4核,但是现在甚至可以看到操作系统报告多达80个“CPU”的服务器。如果规划一个大的升级,必须要同时考虑硬件、服务器版本和工作负载。
某些MySQL扩展性瓶颈在服务器层,而其他一些在存储引擎层。存储引擎是怎么设计的至关重要,有时更换到一个不同的引擎就可以从多处理器上获得更多效果。
我们看到在世纪之交围绕处理器速度的战争在一定程度上已经平息,CPU厂商更多地专注于多核CPU和多线程的变化。CPU设计的未来很可能是数百个处理器核心,四核心和六核心的CPU在今天是很常见的。不同厂商的内部架构差异很大,不可能概括出线程、CPU和内核之间的相互作用。内存和总线如何设计也是非常重要的。归根结底,多个内核和多个物理CPU哪个更好,这是由硬件体系结构决定的。
现代CPU的另外两个复杂之处也值得提一下。首先是频率调整。这是一种电源管理技术,可以根据CPU上的压力而动态地改变CPU的时钟速度。问题是,它有时不能很好地处理间歇性突发的短查询的情况,因为操作系统可能需要一段时间来决定CPU的时钟是否应该变化。结果,查询可能会有一段时间速度较慢,并且响应时间增加了。频率调整可能使间歇性的工作负载性能低下,但可能更重要的是,它会导致性能波动。
第二个复杂之处是boost技术,这个技术改变了我们对CPU模式的看法。我们曾经以为四核2GHz CPU有四个同样强大的核心,不管其中有些是闲置或非闲置。因此,一个完美的可扩展系统,当它使用所有四个内核的时候,可以预计得到四倍的提升。但是现在已经不是这样了,因为当系统只使用一个核心时,处理器会运行在更高的时钟速度上,例如3GHz。这给很多的规划容量和可扩展性建模的工具出了一个难题,因为系统性能表现不再是线性的变化了。这也意味着,“空闲CPU”并不代表相同规模的资源浪费,如果有一台服务器上只运行了备库的复制,而复制执行是单线程的,所以有三个CPU是空闲的,因此认为可以利用这些CPU资源执行其他任务而不影响复制,可能就想错了。
配置大量内存最大的原因其实不是因为可以在内存中保存大量数据:最终目的是避免磁盘I/O,因为磁盘I/O比在内存中访问数据要慢得多。关键是要平衡内存和磁盘的大小、速度、成本和其他因素,以便为工作负载提供高性能的表现。在讨论如何做到这一点之前,暂时先回到基础知识上来。
计算机包含一个金字塔型的缓存体系,更小、更快、更昂贵的缓存在顶端,如图9-1所示。
图9-1:缓存层级
第8章 优化服务器设置
在这一章,我们将解释为这是我的撒旦JFK数据库嘎斯公开就开始打山豆根士大夫 圣诞节复活节是是国家开始大幅机啊可是对方看见噶开暗杀是的JFK开始讲课的感觉爱看书的JFK史蒂夫卡卡萨丁咖啡碱撒快递费始东方会i二位人家儿童科技数据库的房价开始JFK注释MySQL服务器创建一个靠谱的配置文件的过程。这是一个很绕的过程,有很多有意思的关注点和值得关注的思路。关注这些点很有必要,因为创建一个好配置的最快方法不是从学习配置项开始,也不是从问哪个配置项应该怎么设置或者怎么修改开始,更不是从检查服务器行为和询问哪个配置项可以提升性能开始。最好是从理解MySQL内核和行为开始。然后可以利用这些知识来指导配置MySQL。最后,可以将想要的配置和当前配置进行比较,然后纠正重要并且有价值的不同之处。
人们经常问,“我的服务器有32GB内存,12核CPU,怎样配置最好?”很遗憾,问题没这么简单。服务器的配置应该符合它的工作负载、数据,以及应用需求,并不仅仅看硬件的情况。
MySQL有大量可以修改的参数——但不应该随便去修改。通常只需要把基本的项配置正确(大部分情况下只有很少一些参数是真正重要的),应该将更多的时间花在schema的优化、索引,以及查询设计上。在正确地配置了MySQL的基本配置项之后,再花力气去修改其他配置项的收益通常就比较小了。
从另一方面来说,没用的配置导致潜在风险的可能更大。我们碰到过不止一个“高度调优”过的服务器不停地崩溃,停止服务或者运行缓慢,结果都是因为错误的配置导致的。我们将花一点时间来解释为什么会发生这种情况,并且告诉大家什么是不该做的。
那么什么是该做的呢?确保基本的配置是正确的,例如InnoDB的Buffer Pool和日志文件缓存大小,如果想防止出问题(提醒一下,这样做通常不能提升性能——它们只能避免问题),就设置一个比较安全和稳健的值,剩下的配置就不用管了。如果碰到了问题,可以使用第3章提到的技巧小心地进行诊断。如果问题是由于服务器的某部分导致的,而这恰好可以通过某个配置项解决,那么需要做的就是更改配置。
有时候,在某些特定的场景下,也有可能设置某些特殊的配置项会有显著的性能提升。但无论如何,这些特殊的配置项不应该成为服务器基本配置文件的一部分。只有当发现特定的性能问题才应该设置它们。这就是为什么我们不建议通过寻找有问题的地方修改配置项的原因。如果有些地方确实需要提升,也需要在查询响应时间上有所体现。最好是从查询语句和响应时间入手来开始分析问题,而不是通过配置项。这可以节省大量的时间,避免很多的问题。
另一个节省时间和避免麻烦的好办法是使用默认配置,除非是明确地知道默认值会有问题。很多人都是在默认配置下运行的,这种情况非常普遍。这使得默认配置是经过最多实际测试的。对配置项做一些不必要的修改可能会遇到一些意料之外的bug。
在讨论如何配置MySQL之前,我们先来解释一下MySQL的配置机制。MySQL对配置要求非常宽松,但是下面这些建议可能会为你节省大量的工作和时间。
首先应该知道的是MySQL从哪里获得配置信息:命令行参数和配置文件。在类UNIX系统中,配置文件的位置一般在*/etc/my.cnf或者/etc/mysql/my.cnf*。如果使用操作系统的启动脚本,这通常是唯一指定配置设置的地方。如果手动启动MySQL,例如在测试安装时,也可以在命令行指定设置。实际上,服务器会读取配置文件的内容,删除所有注释和换行,然后和命令行选项一起处理。
关于术语的说明:因为很多MySQL命令行选项跟服务器变量相同,我们有时把选项和变量替换使用。大部分变量和它们对应的命令行选项名称一样,但是有一些例外。例如,–memlock选项设置了locked_in_memory变量。
任何打算长期使用的设置都应该写到全局配置文件,而不是在命令行特别指定。否则,如果偶然在启动时忘了设置就会有风险。把所有的配置文件放在同一个地方以方便检查也是个好办法。
一定要清楚地知道服务器配置文件的位置!我们见过有些人尝试修改配置文件但是不生效,因为他们修改的并不是服务器读取的文件,例如Debian下,/etc/mysql/my.cnf才是MySQL读取的配置文件,而不是*/etc/my.cnf*。有时候好几个地方都有配置文件,也许是因为之前的系统管理员也没搞清楚情况(因此在各个可能的位置都放了一份)。如果不知道当前使用的配置文件路径,可以尝试下面的操作:
** $ which mysqld**
/usr/sbin/mysqld
** $ /usr/sbin/mysqld --verbose --help | grep -A 1 'Default options'**
Default options are read from the following files in the given order:
/etc/mysql/my.cnf ~/.my.cnf /usr/etc/my.cnf
对于服务器上只有一个MySQL实例的典型安装,这个命令很有用。也可以设计更复杂的配置,但是没有标准的方法告诉你怎么来做。MySQL发行版包含了一个现在废弃了的程序,叫mysqlmanager,可以在一个有多个独立部分的配置文件上运行多个实例。(现在已经被一样古老的mysqld_multi脚本替代。)然而许多操作系统发行版本在启动脚本中并不包含或使用这个程序。实际上,很多系统甚至没有使用MySQL提供的启动脚本。
配置文件通常分成多个部分,每个部分的开头是一个用方括号括起来的分段名称。MySQL程序通常读取跟它同名的分段部分,许多客户端程序还会读取client部分,这是一个存放公用设置的地方。服务器通常读取mysqld这一段。一定要确认配置项放在了文件正确的分段中,否则配置是不会生效的。
配置项设置都使用小写,单词之间用下画线或横线隔开。下面的例子是等价的,并且可能在命令行和配置文件中都看到这两种格式:
/usr/sbin/mysqld --auto-increment-offset=5
/usr/sbin/mysqld --auto-increment-offset=5
我们建议使用一种固定的风格。这样在配置文件中搜索配置项时会容易得多。
配置项可以有多个作用域。有些设置是服务器级的(全局作用域),有些对每个连接是不同的(会话作用域),剩下的一些是对象级的。许多会话级变量跟全局变量相等,可以认为是默认值。如果改变会话级变量,它只影响改动的当前连接,当连接关闭时所有参数变更都会失效。下面有一些例子,你应该清楚这些不同类型的行为:
另外,除了在配置文件中设置变量,有很多变量(但不是所有)也可以在服务器运行时修改。MySQL把这些归为动态配置变量。下面的语句展示了动态改变sort_buffer_size的会话值和全局值的不同方式:
第7章 MySQL高级特性
MySQL从5.0和5.1版本开始引入了很多高级特性,例如分区、触发器等,这对有其他关系型数据库使用背景的用户来说可能并不陌生。这些新特性吸引了很多用户开始使用MySQL。不过,这些特性的性能到底如何,还需要用户真正使用过才能知道。本章我们将为大家介绍,在真实的世界中,这些特性表现如何,而不是只简单地介绍参考手册或者宣传材料上的数据。
对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成。实现分区的代码实际上是对一组底层表的句柄对象(Handler Object)的封装。对分区表的请求,都会通过句柄对象转化成对存储引擎的接口调用。所以分区对于SQL层来说是一个完全封装底层实现的黑盒子,对应用是透明的,但是从底层的文件系统来看就很容易发现,每一个分区表都有一个使用#分隔命名的表文件。
MySQL实现分区表的方式——对底层表的封装——意味着索引也是按照分区的子表定义的,而没有全局索引。这和Oracle不同,在Oracle中可以更加灵活地定义索引和表是否进行分区。
MySQL在创建表时使用PARTITION BY子句定义每个分区存放的数据。在执行查询的时候,优化器会根据分区定义过滤那些没有我们需要数据的分区,这样查询就无须扫描所有分区——只需要查找包含需要数据的分区就可以了。
分区的一个主要目的是将数据按照一个较粗的粒度分在不同的表中。这样做可以将相关的数据存放在一起,另外,如果想一次批量删除整个分区的数据也会变得很方便。
在下面的场景中,分区可以起到非常大的作用:
MySQL的分区实现非常复杂,我们不打算介绍实现的全部细节。这里我们将专注在分区性能方面,所以如果想了解更多的关于分区的基础知识,我们建议阅读MySQL官方手册中的“分区”一节,其中介绍了很多分区相关的基础知识。另外,还可以阅读CREATE TABLE、SHOW CREATE TABLE、ALTER TABLE和INFORMATION_SCHEMA.PARTITIONS、EXPLAIN关于分区部分的介绍。分区特性使得CREATE TABLE和ALTER TABLE命令变得更加复杂了。
分区表本身也有一些限制,下面是其中比较重要的几点:
如前所述,分区表由多个相关的底层表实现,这些底层表也是由句柄对象(Handler object)表示,所以我们也可以直接访问各个分区。存储引擎管理分区的各个底层表和管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引只是在各个底层表上各自加上一个完全相同的索引。从存储引擎的角度来看,底层表和一个普通表没有任何不同,存储引擎也无须知道这是一个普通表还是一个分区表的一部分。
分区表上的操作按照下面的操作逻辑进行:
SELECT查询
当查询一个分区表的时候,分区层先打开并锁住所有的底层表,优化器先判断是否可以过滤部分分区,然后再调用对应的存储引擎接口访问各个分区的数据。
INSERT操作
当写入一条记录时,分区层先打开并锁住所有的底层表,然后确定哪个分区接收这条记录,再将记录写入对应底层表。
DELETE操作
当删除一条记录时,分区层先打开并锁住所有的底层表,然后确定数据对应的分区,最后对相应底层表进行删除操作。
UPDATE操作
当更新一条记录时,分区层先打开并锁住所有的底层表,MySQL先确定需要更新的记录在哪个分区,然后取出数据并更新,再判断更新后的数据应该放在哪个分区,最后对底层表进行写入操作,并对原数据所在的底层表进行删除操作。
有些操作是支持过滤的。例如,当删除一条记录时,MySQL需要先找到这条记录,如果WHERE条件恰好和分区表达式匹配,就可以将所有不包含这条记录的分区都过滤掉。这对UPDATE语句同样有效。如果是INSERT操作,则本身就是只命中一个分区,其他分区都会被过滤掉。MySQL先确定这条记录属于哪个分区,再将记录写入对应的底层分区表,无须对任何其他分区进行操作。
虽然每个操作都会“先打开并锁住所有的底层表”,但这并不是说分区表在处理过程中是锁住全表的。如果存储引擎能够自己实现行级锁,例如InnoDB,则会在分区层释放对应表锁。这个加锁和解锁过程与普通InnoDB上的查询类似。
后面我们会通过一些例子来看看,当访问一个分区表的时候,打开和锁住所有底层表的代价及其带来的后果。
MySQL支持多种分区表。我们看到最多的是根据范围进行分区,每个分区存储落在某个范围的记录,分区表达式可以是列,也可以是包含列的表达式。例如,下表就可以将每一年的销售额存放在不同的分区里:
CREATE TABLE sales (
order_date DATETIME NOT NULL,
-- Other columns omitted
) ENGINE=InnoDB PARTITION BY RANGE(YEAR(order_date)) (
PARTITION p_2010 VALUES LESS THAN (2010),
PARTITION p_2011 VALUES LESS THAN (2011),
PARTITION p_2012 VALUES LESS THAN (2012),
PARTITION p_catchall VALUES LESS THAN MAXVALUE );
PARTITION分区子句中可以使用各种函数。但有一个要求,表达式返回的值要是一个确定的整数,且不能是一个常数。这里我们使用函数YEAR(),也可以使用任何其他的函数,如TO_DAYS()。根据时间间隔进行分区,是一种很常见的分区方式,后面我们还会再回过头来看这个例子,看看如何优化这个例子来避免一些问题。
第6章 查询性能优化
前面的章节我们介绍了如何设计最优的库表结构、如何建立最好的索引,这些对于高性能来说是必不可少的。但这些还不够——还需要合理的设计查询。如果查询写得很糟糕,即使库表结构再合理、索引再合适,也无法实现高性能。
查询优化、索引优化、库表结构优化需要齐头并进,一个不落。在获得编写MySQL查询的经验的同时,也将学习到如何为高效的查询设计表和索引。同样的,也可以学习到在优化库表结构时会影响到哪些类型的查询。这个过程需要时间,所以建议大家在学习后面章节的时候多回头看看这三章的内容。
本章将从查询设计的一些基本原则开始——这也是在发现查询效率不高的时候首先需要考虑的因素。然后会介绍一些更深的查询优化的技巧,并会介绍一些MySQL优化器内部的机制。我们将展示MySQL是如何执行查询的,你也将学会如何去改变一个查询的执行计划。最后,我们要看一下MySQL优化器在哪些方面做得还不够,并探索查询优化的模式,以帮助MySQL更有效地执行查询。
本章的目标是帮助大家更深刻地理解MySQL如何真正地执行查询,并明白高效和低效的原因何在,这样才能充分发挥MySQL的优势,并避开它的弱点。
在尝试编写快速的查询之前,需要清楚一点,真正重要是响应时间。如果把查询看作是一个任务,那么它由一系列子任务组成,每个子任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减少子任务的执行次数,要么让子任务运行得更快(1)。
MySQL在执行查询的时候有哪些子任务,哪些子任务运行的速度很慢?这里很难给出完整的列表,但如果按照第3章介绍的方法对查询进行剖析,就能看到查询所执行的子任务。通常来说,查询的生命周期大致可以按照顺序来看:从客户端,到服务器,然后在服务器上进行解析,生成执行计划,执行,并返回结果给客户端。其中“执行”可以认为是整个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。
在完成这些任务的时候,查询需要在不同的地方花费时间,包括网络,CPU计算,生成统计信息和执行计划、锁等待(互斥等待)等操作,尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作、CPU操作和内存不足时导致的I/O操作上消耗时间。根据存储引擎不同,可能还会产生大量的上下文切换以及系统调用。
在每一个消耗大量时间的查询案例中,我们都能看到一些不必要的额外操作、某些操作被额外地重复了很多次、某些操作执行得太慢等。优化查询的目的就是减少和消除这些操作所花费的时间。
再次申明一点,对于一个查询的全部生命周期,上面列的并不完整。这里我们只是想说明:了解查询的生命周期、清楚查询的时间消耗情况对于优化查询有很大的意义。有了这些概念,我们再一起来看看如何优化查询。
查询性能低下最基本的原因是访问的数据太多。某些查询可能不可避免地需要筛选大量数据,但这并不常见。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。对于低效的查询,我们发现通过下面两个步骤来分析总是很有效:
有些查询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢弃。这会给MySQL服务器带来额外的负担,并增加网络开销(2),另外也会消耗应用服务器的CPU和内存资源。
这里有一些典型案例:
查询不需要的记录
一个常见的错误是常常会误以为MySQL会只返回需要的数据,实际上MySQL却是先返回全部结果集再进行计算。我们经常会看到一些了解其他数据库系统的人会设计出这类应用程序。这些开发者习惯使用这样的技术,先使用SELECT语句查询大量的结果,然后获取前面的N行后关闭结果集(例如在新闻网站中取出100条记录,但是只是在页面上显示前面10条)。他们认为MySQL会执行查询,并只返回他们需要的10条数据,然后停止查询。实际情况是MySQL会查询出全部的结果集,客户端的应用程序会接收全部的结果集数据,然后抛弃其中大部分数据。最简单有效的解决方法就是在这样的查询后面加上LIMIT。
多表关联时返回全部列
如果你想查询所有在电影Academy Dinosaur中出现的演员,千万不要按下面的写法编写查询:
mysql> ** SELECT * FROM sakila.actor**
-> ** INNER JOIN sakila.film_actor USING(actor_id)**
-> ** INNER JOIN sakila.film USING(film_id)**
-> ** WHERE sakila.film.title = 'Academy Dinosaur';**
这将返回这三个表的全部数据列。正确的方式应该是像下面这样只取需要的列:
mysql> ** SELECT sakila.actor.* FROM sakila.actor...;**
总是取出全部列
每次看到SELECT *的时候都需要用怀疑的眼光审视,是不是真的需要返回全部的列?很可能不是必需的。取出全部列,会让优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的I/O、内存和CPU的消耗。因此,一些DBA是严格禁止SELECT *的写法的,这样做有时候还能避免某些列被修改带来的问题。
当然,查询返回超过需要的数据也不总是坏事。在我们研究过的许多案例中,人们会告诉我们说这种有点浪费数据库资源的方式可以简化开发,因为能提高相同代码片段的复用性,如果清楚这样做的性能影响,那么这种做法也是值得考虑的。如果应用程序使用了某种缓存机制,或者有其他考虑,获取超过需要的数据也可能有其好处,但不要忘记这样做的代价是什么。获取并缓存所有的列的查询,相比多个独立的只获取部分列的查询可能就更有好处。
重复查询相同的数据
如果你不太小心,很容易出现这样的错误——不断地重复执行相同的查询,然后每次都返回完全相同的数据。例如,在用户评论的地方需要查询用户头像的URL,那么用户多次评论的时候,可能就会反复查询这个数据。比较好的方案是,当初次查询的时候将这个数据缓存起来,需要的时候从缓存中取出,这样性能显然会更好。
第5章 创建高性能的索引
索引(在MySQL中也叫做“键(key)”)是存储引擎用于快速找到记录的一种数据结构。这是索引的基本功能,除此之外,本章还将讨论索引其他一些方面有用的属性。
索引对于良好的性能非常关键。尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要。在数据量较小且负载较低时,不恰当的索引对性能的影响可能还不明显,但当数据量逐渐增大时,性能则会急剧下降(1)。
不过,索引却经常被忽略,有时候甚至被误解,所以在实际案例中经常会遇到由糟糕索引导致的问题。这也是我们把索引优化放在了靠前的章节,甚至比查询优化还靠前的原因。
索引优化应该是对查询性能优化最有效的手段了。索引能够轻易将查询性能提高几个数量级,“最优”的索引有时比一个“好的”索引性能要好两个数量级。创建一个真正“最优”的索引经常需要重写查询,所以,本章和下一章的关系非常紧密。
要理解MySQL中索引是如何工作的,最简单的方法就是去看看一本书的“索引”部分:如果想在一本书中找到某个特定主题,一般会先看书的“索引”,找到对应的页码。
在MySQL中,存储引擎用类似的方法使用索引,其先在索引中找到对应值,然后根据匹配的索引记录找到对应的数据行。假如要运行下面的查询:
mysql> ** SELECT first_name FROM sakila.actor WHERE actor_id=5;**
如果在actor_id列上建有索引,则MySQL将使用该索引找到actor_id为5的行,也就是说,MySQL先在索引上按值进行查找,然后返回所有包含该值的数据行。
索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为MySQL只能高效地使用索引的最左前缀列。创建一个包含两个列的索引,和创建两个只包含一列的索引是大不相同的,下面将详细介绍。
如果使用的是ORM,是否还需要关心索引?
简而言之:是的,仍然需要理解索引,即使是使用对象关系映射(ORM)工具。
ORM工具能够生产符合逻辑的、合法的查询(多数时候),除非只是生成非常基本的查询(例如仅是根据主键查询),否则它很难生成适合索引的查询。无论是多么复杂的ORM工具,在精妙和复杂的索引面前都是“浮云”。读完本章后面的内容以后,你就会同意这个观点的!很多时候,即使是查询优化技术专家也很难兼顾到各种情况,更别说ORM了。
索引有很多种类型,可以为不同的场景提供更好的性能。在MySQL中,索引是在存储引擎层而不是服务器层实现的。所以,并没有统一的索引标准:不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同。
下面我们先来看看MySQL支持的索引类型,以及它们的优点和缺点。
当人们谈论索引的时候,如果没有特别指明类型,那多半说的是B-Tree索引,它使用B-Tree数据结构来存储数据(2)。大多数MySQL引擎都支持这种索引。Archive引擎是一个例外:5.1之前Archive不支持任何索引,直到5.1才开始支持单个自增列(AUTO_INCREMENT)的索引。
我们使用术语“B-Tree”,是因为MySQL在CREATE TABLE和其他语句中也使用该关键字。不过,底层的存储引擎也可能使用不同的存储结构,例如,NDB集群存储引擎内部实际上使用了T-Tree结构存储这种索引,即使其名字是BTREE;InnoDB则使用的是B+Tree,各种数据结构和算法的变种不在本书的讨论范围之内。
存储引擎以不同的方式使用B-Tree索引,性能也各有不同,各有优劣。例如,MyISAM使用前缀压缩技术使得索引更小,但InnoDB则按照原数据格式进行存储。再如MyISAM索引通过数据的物理位置引用被索引的行,而InnoDB则根据主键引用被索引的行。
B-Tree通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同。图5-1展示了B-Tree索引的抽象表示,大致反映了InnoDB索引是如何工作的。MyISAM使用的结构有所不同,但基本思想是类似的。
图5-1:建立在B-Tree结构(从技术上来说是B+Tree)上的索引
B-Tree索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点(图示并未画出)开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么是找到对应的值,要么该记录不存在。
叶子节点比较特别,它们的指针指向的是被索引的数据,而不是其他的节点页(不同引擎的“指针”类型不同)。图5-1中仅绘制了一个节点和其对应的叶子节点,其实在根节点和叶子节点之间可能有很多层节点页。树的深度和表的大小直接相关。
B-Tree对索引列是顺序组织存储的,所以很适合查找范围数据。例如,在一个基于文本域的索引树上,按字母顺序传递连续的值进行查找是非常合适的,所以像“找出所有以I到K开头的名字”这样的查找效率会非常高。
假设有如下数据表:
CREATE TABLE People (
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
gender enum('m', 'f') not null,
key(last_name, first_name, dob)
);
对于表中的每一行数据,索引中包含了last_name、frst_name和dob列的值,图5-2显示了该索引是如何组织数据的存储的。
第4章 Schema与数据类型优化
良好的逻辑设计和物理设计是高性能的基石,应该根据系统将要执行的查询语句来设计schema,这往往需要权衡各种因素。例如,反范式的设计可以加快某些类型的查询,但同时可能使另一些类型的查询变慢。比如添加计数表和汇总表是一种很好的优化查询的方式,但这些表的维护成本可能会很高。MySQL独有的特性和实现细节对性能的影响也很大。
本章和聚焦在索引优化的下一章,覆盖了MySQL特有的schema设计方面的主题。我们假设读者已经知道如何设计数据库,所以本章既不会介绍如何入门数据库设计,也不会讲解数据库设计方面的深入内容。这一章关注的是MySQL数据库的设计,主要介绍的是MySQL数据库设计与其他关系型数据库管理系统的区别。如果需要学习数据库设计方面的基础知识,建议阅读Clare Churcher的Beginning Database Design(Apress出版社)一书。
本章内容是为接下来的两个章节做铺垫。在这三章中,我们将讨论逻辑设计、物理设计和查询执行,以及它们之间的相互作用。这既需要关注全局,也需要专注细节。还需要理解整个系统以便弄清楚各个部分如何相互影响。如果在阅读完索引和查询优化章节后再回头来看这一章,也许会发现本章很有用,很多讨论的议题不能孤立地考虑。
MySQL支持的数据类型非常多,选择正确的数据类型对于获得高性能至关重要。不管存储哪种类型的数据,下面几个简单的原则都有助于做出更好的选择。
更小的通常更好。
一般情况下,应该尽量使用可以正确存储数据的最小数据类型(1)。更小的数据类型通常更快,因为它们占用更少的磁盘、内存和CPU缓存,并且处理时需要的CPU周期也更少。
但是要确保没有低估需要存储的值的范围,因为在schema中的多个地方增加数据类型的范围是一个非常耗时和痛苦的操作。如果无法确定哪个数据类型是最好的,就选择你认为不会超过范围的最小类型。(如果系统不是很忙或者存储的数据量不多,或者是在可以轻易修改设计的早期阶段,那之后修改数据类型也比较容易)。
简单就好
简单数据类型的操作通常需要更少的CPU周期。例如,整型比字符操作代价更低,因为字符集和校对规则(排序规则)使字符比较比整型比较更复杂。这里有两个例子:一个是应该使用MySQL内建的类型(2)而不是字符串来存储日期和时间,另外一个是应该用整型存储IP地址。稍后我们将专门讨论这个话题。
尽量避免NULL
很多表都包含可为NULL(空值)的列,即使应用程序并不需要保存NULL也是如此,这是因为可为NULL是列的默认属性(3)。通常情况下最好指定列为NOT NULL,除非真的需要存储NULL值。
如果查询中包含可为NULL的列,对MySQL来说更难优化,因为可为NULL的列使得索引、索引统计和值比较都更复杂。可为NULL的列会使用更多的存储空间,在MySQL里也需要特殊处理。当可为NULL的列被索引时,每个索引记录需要一个额外的字节,在MyISAM里甚至还可能导致固定大小的索引(例如只有一个整数列的索引)变成可变大小的索引。
通常把可为NULL的列改为NOT NULL带来的性能提升比较小,所以(调优时)没有必要首先在现有schema中查找并修改掉这种情况,除非确定这会导致问题。但是,如果计划在列上建索引,就应该尽量避免设计成可为NULL的列。
当然也有例外,例如值得一提的是,InnoDB使用单独的位(bit)存储NULL值,所以对于稀疏数据(4)有很好的空间效率。但这一点不适用于MyISAM。
在为列选择数据类型时,第一步需要确定合适的大类型:数字、字符串、时间等。这通常是很简单的,但是我们会提到一些特殊的不是那么直观的案例。
下一步是选择具体类型。很多MySQL的数据类型可以存储相同类型的数据,只是存储的长度和范围不一样、允许的精度不同,或者需要的物理空间(磁盘和内存空间)不同。相同大类型的不同子类型数据有时也有一些特殊的行为和属性。
例如,DATETIME和TIMESAMP列都可以存储相同类型的数据:时间和日期,精确到秒。
然而TIMESTAMP只使用DATETIME一半的存储空间,并且会根据时区变化,具有特殊的自动更新能力。另一方面,TIMESTAMP允许的时间范围要小得多,有时候它的特殊能力会成为障碍。
本章只讨论基本的数据类型。MySQL为了兼容性支持很多别名,例如INTEGER、BOOL,以及NUMERIC。它们都只是别名。这些别名可能令人不解,但不会影响性能。如果建表时采用数据类型的别名,然后用SHOW CREATE TABLE检查,会发现MySQL报告的是基本类型,而不是别名。
有两种类型的数字:整数(whole number)和实数(real number)。如果存储整数,可以使用这几种整数类型:TINYINT,SMALLINT,MEDIUMINT,INT,BIGINT。分别使用8,16,24,32,64位存储空间。它们可以存储的值的范围从−2(N−1)到2(N−1)−1,其中N是存储空间的位数。
整数类型有可选的UNSIGNED属性,表示不允许负值,这大致可以使正数的上限提高一倍。例如TINYINT UNSIGNED可以存储的范围是0~255,而TINYINT的存储范围是−128~127。
有符号和无符号类型使用相同的存储空间,并具有相同的性能,因此可以根据实际情况选择合适的类型。
你的选择决定MySQL是怎么在内存和磁盘中保存数据的。然而,整数计算一般使用64位的BIGINT整数,即使在32位环境也是如此。(一些聚合函数是例外,它们使用DECIMAL或DOUBLE进行计算)。
MySQL可以为整数类型指定宽度,例如INT(11),对大多数应用这是没有意义的:它不会限制值的合法范围,只是规定了MySQL的一些交互工具(例如MySQL命令行客户端)用来显示字符的个数。对于存储和计算来说,INT(1)和INT(20)是相同的。
一些第三方存储引擎,比如Infobright,有时也有自定义的存储格式和压缩方案,并不一定使用常见的MySQL内置引擎的方式。
实数是带有小数部分的数字。然而,它们不只是为了存储小数部分;也可以使用DECIMAL存储比BIGINT还大的整数。MySQL既支持精确类型,也支持不精确类型。
FLOAT和DOUBLE类型支持使用标准的浮点运算进行近似计算。如果需要知道浮点运算是怎么计算的,则需要研究所使用的平台的浮点数的具体实现。
DECIMAL类型用于存储精确的小数。在MySQL 5.0和更高版本,DECIMAL类型支持精确计算。MySQL 4.1以及更早版本则使用浮点运算来实现DECIAML的计算,这样做会因为精度损失导致一些奇怪的结果。在这些版本的MySQL中,DECIMAL只是一个“存储类型”。
因为CPU不支持对DECIMAL的直接计算,所以在MySQL 5.0以及更高版本中,MySQL服务器自身实现了DECIMAL的高精度计算。相对而言,CPU直接支持原生浮点计算,所以浮点运算明显更快。
浮点和DECIMAL类型都可以指定精度。对于DECIMAL列,可以指定小数点前后所允许的最大位数。这会影响列的空间消耗。MySQL 5.0和更高版本将数字打包保存到一个二进制字符串中(每4个字节存9个数字)。例如,DECIMAL(18,9)小数点两边将各存储9个数字,一共使用9个字节:小数点前的数字用4个字节,小数点后的数字用4个字节,小数点本身占1个字节。
MySQL 5.0和更高版本中的DECIMAL类型允许最多65个数字。而早期的MySQL版本中这个限制是254个数字,并且保存为未压缩的字符串(每个数字一个字节)。然而,这些(早期)版本实际上并不能在计算中使用这么大的数字,因为DECIMAL只是一种存储格式;在计算中DECIMAL会转换为DOUBLE类型。
有多种方法可以指定浮点列所需要的精度,这会使得MySQL悄悄选择不同的数据类型,或者在存储时对值进行取舍。这些精度定义是非标准的,所以我们建议只指定数据类型,不指定精度。
浮点类型在存储同样范围的值时,通常比DECIMAL使用更少的空间。FLOAT使用4个字节存储。DOUBLE占用8个字节,相比FLOAT有更高的精度和更大的范围。和整数类型一样,能选择的只是存储类型;MySQL使用DOUBLE作为内部浮点计算的类型。
因为需要额外的空间和计算开销,所以应该尽量只在对小数进行精确计算时才使用DECIMAL——例如存储财务数据。但在数据量比较大的时候,可以考虑使用BIGINT代替DECIMAL,将需要存储的货币单位根据小数的位数乘以相应的倍数即可。假设要存储财务数据精确到万分之一分,则可以把所有金额乘以一百万,然后将结果存储在BIGINT里,这样可以同时避免浮点存储计算不精确和DECIMAL精确计算代价高的问题。
MySQL支持多种字符串类型,每种类型还有很多变种。这些数据类型在4.1和5.0版本发生了很大的变化,使得情况更加复杂。从MySQL 4.1开始,每个字符串列可以定义自己的字符集和排序规则,或者说校对规则(collation)(更多关于这个主题的信息请参考第7章)。这些东西会很大程度上影响性能。
VARCHAR和CHAR是两种最主要的字符串类型。不幸的是,很难精确地解释这些值是怎么存储在磁盘和内存中的,因为这跟存储引擎的具体实现有关。下面的描述假设使用的存储引擎是InnoDB和/或者MyISAM。如果使用的不是这两种存储引擎,请参考所使用的存储引擎的文档。
先看看VARCHAR和CHAR值通常在磁盘上怎么存储。请注意,存储引擎存储CHAR或者VARCHAR值的方式在内存中和在磁盘上可能不一样,所以MySQL服务器从存储引擎读出的值可能需要转换为另一种存储格式。下面是关于两种类型的一些比较。
VARCHAR
VARCHAR类型用于存储可变长字符串,是最常见的字符串数据类型。它比定长类型更节省空间,因为它仅使用必要的空间(例如,越短的字符串使用越少的空间)。有一种情况例外,如果MySQL表使用ROW_FORMAT=FIXED创建的话,每一行都会使用定长存储,这会很浪费空间。
VARCHAR需要使用1或2个额外字节记录字符串的长度:如果列的最大长度小于或等于255字节,则只使用1个字节表示,否则使用2个字节。假设采用latin1字符集,一个VARCHAR(10)的列需要11个字节的存储空间。VARCHAR(1000)的列则需要1002个字节,因为需要2个字节存储长度信息。
VARCHAR节省了存储空间,所以对性能也有帮助。但是,由于行是变长的,在UPDATE时可能使行变得比原来更长,这就导致需要做额外的工作。如果一个行占用的空间增长,并且在页内没有更多的空间可以存储,在这种情况下,不同的存储引擎的处理方式是不一样的。例如,MyISAM会将行拆成不同的片段存储,InnoDB则需要分裂页来使行可以放进页内。其他一些存储引擎也许从不在原数据位置更新数据。
下面这些情况下使用VARCHAR是合适的:字符串列的最大长度比平均长度大很多;列的更新很少,所以碎片不是问题;使用了像UTF-8这样复杂的字符集,每个字符都使用不同的字节数进行存储。
第3章 服务器性能剖析
在我们的技术咨询生涯中,最常碰到的三个性能相关的服务请求是:如何确认服务器是否达到了性能最佳的状态、找出某条语句为什么执行不够快,以及诊断被用户描述成“停顿”、“堆积”或者“卡死”的某些间歇性疑难故障。本章将主要针对这三个问题做出解答。我们将提供一些工具和技巧来优化整机的性能、优化单条语句的执行速度,以及诊断或者解决那些很难观察到的问题(这些问题用户往往很难知道其根源,有时候甚至都很难察觉到它的存在)。
这看起来是个艰巨的任务,但是事实证明,有一个简单的方法能够从噪声中发现苗头。这个方法就是专注于测量服务器的时间花费在哪里,使用的技术则是性能剖析(profiling)。在本章,我们将展示如何测量系统并生成剖析报告,以及如何分析系统的整个堆栈(stack),包括从应用程序到数据库服务器到单个查询。
首先我们要保持空杯精神,抛弃掉一些关于性能的常见的误解。这有一定的难度,下面我们一起通过一些例子来说明问题在哪里。
问10个人关于性能的问题,可能会得到10个不同的回答,比如“每秒查询次数”、“CPU利用率”、“可扩展性”之类。这其实也没有问题,每个人在不同场景下对性能有不同的理解,但本章将给性能一个正式的定义。我们将性能定义为完成某件任务所需要的时间度量,换句话说,性能即响应时间,这是一个非常重要的原则。我们通过任务和时间而不是资源来测量性能。数据库服务器的目的是执行SQL语句,所以它关注的任务是查询或者语句,如SELECT、UPDATE、DELETE等(1)。数据库服务器的性能用查询的响应时间来度量,单位是每个查询花费的时间。
还有另外一个问题:什么是优化?我们暂时不讨论这个问题,而是假设性能优化就是在一定的工作负载下尽可能地(2)降低响应时间。
很多人对此很迷茫。假如你认为性能优化是降低CPU利用率,那么可以减少对资源的使用。但这是一个陷阱,资源是用来消耗并用来工作的,所以有时候消耗更多的资源能够加快查询速度。很多时候将使用老版本InnoDB引擎的MySQL升级到新版本后,CPU利用率会上升得很厉害,这并不代表性能出现了问题,反而说明新版本的InnoDB对资源的利用率上升了。查询的响应时间则更能体现升级后的性能是不是变得更好。版本升级有时候会带来一些bug,比如不能利用某些索引从而导致CPU利用率上升。CPU利用率只是一种现象,而不是很好的可度量的目标。
同样,如果把性能优化仅仅看成是提升每秒查询量,这其实只是吞吐量优化。吞吐量的提升可以看作性能优化的副产品(3)。对查询的优化可以让服务器每秒执行更多的查询,因为每条查询执行的时间更短了(吞吐量的定义是单位时间内的查询数量,这正好是我们对性能的定义的倒数)。
所以如果目标是降低响应时间,那么就需要理解为什么服务器执行查询需要这么多时间,然后去减少或者消除那些对获得查询结果来说不必要的工作。也就是说,先要搞清楚时间花在哪里。这就引申出优化的第二个原则:无法测量就无法有效地优化。所以第一步应该测量时间花在什么地方。
我们观察到,很多人在优化时,都将精力放在修改一些东西上,却很少去进行精确的测量。我们的做法完全相反,将花费非常多,甚至90%的时间来测量响应时间花在哪里。如果通过测量没有找到答案,那要么是测量的方式错了,要么是测量得不够完整。如果测量了系统中完整而且正确的数据,性能问题一般都能暴露出来,对症下药的解决方案也就比较明了。测量是一项很有挑战性的工作,并且分析结果也同样有挑战性,测出时间花在哪里,和知道为什么花在那里,是两码事。
前面提到需要合适的测量范围,这是什么意思呢?合适的测量范围是说只测量需要优化的活动。有两种比较常见的情况会导致不合适的测量:
例如,一个常见的错误是先查看慢查询,然后又去排查整个服务器的情况来判断问题在哪里。如果确认有慢查询,那么就应该测量慢查询,而不是测量整个服务器。测量的应该是从慢查询的开始到结束的时间,而不是查询之前或查询之后的时间。
完成一项任务所需要的时间可以分成两部分:执行时间和等待时间。如果要优化任务的执行时间,最好的办法是通过测量定位不同的子任务花费的时间,然后优化去掉一些子任务、降低子任务的执行频率或者提升子任务的效率。而优化任务的等待时间则相对要复杂一些,因为等待有可能是由其他系统间接影响导致,任务之间也可能由于争用磁盘或者CPU资源而相互影响。根据时间是花在执行还是等待上的不同,诊断也需要不同的工具和技术。
刚才说到需要定位和优化子任务,但只是一笔带过。一些运行不频繁或者很短的子任务对整体响应时间的影响很小,通常可以忽略不计。那么如何确认哪些子任务是优化的目标呢?这个时候性能剖析就可以派上用场了。
如何判断测量是正确的?
如果测量是如此重要,那么测量错了会有什么后果?实际上,测量经常都是错误的。对数量的测量并不等于数量本身。测量的错误可能很小,跟实际情况区别不大,但错的终归是错的。所以这个问题其实应该是:“测量到底有多么不准确?”这个问题在其他一些书中有详细的讨论,但不是本书的主题。但是要意识到使用的是测量数据,而不是其所代表的实际数据。通常来说,测量的结果也可能有多种模糊的表现,这可能导致推断出错误的结论。
一旦掌握并实践面向响应时间的优化方法,就会发现需要不断地对系统进行性能剖析(profiling)。
性能剖析是测量和分析时间花费在哪里的主要方法。性能剖析一般有两个步骤:测量任务所花费的时间;然后对结果进行统计和排序,将重要的任务排到前面。
性能剖析工具的工作方式基本相同。在任务开始时启动计时器,在任务结束时停止计时器,然后用结束时间减去启动时间得到响应时间。也有些工具会记录任务的父任务。这些结果数据可以用来绘制调用关系图,但对于我们的目标来说更重要的是,可以将相似的任务分组并进行汇总。对相似的任务分组并进行汇总可以帮助对那些分到一组的任务做更复杂的统计分析,但至少需要知道每一组有多少任务,并计算出总的响应时间。通过性能剖析报告(profile report)可以获得需要的结果。性能剖析报告会列出所有任务列表。每行记录一个任务,包括任务名、任务的执行时间、任务的消耗时间、任务的平均执行时间,以及该任务执行时间占全部时间的百分比。性能剖析报告会按照任务的消耗时间进行降序排序。
为了更好地说明,这里举一个对整个数据库服务器工作负载的性能剖析的例子,主要输出的是各种类型的查询和执行查询的时间。这是从整体的角度来分析响应时间,后面会演示其他角度的分析结果。下面的输出是用Percona Toolkit中的pt-query-digest(实际上就是著名的Maatkit工具中的mk-query-digest)分析得到的结果。为了显示方便,对结果做了一些微调,并且只截取了前面几行结果:
Rank Response time Calls R/Call Item
==== ================ ===== ====== =======
1 11256.3618 68.1% 78069 0.1442 SELECT InvitesNew
2 2029.4730 12.3% 14415 0.1408 SELECT StatusUpdate
3 1345.3445 8.1% 3520 0.3822 SHOW STATUS
上面只是性能剖析结果的前几行,根据总响应时间进行排名,只包括剖析所需要的最小列组合。每一行都包括了查询的响应时间和占总时间的百分比、查询的执行次数、单次执行的平均响应时间,以及该查询的摘要。通过这个性能剖析可以很清楚地看到每个查询相互之间的成本比较,以及每个查询占总成本的比较。在这个例子中,任务指的就是查询,实际上在分析MySQL的时候经常都指的是查询。
我们将实际地讨论两种类型的性能剖析:基于执行时间的分析和基于等待的分析。基于执行时间的分析研究的是什么任务的执行时间最长,而基于等待的分析则是判断任务在什么地方被阻塞的时间最长。
如果任务执行时间长是因为消耗了太多的资源且大部分时间花费在执行上,等待的时间不多,这种情况下基于等待的分析作用就不大。反之亦然,如果任务一直在等待,没有消耗什么资源,去分析执行时间就不会有什么结果。如果不能确认问题是出在执行还是等待上,那么两种方式都需要试试。后面会给出详细的例子。
事实上,当基于执行时间的分析发现一个任务需要花费太多时间的时候,应该深入去分析一下,可能会发现某些“执行时间”实际上是在等待。例如,上面简单的性能剖析的输出显示表InvitesNew上的SELECT查询花费了大量时间,如果深入研究,则可能发现时间都花费在等待I/O完成上。
在对系统进行性能剖析前,必须先要能够进行测量,这需要系统可测量化的支持。可测量的系统一般会有多个测量点可以捕获并收集数据,但实际系统很少可以做到可测量化。大部分系统都没有多少可测量点,即使有也只提供一些活动的计数,而没有活动花费的时间统计。MySQL就是一个典型的例子,直到版本5.5才第一次提供了Performance Schema,其中有一些基于时间的测量点(4),而版本5.1及之前的版本没有任何基于时间的测量点。能够从MySQL收集到的服务器操作的数据大多是show status计数器的形式,这些计数器统计的是某种活动发生的次数。这也是我们最终决定创建Percona Server的主要原因,Percona Server从版本5.0开始提供很多更详细的查询级别的测量点。
第2章 MySQL基准测试
基准测试(benchmark)是MySQL新手和专家都需要掌握的一项基本技能。简单地说,基准测试是针对系统设计的一种压力测试。通常的目标是为了掌握系统的行为。但也有其他原因,如重现某个系统状态,或者是做新硬件的可靠性测试。本章将讨论MySQL和基于MySQL的应用的基准测试的重要性、策略和工具。我们将特别讨论一下sysbench,这是一款非常优秀的MySQL基准测试工具。
为什么基准测试很重要?因为基准测试是唯一方便有效的、可以学习系统在给定的工作负载下会发生什么的方法。基准测试可以观察系统在不同压力下的行为,评估系统的容量,掌握哪些是重要的变化,或者观察系统如何处理不同的数据。基准测试可以在系统实际负载之外创造一些虚构场景进行测试。基准测试可以完成以下工作,或者更多:
基准测试还可以用于其他目的,比如为应用创建单元测试套件。但本章我们只关注与性能有关的基准测试。
基准测试的一个主要问题在于其不是真实压力的测试。基准测试施加给系统的压力相对真实压力来说,通常比较简单。真实压力是不可预期而且变化多端的,有时候情况会过于复杂而难以解释。所以使用真实压力测试,可能难以从结果中分析出确切的结论。
基准测试的压力和真实压力在哪些方面不同?有很多因素会影响基准测试,比如数据量、数据和查询的分布,但最重要的一点还是基准测试通常要求尽可能快地执行完成,所以经常给系统造成过大的压力。在很多案例中,我们都会调整给测试工具的最大压力,以在系统可以容忍的压力阈值内尽可能快地执行测试,这对于确定系统的最大容量非常有帮助。然而大部分压力测试工具不支持对压力进行复杂的控制。务必要记住,测试工具自身的局限也会影响到结果的有效性。
使用基准测试进行容量规划也要掌握技巧,不能只根据测试结果做简单的推断。例如,假设想知道使用新数据库服务器后,系统能够支撑多大的业务增长。首先对原系统进行基准测试,然后对新系统做测试,结果发现新系统可以支持原系统40倍的TPS(每秒事务数),这时候就不能简单地推断说新系统一定可以支持40倍的业务增长。这是因为在业务增长的同时,系统的流量、用户、数据以及不同数据之间的交互都在增长,它们不可能都有40倍的支撑能力,尤其是相互之间的关系。而且当业务增长到40倍时,应用本身的设计也可能已经随之改变。可能有更多的新特性会上线,其中某些特性可能对数据库造成的压力远大于原有功能。而这些压力、数据、关系和特性的变化都很难模拟,所以它们对系统的影响也很难评估。
结论就是,我们只能进行大概的测试,来确定系统大致的余量有多少。当然也可以做一些真实压力测试(和基准测试有区别),但在构造数据集和压力的时候要特别小心,而且这样就不再是基准测试了。基准测试要尽量简单直接,结果之间容易相互比较,成本低且易于执行。尽管有诸多限制,基准测试还是非常有用的(只要搞清楚测试的原理,并且了解如何分析结果所代表的意义)。
基准测试有两种主要的策略:一是针对整个系统的整体测试,另外是单独测试MySQL。这两种策略也被称为集成式(full-stack)以及单组件式(single-component)基准测试。针对整个系统做集成式测试,而不是单独测试MySQL的原因主要有以下几点:
另外一方面,应用的整体基准测试很难建立,甚至很难正确设置。如果基准测试的设计有问题,那么结果就无法反映真实的情况,从而基于此做的决策也就可能是错误的。
不过,有时候不需要了解整个应用的情况,而只需要关注MySQL的性能,至少在项目初期可以这样做。基于以下情况,可以选择只测试MySQL:
另外,如果能够在真实的数据集上执行重复的查询,那么针对MySQL的基准测试也是有用的,但是数据本身和数据集的大小都应该是真实的。如果可能,可以采用生产环境的数据快照。
不幸的是,设置一个基于真实数据的基准测试复杂而且耗时。如果能得到一份生产数据集的拷贝,当然很幸运,但这通常不太可能。比如要测试的是一个刚开发的新应用,它只有很少的用户和数据。如果想测试该应用在规模扩张到很大以后的性能表现,就只能通过模拟大量的数据和压力来进行。
在开始执行甚至是在设计基准测试之前,需要先明确测试的目标。测试目标决定了选择什么样的测试工具和技术,以获得精确而有意义的测试结果。可以将测试目标细化为一系列的问题,比如,“这种CPU是否比另外一种要快?”,或“新索引是否比当前索引性能更好?”
有时候需要用不同的方法测试不同的指标。比如,针对延迟(latency)和吞吐量(throughput)就需要采用不同的测试方法。
请考虑以下指标,看看如何满足测试的需求。
吞吐量
吞吐量指的是单位时间内的事务处理数。这一直是经典的数据库应用测试指标。一些标准的基准测试被广泛地引用,如TPC-C(参考 http://www.tpc.org),而且很多数据库厂商都努力争取在这些测试中取得好成绩。这类基准测试主要针对在线事务处理(OLTP)的吞吐量,非常适用于多用户的交互式应用。常用的测试单位是每秒事务数(TPS),有些也采用每分钟事务数(TPM)。
响应时间或者延迟
这个指标用于测试任务所需的整体时间。根据具体的应用,测试的时间单位可能是微秒、毫秒、秒或者分钟。根据不同的时间单位可以计算出平均响应时间、最小响应时间、最大响应时间和所占百分比。最大响应时间通常意义不大,因为测试时间越长,最大响应时间也可能越大。而且其结果通常不可重复,每次测试都可能得到不同的最大响应时间。因此,通常可以使用百分比响应时间(percentile response time)来替代最大响应时间。例如,如果95%的响应时间都是5毫秒,则表示任务在95%的时间段内都可以在5毫秒之内完成。
使用图表有助于理解测试结果。可以将测试结果绘制成折线图(比如平均值折线或者95%百分比折线)或者散点图,直观地表现数据结果集的分布情况。通过这些图可以发现长时间测试的趋势。本章后面将更详细地讨论这一点。
并发性
并发性是一个非常重要又经常被误解和误用的指标。例如,它经常被表示成多少用户在同一时间浏览一个Web站点,经常使用的指标是有多少个会话(1)。然而,HTTP协议是无状态的,大多数用户只是简单地读取浏览器上显示的信息,这并不等同于Web服务器的并发性。而且,Web服务器的并发性也不等同于数据库的并发性,而仅仅只表示会话存储机制可以处理多少数据的能力。Web服务器的并发性更准确的度量指标,应该是在任意时间有多少同时发生的并发请求。
在应用的不同环节都可以测量相应的并发性。Web服务器的高并发,一般也会导致数据库的高并发,但服务器采用的语言和工具集对此都会有影响。注意不要将创建数据库连接和并发性搞混淆。一个设计良好的应用,同时可以打开成百上千个MySQL数据库服务器连接,但可能同时只有少数连接在执行查询。所以说,一个Web站点“同时有50000个用户”访问,却可能只有10~15个并发请求到MySQL数据库。
换句话说,并发性基准测试需要关注的是正在工作中的并发操作,或者是同时工作中的线程数或者连接数。当并发性增加时,需要测量吞吐量是否下降,响应时间是否变长,如果是这样,应用可能就无法处理峰值压力。
并发性的测量完全不同于响应时间和吞吐量。它不像是一个结果,而更像是设置基准测试的一种属性。并发性测试通常不是为了测试应用能达到的并发度,而是为了测试应用在不同并发下的性能。当然,数据库的并发性还是需要测量的。可以通过sysbench指定32、64或者128个线程的测试,然后在测试期间记录MySQL数据库的Threads_running状态值。在第11章将讨论这个指标对容量规划的影响。
可扩展性
在系统的业务压力可能发生变化的情况下,测试可扩展性就非常必要了。第11章将更进一步讨论可扩展性的话题。简单地说,可扩展性指的是,给系统增加一倍的工作,在理想情况下就能获得两倍的结果(即吞吐量增加一倍)。或者说,给系统增加一倍的资源(比如两倍的CPU数),就可以获得两倍的吞吐量。当然,同时性能(响应时间)也必须在可以接受的范围内。大多数系统是无法做到如此理想的线性扩展的。随着压力的变化,吞吐量和性能都可能越来越差。
可扩展性指标对于容量规范非常有用,它可以提供其他测试无法提供的信息,来帮助发现应用的瓶颈。比如,如果系统是基于单个用户的响应时间测试(这是一个很糟糕的测试策略)设计的,虽然测试的结果很好,但当并发度增加时,系统的性能有可能变得非常糟糕。而一个基于不断增加用户连接的情况下的响应时间测试则可以发现这个问题。
一些任务,比如从细粒度数据创建汇总表的批量工作,需要的是周期性的快速响应时间。当然也可以测试这些任务纯粹的响应时间,但要注意考虑这些任务之间的相互影响。批量工作可能导致相互之间有影响的查询性能变差,反之亦然。
归根结底,应该测试那些对用户来说最重要的指标。因此应该尽可能地去收集一些需求,比如,什么样的响应时间是可以接受的,期待多少的并发性,等等。然后基于这些需求来设计基准测试,避免目光短浅地只关注部分指标,而忽略其他指标。
在了解基本概念之后,现在可以来具体讨论一下如何设计和执行基准测试。但在讨论如何设计好的基准测试之前,先来看一下如何避免一些常见的错误,这些错误可能导致测试结果无用或者不精确:
只有避免了上述错误,才能走上改进测试质量的漫漫长路。