MySQL

第8章优化服务器设置

第8章 优化服务器设置

在这一章,我们将解释为这是我的撒旦JFK数据库嘎斯公开就开始打山豆根士大夫 圣诞节复活节是是国家开始大幅机啊可是对方看见噶开暗杀是的JFK开始讲课的感觉爱看书的JFK史蒂夫卡卡萨丁咖啡碱撒快递费始东方会i二位人家儿童科技数据库的房价开始JFK注释MySQL服务器创建一个靠谱的配置文件的过程。这是一个很绕的过程,有很多有意思的关注点和值得关注的思路。关注这些点很有必要,因为创建一个好配置的最快方法不是从学习配置项开始,也不是从问哪个配置项应该怎么设置或者怎么修改开始,更不是从检查服务器行为和询问哪个配置项可以提升性能开始。最好是从理解MySQL内核和行为开始。然后可以利用这些知识来指导配置MySQL。最后,可以将想要的配置和当前配置进行比较,然后纠正重要并且有价值的不同之处。

人们经常问,“我的服务器有32GB内存,12核CPU,怎样配置最好?”很遗憾,问题没这么简单。服务器的配置应该符合它的工作负载、数据,以及应用需求,并不仅仅看硬件的情况。

MySQL有大量可以修改的参数——但不应该随便去修改。通常只需要把基本的项配置正确(大部分情况下只有很少一些参数是真正重要的),应该将更多的时间花在schema的优化、索引,以及查询设计上。在正确地配置了MySQL的基本配置项之后,再花力气去修改其他配置项的收益通常就比较小了。

从另一方面来说,没用的配置导致潜在风险的可能更大。我们碰到过不止一个“高度调优”过的服务器不停地崩溃,停止服务或者运行缓慢,结果都是因为错误的配置导致的。我们将花一点时间来解释为什么会发生这种情况,并且告诉大家什么是不该做的。

那么什么是该做的呢?确保基本的配置是正确的,例如InnoDB的Buffer Pool和日志文件缓存大小,如果想防止出问题(提醒一下,这样做通常不能提升性能——它们只能避免问题),就设置一个比较安全和稳健的值,剩下的配置就不用管了。如果碰到了问题,可以使用第3章提到的技巧小心地进行诊断。如果问题是由于服务器的某部分导致的,而这恰好可以通过某个配置项解决,那么需要做的就是更改配置。

有时候,在某些特定的场景下,也有可能设置某些特殊的配置项会有显著的性能提升。但无论如何,这些特殊的配置项不应该成为服务器基本配置文件的一部分。只有当发现特定的性能问题才应该设置它们。这就是为什么我们不建议通过寻找有问题的地方修改配置项的原因。如果有些地方确实需要提升,也需要在查询响应时间上有所体现。最好是从查询语句和响应时间入手来开始分析问题,而不是通过配置项。这可以节省大量的时间,避免很多的问题。

另一个节省时间和避免麻烦的好办法是使用默认配置,除非是明确地知道默认值会有问题。很多人都是在默认配置下运行的,这种情况非常普遍。这使得默认配置是经过最多实际测试的。对配置项做一些不必要的修改可能会遇到一些意料之外的bug。

8.1 MySQL配置的工作原理 #

在讨论如何配置MySQL之前,我们先来解释一下MySQL的配置机制。MySQL对配置要求非常宽松,但是下面这些建议可能会为你节省大量的工作和时间。

首先应该知道的是MySQL从哪里获得配置信息:命令行参数和配置文件。在类UNIX系统中,配置文件的位置一般在*/etc/my.cnf或者/etc/mysql/my.cnf*。如果使用操作系统的启动脚本,这通常是唯一指定配置设置的地方。如果手动启动MySQL,例如在测试安装时,也可以在命令行指定设置。实际上,服务器会读取配置文件的内容,删除所有注释和换行,然后和命令行选项一起处理。

i-关于术语的说明:因为很多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这一段。一定要确认配置项放在了文件正确的分段中,否则配置是不会生效的。

8.1.1 语法、作用域和动态性 #

配置项设置都使用小写,单词之间用下画线或横线隔开。下面的例子是等价的,并且可能在命令行和配置文件中都看到这两种格式:

    /usr/sbin/mysqld --auto-increment-offset=5
    /usr/sbin/mysqld --auto-increment-offset=5

我们建议使用一种固定的风格。这样在配置文件中搜索配置项时会容易得多。

配置项可以有多个作用域。有些设置是服务器级的(全局作用域),有些对每个连接是不同的(会话作用域),剩下的一些是对象级的。许多会话级变量跟全局变量相等,可以认为是默认值。如果改变会话级变量,它只影响改动的当前连接,当连接关闭时所有参数变更都会失效。下面有一些例子,你应该清楚这些不同类型的行为:

  • query_cache_sizey变量是全局的。
  • sort_buffer_sizey变量默认是全局相同的,但是每个线程里也可以设置。
  • join_buffer_sizey变量也有全局默认值且每个线程是可以设置的,但是若一个查询中关联多张表,可以为每个关联分配一个关联缓冲(join buffer),所以每个查询可能有多个关联缓冲。

另外,除了在配置文件中设置变量,有很多变量(但不是所有)也可以在服务器运行时修改。MySQL把这些归为动态配置变量。下面的语句展示了动态改变sort_buffer_size的会话值和全局值的不同方式:

...

第7章MySQL高级特性

第7章 MySQL高级特性

MySQL从5.0和5.1版本开始引入了很多高级特性,例如分区、触发器等,这对有其他关系型数据库使用背景的用户来说可能并不陌生。这些新特性吸引了很多用户开始使用MySQL。不过,这些特性的性能到底如何,还需要用户真正使用过才能知道。本章我们将为大家介绍,在真实的世界中,这些特性表现如何,而不是只简单地介绍参考手册或者宣传材料上的数据。

7.1 分区表 #

对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成。实现分区的代码实际上是对一组底层表的句柄对象(Handler Object)的封装。对分区表的请求,都会通过句柄对象转化成对存储引擎的接口调用。所以分区对于SQL层来说是一个完全封装底层实现的黑盒子,对应用是透明的,但是从底层的文件系统来看就很容易发现,每一个分区表都有一个使用#分隔命名的表文件。

MySQL实现分区表的方式——对底层表的封装——意味着索引也是按照分区的子表定义的,而没有全局索引。这和Oracle不同,在Oracle中可以更加灵活地定义索引和表是否进行分区。

MySQL在创建表时使用PARTITION BY子句定义每个分区存放的数据。在执行查询的时候,优化器会根据分区定义过滤那些没有我们需要数据的分区,这样查询就无须扫描所有分区——只需要查找包含需要数据的分区就可以了。

分区的一个主要目的是将数据按照一个较粗的粒度分在不同的表中。这样做可以将相关的数据存放在一起,另外,如果想一次批量删除整个分区的数据也会变得很方便。

在下面的场景中,分区可以起到非常大的作用:

  • 表非常大以至于无法全部都放在内存中,或者只在表的最后部分有热点数据,其他均是历史数据。
  • 分区表的数据更容易维护。例如,想批量删除大量数据可以使用清除整个分区的方式。另外,还可以对一个独立分区进行优化、检查、修复等操作。
  • 分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备。
  • 可以使用分区表来避免某些特殊的瓶颈,例如InnoDB的单个索引的互斥访问、ext3文件系统的inode锁竞争等。
  • 如果需要,还可以备份和恢复独立的分区,这在非常大的数据集的场景下效果非常好。

MySQL的分区实现非常复杂,我们不打算介绍实现的全部细节。这里我们将专注在分区性能方面,所以如果想了解更多的关于分区的基础知识,我们建议阅读MySQL官方手册中的“分区”一节,其中介绍了很多分区相关的基础知识。另外,还可以阅读CREATE TABLE、SHOW CREATE TABLE、ALTER TABLE和INFORMATION_SCHEMA.PARTITIONS、EXPLAIN关于分区部分的介绍。分区特性使得CREATE TABLE和ALTER TABLE命令变得更加复杂了。

分区表本身也有一些限制,下面是其中比较重要的几点:

  • 一个表最多只能有1024个分区。
  • 在MySQL 5.1中,分区表达式必须是整数,或者是返回整数的表达式。在MySQL 5.5中,某些场景中可以直接使用列来进行分区。
  • 如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来。
  • 分区表中无法使用外键约束。

7.1.1 分区表的原理 #

如前所述,分区表由多个相关的底层表实现,这些底层表也是由句柄对象(Handler object)表示,所以我们也可以直接访问各个分区。存储引擎管理分区的各个底层表和管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引只是在各个底层表上各自加上一个完全相同的索引。从存储引擎的角度来看,底层表和一个普通表没有任何不同,存储引擎也无须知道这是一个普通表还是一个分区表的一部分。

分区表上的操作按照下面的操作逻辑进行:

SELECT查询

当查询一个分区表的时候,分区层先打开并锁住所有的底层表,优化器先判断是否可以过滤部分分区,然后再调用对应的存储引擎接口访问各个分区的数据。

INSERT操作

当写入一条记录时,分区层先打开并锁住所有的底层表,然后确定哪个分区接收这条记录,再将记录写入对应底层表。

DELETE操作

当删除一条记录时,分区层先打开并锁住所有的底层表,然后确定数据对应的分区,最后对相应底层表进行删除操作。

UPDATE操作

当更新一条记录时,分区层先打开并锁住所有的底层表,MySQL先确定需要更新的记录在哪个分区,然后取出数据并更新,再判断更新后的数据应该放在哪个分区,最后对底层表进行写入操作,并对原数据所在的底层表进行删除操作。

有些操作是支持过滤的。例如,当删除一条记录时,MySQL需要先找到这条记录,如果WHERE条件恰好和分区表达式匹配,就可以将所有不包含这条记录的分区都过滤掉。这对UPDATE语句同样有效。如果是INSERT操作,则本身就是只命中一个分区,其他分区都会被过滤掉。MySQL先确定这条记录属于哪个分区,再将记录写入对应的底层分区表,无须对任何其他分区进行操作。

虽然每个操作都会“先打开并锁住所有的底层表”,但这并不是说分区表在处理过程中是锁住全表的。如果存储引擎能够自己实现行级锁,例如InnoDB,则会在分区层释放对应表锁。这个加锁和解锁过程与普通InnoDB上的查询类似。

后面我们会通过一些例子来看看,当访问一个分区表的时候,打开和锁住所有底层表的代价及其带来的后果。

7.1.2 分区表的类型 #

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章查询性能优化

第6章 查询性能优化

前面的章节我们介绍了如何设计最优的库表结构、如何建立最好的索引,这些对于高性能来说是必不可少的。但这些还不够——还需要合理的设计查询。如果查询写得很糟糕,即使库表结构再合理、索引再合适,也无法实现高性能。

查询优化、索引优化、库表结构优化需要齐头并进,一个不落。在获得编写MySQL查询的经验的同时,也将学习到如何为高效的查询设计表和索引。同样的,也可以学习到在优化库表结构时会影响到哪些类型的查询。这个过程需要时间,所以建议大家在学习后面章节的时候多回头看看这三章的内容。

本章将从查询设计的一些基本原则开始——这也是在发现查询效率不高的时候首先需要考虑的因素。然后会介绍一些更深的查询优化的技巧,并会介绍一些MySQL优化器内部的机制。我们将展示MySQL是如何执行查询的,你也将学会如何去改变一个查询的执行计划。最后,我们要看一下MySQL优化器在哪些方面做得还不够,并探索查询优化的模式,以帮助MySQL更有效地执行查询。

本章的目标是帮助大家更深刻地理解MySQL如何真正地执行查询,并明白高效和低效的原因何在,这样才能充分发挥MySQL的优势,并避开它的弱点。

6.1 为什么查询速度会慢 #

在尝试编写快速的查询之前,需要清楚一点,真正重要是响应时间。如果把查询看作是一个任务,那么它由一系列子任务组成,每个子任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减少子任务的执行次数,要么让子任务运行得更快(1)。

MySQL在执行查询的时候有哪些子任务,哪些子任务运行的速度很慢?这里很难给出完整的列表,但如果按照第3章介绍的方法对查询进行剖析,就能看到查询所执行的子任务。通常来说,查询的生命周期大致可以按照顺序来看:从客户端,到服务器,然后在服务器上进行解析,生成执行计划,执行,并返回结果给客户端。其中“执行”可以认为是整个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。

在完成这些任务的时候,查询需要在不同的地方花费时间,包括网络,CPU计算,生成统计信息和执行计划、锁等待(互斥等待)等操作,尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作、CPU操作和内存不足时导致的I/O操作上消耗时间。根据存储引擎不同,可能还会产生大量的上下文切换以及系统调用。

在每一个消耗大量时间的查询案例中,我们都能看到一些不必要的额外操作、某些操作被额外地重复了很多次、某些操作执行得太慢等。优化查询的目的就是减少和消除这些操作所花费的时间。

再次申明一点,对于一个查询的全部生命周期,上面列的并不完整。这里我们只是想说明:了解查询的生命周期、清楚查询的时间消耗情况对于优化查询有很大的意义。有了这些概念,我们再一起来看看如何优化查询。

6.2 慢查询基础:优化数据访问 #

查询性能低下最基本的原因是访问的数据太多。某些查询可能不可避免地需要筛选大量数据,但这并不常见。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。对于低效的查询,我们发现通过下面两个步骤来分析总是很有效:

  1. 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候也可能是访问了太多的列。
  2. 确认MySQL服务器层是否在分析大量超过需要的数据行。

6.2.1 是否向数据库请求了不需要的数据 #

有些查询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢弃。这会给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...;**    

总是取出全部列

...

第5章创建高性能的索引

第5章 创建高性能的索引

索引(在MySQL中也叫做“键(key)”)是存储引擎用于快速找到记录的一种数据结构。这是索引的基本功能,除此之外,本章还将讨论索引其他一些方面有用的属性。

索引对于良好的性能非常关键。尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要。在数据量较小且负载较低时,不恰当的索引对性能的影响可能还不明显,但当数据量逐渐增大时,性能则会急剧下降(1)。

不过,索引却经常被忽略,有时候甚至被误解,所以在实际案例中经常会遇到由糟糕索引导致的问题。这也是我们把索引优化放在了靠前的章节,甚至比查询优化还靠前的原因。

索引优化应该是对查询性能优化最有效的手段了。索引能够轻易将查询性能提高几个数量级,“最优”的索引有时比一个“好的”索引性能要好两个数量级。创建一个真正“最优”的索引经常需要重写查询,所以,本章和下一章的关系非常紧密。

5.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了。

5.1.1 索引的类型 #

索引有很多种类型,可以为不同的场景提供更好的性能。在MySQL中,索引是在存储引擎层而不是服务器层实现的。所以,并没有统一的索引标准:不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同。

下面我们先来看看MySQL支持的索引类型,以及它们的优点和缺点。

B-Tree索引 #

当人们谈论索引的时候,如果没有特别指明类型,那多半说的是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与数据类型优化

第4章 Schema与数据类型优化

良好的逻辑设计和物理设计是高性能的基石,应该根据系统将要执行的查询语句来设计schema,这往往需要权衡各种因素。例如,反范式的设计可以加快某些类型的查询,但同时可能使另一些类型的查询变慢。比如添加计数表和汇总表是一种很好的优化查询的方式,但这些表的维护成本可能会很高。MySQL独有的特性和实现细节对性能的影响也很大。

本章和聚焦在索引优化的下一章,覆盖了MySQL特有的schema设计方面的主题。我们假设读者已经知道如何设计数据库,所以本章既不会介绍如何入门数据库设计,也不会讲解数据库设计方面的深入内容。这一章关注的是MySQL数据库的设计,主要介绍的是MySQL数据库设计与其他关系型数据库管理系统的区别。如果需要学习数据库设计方面的基础知识,建议阅读Clare Churcher的Beginning Database Design(Apress出版社)一书。

本章内容是为接下来的两个章节做铺垫。在这三章中,我们将讨论逻辑设计、物理设计和查询执行,以及它们之间的相互作用。这既需要关注全局,也需要专注细节。还需要理解整个系统以便弄清楚各个部分如何相互影响。如果在阅读完索引和查询优化章节后再回头来看这一章,也许会发现本章很有用,很多讨论的议题不能孤立地考虑。

4.1 选择优化的数据类型 #

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报告的是基本类型,而不是别名。

4.1.1 整数类型 #

有两种类型的数字:整数(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内置引擎的方式。

4.1.2 实数类型 #

实数是带有小数部分的数字。然而,它们不只是为了存储小数部分;也可以使用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精确计算代价高的问题。

4.1.3 字符串类型 #

MySQL支持多种字符串类型,每种类型还有很多变种。这些数据类型在4.1和5.0版本发生了很大的变化,使得情况更加复杂。从MySQL 4.1开始,每个字符串列可以定义自己的字符集和排序规则,或者说校对规则(collation)(更多关于这个主题的信息请参考第7章)。这些东西会很大程度上影响性能。

...

第3章服务器性能剖析

第3章 服务器性能剖析

在我们的技术咨询生涯中,最常碰到的三个性能相关的服务请求是:如何确认服务器是否达到了性能最佳的状态、找出某条语句为什么执行不够快,以及诊断被用户描述成“停顿”、“堆积”或者“卡死”的某些间歇性疑难故障。本章将主要针对这三个问题做出解答。我们将提供一些工具和技巧来优化整机的性能、优化单条语句的执行速度,以及诊断或者解决那些很难观察到的问题(这些问题用户往往很难知道其根源,有时候甚至都很难察觉到它的存在)。

这看起来是个艰巨的任务,但是事实证明,有一个简单的方法能够从噪声中发现苗头。这个方法就是专注于测量服务器的时间花费在哪里,使用的技术则是性能剖析(profiling)。在本章,我们将展示如何测量系统并生成剖析报告,以及如何分析系统的整个堆栈(stack),包括从应用程序到数据库服务器到单个查询。

首先我们要保持空杯精神,抛弃掉一些关于性能的常见的误解。这有一定的难度,下面我们一起通过一些例子来说明问题在哪里。

3.1 性能优化简介 #

问10个人关于性能的问题,可能会得到10个不同的回答,比如“每秒查询次数”、“CPU利用率”、“可扩展性”之类。这其实也没有问题,每个人在不同场景下对性能有不同的理解,但本章将给性能一个正式的定义。我们将性能定义为完成某件任务所需要的时间度量,换句话说,性能即响应时间,这是一个非常重要的原则。我们通过任务和时间而不是资源来测量性能。数据库服务器的目的是执行SQL语句,所以它关注的任务是查询或者语句,如SELECT、UPDATE、DELETE等(1)。数据库服务器的性能用查询的响应时间来度量,单位是每个查询花费的时间。

还有另外一个问题:什么是优化?我们暂时不讨论这个问题,而是假设性能优化就是在一定的工作负载下尽可能地(2)降低响应时间。

很多人对此很迷茫。假如你认为性能优化是降低CPU利用率,那么可以减少对资源的使用。但这是一个陷阱,资源是用来消耗并用来工作的,所以有时候消耗更多的资源能够加快查询速度。很多时候将使用老版本InnoDB引擎的MySQL升级到新版本后,CPU利用率会上升得很厉害,这并不代表性能出现了问题,反而说明新版本的InnoDB对资源的利用率上升了。查询的响应时间则更能体现升级后的性能是不是变得更好。版本升级有时候会带来一些bug,比如不能利用某些索引从而导致CPU利用率上升。CPU利用率只是一种现象,而不是很好的可度量的目标。

同样,如果把性能优化仅仅看成是提升每秒查询量,这其实只是吞吐量优化。吞吐量的提升可以看作性能优化的副产品(3)。对查询的优化可以让服务器每秒执行更多的查询,因为每条查询执行的时间更短了(吞吐量的定义是单位时间内的查询数量,这正好是我们对性能的定义的倒数)。

所以如果目标是降低响应时间,那么就需要理解为什么服务器执行查询需要这么多时间,然后去减少或者消除那些对获得查询结果来说不必要的工作。也就是说,先要搞清楚时间花在哪里。这就引申出优化的第二个原则:无法测量就无法有效地优化。所以第一步应该测量时间花在什么地方。

我们观察到,很多人在优化时,都将精力放在修改一些东西上,却很少去进行精确的测量。我们的做法完全相反,将花费非常多,甚至90%的时间来测量响应时间花在哪里。如果通过测量没有找到答案,那要么是测量的方式错了,要么是测量得不够完整。如果测量了系统中完整而且正确的数据,性能问题一般都能暴露出来,对症下药的解决方案也就比较明了。测量是一项很有挑战性的工作,并且分析结果也同样有挑战性,测出时间花在哪里,和知道为什么花在那里,是两码事。

前面提到需要合适的测量范围,这是什么意思呢?合适的测量范围是说只测量需要优化的活动。有两种比较常见的情况会导致不合适的测量:

  • 在错误的时间启动和停止测量。
  • 测量的是聚合后的信息,而不是目标活动本身。

例如,一个常见的错误是先查看慢查询,然后又去排查整个服务器的情况来判断问题在哪里。如果确认有慢查询,那么就应该测量慢查询,而不是测量整个服务器。测量的应该是从慢查询的开始到结束的时间,而不是查询之前或查询之后的时间。

完成一项任务所需要的时间可以分成两部分:执行时间和等待时间。如果要优化任务的执行时间,最好的办法是通过测量定位不同的子任务花费的时间,然后优化去掉一些子任务、降低子任务的执行频率或者提升子任务的效率。而优化任务的等待时间则相对要复杂一些,因为等待有可能是由其他系统间接影响导致,任务之间也可能由于争用磁盘或者CPU资源而相互影响。根据时间是花在执行还是等待上的不同,诊断也需要不同的工具和技术。

刚才说到需要定位和优化子任务,但只是一笔带过。一些运行不频繁或者很短的子任务对整体响应时间的影响很小,通常可以忽略不计。那么如何确认哪些子任务是优化的目标呢?这个时候性能剖析就可以派上用场了。

如何判断测量是正确的?

如果测量是如此重要,那么测量错了会有什么后果?实际上,测量经常都是错误的。对数量的测量并不等于数量本身。测量的错误可能很小,跟实际情况区别不大,但错的终归是错的。所以这个问题其实应该是:“测量到底有多么不准确?”这个问题在其他一些书中有详细的讨论,但不是本书的主题。但是要意识到使用的是测量数据,而不是其所代表的实际数据。通常来说,测量的结果也可能有多种模糊的表现,这可能导致推断出错误的结论。

3.1.1 通过性能剖析进行优化 #

一旦掌握并实践面向响应时间的优化方法,就会发现需要不断地对系统进行性能剖析(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的时候经常都指的是查询。

我们将实际地讨论两种类型的性能剖析:基于执行时间的分析和基于等待的分析。基于执行时间的分析研究的是什么任务的执行时间最长,而基于等待的分析则是判断任务在什么地方被阻塞的时间最长。

如果任务执行时间长是因为消耗了太多的资源且大部分时间花费在执行上,等待的时间不多,这种情况下基于等待的分析作用就不大。反之亦然,如果任务一直在等待,没有消耗什么资源,去分析执行时间就不会有什么结果。如果不能确认问题是出在执行还是等待上,那么两种方式都需要试试。后面会给出详细的例子。

...

第2章MySQL基准测试

第2章 MySQL基准测试

基准测试(benchmark)是MySQL新手和专家都需要掌握的一项基本技能。简单地说,基准测试是针对系统设计的一种压力测试。通常的目标是为了掌握系统的行为。但也有其他原因,如重现某个系统状态,或者是做新硬件的可靠性测试。本章将讨论MySQL和基于MySQL的应用的基准测试的重要性、策略和工具。我们将特别讨论一下sysbench,这是一款非常优秀的MySQL基准测试工具。

2.1 为什么需要基准测试 #

为什么基准测试很重要?因为基准测试是唯一方便有效的、可以学习系统在给定的工作负载下会发生什么的方法。基准测试可以观察系统在不同压力下的行为,评估系统的容量,掌握哪些是重要的变化,或者观察系统如何处理不同的数据。基准测试可以在系统实际负载之外创造一些虚构场景进行测试。基准测试可以完成以下工作,或者更多:

  • 验证基于系统的一些假设,确认这些假设是否符合实际情况。
  • 重现系统中的某些异常行为,以解决这些异常。
  • 测试系统当前的运行情况。如果不清楚系统当前的性能,就无法确认某些优化的效果如何。也可以利用历史的基准测试结果来分析诊断一些无法预测的问题。
  • 模拟比当前系统更高的负载,以找出系统随着压力增加而可能遇到的扩展性瓶颈。
  • 规划未来的业务增长。基准测试可以评估在项目未来的负载下,需要什么样的硬件,需要多大容量的网络,以及其他相关资源。这有助于降低系统升级和重大变更的风险。
  • 测试应用适应可变环境的能力。例如,通过基准测试,可以发现系统在随机的并发峰值下的性能表现,或者是不同配置的服务器之间的性能表现。基准测试也可以测试系统对不同数据分布的处理能力。
  • 测试不同的硬件、软件和操作系统配置。比如RAID 5还是RAID 10更适合当前的系统?如果系统从ATA硬盘升级到SAN存储,对于随机写性能有什么帮助?Linux 2.4系列的内核会比2.6系列的可扩展性更好吗?升级MySQL的版本能改善性能吗?为当前的数据采用不同的存储引擎会有什么效果?所有这类问题都可以通过专门的基准测试来获得答案。
  • 证明新采购的设备是否配置正确。笔者曾经无数次地通过基准测试来对新系统进行压测,发现了很多错误的配置,以及硬件组件的失效等问题。因此在新系统正式上线到生产环境之前进行基准测试是一个好习惯,永远不要相信主机提供商或者硬件供应商的所谓系统已经安装好,并且能运行多快的说法。如果可能,执行实际的基准测试永远是一个好主意。

基准测试还可以用于其他目的,比如为应用创建单元测试套件。但本章我们只关注与性能有关的基准测试。

基准测试的一个主要问题在于其不是真实压力的测试。基准测试施加给系统的压力相对真实压力来说,通常比较简单。真实压力是不可预期而且变化多端的,有时候情况会过于复杂而难以解释。所以使用真实压力测试,可能难以从结果中分析出确切的结论。

基准测试的压力和真实压力在哪些方面不同?有很多因素会影响基准测试,比如数据量、数据和查询的分布,但最重要的一点还是基准测试通常要求尽可能快地执行完成,所以经常给系统造成过大的压力。在很多案例中,我们都会调整给测试工具的最大压力,以在系统可以容忍的压力阈值内尽可能快地执行测试,这对于确定系统的最大容量非常有帮助。然而大部分压力测试工具不支持对压力进行复杂的控制。务必要记住,测试工具自身的局限也会影响到结果的有效性。

使用基准测试进行容量规划也要掌握技巧,不能只根据测试结果做简单的推断。例如,假设想知道使用新数据库服务器后,系统能够支撑多大的业务增长。首先对原系统进行基准测试,然后对新系统做测试,结果发现新系统可以支持原系统40倍的TPS(每秒事务数),这时候就不能简单地推断说新系统一定可以支持40倍的业务增长。这是因为在业务增长的同时,系统的流量、用户、数据以及不同数据之间的交互都在增长,它们不可能都有40倍的支撑能力,尤其是相互之间的关系。而且当业务增长到40倍时,应用本身的设计也可能已经随之改变。可能有更多的新特性会上线,其中某些特性可能对数据库造成的压力远大于原有功能。而这些压力、数据、关系和特性的变化都很难模拟,所以它们对系统的影响也很难评估。

结论就是,我们只能进行大概的测试,来确定系统大致的余量有多少。当然也可以做一些真实压力测试(和基准测试有区别),但在构造数据集和压力的时候要特别小心,而且这样就不再是基准测试了。基准测试要尽量简单直接,结果之间容易相互比较,成本低且易于执行。尽管有诸多限制,基准测试还是非常有用的(只要搞清楚测试的原理,并且了解如何分析结果所代表的意义)。

2.2 基准测试的策略 #

基准测试有两种主要的策略:一是针对整个系统的整体测试,另外是单独测试MySQL。这两种策略也被称为集成式(full-stack)以及单组件式(single-component)基准测试。针对整个系统做集成式测试,而不是单独测试MySQL的原因主要有以下几点:

  • 测试整个应用系统,包括Web服务器、应用代码、网络和数据库是非常有用的,因为用户关注的并不仅仅是MySQL本身的性能,而是应用整体的性能。
  • MySQL并非总是应用的瓶颈,通过整体的测试可以揭示这一点。
  • 只有对应用做整体测试,才能发现各部分之间的缓存带来的影响。
  • 整体应用的集成式测试更能揭示应用的真实表现,而单独组件的测试很难做到这一点。

另外一方面,应用的整体基准测试很难建立,甚至很难正确设置。如果基准测试的设计有问题,那么结果就无法反映真实的情况,从而基于此做的决策也就可能是错误的。

不过,有时候不需要了解整个应用的情况,而只需要关注MySQL的性能,至少在项目初期可以这样做。基于以下情况,可以选择只测试MySQL:

  • 需要比较不同的schema或查询的性能。
  • 针对应用中某个具体问题的测试。
  • 为了避免漫长的基准测试,可以通过一个短期的基准测试,做快速的“周期循环”,来检测出某些调整后的效果。

另外,如果能够在真实的数据集上执行重复的查询,那么针对MySQL的基准测试也是有用的,但是数据本身和数据集的大小都应该是真实的。如果可能,可以采用生产环境的数据快照。

不幸的是,设置一个基于真实数据的基准测试复杂而且耗时。如果能得到一份生产数据集的拷贝,当然很幸运,但这通常不太可能。比如要测试的是一个刚开发的新应用,它只有很少的用户和数据。如果想测试该应用在规模扩张到很大以后的性能表现,就只能通过模拟大量的数据和压力来进行。

2.2.1 测试何种指标 #

在开始执行甚至是在设计基准测试之前,需要先明确测试的目标。测试目标决定了选择什么样的测试工具和技术,以获得精确而有意义的测试结果。可以将测试目标细化为一系列的问题,比如,“这种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数),就可以获得两倍的吞吐量。当然,同时性能(响应时间)也必须在可以接受的范围内。大多数系统是无法做到如此理想的线性扩展的。随着压力的变化,吞吐量和性能都可能越来越差。

可扩展性指标对于容量规范非常有用,它可以提供其他测试无法提供的信息,来帮助发现应用的瓶颈。比如,如果系统是基于单个用户的响应时间测试(这是一个很糟糕的测试策略)设计的,虽然测试的结果很好,但当并发度增加时,系统的性能有可能变得非常糟糕。而一个基于不断增加用户连接的情况下的响应时间测试则可以发现这个问题。

一些任务,比如从细粒度数据创建汇总表的批量工作,需要的是周期性的快速响应时间。当然也可以测试这些任务纯粹的响应时间,但要注意考虑这些任务之间的相互影响。批量工作可能导致相互之间有影响的查询性能变差,反之亦然。

归根结底,应该测试那些对用户来说最重要的指标。因此应该尽可能地去收集一些需求,比如,什么样的响应时间是可以接受的,期待多少的并发性,等等。然后基于这些需求来设计基准测试,避免目光短浅地只关注部分指标,而忽略其他指标。

2.3 基准测试方法 #

在了解基本概念之后,现在可以来具体讨论一下如何设计和执行基准测试。但在讨论如何设计好的基准测试之前,先来看一下如何避免一些常见的错误,这些错误可能导致测试结果无用或者不精确:

  • 使用真实数据的子集而不是全集。例如应用需要处理几百GB的数据,但测试只有1GB数据;或者只使用当前数据进行测试,却希望模拟未来业务大幅度增长后的情况。
  • 使用错误的数据分布。例如使用均匀分布的数据测试,而系统的真实数据有很多热点区域(随机生成的测试数据通常无法模拟真实的数据分布)。
  • 使用不真实的分布参数,例如假定所有用户的个人信息(profile)都会被平均地读取(2)。
  • 在多用户场景中,只做单用户的测试。
  • 在单服务器上测试分布式应用。
  • 与真实用户行为不匹配。例如Web页面中的“思考时间”。真实用户在请求到一个页面后会阅读一段时间,而不是不停顿地一个接一个点击相关链接。
  • 反复执行同一个查询。真实的查询是不尽相同的,这可能会导致缓存命中率降低。而反复执行同一个查询在某种程度上,会全部或者部分缓存结果。
  • 没有检查错误。如果测试的结果无法得到合理的解释,比如一个本应该很慢的查询突然变快了,就应该检查是否有错误产生。否则可能只是测试了MySQL检测语法错误的速度了。基准测试完成后,一定要检查一下错误日志,这应当是基本的要求。
  • 忽略了系统预热(warm up)的过程。例如系统重启后马上进行测试。有时候需要了解系统重启后需要多长时间才能达到正常的性能容量,要特别留意预热的时长。反过来说,如果要想分析正常的性能,需要注意,若基准测试在重启以后马上启动,则缓存是冷的、还没有数据,这时即使测试的压力相同,得到的结果也和缓存已经装满数据时是不同的。
  • 使用默认的服务器配置。第3章将详细地讨论服务器的优化配置。
  • 测试时间太短。基准测试需要持续一定的时间。后面会继续讨论这个话题。

只有避免了上述错误,才能走上改进测试质量的漫漫长路。

...

第1章MySQL架构与历史

第1章 MySQL架构与历史

和其他数据库系统相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥好的作用,但同时也会带来一点选择上的困难。MySQL并不完美,却足够灵活,能够适应高要求的环境,例如Web类应用。同时,MySQL既可以嵌入到应用程序中,也可以支持数据仓库、内容索引和部署软件、高可用的冗余系统、在线事务处理系统(OLTP)等各种应用类型。

为了充分发挥MySQL的性能并顺利地使用,就必须理解其设计。MySQL的灵活性体现在很多方面。例如,你可以通过配置使它在不同的硬件上都运行得很好,也可以支持多种不同的数据类型。但是,MySQL最重要、最与众不同的特性是它的存储引擎架构,这种架构的设计将查询处理(Query Processing)及其他系统任务(Server Task)和数据的存储/提取相分离。这种处理和存储分离的设计可以在使用时根据性能、特性,以及其他需求来选择数据存储的方式。

本章概要地描述了MySQL的服务器架构、各种存储引擎之间的主要区别,以及这些区别的重要性。另外也会回顾一下MySQL的历史背景和基准测试,并试图通过简化细节和演示案例来讨论MySQL的原理。这些讨论无论是对数据库一无所知的新手,还是熟知其他数据库的专家,都不无裨益。

1.1 MySQL逻辑架构 #

如果能在头脑中构建出一幅MySQL各组件之间如何协同工作的架构图,就会有助于深入理解MySQL服务器。图1-1展示了MySQL的逻辑架构图。

图1-1:MySQL服务器逻辑架构图

最上层的服务并不是MySQL所独有的,大多数基于网络的客户端/服务器的工具或者服务都有类似的架构。比如连接处理、授权认证、安全等等。

第二层架构是MySQL比较有意思的部分。大多数MySQL的核心服务功能都在这一层,包括查询解析、分析、优化、缓存以及所有的内置函数(例如,日期、时间、数学和加密函数),所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。

第三层包含了存储引擎。存储引擎负责MySQL中数据的存储和提取。和GNU/Linux下的各种文件系统一样,每个存储引擎都有它的优势和劣势。服务器通过API与存储引擎进行通信。这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询过程透明。存储引擎API包含几十个底层函数,用于执行诸如“开始一个事务”或者“根据主键提取一行记录”等操作。但存储引擎不会去解析SQL(1),不同存储引擎之间也不会相互通信,而只是简单地响应上层服务器的请求。

1.1.1 连接管理与安全性 #

每个客户端连接都会在服务器进程中拥有一个线程,这个连接的查询只会在这个单独的线程中执行,该线程只能轮流在某个CPU核心或者CPU中运行。服务器会负责缓存线程,因此不需要为每一个新建的连接创建或者销毁线程(2)。

当客户端(应用)连接到MySQL服务器时,服务器需要对其进行认证。认证基于用户名、原始主机信息和密码。如果使用了安全套接字(SSL)的方式连接,还可以使用X.509证书认证。一旦客户端连接成功,服务器会继续验证该客户端是否具有执行某个特定查询的权限(例如,是否允许客户端对world数据库的Country表执行SELECT语句)。

1.1.2 优化与执行 #

MySQL会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括重写查询、决定表的读取顺序,以及选择合适的索引等。用户可以通过特殊的关键字提示(hint)优化器,影响它的决策过程。也可以请求优化器解释(explain)优化过程的各个因素,使用户可以知道服务器是如何进行优化决策的,并提供一个参考基准,便于用户重构查询和schema、修改相关配置,使应用尽可能高效运行。第6章我们将讨论更多优化器的细节。

优化器并不关心表使用的是什么存储引擎,但存储引擎对于优化查询是有影响的。优化器会请求存储引擎提供容量或某个具体操作的开销信息,以及表数据的统计信息等。例如,某些存储引擎的某种索引,可能对一些特定的查询有优化。关于索引与schema的优化,请参见第4章和第5章。

对于SELECT语句,在解析查询之前,服务器会先检查查询缓存(Query Cache),如果能够在其中找到对应的查询,服务器就不必再执行查询解析、优化和执行的整个过程,而是直接返回查询缓存中的结果集。第7章详细讨论了相关内容。

1.2 并发控制 #

无论何时,只要有多个查询需要在同一时刻修改数据,都会产生并发控制的问题。本章的目的是讨论MySQL在两个层面的并发控制:服务器层与存储引擎层。并发控制是一个内容庞大的话题,有大量的理论文献对其进行过详细的论述。本章只简要地讨论MySQL如何控制并发读写,因此读者需要有相关的知识来理解本章接下来的内容。

以Unix系统的email box为例,典型的mbox文件格式是非常简单的。一个mbox邮箱中的所有邮件都串行在一起,彼此首尾相连。这种格式对于读取和分析邮件信息非常友好,同时投递邮件也很容易,只要在文件末尾附加新的邮件内容即可。

但如果两个进程在同一时刻对同一个邮箱投递邮件,会发生什么情况?显然,邮箱的数据会被破坏,两封邮件的内容会交叉地附加在邮箱文件的末尾。设计良好的邮箱投递系统会通过锁(lock)来防止数据损坏。如果客户试图投递邮件,而邮箱已经被其他客户锁住,那就必须等待,直到锁释放才能进行投递。

这种锁的方案在实际应用环境中虽然工作良好,但并不支持并发处理。因为在任意一个时刻,只有一个进程可以修改邮箱的数据,这在大容量的邮箱系统中是个问题。

1.2.1 读写锁 #

从邮箱中读取数据没有这样的麻烦,即使同一时刻多个用户并发读取也不会有什么问题。因为读取不会修改数据,所以不会出错。但如果某个客户正在读取邮箱,同时另外一个用户试图删除编号为25的邮件,会产生什么结果?结论是不确定,读的客户可能会报错退出,也可能读取到不一致的邮箱数据。所以,为安全起见,即使是读取邮箱也需要特别注意。

如果把上述的邮箱当成数据库中的一张表,把邮件当成表中的一行记录,就很容易看出,同样的问题依然存在。从很多方面来说,邮箱就是一张简单的数据库表。修改数据库表中的记录,和删除或者修改邮箱中的邮件信息,十分类似。

解决这类经典问题的方法就是并发控制,其实非常简单。在处理并发读或者写时,可以通过实现一个由两种类型的锁组成的锁系统来解决问题。这两种类型的锁通常被称为共享锁(shared lock)和排他锁(exclusive lock),也叫读锁(read lock)和写锁(write lock)。

这里先不讨论锁的具体实现,描述一下锁的概念如下:读锁是共享的,或者说是相互不阻塞的。多个客户在同一时刻可以同时读取同一个资源,而互不干扰。写锁则是排他的,也就是说一个写锁会阻塞其他的写锁和读锁,这是出于安全策略的考虑,只有这样,才能确保在给定的时间里,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源。

在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改某一部分数据时,MySQL会通过锁定防止其他用户读取同一数据。大多数时候,MySQL锁的内部管理都是透明的。

1.2.2 锁粒度 #

一种提高共享资源并发性的方式就是让锁定对象更有选择性。尽量只锁定需要修改的部分数据,而不是所有的资源。更理想的方式是,只对会修改的数据片进行精确的锁定。任何时候,在给定的资源上,锁定的数据量越少,则系统的并发程度越高,只要相互之间不发生冲突即可。

问题是加锁也需要消耗资源。锁的各种操作,包括获得锁、检查锁是否已经解除、释放锁等,都会增加系统的开销。如果系统花费大量的时间来管理锁,而不是存取数据,那么系统的性能可能会因此受到影响。

所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡,这种平衡当然也会影响到性能。大多数商业数据库系统没有提供更多的选择,一般都是在表上施加行级锁(row-level lock),并以各种复杂的方式来实现,以便在锁比较多的情况下尽可能地提供更好的性能。

而MySQL则提供了多种选择。每种MySQL存储引擎都可以实现自己的锁策略和锁粒度。在存储引擎的设计中,锁管理是个非常重要的决定。将锁粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,但同时却会失去对另外一些应用场景的良好支持。好在MySQL支持多个存储引擎的架构,所以不需要单一的通用解决方案。下面将介绍两种最重要的锁策略。

表锁(table lock) #

表锁是MySQL中最基本的锁策略,并且是开销最小的策略。表锁非常类似于前文描述的邮箱加锁机制:它会锁定整张表。一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获得读锁,读锁之间是不相互阻塞的。

在特定的场景中,表锁也可能有良好的性能。例如,READ LOCAL表锁支持某些类型的并发写操作。另外,写锁也比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面(写锁可以插入到锁队列中读锁的前面,反之读锁则不能插入到写锁的前面)。

...

第13章云端的MySQL

第13章 云端的MySQL

许多人在云中使用MySQL,有时候规模还非常庞大,这并不奇怪。从我们的经验来看,大多数人使用的是Amazon Web Services平台(AWS):特别是Amazon的弹性计算云(Elastic Compute Cloud,EC2),弹性块存储(Elastic Block Store,EBS),以及更小众的关系数据库服务(Relational Database Service,RDS)。

为了便于讨论MySQL在云中的应用,可以将其粗略分为两类。

IaaS(基础设施即服务)

Iaas是用于托管自有的MySQL服务器的云端基础架构。可以在云端购买虚拟的服务器资源来安装运行MySQL实例。也可以根据需求随意配置MySQL和操作系统,但没有权限也无法看到处于底层的物理硬件设备。

DBaaS(数据库即服务)

MySQL本身作为由云端管理的资源。用户需要先收到MySQL服务器的访问许可(通常是一个连接串)才能访问。也可以配置一些MySQL选项,但没有权限去控制或查看底层的操作系统或虚拟服务器实例。例如 Amazon运行MySQL的RDS。其中一些服务器并非真的使用MySQL,但它们能兼容MySQL协议和查询语言。

我们讨论的重点主要集中在第一类:云托管平台,例如AWS、Rackspace Cloud以及Joyent(1)。有许多很好的资源介绍如何部署和管理MySQL及其运行所需要的资源,并且也有非常多的平台来完全满足这样的需求,所以我们不会展示代码样例或讨论具体的操作技术。因此,本章关注的重点是,在云端运行MySQL还是在传统服务器上部署MySQL,它们在最终经济上和性能特性上的关键区别是什么。我们假定你对云计算很熟悉。这里不是对云计算概念的简单介绍,我们的目的只是帮助那些还不熟悉在云端部署MySQL的用户在使用时避免一些可能遇到的陷阱。

一般来说,MySQL能够在云中很好地运行。在云中运行MySQL并不比在其他平台困难,但有一些非常重要的差别。你需要注意这些差别并据此设计应用和架构来获得好的效果。某些场景下在云端托管MySQL并不是非常适合,有时候则很适合,但大多数时候云仅仅是另外一个部署平台而已。

云是一个部署平台,而不是一种架构,理解这一点很重要。架构会受平台的影响,但平台和架构明显不同。如果你把架构和平台搞混了,就可能会做出不合适的选择而给以后带来麻烦。这也正是我们要花时间讨论云端的MySQL到底有什么不同的原因。

13.1 云的优点、缺点和相关误解 #

云计算有许多优点,但很少是为MySQL特别设计。有一些书籍已经介绍了相关的话题(2),这里我们不再赘述。不过我们会列出一些比较重要的条目供参考,因为接下来会讨论到云计算的缺点,我们不希望你认为我们是在过分苛求云计算。

  • 云是一种将基础设施外包出去无须自己管理的方法。你不需要寻找供应商购买硬件,也不需要维护和供应商之间的关系,更无须替换失效的硬盘驱动器等。
  • 云一般是按照即用即付的方式支付,可以把前期的大量资本支出转换为持续的运营成本。
  • 随着供应商发布新的服务和成本降低,云提供的价值越来越大。你自己无须做任何事情(例如升级服务器),就可以从这些提升中获益;随着时间推移你会很容易地获得更多更好的选择并且费用更低。
  • 云能够帮助你轻松地准备好服务器和其他资源,在用完后直接将其关闭,而无须关注怎么处理它们,或者怎么卖掉它们收回成本。
  • 云代表了对基础设施的另一种思考方式——作为通过API来定义和控制的资源——支持更多的自动化操作。从“私有云”中也可以获得这些好处。

当然,不是所有跟云相关的东西都是好的。这里有一些缺点可能会构成挑战(在本章稍后部分我们会列出MySQL特有的缺点)。

  • 资源是共享并且不可预测的,实际上你可以获得比你支付的更多的资源。这听起来很不错,但却导致容量规划很难做。如果你在不知情的情况下获得了比理应享受到的更多的计算资源,那么就存在这样的风险:别人也许会索要他们应得的资源,这会使你的应用性能退化到应有的水平。一般来说,很难确切地知道本来应该得到多少(资源),大多数云托管服务提供商不会对此给出确切的答案。
  • 无法保证容量和可用性。你可能以为还可以获得新实例,但如果供应商已经超额销售了呢?这在有很多共享资源的情况下会发生,同样也会发生在云中。
  • 虚拟的共享资源导致排查故障更加困难,特别是在无法访问底层物理硬件的情况下无法检查并弄清到底发生了什么。例如,我们曾经看到过一些系统的iostat显示的I/O很正常或者vmstat显示的CPU很正常,而当实际衡量完成一个任务需要的时间时,资源却被系统上的其他东西严重占用了。如果在云平台上出现了性能问题,尤其需要去仔细地分析检测。如果对此并不擅长,可能就无法确认到底是底层系统性能差,还是你做了什么事情导致应用出现不合理的资源需求。

总的来说,云平台上对性能、可用性和容量的透明性和控制力都有所下降。最后,还有一些对云的误解需要记住。

云天生具备更好的可扩展性

应用、云的架构,以及管理云服务的组织是不是都是可扩展的。云并不是天生可扩展的,云也仅仅是云而已,选择一个可扩展的平台并不能自动使应用变得可扩展。的确,如果云托管提供商没有超售,那么你可以根据需求来购买资源,但在需要时能够获得资源仅仅是扩展性的一个方面而已。

云可以自动改善甚至保证可用时间

一般来说,个别在云端托管的服务器比那些经过良好设计的专用基础设施更容易发生故障或运行中断。但是许多人并没有意识到这一点。例如,有人这样写道:“我们将基础设施升级到基于云构建的系统以保证100%的可用时间和可扩展性”。而就在这之前AWS遭受了两次大规模的运行中断故障,导致很大一部分用户受影响。好的架构能够用不可靠的组件设计出可靠的系统,但通常更可靠的基础设施可以获得更高的可用性。(当然不可能有100%的可用时间的系统。)

另一方面,购买云计算服务,实际上是购买一个由专家构建的平台。他们已经考虑了许多底层的东西,这意味着你可以更专注于上层工作。如果构建自己的平台而对其中的那些细枝末节并不精通,就可能犯一些初学者的错误,早晚会导致一些宕机时间。从这一点来说,云计算能够帮助改善可用时间。

云是唯一能提供[这里填入任意的优点]的东西

事实上,许多云的优点是继承自构建云平台所用到的技术,即使不使用云也可以获得(3)。例如,通过管理得当的虚拟化和容量规划,可以像任何一个云平台那样简单快速地启动(spin up)一台新的机器。完全没必要专门使用云来做到这一点。

云是一个“银弹”(silver bullet)

虽然大部分人会认为这很荒谬,但确实有人会这么认为。实际上完全没有这回事。

无可否认,云计算提供了独特的优点,随着时间的推移,关于云计算是什么,以及它们在什么情况下会有帮助,我们会获得更多的共识。但有一点非常肯定:它是全新的,我们现在所做的任何预测都未必经得起时间的考验。我们会在本书讨论相对安全的部分,而将剩下的部分留给读者讨论。

13.2 MySQL在云端的经济价值 #

在一些场景下云托管比传统的服务器部署方式更经济。以我们的经验来看,云托管比较适合尚处于初级阶段的企业,或者那些持续接触新概念并且本质上是以适用为主的企业,例如移动应用开发者或游戏开发者。这些技术的市场随着移动计算的扩张出现了爆炸式增长,并且仍然是快速发展的领域。在许多情况下,成功的因素并不为开发者所控制,例如口口相传的推荐或者恰逢重要国际事件的时机。

我们已经帮助很多公司在云中构建移动应用、社交网络以及游戏应用。其中一个他们大量使用的策略是尽可能又快又便宜地开发和发布应用。如果一个应用碰巧变得流行了,公司将投入资源扩大其规模;否则就会很快终结这些应用。一些公司构建并发布的应用的生命周期甚至只有几个星期,在这样的环境下,可以毫不犹豫地选择云托管。

如果是一个小规模的公司,可能无法提供足够的硬件来自建数据中心以满足一个非常流行的Facebook应用的发展曲线。我们也协助过一些大型的Facebook应用进行扩展,它们能够以今人惊讶的速度增长——有时甚至会快到让一个主机托管公司耗尽资源。更为严重的是,这些应用的增长是完全无法预测的;它们可能只有极少量的用户(也可能突然有了爆炸性的用户数量增长)。我们在数据中心和云中都遇到过这样的应用。如果是一个小公司,云可以帮你避免前期快速注入大量的资金来获得更快更大规模的风险。

云的另一种潜在的大用途是运行不是很重要的基础设施,例如集成环境、开发测试平台,以及评估环境。假设部署周期是两个星期。你会每天每个小时都测试部署一次,还是只在项目最后的冲刺时测试?许多用户只是偶尔需要筹划和部署测试环境。在这种场景下,云可以帮助节约不少钱。

以下是我们使用云的两种方式。第一个是作为我们对技术职员面试的一部分,我们会询问如何解决一些实际的问题。我们使用AMI(Amazon Machine Images)来模拟一些被“破坏”的机器,然后让求职者登录并在服务器上执行一系列任务。我们不必开放他们到内部网络的授权,这种方案显然要方便得多。另一个是作为新项目的工作平台和开发服务器。有一个这样的项目已经在一台云端开发服务器上运行了数个月,而花费不足一美元!这在我们自己的基础设施上是不可能做到的。单是发送一封邮件给系统管理员申请开发服务器的时间价值就不止一美元。

但是另一方面,云托管对于长期项目而言可能会更加昂贵。如果打算长远地使用云,就需要花时间来计算一下(它是否划算)。除了猜想未来的创新能给云计算和商用硬件带来什么,还需要做基准测试以及一个完整的总体持有成本(TCO)账单。为了理清事情的本质并考虑全面所有相关的细节,你需要把所有的事情最终归结为一个数字:每美元的业务交易数。事情变化得太快,所以我们将这个留给读者思考。

13.3 云中的MySQL的可扩展性和高可用性 #

正如我们之前提到的,MySQL并不会在云端自动变得更具扩展性。事实上,如果机器的性能较差,会导致过早使用横向扩展策略。况且云托管服务器相比专用的硬件可靠性和可预测性要更差些,所以想在云端获得高可用性需要更多的创新。

但是总的来说,在云端中扩展MySQL和在其他地方扩展没有太多的差别。最大的不同就是按需提供服务器的能力。但是也有某些限制会导致扩展和高可用实现起来有点麻烦,至少在有些云环境中是这样的。例如,在AWS云平台中,无法使用类似虚拟IP地址的功能来完成快速原子故障转移。像这种对资源的有限控制意味着你需要使用其他办法,例如代理。(ScaleBase也值得去看看。)

云另外一个迷惑人的地方是梦想中的自动扩展——就是根据需求的增加或减少来启动或关闭实例。尽管对于诸如Web服务器这样的无状态部分是可行的,但对于数据库服务器而言则很难做到,因为它是有状态的。对于一些特定的场景,例如以读为主的应用,可以通过增加备库的方式来获得有限的自动扩展(4),但这并不是一个通用的解决方案。实际上,虽然许多应用在Web层使用了自动扩展,但MySQL并不具备在一个无共享(Shared Nothing)集群中的对等角色服务器之间迁移的能力。你可以通过分片架构来自动重新分片并自动增长或收缩(5),但MySQL本身是无法自动扩展的。

事实上,因为数据库通常是一个应用系统中主要或唯一的有状态并且持久化的组件,所以把应用服务迁移到云端是很普遍的事情,因为除数据库之外的所有部分都可以从云中收益——Web服务器、工作队列服务器、缓存等——而MySQL只需要处理剩下的东西。毕竟,数据库并非世界的中心。如果应用系统其他部分获得的好处,超过了让MySQL运行得足够好而投入的额外开销和必需的工作量,那这不是一个是否会发生的问题,而是怎么发生的问题。要回答这个问题,最好先了解你在云中可能碰到的额外的挑战。这些通常围绕着数据库服务器的可用资源。

13.4 四种基础资源 #

MySQL需要四种基础资源来完成工作:CPU周期、内存、I/O,以及网络。这四种资源的特性和重要程度在不同的云平台上各不相同。可以通过了解它们的不同之处和对MySQL的影响,以决定是否选择在云中托管MySQL。

...

第12章高可用性

第12章 高可用性

本章将讲述我们提到的复制、可扩展性以及高可用性三个主题中的第三个。归根结底,高可用性实际上意味着“更少的宕机时间”。然而糟糕的是,高可用性经常和其他相关的概念混淆,例如冗余、保障数据不丢失,以及负载均衡。我们希望之前的两章已经为清楚地理解高可用性做了足够的铺垫。跟其他两章一样,这一章也不仅仅是关注高可用性的内容,一些相关的话题也会综合阐述。

12.1 什么是高可用性 #

高可用性实际上有点像神秘的野兽。它通常以百分比表示,这本身也是一种暗示:高可用性不是绝对的,只有相对更高的可用性。100%的可用性是不可能达到的。可用性的“9”规则是表示可用性目标最普遍的方法。你可能也知道,“5个9”表示99.999%的正常可用时间。换句话说,每年只允许5分钟的宕机时间。对于大多数应用这已经是令人惊叹的数字,尽管还有一些人试图获得更多的“9”。

每个应用对可用性的需求各不相同。在设定一个可用时间的目标之前,先问问自己,是不是确实需要达到这个目标。可用性每提高一点,所花费的成本都会远超之前;可用性的效果和开销的比例并不是线性的。需要保证多少可用时间,取决于能够承担多少成本。高可用性实际上是在宕机造成的损失与降低宕机时间所花费的成本之间取一个平衡。换句话说,如果需要花大量金钱去获得更好的可用时间,但所带来的收益却很低,可能就不值得去做。总的来说,应用在超过一定的点以后追求更高的可用性是非常困难的,成本也会很高,因此我们建议设定一个更现实的目标并且避免过度设计。幸运的是,建立2个9或3个9的可用时间的目标可能并不困难,具体情况取决于应用。

有时候人们将可用性定义成服务正在运行的时间段。我们认为可用性的定义还应该包括应用是否能以足够好的性能处理请求。有许多方法可以让一个服务器保持运行,但服务并不是真正可用。对一个很大的服务器而言,重启MySQL之后,可能需要几个小时才能充分预热以保证查询请求的响应时间是可以接受的,即使服务器只接收了正常流量的一小部分也是如此。

另一个需要考虑的问题是,即使应用并没有停止服务,但是否可能丢失了数据。如果服务器遭遇灾难性故障,可能多少都会丢失一些数据,例如最近已经写入(最新丢失的)二进制日志但尚未传递到备库的中继日志中的事务。你能够容忍吗?大多数应用能够容忍;因为替代方案大多非常昂贵且复杂,或者有一些性能开销。例如,可以使用同步复制,或是将二进制日志放到一个通过DRBD进行复制的设备上,这样就算服务器完全失效也不用担心丢失数据。(但是整个数据中心也有可能会掉电。)

一个良好的应用架构通常可以降低可用性方面的需求,至少对部分系统而言是这样的,良好的架构也更容易做到高可用。将应用中重要和不重要的部分进行分离可以节约不少工作量和金钱,因为对于一个更小的系统改进可用性会更容易。可以通过计算“风险敞口(risk exposure)”,将失效概率与失效代价相乘来确认高优先级的风险。画一个简单的风险计算表,以概率、代价和风险敞口作为列,这样很容易找到需要优先处理的项目。

在前一章我们通过讨论如何避免导致糟糕的可扩展性的原因,来推出如何获得更好的可扩展性。这里也会使用相似的方法来讨论可用性,因为我们相信,理解可用性最好的方法就是研究它的反面——宕机时间。接下来的小节我们会讨论为什么会出现宕机。

12.2 导致宕机的原因 #

我们经常听到导致数据库宕机最主要的原因是编写的SQL查询性能很差,真的是这样吗?2009年我们决定分析我们客户的数据库所遇到的问题,以找出那些真正引起宕机的问题,以及如何避免这些问题(1)。结果证实了一些我们已有的猜想,但也否定了一些(错误的)认识,我们从中学到了很多。

我们首先对宕机事件按表现方式而非导致的原因进行分类。一般来说,“运行环境”是排名第一的宕机类别,大约35%的事件属于这一类。运行环境可以看作是支持数据库服务器运行的系统和资源集合,包括操作系统、硬盘以及网络等。性能问题紧随其后,也是约占35%;然后是复制,占20%;最后剩下的10%包含各种类型的数据丢失或损坏,以及其他问题。

我们对事件按类型进行分类后,确定了导致这些事件的原因。以下是一些需要注意的地方:

  • 在运行环境的问题中,最普遍的问题是磁盘空间耗尽。
  • 在性能问题中,最普遍的宕机原因确实是运行很糟糕的SQL,但也不一定都是这个原因,比如也有很多问题是由于服务器Bug或错误的行为导致的。
  • 糟糕的Schema和索引设计是第二大影响性能的问题。
  • 复制问题通常由于主备数据不一致导致。
  • 数据丢失问题通常由于DROP TABLE的误操作导致,并总是伴随着缺少可用备份的问题。

复制虽然常被人们用来改善可用时间,但却也可能导致宕机。这主要是由于不正确的使用导致的,即便如此,它也阐明了一个普遍的情况:许多高可用性策略可能会产生反作用,我们会在后面讨论这个话题。

现在我们已经知道了主要宕机类别,以及有什么需要注意,下面我们将专门介绍如何获得高可用性。

12.3 如何实现高可用性 #

可以通过同时进行以下两步来获得高可用性。首先,可以尝试避免导致宕机的原因来减少宕机时间。许多问题其实很容易避免,例如通过适当的配置、监控,以及规范或安全保障措施来避免人为错误。第二,尽量保证在发生宕机时能够快速恢复。最常见的策略是在系统中制造冗余,并且具备故障转移能力。这两个维度的高可用性可以通过两个相关的度量来确定:平均失效时间(MTBF)和平均恢复时间(MTTR)。一些组织会非常仔细地追踪这些度量值。

第二步——通过冗余快速恢复——很不幸,这里是最应该注意的地方,但预防措施的投资回报率会很高。接下来我们来探讨一些预防措施。

12.3.1 提升平均失效时间(MTBF) #

其实只要尽职尽责地做好一些应做的事情,就可以避免很多宕机。在分类整理宕机事件并追查导致宕机的根源时,我们还发现,很多宕机本来是有一些方法可以避免的。我们发现大部分宕机事件都可以通过全面的常识性系统管理办法来避免。以下是从我们的白皮书中摘录的指导性建议,在白皮书中有我们详细的分析结果。

  • 测试恢复工具和流程,包括从备份中恢复数据。
  • 遵从最小权限原则。
  • 保持系统干净、整洁。
  • 使用好的命名和组织约定来避免产生混乱,例如服务器是用于开发还是用于生产环境。
  • 谨慎安排升级数据库服务器。
  • 在升级前,使用诸如Percona Toolkit中的pt-upgrade之类的工具仔细检查系统。
  • 使用InnoDB并进行适当的配置,确保InnoDB是默认存储引擎。如果存储引擎被禁止,服务器就无法启动。
  • 确认基本的服务器配置是正确的。
  • 通过skip_name_resolve禁止DNS。
  • 除非能证明有效,否则禁用查询缓存。
  • 避免使用复杂的特性,例如复制过滤和触发器,除非确实需要。
  • 监控重要的组件和功能,特别是像磁盘空间和RAID卷状态这样的关键项目,但也要避免误报,只有当确实发生问题时才发送告警。
  • 尽量记录服务器的状态和性能指数,如果可能就尽量久地保存。
  • 定期检查复制完整性。
  • 将备库设置为只读,不要让复制自动启动。
  • 定期进行查询语句审查。
  • 归档并清理不需要的数据。
  • 为文件系统保留一些空间。在GNU/Linux中,可以使用-m选项来为文件系统本身保留空间。还可以在LVM卷组中留下一些空闲空间。或者,更简单的方法,仅仅创建一个巨大的空文件,在文件系统快满时,直接将其删除。(2)
  • 养成习惯,评估和管理系统的改变、状态以及性能信息。

我们发现对系统变更管理的缺失是所有导致宕机的事件中最普遍的原因。典型的错误包括粗心的升级导致升级失败并遭遇一些Bug,或是尚未测试就将Schema或查询语句的更改直接运行到线上,或者没有为一些失败的情况制定计划,例如达到了磁盘容量限制。另外一个导致问题的主要原因是缺少严格的评估,例如因为疏忽没有确认备份是否是可以恢复的。最后,可能没有正确地监控MySQL的相关信息。例如缓存命中率报警并不能说明出现问题,并且可能产生大量的误报,这会使监控系统被认为不太有用,于是一些人就会忽略报警。有时候监控系统失效了,甚至没人会注意到,直至你的老板质问你,“为什么Nagios没有告诉我们磁盘已经满了”。

12.3.2 降低平均恢复时间(MTTR) #

之前提到,可以通过减少恢复时间来获得高可用性。事实上,一些人走得更远,只专注于减少恢复时间的某个方面:通过在系统中建立冗余来避免系统完全失效,并避免单点失效问题。

在降低恢复时间上进行投资非常重要,一个能够提供冗余和故障转移能力的系统架构,则是降低恢复时间的关键环节。但实现高可用性不单单是一个技术问题,还有许多个人和组织的因素。组织和个人在避免宕机和从宕机事件中恢复的成熟度和能力层次各不相同。

团队成员是最重要的高可用性资产,所以为恢复制定一个好的流程非常重要。拥有熟练技能、应变能力、训练有素的雇员,以及处理紧急事件的详细文档和经过仔细测试的流程,对从宕机中恢复有巨大的作用。但也不能完全依赖工具和系统,因为它们并不能理解实际情况的细微差别,有时候它们的行为在一般情况下是正确的,但在某些场景下却会是个灾难!

对宕机事件进行评估有助于提升组织学习能力,可以帮助避免未来发生相似的错误,但是不要对“事后反思”或“事后的调查分析”期待太高。后见之明被严重曲解,并且一味想找到导致问题的唯一根源,这可能会影响你的判断力(3)。许多流行的方法,例如“五个为什么”,可能会被过度使用,导致一些人将他们的精力集中在找到唯一的替罪羊。很难去回顾我们解决的问题当时所处的状况,也很难理解真正的原因,因为原因通常是多方面的。因此,尽管事后反思可能是有用的,但也应该对结论有所保留。即使是我们给出的建议,也是基于长期研究导致宕机事件的原因以及如何预防它们所得,并且只是我们的观点而已。

这里我们要反复提醒:所有的宕机事件都是由多方面的失效联合在一起导致的。因此,可以通过利用合适的方法确保单点的安全来避免。整个链条必须要打断,而不仅仅是单个环节。例如,那些向我们求助恢复数据的人不仅遭受数据丢失(存储失效,DBA误操作等),同时还缺少一个可用的备份。

...