文章目录
- [8.1 MySQL 配置的工作原理](#8.1 MySQL 配置的工作原理)
-
- [8.1.1 语法、作用域和动态性](#8.1.1 语法、作用域和动态性)
- [8.1.2 设置变量的副作用](#8.1.2 设置变量的副作用)
- [8.1.3 入门](#8.1.3 入门)
- [8.1.4 通过基准测试迭代优化](#8.1.4 通过基准测试迭代优化)
- [8.2 什么不该做](#8.2 什么不该做)
- [8.3 创建MySQL配置文件](#8.3 创建MySQL配置文件)
-
- [8.3.1 检查 MySQL 服务器状态变量](#8.3.1 检查 MySQL 服务器状态变量)
- [8.4 配置内存使用](#8.4 配置内存使用)
-
- [8.4.1 MySQL 可以使用多少内存](#8.4.1 MySQL 可以使用多少内存)
- [8.4.2 每个连接需要的内存](#8.4.2 每个连接需要的内存)
- [8.4.3 为操作系统保留内存](#8.4.3 为操作系统保留内存)
- [8.4.4 为缓存分配内存](#8.4.4 为缓存分配内存)
- [8.4.5 InnoDB 缓冲池(Buffer Pool)](#8.4.5 InnoDB 缓冲池(Buffer Pool))
- [8.4.6 MyISAM 键缓存(Key Caches)](#8.4.6 MyISAM 键缓存(Key Caches))
-
- [MySQL 键缓存块大小(Key Block Size)](#MySQL 键缓存块大小(Key Block Size))
- [8.4.7 线程缓存](#8.4.7 线程缓存)
- [8.4.8 表缓存(Table Cache)](#8.4.8 表缓存(Table Cache))
- [8.4.9 InnoDB 数据字典(Data Dictionary)](#8.4.9 InnoDB 数据字典(Data Dictionary))
- [8.5 配置 MySQL 的 I/O 行为](#8.5 配置 MySQL 的 I/O 行为)
-
- [8.5.1 InnoDB I/O 配置](#8.5.1 InnoDB I/O 配置)
- [8.5.2 MyISAM的I/O配置(略)](#8.5.2 MyISAM的I/O配置(略))
- [8.6 配置 MySQL 并发](#8.6 配置 MySQL 并发)
-
- [8.6.1 InnoDB 并发配置](#8.6.1 InnoDB 并发配置)
- 8.6.2 MyISAM并发配置(略)
- [8.7 基于工作负载的配置](#8.7 基于工作负载的配置)
-
- [8. 7.1 优化 BLOB 和 TEXT 的场景](#8. 7.1 优化 BLOB 和 TEXT 的场景)
- [8.7.2 优化排序(Filesorts)](#8.7.2 优化排序(Filesorts))
- [8.8 完成基本配置](#8.8 完成基本配置)
- [8.9 安全和稳定的设置](#8.9 安全和稳定的设置)
- [8.10 高级InnoDB设置](#8.10 高级InnoDB设置)
- [8.11 总结](#8.11 总结)
在这一章,我们将解释为MySQL服务器创建一个靠谱的配置文件的过程。创建一个好配置的最快方法最好是从理解MySQL内核和行为开始 ,而不是从学习配置项开始,也不是从问哪个配置项应该怎么设置或者怎么修改开始,更不是从检查服务器行为和询问哪个配置项可以提升性能开始。然后可以利用这些知识来指导配置MySQL。最后,可以将想要的配置和当前配置进行比较,然后纠正重要并且有价值的不同之处。
人们经常问,"我的服务器有32GB内存,12核CPU,怎样配置最好?"很遗憾,问题没这么简单。服务器的配置应该符合它的工作负载、数据,以及应用需求,并不仅仅看硬件的情况。
一个最简便的节省时间和避免麻烦的好办法是使用默认配置,除非是明确地知道默认值会有问题 。很多人都是在默认配置下运行的,这种情况非常普遍。这使得默认配置是经过最多实际测试的。对配置项做一些不必要的修改可能会遇到一些意料之外的 bug。但使用默认配置的服务器无法满足性能优化以及对特定工作负载、服务器资源充分利用的需求。
MySQL 有大量可以修改的参数------但不应该随便去修改。通常只需要把基本的项配置正确 (大部分情况下只有很少一些参数是真正重要的),应该将更多的时间花在 schema 的优化、索引,以及查询设计上。在正确地配置了 MySQL 的基本配置项之后,再花力气去修改其他配置项的收益通常就比较小了。
从另一方面来说,没用的配置导致潜在风险的可能更大。我们碰到过不止一个"高度调优"过的服务器不停地崩溃,停止服务或者运行缓慢,结果都是因为错误的配置导致的。
那么什么是该做的呢?确保基本的配置是正确的,例如 InnoDB 的 Buffer Pool 和日志文件缓存大小,如果想防止出问题(提醒一下,这样做通常不能提升性能------它们只能避免问题),就设置一个比较安全和稳健的值,剩下的配置就不用管了。如果碰到了问题,可以使用第 3 章提到的技巧小心地进行诊断。如果问题是由于服务器的某部分导致的,而这恰好可以通过某个配置项解决,那么需要做的就是更改配置。
有时候,在某些特定的场景下,也有可能设置某些特殊的配置项会有显著的性能提升。但无论如何,这些特殊的配置项不应该成为服务器基本配置文件的一部分。只有当发现特定的性能问题才应该设置它们。这就是为什么我们不建议通过寻找有问题的地方修改配置项的原因。如果有些地方确实需要提升,也需要在查询响应时间上有所体现。最好是从查询语句和响应时间入手来开始分析问题,而不是通过配置项。这可以节省大量的时间,避免很多的问题。
8.1 MySQL 配置的工作原理
首先应该知道的是MySQL从哪里获得配置信息:命令行参数和配置文件 。在类 UNIX 系统中,配置文件的位置一般在 /etc/my.cnf
或者 /etc/mysql/my.cnf
。如果手动启动MySQL,例如在测试安装时,也可以在命令行指定设置。实际上,服务器会读取配置文件的内容,删除所有注释和换行,然后和命令行选项一起处理。
更多有关 MySQL 命令行选项和配置文件的信息,请参阅我的这篇文章 【MySQL】详解 MySQL 程序的选项配置 和专栏 第 4 章 MySQL 程序 中 4.2 节。
一定要清楚地知道服务器配置文件的位置!不同操作系统中的配置文件位置会有所差异。如果不知道当前使用的配置文件路径,可以尝试下面的操作:
bash
$ 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 程序通常读取跟它同名的分段部分,许多客户端程序还会读取 client
部分,这是一个存放公用设置的地方。服务器通常读取 mysqld
这一段。一定要确认配置项放在了文件正确的分段中,否则配置是不会生效的。
8.1.1 语法、作用域和动态性
配置项设置都使用小写,单词之间用下画线或短划线隔开。在选项名称中,短划线(-)和下划线(_)在大多数情况下可以互换使用,尽管前导短划线不能以下划线给出。例如,--skip-grant
表和--skip_grant_tables
是等效的。一般我们在选项名称中使用破折号,除非下划线别有含义。例如,--log-bin
(点击跳转至详情)和--log_bin
(点击跳转至详情)是不同的选项。
我们建议使用一种固定的风格。这样在配置文件中搜索配置项时会容易得多。
配置项可以有多个作用域。有些设置是服务器级的(全局作用域),有些对每个连接是不同的(会话作用域),剩下的一些是对象级的。许多会话级变量跟全局变量相等,可以认为是默认值。如果改变会话级变量,它只影响改动的当前连接,当连接关闭时所有参数变更都会失效。下面有一些例子,你应该清楚这些不同类型的行为:
query_cache_size
变量是全局的。sort_buffer_size
变量默认是全局相同的,但是每个线程里也可以设置。- `join_buffer_size``变量也有全局默认值且每个线程是可以设置的,但是若一个查询中关联多张表,可以为每个关联分配一个关联缓冲(join buffer),所以每个查询可能有多个关联缓冲。
另外,除了在配置文件中设置变量,有很多变量(但不是所有)也可以在服务器运行时修改。MySQL 把这些归为动态配置变量。下面的语句展示了动态改变 sort_buffer_size 的会话值和全局值的不同方式:
SET sort_buffer_size = <value>;
SET GLOBAL sort_buffer_size = <value>;
SET @@sort_buffer_size := <value>;
SET @@session.sort_buffer_size := <value>;
SET @@global.sort_buffer_size := <value>;
如果动态地设置变量,要注意 MySQL 关闭时可能丢失这些设置。如果想保持这些设置,还是需要修改配置文件。
MySQL 新版本可以使用
SET PERSIST
语句来将变量持久化到 MySQL 数据目录中的mysqld-auto.cnf
JSON 格式文件。
如果在服务器运行时修改了变量的全局值,这个值对当前会话和其他任何已经存在的会话都不起效果,这是因为会话的变量值是在连接创建时从全局值初始化来的。在每次变更之后,应该检查 SHOW GLOBAL VARIABLES
的输出,确认已经按照期望变更了。
有些变量使用了不同的单位,所以必须知道每个变量的正确单位 。例如,table_cache
变量指定了表可以被缓存的数量,而不是表可以被缓存的字节数。key_buffer_size
则是以字节为单位,还有一些其他变量指定的是页的数量或者其他单位,例如百分比。
许多变量可以通过后缀指定单位 ,例如 1M 表示一百万字节。然而,这只能在配置文件或者作为命令行参数时有效 。当使用 SQL 的 SET
命令时,必须使用数字值 1048576 , 或者 1024 1024* 这样的表达式。但在配置文件中不能使用表达式。
有个特殊的值可以通过 SET
命令赋值给变量: DEFAULT
。 把这个值赋给会话级变量可以把变量改为使用全局值,把它赋值给全局变量可以设置这个变量为编译内置的默认值(不是在配置文件中指定的值)。当需要重置会话级变量的值回到连接刚打开的时候,这是很有用的。建议不要对全局变量这么用,因为可能它做的事不是你希望的,它不会把值设置到服务器刚启动时候的那个状态。
8.1.2 设置变量的副作用
动态设置变量可能导致意外的副作用,例如从缓冲中刷新脏块。务必小心那些可以在线更改的设置,因为它们可能导致数据库做大量的工作。
有时可以通过名称推断一个变量的作用。例如,max_heap_table_size
的作用就像听起来那样:它指定隐式内存临时表最大允许的大小。然而,命名约定并不完全一样,所以不能总是通过看名称来猜测一个变量有什么效果。
让我们来看一些常用的变量和动态修改它们的效果。
-
key_buffer_size
设置这个变量可以一次性为键缓冲区( key buffer, 也叫键缓存 key cache) 分配所有指定的空间。然而,操作系统不会真的立刻分配内存,而是到使用时才真正分配。例如设置键缓冲的大小为 1GB, 并不意味着服务器立刻分配 1GB 的内存。(我们下一章会讨论如何查看服务器的内存使用。)
MySQL 允许创建多个键缓存,这一章后面我们会探讨这个问题。如果把非默认键缓存的这个变量设置为 0,MySQL 将丢弃缓存在该键缓存中的索引,转而使用默认键缓存,并且当不再有任何引用时会删除该键缓存。为一个不存在的键缓存设置这个变量,将会创建新的键缓存。对一个已经存在的键缓存设置非零值,会导致刷新该键缓存的内容。这会阻塞所有尝试访问该键缓存的操作,直到刷新操作完成。
-
table_cache_size
设置这个变量不会立即生效------会延迟到下次有线程打开表才有效果。当有线程打开表时,MySQL 会检查这个变量的值。如果值大于缓存中的表的数量,线程可以把最新打开的表放入缓存;如果值比缓存中的表数小,MySQL 将从缓存中删除不常使用的表。
-
thread_cache_size
设置这个变量不会立即生效------将在下次有连接被关闭时产生效果。当有连接被关闭时,MySQL 检查缓存中是否还有空间来缓存线程。如果有空间,则缓存该线程以备下次连接重用;如果没有空间,它将销毁该线程而不再缓存。在这个场景中,缓存中的线程数,以及线程缓存使用的内存,并不会立刻减少;只有在新的连接删除缓存中的一个线程并使用后才会减少。( MySQL 只在关闭连接时才在缓存中增加线程,只在创建新连接时才从缓存中删除线程。)
-
query_cache_size
MySQL 在启动的时候,一次性分配并且初始化这块内存。如果修改这个变量(即使设置为与当前一样的值),MySQL 会立刻删除所有缓存的查询,重新分配这片缓存到指定大小,并且重新初始化内存。这可能花费较长的时间,在完成初始化之前服务器都无法提供服务,因为 MySQL 是逐个清理缓存的查询,不是一次性全部删掉。
-
read_buffer_size
MySQL 只会在有查询需要使用时才会为该缓存分配内存,并且会一次性分配该参数指定大小的全部内存。
-
read_rnd_buffer_size
MySQL 只会在有查询需要使用时才会为该缓存分配内存,并且只会分配需要的内存大小而不是全部指定的大小。(
max_read_rnd_buffer_size
这个名字更能表达这个变量实际的含义。) -
sort_buffer_size
MySQL 只会在有查询需要做排序操作时才会为该缓存分配内存。然后,一旦需要排序,MySQL 就会立刻分配该参数指定大小的全部内存,而不管该排序是否需要这么大的内存。
对于连接级别的设置,不要轻易地在全局级别增加它们的值,除非确认这样做是对的。有一些缓存会一次性分配指定大小的全部内存,而不管实际上是否需要这么大,所以一个很大的全局设置可能导致浪费大量内存。更好的办法是,当查询需要时在连接级别单独调大这些值。
最常见的例子是sort_buffer_size
,该参数控制排序操作的缓存大小,应该在配置文件里把它配置得小一些,然后在某些查询需要排序时,再在连接中把它调大。在分配内存后, MySQL会执行一些初始化的工作。
另外,即使是非常小的排序操作,排序缓存也会分配全部大小的内存,所以如果把参数设置得超过平均排序需求太多,将会浪费很多内存,增加额外的内存分配开销。许多读者认为内存分配是一个很简单的操作,听到内存分配的代价可能会很吃惊。不需要深入很多技术细节就可以讲清楚为什么内存分配也是昂贵的操作,内存分配包括了地址空间的分配,这相对来说是比较昂贵的。特别在Linux上,内存分配根据大小使用多种开销不同的策略。
总的来说,设置很大的排序缓存代价可能非常高,所以除非确定必须要这么大,否则不要增加排序缓存的大小。
如果查询必须使用一个更大的排序缓存才能比较好地执行,可以在查询执行前增加sort_buffer_size
的值,执行完成后恢复为DEFAULT
。
下面是一个实际的例子:
SET @@session.sort_buffer_size := >value>;
-- Execute the query...
SET @@session.sort_buffer_size := DEFAULT;
可以将类似的代码封装在函数中以方便使用。其他可以设置的单个连接级别的变量有read_buffer_size
、read_rnd_buffer_size
、tmp_table_size
、以及myisam_sort_buffer_size
(在修复表的操作中会用到)。
如果有需要也可以保存并还原原来的自定义值,可以像下面这样做:
SET @saved_<unique_variable_name> := @@session.sort_buffer_size;
SET @@session.sort_buffer_size := <value>;
-- Execute the query...
SET @@session.sort_buffer_size := @saved_<unique_variable_name>;
排序缓冲大小是关注的众多"调优"中一个设置。一些人似乎认为越大越好,我们甚至见过把这个变量设为 1GB 的。这可能导致服务器尝试分配太多内存而崩溃,或者为查询初始化排序缓存时消耗大量的 CPU,这不是什么出乎意料的事。从 MySQL 的 Bug 37359 可以看到有关于这个问题的细节。
不要把排序缓存大小放在太重要的位置。查询真的需要 128MB 的内存来排序 10 行数据然后返回给客户端吗?思考一下查询语句是什么类型的排序、多大的排序,首先考虑通过索引和 SQL 写法来避免排序(看第 5 章和第 6 章),这比调优排序缓存要快得多。并且应该仔细分析查询开销,看看排序是否是无论如何都需要重点关注的部分。第 3 章有一个例子,一个查询执行了一个排序,但是没有花很多排序时间。
8.1.3 入门
设置变量时请小心,并不是值越大就越好,而且如果设置的值太高,可能更容易导致问题:可能会由于内存不足导致服务器内存交换,或者超过地址空间。
我们见过的一个常见的错误是,配置一台新服务器的内存是另一台已经存在的服务器的两倍,并且------使用旧服务器的配置作为基线------创建一份新的配置,只是简单地在旧服务器的配置上乘以 2 。这不起作用。
如果你经常做笔记,在配置文件中写好注释,可能会节省自己(和同事)大量的工作。一个更好的主意是把配置文件置于版本控制之下。无论如何,这是一个很好的做法,因为它让你有机会撤销变更。要降低管理很多配置文件的复杂性,简单地创建一个从配置文件到中央版本控制库的符号链接。
在开始改变配置之前,应该优化查询和 schema, 至少先做明显要做的事情,例如添加索引 。如果先深入调整配置,然后修改了查询语句和 schema, 也许需要回头再次评估配置。请记住,除非硬件、工作负载和数据是完全静态的,否则都可能需要重新检查配置文件。实际上,大部分人的服务器甚至在一天中都没有稳定的工作负载------意味着对上午来说"完美"的配置,下午就不对了!显然,追求传说中的"完美"配置是完全不切实际的。因此,没有必要榨干服务器的每一点性能,实际上,这种调优的时间投入产出是非常小的。我们建议在"足够好"的时候就可以停下了,除非有理由相信停下会导致放弃重大的性能提升的机会。
8.1.4 通过基准测试迭代优化
有的时候我们运行某些组合的基准测试,来仔细验证或压测服务器的某些特定部分,使得我们可以更好地理解这些行为。一个很好的例子是,我们使用了很多年的一些基准测试,用来理解InnoDB的刷新行为,来寻找更好的刷新算法,以适应多种工作负载和多种硬件类型。我们经常测试各种各样的设置,来理解它们的影响以及怎么优化它们。但这不是一件简单的事------这可能会花费很多天甚至很多个星期------而且对大部分人来说这没有收益,因为服务器特定部分的认识局限往往会掩盖了其他问题。例如,有时我们发现,特定的设置项组合,在特定的边缘场景可能有更好的性能,但是在实际生产环境这些配置项并不真的合适,例如,浪费大量的内存,或者优化了吞吐量却忽略了崩溃恢复的影响。
如果必须这样做,我们建议在开始配置服务器之前,开发一个定制的基准测试包。你必须做这些事情来包含所有可能的工作负载,甚至包含一些边缘的场景,例如很庞大很复杂的查询语句。在实际的数据上重放工作负载通常是一个好办法。如果已经定位到了一个特定的问题点------例如一个查询语句运行很慢------也可以尝试专门优化这个点,但是可能不知道这会对其他查询有什么负面影响。
最好的办法是一次改变一个或两个变量,每次一点点,每次更改后运行基准测试,确保运行足够长的时间来确认性能是否稳定 。有时结果可能会令你感到惊讶,可能把一个变量调大了一点,观察到性能提升,然后再调大一点,却发现性能大幅下降。如果变更后性能有隐患,可能是某些资源用得太多了,例如,为缓冲区分配太多内存、频繁地申请和释放内存。另外,可能导致 MySQL 和操作系统或硬件之间的不匹配 。例如,我们发现 sort_buffer_size
的最佳值可能会被 CPU 缓存的工作方式影响,还有 read_buffer_size
需要服务器的预读和 I/O 子系统的配置相匹配。更大并不总是更好,还可能更糟糕。一些变量也依赖于一些其他的东西,这需要通过经验和对系统架构的理解来学习。
什么情况下进行基准测试是好的建议
对于前面提到不建议大多数人执行基准测试的情况也有例外的时候。我们有时会建议人们跑一些迭代基准测试,尽管通常跟"服务器调优"有不同的内容。这里有一些例子:
- 如果有一笔大的投资,如购买大量新的服务器,可以运行一下基准测试以了解硬件需求。(这里的上下文是指容量规划,不是服务器调优),我们特别喜欢对不同大小的 InnoDB 缓冲池进行基准测试,这有助于我们制定一个"内存曲线",以展示真正需要多少内存,不同的内存容量如何影响存储系统的要求。
- 如果想了解 InnoDB 从崩溃中恢复需要多久时间,可以反复设置一个备库,故意让它崩溃,然后"测试" InnoDB 在重启中需要花费多久时间来做恢复。这里的背景是做高可用性的规划。
- 以读为主的应用程序,在慢查询日志中捕捉所有的查询(或者用 pt-query-digest 分析 TCP 流量)是个很好的主意,在服务器完全打开慢查询日志记录时,使用 pt-log-player 重放所有的慢查询,然后用 pt-query-digest 来分析输出报告。这可以观察在不同硬件、软件和服务器设置下,查询语句运行的情况。例如,我们曾经帮助客户评估迁移到更多的内存但硬盘更慢的服务器上的性能变化。大多数查询变得更快,但一些分析型查询语句变慢,因为它们是 I/O 密集型的。这个测试的上下文背景就是不同工作负载的比较。
8.2 什么不该做
首先,不要根据一些"比率"来调优。一个经典的按"比率"调优的经验法则是,键缓存的命中率应该高于某个百分比,如果命中率过低,则应该增加缓存的大小。这是非常错误的意见 。无论别人怎么跟你说,缓存命中率跟缓存是否过大或过小没有关系。首先,命中率取决于工作负载 ------某些工作负载就是无法缓存的,不管缓存有多大------其次,缓存命中没有什么意义,我们将在后面解释原因。有时当缓存太小时,命中率比较低,增加缓存的大小确实可以提高命中率。然而,这只是个偶然情况,并不表示这与性能或适当的缓存大小有任何关系。
这种相关性,有时候看起来似乎真正的问题是,人们开始相信它们将永远是真的。Oracle DBA 很多年前就放弃了基于命中率的调优,我们希望 MySQL DBA 也能跟着走(1) 。我们更强烈地希望人们不要去写"调优脚本",把这些危险的做法编写到一起,并教导成千上万的人这么做。这引出了我们第二个不该做的建议:不要使用调优脚本! 有几个这样的可以在互联网上找到的脚本非常受欢迎,最好是忽略它们(2)。
注 1 :
如果你还是不相信"按比率调优 "的方法是错误的,请阅读 Cary Millsap 的 Optimizing Oracle Performance(O'Reilly出版)。他甚至为这个主题专门写了一个附录,提供了一个可以智能地产生任何你想要的命中率的工具,甚至不管系统正运行得多么糟糕都可以做到很好的命中率!当然,这一切的目的都是为了说明比率是多么无用。
注 2 :一个例外:我们维护了一个(好用的)免费的在线配置工具,在 http://tools.percona.com 。是的,我们确实有倾向性。
我们还建议避免**调优(tuning)**这个词,我们在前面几段中使用这个词是有点随意的。我们更喜欢使用"配置(Configuration) "或"优化(Optimize)"来代替(只要这是你真正在做的,见第 3 章)。"调优"这个词,容易让人联想到一个缺乏纪律的新手对服务器进行微调,并观察发生了什么。我们建议上一节的练习最好留给那些正在研究服务器内核的人。"调优"服务器可能浪费大量的时间。
最后,不要相信很流行的内存消耗公式------是的,就是MySQL崩溃时自身输出的那个内存消耗公式(我们这里就不再重复了)。这个公式已经很古老了,它并不可靠,甚至也不是一个理解MySQL在最差情况下需要使用多少内存的有用的办法。在互联网上可能还会看到这个公式的很多变种。即使在原公式上增加了更多原来没有考虑到的因素,还是有同样的缺陷。事实上不可能非常准确地把握MySQL内存消耗的上限。MySQL不是一个完全严格控制内存分配的数据库服务器。这个结论可以非常简单地证明,登录到服务器,并执行一些大量消耗内存的查询:
sql
mysql> SET @crash_me_1 := REPEAT('a', @@max_allowed_packet);
mysql> SET @crash_me_2 := REPEAT('a', @@max_allowed_packet);
# ... run a lot of these ...
mysql> SET @crash_me_1000000 := REPEAT('a', @@max_allowed_packet);
在一个循环中运行这些语句,每次都创建新的变量,最后服务器内存必然耗尽,然后系统崩溃!运行这个测试不需要任何特殊权限。
8.3 创建MySQL配置文件
正如我们在本章开头提到的,没有一个适合所有场景的"最佳配置文件",比方说,对一台有 16 GB 内存和 12 块硬盘的 4 路 CPU 服务器,不会有一个相应的"最佳配置文件"。应该开发自己的配置,因为即使是一个好的起点,也依赖于具体是如何使用服务器的。
不要使用这些 MySQL 发行版本中的示例配置文件作为(创建配置文件的)起点,也不要使用操作系统的安装包自带的配置文件。最好是从头开始。
这就是本章要做的事情。实际上MySQL的可配置性太强也可以说是个弱点,看起来好像需要花很多时间在配置上,其实大多数配置的默认值已经是最佳配置了,所以最好不要改动太多配置,甚至可以忘记某些配置的存在。这就是为什么我们为本书创建了一个完整的最小的示例配置文件,可以作为自己的服务器配置文件的一个好的起点。有一些配置项是必选的,我们将在本章稍后解释。下面就是这个基础配置文件:
[mysqld]
# GENERAL
datadir = /var/lib/mysql
socket = /var/lib/mysql/mysql.sock
pid_file = /var/lib/mysql/mysql.pid
user = mysql
port = 3306
storage_engine = InnoDBdefault_storage_engine
# INNODB
innodb_buffer_pool_size = <value>
innodb_log_file_size = <value>
innodb_file_per_table = 1
innodb_flush_method = O_DIRECT
# MyISAM
key_buffer_size = <value>
# LOGGING
log_error = /var/lib/mysql/mysql-error.log
log_slow_queries = /var/lib/mysql/mysql-slow.logslow_query_log
# OTHER
tmp_table_size = 32M
max_heap_table_size = 32M
query_cache_type = 0
query_cache_size = 0
max_connections = <value>
thread_cache_size = <value>thread_cache
table_cache_size = <value>table_cache
open_files_limit = 65535
[client]
socket = /var/lib/mysql/mysql.sock
port = 3306
和你见过的其他配置文件(3)相比,这里的配置选项可能太少了。但实际上已经超过了许多人的需要。有一些其他类型的配置选项可能也会用到,比如二进制日志,我们会在本章后面以及其他章节覆盖这些内容。
注 3:
问:为排序缓存(Sort Buffer)和读缓存(Read Buffer)设置大小的选项在哪?
答:它们已经很专注自己的事情了,除非觉得默认值不够好,否则保留默认值就可以了。
配置文件的第一件事是设置数据的位置 。我们选择了/var/lib/mysql
路径存储数据,因为在许多类 UNIX 系统中这是最常见的位置。选择另外的位置也没有错,可以根据需要决定。我们把 PID 文件也放到相同的位置,但许多操作系统希望放在/var/run
目录下,这也可以。只需要简单地为这些选项配置一下就可以了。顺便说一下,不要把 Socket 文件和 PID 文件放到 MySQL 编译默认的位置,在不同的 MySQL 版本里这可能会导致一些错误。最好明确地设置这些文件的位置 。(这么说并不是建议选择不同的位置,只是建议确保在my.cnf
文件中明确指定了这些文件的存放地点,这样升级 MySQL 版本时这些路径就不会改变。)
这里还指定了操作系统必须用mysql
用户来运行mysqld
进程。需要确保这个账户存在,并且拥有操作数据目录的权限。端口设置为默认的3306
,但有时可能需要修改一下。
我们选择 InnoDB 作为默认的存储引擎,这个值得向大家解释一下。InnoDB 在大多数情况下是最好的选择,但并不总是如此。一般情况下,如果决定使用一个存储引擎作为默认引擎,最好显式地进行配置。许多用户认为只使用了某个特定的存储引擎,但后来发现正在用的其实是另一个引擎,就是因为默认配置的是另外一个引擎。
接下来我们将阐述 InnoDB 的基础配置 。InnoDB 在大多数情况下如果要运行得很好,配置大小合适的缓冲池(Buffer Pool)和日志文件(Log File)是必须的。默认值都太小了。其他所有的 InnoDB 设置都是可选的,尽管示例配置中因为可管理性和灵活性的原因启用了innodb_file_per_table
。设置 InnoDB 日志文件的大小和innodb_flush_method
是本章后面要讨论的主题,其中innodb_flush_method
是类 UNIX 系统特有的选项。
有一个流行的经验法则说,应该把缓冲池大小设置为服务器内存的约75%~80%。这是另一个偶然有效的"比率",但并不总是正确的。有一个更好的办法来设置缓冲池大小,大致如下:
- 从服务器内存总量开始。
- 减去操作系统的内存占用,如果MySQL不是唯一运行在这个服务器上的程序,还要扣掉其他程序可能占用的内存。
- 减去一些 MySQL 自身需要的内存,例如为每个查询操作分配的一些缓冲。
- 减去足够让操作系统缓存 InnoDB 日志文件的内存,至少是足够缓存最近经常访问的部分。 (此建议适用于标准的 MySQL,Percona Server 可以配置日志文件用
O_DIRECT
方式打开,绕过操作系统缓存,)留一些内存至少可以缓存二进制日志的最后一部分也是个很好的选择,尤其是如果复制产生了延迟,备库就可能读取主库上旧的二进制日志文件,给主库的内存造成一些压力。 - 减去其他配置的 MySQL 缓冲和缓存需要的内存,例如 MyISAM 的键缓存(Key Cache),或者查询缓存(Query Cache)。
- 除以 105%,这差不多接近 InnoDB 管理缓冲池增加的自身管理开销。
- 把结果四舍五入,向下取一个合理的数值。向下舍入不会太影响结果,但是如果分配太多可能就会是件很糟糕的事情。
注 4:
MySQL 8.x 版本使用 InnoDB 作为默认存储引擎和元数据库引擎,所以在使用该版本时以上建议可能需要有所调整。
下面是一个例子,假设有一个 192GB 内存的服务器,只运行 MySQL 并且只使用 InnoDB, 没有查询缓存(Query Cache), 也没有非常多的连接连到服务器。如果日志文件总大小是 4 GB, 可能会像这样处理:"我认为所有内存的 5% 或者 2GB, 取较大的那个,应该足够操作系统和 MySQL 的其他内存需求,为日志文件减去 4 GB, 剩下的都给 InnoDB 用"。结果差不多是 177 GB, 但是配置得稍微低一点可能是个好主意。比如可以先配置缓存池为 168GB。 在服务器实际运行中若发现还有不少内存没有分配使用,在出于某些目的有机会重启时,可以再适当调大缓冲池的大小。
如果有大量 MyISAM 表需要缓存它们的索引,结果自然会有很大不同。在 Windows 下这也是完全不同的,大多数的 MySQL 版本在 Windows 下使用大内存都有问题(虽然在 MySQL 5.5 中有所改进),或者是出于某种原因不使用 O_DIRECT 也会有不同的结果。
正如你所看到的,从一开始就获得精确的设置并不是关键。从一个比默认值大一点但不是大得很离谱的安全值开始是比较好的,在服务器运行一段时间后,可以看看服务器真实情况需要使用多少内存。这些东西是很难预测,因为 MySQL 的内存利用率并不总是可以预测的:它可能依赖很多的因素,例如查询的复杂性和并发性。如果是简单的工作负载,MySQL 的内存需求是非常小的------大约 256 KB的每个连接。但是,使用临时表、排序、存储过程等的复杂查询,可能使用更多的内存。
这就是我们选择一个非常安全的起点的原因。可以看到,即使是保守的 InnoDB 的缓冲池设置,实际上也是服务器内存的 87.5% ------ 超过 75%, 这就是为什么我们说简单地按比例是不正确的方法的原因。
我们建议,当配置内存缓冲区的时候,宁可谨慎,而不是把它们配置得过大。如果把缓冲池配置得比它可以设的值少了 20%, 很可能只会对性能产生小的影响,也许就只影响几个百分点。如果设置得大了 20%, 则可能会造成更严重的问题:内存交换、磁盘抖动,甚至内存耗尽和硬件死机。
这份 InnoDB 配置的例子说明了我们配置服务器的首选途径:了解它内部做了什么,以及参数之间如何相互影响,然后再决定。
随着时间的推移,硬件水平高速发展,精确地配置MySQL的内存缓冲区变得不那么重要。例如,在一个有 144 GB 内存的服务器上为 MySQL 保留 4 GB 的内存简直是九牛一毛。
示例配置文件中的其他一些设置,大多是不言自明的,其中很多配置都是是与否的判断。在本章的其余部分,我们将探讨其中的几个。可以看到,我们已经启用日志记录、禁用了查询缓存,等等。
这里需要解释的一个选项是open_files_limit
。在典型的 Linux 系统上我们把它设置得尽可能大。现代操作系统中打开文件句柄开销都很小。如果这个参数不够大,将会碰到经典的 24 号错误,"打开的文件太多(too many open files)"。
跳过其他的直接看到末尾,在配置文件的最后一节,是为了如 mysql 和 mysqladmin 之类的客户端程序做的设置,可以简化这些程序连接到服务器的步骤。应该为客户端程序设置那些匹配服务器的配置项。
8.3.1 检查 MySQL 服务器状态变量
有时可以使用SHOW GLOBAL STATUS
的输出,作为配置的输入,以更好地通过工作负载来自定义配置。为了达到最佳效果,既要看绝对值,又要看值是如何随时间而改变的,最好为高峰和非高峰时间的值做几个快照。可以使用以下命令每隔 60 秒来查看状态变量的增量变化:
bash
$ mysqladmin extended-status -ri60
在解释配置设置的时候,我们经常会提到随着时间的推移各种状态变量的变化。所以通常可以预料到需要分析如刚才那个命令的输出的情况。有一些有用的工具,如 Percona Toolkit 中的 pt-mext 或 pt-mysql-summary ,可以简洁地显示状态计数器的变化,不用直接看那些SHOW
命令的输出。
8.4 配置内存使用
配置 MySQL 正确地使用内存量对高性能是至关重要的。肯定要根据需求来定制内存使用。可以认为 MySQL 的内存消耗分为两类:可以控制的内存和不可以控制的内存。无法控制 MySQL 服务器运行、解析查询,以及其内部管理所消耗的内存,但是为特定目的而使用多少内存则有很多参数可以控制( 例如 Join Buffer/Sort Buffer 等)。 用好可以控制的内存并不难,但需要对配置的含义非常清楚。
像前面展示的,按下面的步骤来配置内存:
- 确定可以使用的内存上限。
- 确定每个连接 MySQL 需要使用多少内存,例如排序缓冲和临时表。
- 确定操作系统需要多少内存才够用。包括同一台机器上其他程序使用的内存,如定时任务。
- 把剩下的内存全部给 MySQL 的缓存,例如 InnoDB 的缓冲池,这样做很有意义。
8.4.1 MySQL 可以使用多少内存
在任何给定的操作系统上,MySQL 都有允许使用的内存上限。基本出发点是机器上安装了多少物理内存。如果服务器就没装这么多内存,MySQL 肯定也不能用这么多内存。
还需要考虑操作系统或架构的限制 ,如 32 位操作系统对一个给定的进程可以处理多少内存是有限制的。因为 MySQL 是单进程多线程的运行模式,它整体可用的内存量也许会受操作系统位数的严格限制------例如,32 位 Linux 内核通常限制任意进程可以使用的内存量在 2.5GB ~ 2.7GB 范围内。运行时地址空间溢出是非常危险的,可能导致 MySQL 崩溃。现在这种情况非常难得一见,但以前这种情况很常见。
有许多其他的操作系统------特殊的参数和古怪的事情必须考虑到,例如不只是每个进程有限制,而且堆栈大小和其他设置也有限制。系统的glibc
库也可能限制每次分配的内存大小 。例如,若glibc
库支持单次分配的最大大小是 2 GB,那么可能就无法设置innodb_buffer_pool
的值大于 2 GB。
即使在 64 位服务器上,依然有一些限制。例如,许多我们讨论的缓冲区,如键缓存( Key Buffer), 在 5.0 以及更早的 MySQL 版本上,有 4GB 的限制,即使在 64 位服务器上也是如此。在 MySQL 5.1 中,部分限制被取消了,在 MySQL 手册中记载了每个变量的最大值,有需要可以查阅。
8.4.2 每个连接需要的内存
MySQL 保持一个连接(线程)只需要少量的内存。它还要求一个基本量的内存来执行任何给定查询。你需要为高峰时期执行的大量查询预留好足够的内存。否则,查询执行可能因为缺乏内存而导致执行效率不佳或执行失败。
知道在高峰时期 MySQL 将消耗多少内存是非常有用的,但一些习惯性用法可能意外地消耗大量内存,这使得对内存使用量的预测变得比较困难 。绑定变量就是一个例子 ,因为可以一次打开很多绑定变量语句。另一个例子是 InnoDB 数据字典(关于这个后面我们再细说)。
当预测内存峰值消耗时,没必要假设一个最坏情况 。例如,配置 MySQL 允许最多 100 个连接,在理论上可能出现 100 个连接同时在运行很大的查询,但在现实情况中,这可能不会发生。例如,设置 myisam_sort_buffer_size
为 256 MB, 最差情况下至少需要使用 25 GB 内存,但这种最差情况在实际中几乎是不可能发生的。使用了许多大的临时表或复杂存储过程的查询,通常是导致高内存消耗最可能的原因。
相对于计算最坏情况下的开销,更好的办法是观察服务器在真实的工作压力下使用了多少内存,可以在进程的虚拟内存大小那里看到。在许多类 UNIX 系统里,可以观察top
命令中的VIRT
列,或者ps
命令中的VSZ
列的值。下一章有更多关于如何监视内存使用情况的信息。
8.4.3 为操作系统保留内存
跟查询一样,操作系统也需要保留足够的内存给它工作。如果没有虚拟内存正在交换(Paging)到磁盘,就是表明操作系统内存足够的最佳迹象。
至少应该为操作系统保留 1GB ~ 2GB 的内存------如果机器内存更多就再多预留一些。我们建议 2 GB 或总内存的 5% 作为基准,以较大者为准 。为了安全再额外增加一些预留,并且如果机器上还在运行内存密集型任务(如备份),则可以再多增加一些预留。不要为操作系统的缓存增加任何内存,因为它们可能会变得非常大。操作系统通常会利用所有剩下的内存来做文件系统缓存,我们认为,这应该从操作系统自身的需求里分离出来。
8.4.4 为缓存分配内存
如果服务器只运行 MySQL, 所有不需要为操作系统以及查询处理保留的内存都可以用作 MySQL 缓存。
相比其他,MySQL 需要为缓存分配更多的内存。它使用缓存来避免磁盘访问,磁盘访问比内存访问数据要慢得多。操作系统可能会缓存一些数据,这对 MySQL 有些好处(尤其是对 MyISAM), 但是 MySQL 自身也需要大量内存。
下面是我们认为对大部分情况来说最重要的缓存:
- InnoDB 缓冲池
- InnoDB 日志文件和 MyISAM 数据的操作系统缓存
- MyISAM 键缓存
- 查询缓存
- 无法手工配置的缓存,例如二进制日志和表定义文件的操作系统缓存
还有些其他缓存,但是它们通常不会使用太多内存。我们在前面的章节中讨论了查询缓存( Query Cache) 的细节,所以接下来的部分我们专注于 InnoDB 和 MyISAM 良好工作需要的缓存。
如果只使用单一存储引擎,配置服务器就简单多了。如果只使用 MyISAM 表,就可以完全关闭 InnoDB, 而如果只使用 InnoDB, 就只需要分配最少的资源给 MyISAM(MySQL 旧版本内部系统表采用 MyISAM,MySQL 8.x 就不需要了)。 但是如果正混合使用各种存储引擎,就很难在它们之间找到恰当的平衡。我们发现最好的办法是先做一个有根据的猜测,然后在运行中观察服务器(再进行调整)。
8.4.5 InnoDB 缓冲池(Buffer Pool)
如果大部分都是 InnoDB 表,InnoDB 缓冲池或许比其他任何东西更需要内存。InnoDB 缓冲池并不仅仅缓存索引:它还会缓存行的数据、自适应哈希索引、插入缓冲(Insert Buffer)、锁,以及其他内部数据结构 。可以使用通过SHOW
命令得到的变量或者例如innotop
这样的工具监控 InnoDB 缓冲池的内存利用情况。
如果数据量不大,并且不会快速增长,就没必要为缓冲池分配过多的内存。把缓冲池配置得比需要缓存的表和索引还要大很多实际上没有什么意义。当然,对一个迅速增长的数据库做超前的规划没有错,但有时我们也会看到一个巨大的缓冲池只缓存一点点数据,这就没有必要了。
很大的缓冲池预热和关闭都会花费很长的时间 。如果有很多脏页在缓冲池里,InnoDB 关闭时可能会花费较长的时间,因为在关闭之前需要把脏页写回数据文件。也可以强制快速关闭,但是重启时就必须多做更多的恢复工作,也就是说无法同时加速关闭和重启两个动作 。如果事先知道什么时候需要关闭 InnoDB, 可以在运行时修改 innodb_max_dirty_pages_pct
变量,将值改小,等待刷新线程清理缓冲池,然后在脏页数量较少时关闭。可以监控 the Innodb_buffer_pool_pages_dirty
状态变量或者使用 innotop
来监控 SHOW INNODB STATUS
来观察脏页的刷新量。
更小的 innodb_max_dirty_pages_pct
变量值并不保证 InnoDB 将在缓冲池中保持更少的脏页。它只是控制 InnoDB 是否可以"偷懒( Lazy)" 的阈值。InnoDB 默认通过一个后台线程来刷新脏页,并且会合并写入,更高效地顺序写出到磁盘。这个行为之所以被称为"偷懒( Lazy)", 是因为它使得 InnoDB 延迟了缓冲池中刷写脏页的操作,直到一些其他数据必须使用空间时才刷写 。当脏页的百分比超过了这个阈值,InnoDB 将快速地刷写脏页,尝试让脏页的数量更低。当事务日志没有足够的空间剩余时,InnoDB 也将进入"激烈刷写( Furious Flushing)" 模式,这就是大日志可以提升性能的一个原因。
当有一个很大的缓冲池,重启后服务器也许需要花很长的时间(几个小时甚至几天)来预热缓冲池,尤其是磁盘很慢的时候。在这种情况下,可以利用 Percona Server 的功能来重新载入缓冲池的页(这个功能是 Dump/Restore of the Buffer Pool),从而节省时间。这可以让预热时间减少到几分钟。MySQL 5.6 也提供了一个类似的功能。这个功能对复制尤其有好处,因为单线程复制导致备库需要额外的预热时间。
如果不能使用 Percona Server 的快速预热功能,也可以在重启后立刻进行全表扫描或者索引扫描,把索引载入缓冲池 。这是比较粗暴的方式,但是有时候比什么都不做还是要好。可以使用init_file
设置来实现这个功能。把 SQL 放到一个文件里,然后当 MySQL 启动的时候来执行。文件名必须在init_file
选项中指定,文件中可以包含多条 SQL 命令,每一条单独一行(不允许使用注释)。
8.4.6 MyISAM 键缓存(Key Caches)
MyISAM的键缓存也被称为键缓冲,默认只有一个键缓存,但也可以创建多个 。不像 InnoDB 和其他一些存储引擎,MyISAM自身只缓存索引,不缓存数据(依赖操作系统缓存数据)。如果大部分是 MyISAM 表,就应该为键缓存分配比较多的内存。
最重要的配置项是key_buffer_size
。任何没有分配给它的内存(当然还要排除各种操作系统自身占用的内存,还有 MySQL 自身占用的内存等)都可以被操作系统缓存利用。
查询INFORMATION_SCHEMA.TABLES
表的INDEX_LENGTH
字段,把它们的值相加,就可以得到索引存储占用的空间:
bash
SELECT SUM(INDEX_LENGTH) FROM INFORMATION_SCHEMA.TABLES WHERE ENGINE='MYISAM';
如果是类 UNIX 系统,也可以使用下面的命令:
bash
$ du -sch `find /path/to/mysql/data/directory/ -name"*.MYI"`
应该把键缓存设置得多大?不要超过索引的总大小,或者不超过为操作系统缓存保留总内存的25%~50%,以更小的为准。
默认情况下,MyISAM将所有索引都缓存在默认键缓存中,但也可以创建多个命名的键缓冲。这样就可以同时缓存超过4GB的内存。如果要创建名为key_buffer_1和key_buffer_2的键缓冲,每个大小为1GB,则可以在配置文件中添加如下配置项:
key_buffer_1.key_buffer_size = 1G
key_buffer_2.key_buffer_size = 1G
现在有了三个键缓冲:两个由这两行配置明确定义,还有一个是默认键缓冲。可以使用CACHE INDEX
命令来将表映射到对应的缓冲区 。使用下面的语句,让 MySQL 使用 key_buffer_1 缓冲区来缓存 t1 和 t2 表的索引:
sql
mysql> CACHE INDEX t1, t2 IN key_buffer_1;
现在当 MySQL 从这些表的索引读取块时,将会在指定的缓冲区内缓存这些块。也可以把表的索引预载入到缓存中,通过init_file
设置或者LOAD INDEX
命令:
sql
mysql> LOAD INDEDX INTO CACHE t1, t2;
任何没明确指定映射到哪个键缓冲区的索引,在 MySQL 第一次需要访问 .MYI
文件的时候,都会被分配到默认缓冲区。
可以通过SHOW STATUS
和SHOW VARIABLES
命令的信息来监控键缓冲的使用情况。下面的公式可以计算缓冲区的使用率:
100 - ( (Key_blocks_unused * key_cache_block_size) * 100 / key_buffer_size )
如果服务器运行了很长一段时间后,还是没有使用完所有的键缓冲,就可以把缓冲区调小一点。
从经验上来说,每秒缓存未命中的次数要更有用。假定有一个独立的磁盘,每秒可以做100个随机读。每秒5次缓存未命中可能不会导致I/O繁忙,但是每秒80次缓存未命中则可能出现问题。可以使用下面的公式来计算这个值:
Key_reads / Uptime
通过间隔 10~100 秒来计算这段时间内缓存未命中次数的增量值,可以获得当前性能的情况。下面的命令可以每 10 秒钟获取一次状态值的变化量:
bash
$ mysqladmin extended-status -r -i 10 | grey Key_reads
记住,MyISAM 使用操作系统缓存来缓存数据文件,通常数据文件比索引要大。因此,把更多的内存保留给操作系统缓存而不是键缓存是有意义的。即使你有足够的内存来缓存所有索引,并且键缓存未命中率很低,当 MySQL 尝试读取数据文件时(不是索引文件),在操作系统层还是可能发生缓存未命中,这对 MySQL 完全透明,MySQL 并不能感知到。因此,这种情况下可能会有大量数据文件缓存未命中,这和索引的键缓存未命中率是完全不相关的。
最后,即使没有任何 MyISAM 表,MySQL 8.0 以前版本依然需要将 key_buffer_size
设置为较小的值,例如 32M 。MySQL 服务器有时会在内部使用 MyISAM 表,例如 GROUP BY
语句可能会使用 MyISAM 做临时表。MySQL 8.x 使用 InnoDB 作为数据字典的事务型存储引擎。
MySQL 键缓存块大小(Key Block Size)
块大小也是很重要的(尤其是写密集型负载),因为它影响了 MyISAM、 操作系统缓存,以及文件系统之间的交互。如果缓存块太小了,可能会碰到写时读取( read-around write), 就是操作系统在执行写操作之前必须先从磁盘上读取一些数据。下面说明一下这种情况是怎么发生的,假设操作系统的页大小是 4KB( 在 x86 架构上通常都是这样),并且索引块大小是 1KB :
- MyISAM 请求从磁盘上读取 1KB 的块。
- 操作系统从磁盘上读取 4KB 的数据并缓存,然后发送需要的 1KB 数据给 MyISAM。
- 操作系统丢弃缓存数据以给其他数据腾出缓存。
- MyISAM 修改 1KB 的索引块,然后请求操作系统把它写回磁盘。
- 操作系统从磁盘读取同一个 4KB 的数据,写入操作系统缓存,修改 MyISAM 改动的这 1KB 数据,然后把整个 4KB 的块写回磁盘。
在第 5 步中,当 MyISAM 请求操作系统去写 4KB 页的部分内容时,就发生了写时读取( read-around write)。 如果 MyISAM 的块大小跟操作系统的相匹配,在第5步的磁盘读就可以避免(5)。
注 5:
理论上,如果能确认原生 4 KB的数据依然在操作系统缓存中,读操作就不需要了。然而,你没法控制操作系统把哪些块放到缓存中。通过
fncore
工具可以看到哪些块在缓存中,地址在:http://net.doit.wisc.edu/~plonka/fncore/ 。
很遗憾,MySQL 5.0 以及更早的版本没有办法配置索引块大小。但是,在 MySQL 5.1 以及更新版本中,可以设置 MyISAM 的索引块大小跟操作系统一样,以避免写时读取。myisam_block_size
变量控制着索引块大小。也可以指定每个索引的块大小,在 CREATE TABLE
或者 CREATE INDEX
语句中使用 KEY_BLOCK_SIZE
选项即可,但是因为同一个表的所有索引都保存在同一个文件中,因此该表所有索引的块大小都需要大于或者等于操作系统的块大小,才能避免由于边界对齐导致的写时读取。(例如,若同一个表的两个索引,一个块大小是 1KB, 另一个是 4KB。 那么 4KB 的索引块边界很可能和操作系统的页边界是不对齐的,这样还是会发生写时读取。)
8.4.7 线程缓存
线程缓存保存那些当前没有与连接关联但是准备为后面新的连接服务的线程。当一个新的连接创建时,如果缓存中有线程存在,MySQL 从缓存中删除一个线程,并且把它分配给这个新的连接。当连接关闭时,如果线程缓存还有空间的话,MySQL 又会把线程放回缓存。如果没有空间的话,MySQL 会销毁这个线程。只要 MySQL 在缓存里还有空闲的线程,它就可以迅速地响应连接请求,因为这样就不用为每个连接创建新的线程。类似于线程池。
thread_cache_size
变量指定了 MySQL 可以保持在缓存中的线程数 。一般不需要配置这个值,除非服务器会有很多连接请求。要检查线程缓存是否足够大,可以查看Threads_created
状态变量。如果我们观察到很少有每秒创建的新线程数少于 10 个的时候,通常应该尝试保持线程缓存足够大,但是实际上经常也可能看到每秒少于 1 个新线程的情况。
一个好的办法是观察 Threads_connected
变量并且尝试设置 thread_cache_size
足够大以便能处理业务压力正常的波动 。例如,若 Threads_connected
通常保持在 100 ~ 120, 则可以设置缓存大小为 20。 如果它保持在 500 ~ 700,200 的线程缓存应该足够大了。可以这样认为:在 700 个连接的时候,可能没有线程在缓存中;在 500 个连接的时候,有 200 个缓存的线程准备为负载再次增加到 700 个连接时使用。
把线程缓存设置得非常大在大部分时候是没有必要的,但是设置得很小也不能节省太多内存,所以也没什么好处。每个在线程缓存中的线程或者休眠状态的线程,通常使用 256KB 左右的内存 。相对于正在处理查询的线程来说,这个内存不算很大。通常应该保证线程缓存足够大,以避免 Threads_created
频繁增长 。如果这个数字很大(例如,几千个线程),可能需要把 thread_cache_size
设置得稍微小一些,因为一些操作系统不能很好地处理庞大的线程数,即使其中大部分是休眠的。
8.4.8 表缓存(Table Cache)
表缓存和线程缓存的概念是相似的,但存储的对象代表的是表。每个在缓存中的对象包含相关表 .frm
文件的解析结果,加上一些其他数据。准确地说,在对象里的其他数据的内容依赖于表的存储引擎。例如,对 MyISAM, 是表的数据和索引的文件描述符。对于 Merge 表则可能是多个文件描述符,因为 Merge 表可以有很多的底层表。
表缓存可以重用资源 。举个实际的例子,当一个查询请求访问一张 MyISAM 表, MySQL 也许可以从缓存的对象中获取到文件描述符。尽管这样做可以避免打开一个文件描述符的开销,但这个开销其实并不大。打开和关闭文件描述符在本地存储是很快的,服务器可以轻松地完成每秒 100 万次的操作(尽管这跟网络存储不同)。对 MyISAM 表来说,表缓存的真正好处是,可以让服务器避免修改 MyISAM 文件头来标记表"正在使用中"( 6)。
注 6:
"打开的表(Opened Table)"的概念,可能有点混乱。当不同的查询同时访问一张表,或者是一个单独的查询引用同一张表超过一次,比如子查询或者自关联,MySQL 都会对一张表作为打开状态多次计数。MyISAM 表的索引文件包含一个计数器,MyISAM 表打开时递增,关闭时递减。这使得对于 MyISAM 表可以看到是不是关闭干净了:如果首次打开一个表,计数器不为零,说明表没有关闭干净。
表缓存的设计是服务器和存储引擎之间分离不彻底的产物,属于历史问题 。表缓存对 InnoDB 重要性就小多了,因为 InnoDB 不依赖它来做那么多的事(例如持有文件描述符,InnoDB 有自己的表缓存版本)。尽管如此,InnoDB 也能从缓存解析的 .frm
文件中获益。
在 MySQL 5.1 版本中,表缓存分离成两部分:一个是打开表的缓存,一个是表定义缓存 (通过 table_open_cache
和 table_defnition_cache
变量来配置)。其结果是,表定义(解析 .frm
文件的结果)从其他资源中分离出来了,例如表描述符。打开的表依然是每个线程、每个表用的,但是表定义是全局的,可以被所有连接有效地共享。通常可以把 table_definition_cache
设置得足够高,以缓存所有的表定义。除非有上万张表,否则这可能是最简单的方法。
如果 Opened_tables
状态变量很大或者在增长,可能是因为表缓存不够大,那么可以人为增加 table_cache
系统变量(或者是 MySQL 5.1 中的 table_open_cache
)。 然而,当创建和删除临时表时,要注意这个计数器的增长,如果经常需要创建和删除临时表,那么该计数器就会不停地增长。
把表缓存设置得非常大的缺点是,当服务器有很多 MyISAM 表时,可能会导致关机时间较长,因为关机前索引块必须完成刷新,表都必须标记为不再打开。同样的原因,也可能使FLUSH TABLES WITH READ LOCK
操作花费很长一段时间。更为严重的是,检查表缓存算法不是很有效,稍后会更详细地说明。
如果遇到 MySQL 无法打开更多文件的错误(可以使用 perror
工具来检查错误号代表的含义),那么可能需要增加 MySQL 允许打开文件的数量。这可以通过在 my.cnf
文件中设置 open_files_limit
服务器变量来实现。
线程和表缓存实际上用的内存并不多,相反却可以有效节约资源。虽然创建一个新线程或者打开一个新的表,相对于其他 MySQL 操作来说代价并不算高,但它们的开销是会累加的。所以缓存线程和表有时可以提升效率。
8.4.9 InnoDB 数据字典(Data Dictionary)
InnoDB 有自己的表缓存,可以称为表定义缓存或者数据字典 ,在目前的 MySQL 版本中还不能对它进行配置。当 InnoDB 打开一张表,就增加了一个对应的对象到数据字典。每张表可能占用 4KB 或者更多的内存 (尽管在 MySQL 5.1 中对空间的需求小了很多)。当表关闭的时候也不会从数据字典中移除它们。
因此,随着时间的推移,服务器可能出现内存泄露,导致数据字典中的元素不断地增长。但这不是真的内存泄露,只是没有对数据字典实现任何一种缓存过期策略。通常只有当有很多(数千或数万)张大表时才是个问题。如果这个问题有影响,可以使用 Percona Server, 有一个选项可以控制数据字典的大小,它会从数据字典中移除没有使用的表。MySQL 5.6 尚未发布的版本中也有个类似的功能。
另一个性能问题是第一次打开表时会计算统计信息,这需要很多 I/O 操作,所以代价很高。相比 MyISAM,InnoDB 没有将统计信息持久化,而是在每次打开表时重新计算,在打开之后,每隔一段过期时间或者遇到触发事件(改变表的内容或者查询 INFORMATION_SCHEMA
表,等等),也会重新计算统计信息 。如果有很多表,服务器可能会花费数个小时来启动并完全预热,在这个时候服务器可能花费更多的时间在等待 I/O 操作,而不是做其他事。可以在 Percona Server( 在 MySQL 5.6 中也可以,但是叫做 innodb_analyze_is_persistent
) 中打开 innodb_use_sys_stats_table
选项来持久化存储统计信息到磁盘,以解决这个问题。
即使在启动之后,InnoDB 统计操作还可能对服务器和一些特定的查询产生冲击。可以关闭 innodb_stats_on_metadata
选项来避免耗时的表统计信息刷新。当例如 IDE 这样的工具执行 INFORMATION_SCHEMA
表的查询时,关闭这个选项后的表现是很不一样的(当然是快了不少)。
如果设置了 InnoDB 的 innodb_file_per_table
选项(后面会描述),InnoDB 任意时刻可以保持打开 .ibd
文件的数量也是有其限制的。这由 InnoDB 存储引擎负责,而不是 MySQL 服务器管理,并且由 innodb_open_files
来控制。InnoDB 打开文件和 MyISAM 的方式不一样,MyISAM 用表缓存来持有打开表的文件描述符,而 InnoDB 在打开表和打开文件之间没有直接的关系。InnoDB 为每个 .ibd
文件使用单个、全局的文件描述符。如果可以,最好把 innodb_open_files
的值设置得足够大以使服务器可以保持所有的 .ibd
文件同时打开。
8.5 配置 MySQL 的 I/O 行为
有一些配置项影响着MySQL怎样同步数据到磁盘以及如何做恢复操作。这些操作对性能的影响非常大,因为都涉及到昂贵的I/O操作。它们也表现了性能和数据安全之间的权衡。通常,保证数据立刻并且一致地写到磁盘是很昂贵的。如果能够冒一点磁盘写可能没有真正持久化到磁盘的风险,就可以增加并发性和减少I/O等待,但是必须决定可以容忍多大的风险。
8.5.1 InnoDB I/O 配置
InnoDB 不仅允许控制怎么恢复,还允许控制怎么打开和刷新数据(文件),这会对恢复和整体性能产生巨大的影响。尽管可以影响它的行为,InnoDB 的恢复流程实际上是自动的,并且经常在 InnoDB 启动时运行。撇开恢复并假设 InnoDB 没有崩溃或者出错, InnoDB 依然有很多需要配置的地方。它有一系列复杂的缓存和文件设计可以提升性能,以及保证 ACID 特性,并且每一部分都是可配置的,图 8-1 阐述了这些文件和缓存。
对于常见的应用,最重要的一小部分内容是 InnoDB 日志文件大小、 InnoDB 怎样刷新它的日志缓冲,以及 InnoDB 怎样执行 I/O。
InnoDB 事务日志
InnoDB 使用日志来减少提交事务时的开销。因为日志中已经记录了事务,就无须在每个事务提交时把缓冲池的脏块刷新( flush) 到磁盘中。事务修改的数据和索引通常会映射到表空间的随机位置,所以刷新这些变更到磁盘需要很多随机 I/O。InnoDB 假设使用的是常规磁盘(机械磁盘),随机 I/O 比顺序 I/O 要昂贵得多,因为一个 I/O 请求需要时间把磁头移到正确的位置,然后等待磁盘上读出需要的部分,再转到开始位置。
InnoDB 用日志把随机 I/O 变成顺序 I/O。 一旦日志安全写到磁盘,事务就持久化了,即使变更还没写到数据文件。如果一些糟糕的事情发生了(例如断电了),InnoDB 可以重放日志并且恢复已经提交的事务。
当然,InnoDB 最后还是必须把变更写到数据文件,因为日志有固定的大小。InnoDB 的日志是环形方式写的:当写到日志的尾部,会重新跳转到开头继续写,但不会覆盖还没应用到数据文件的日志记录,因为这样做会清掉已提交事务的唯一持久化记录。
InnoDB 使用一个后台线程智能地刷新这些变更到数据文件。这个线程可以批量组合写入,使得数据写入更顺序,以提高效率。实际上,事务日志把数据文件的随机 I/O 转换为几乎顺序的日志文件和数据文件 I/O。 把刷新操作转移到后台使查询可以更快完成,并且缓和查询高峰时 I/O 系统的压力。
整体的日志文件大小受控于 innodb_log_file_size 和 innodb_log_files_in_group 两个参数,这对写性能非常重要。日志文件的总大小是每个文件的大小之和。默认情况下,只有两个 5MB 的文件,总共 10MB。 对高性能工作来说这太小了。至少需要几百 MB, 或者甚至上 GB 的日志文件。
InnoDB 使用多个文件作为一组循环日志。通常不需要修改默认的日志数量,只修改每个日志文件的大小即可。要修改日志文件大小,需要完全关闭 MySQL, 将旧的日志文件移到其他地方保存,重新配置参数,然后重启。一定要确保 MySQL 干净地关闭了,或者还有日志文件可以保证需要应用到数据文件的事务记录,否则数据库就无法恢复了!当重启服务器的时候,查看 MySQL 的错误日志。在重启成功之后,才可以删除旧的日志文件。
日志文件大小和日志缓存。要确定理想的日志文件大小,必须权衡正常数据变更的开销和崩溃恢复需要的时间。如果日志太小,InnoDB 将必须做更多的检查点,导致更多的日志写。在极个别情况下,写语句可能被拖累,在日志没有空间继续写入前,必须等待变更被应用到数据文件。另一方面,如果日志太大了,在崩溃恢复时 InnoDB 可能不得不做大量的工作。这可能极大地增加恢复时间,尽管这个处理在新的 MySQL 版本中已经改善很多。
数据大小和访问模式也将影响恢复时间。假设有一个 1TB 的数据和 16GB 的缓冲池,并且全部日志大小是 128MB。 如果缓冲池里有很多脏页(例如,页被修改了还没被刷写回数据文件),并且它们均匀地分布在 1TB 数据中,崩溃后恢复将需要相当长一段时间。InnoDB 必须从头到尾扫描日志,仔细检查数据文件,如果需要还要应用变更到数据文件。这是很庞大的读写操作!另一方面,如果变更是局部性的------就是说,如果只有几百 MB 数据被频繁地变更------恢复可能就很快,即使数据和日志文件很大。恢复时间也依赖于普通修改操作的大小,这跟数据行的平均长度有关系。较短的行使得更多的修改可以放在同样的日志中,所以 InnoDB 可能必须在恢复时重放更多修改操作。
当 InnoDB 变更任何数据时,会写一条变更记录到内存日志缓冲区。如果有大事务,增加日志缓冲区(默认 16 MB) 大小可以帮助减少 I/O。 变量 innodb_log_buffer_size
可以控制日志缓冲区的大小。
通常不需要把日志缓冲区设置得非常大。相对于 InnoDB 的普通数据,日志条目是非常紧凑的。它们不是基于页的,所以不会浪费空间来一次存储整个页。InnoDB 也使得日志条目尽可能地短。有时甚至会保存为函数号和C函数的参数!
较大的日志缓冲区在某些情况下也是有好处的:可以减少缓冲区中空间分配的争用。当配置一台有大内存的服务器时,有时简单地分配 32MB ~ 128MB 的日志缓冲,因为花费这么点相对(整机)而言比较小的内存并没有什么不好,还可以帮助避免压力瓶颈。如果有问题,瓶颈一般会表现为日志缓冲 Mutex 的竞争。
可以通过检查 SHOW INNODB STATUS
的输出中 LOG
部分来监控 InnoDB 的日志和日志缓冲区的 I/O 性能,通过观察 Innodb_os_log_written
状态变量来查看 InnoDB 对日志文件写出了多少数据。一个好用的经验法则是,查看 10 ~ 100 秒间隔的数字,然后记录峰值。可以用这个来判断日志缓冲是否设置得正好 。例如,若看到峰值是每秒写 100KB 数据到日志,那么 1MB 的日志缓冲可能足够了。也可以使用这个衡量标准来决定日志文件设置多大会比较好。如果峰值是 100KB/s, 那么 256MB 的日志文件足够存储至少 2 560 秒的日志记录。这看起来足够了。作为一个经验法则,日志文件的全部大小,应该足够容纳服务器一个小时的活动内容。
InnoDB 怎样刷新日志缓冲
当 InnoDB 把日志缓冲刷新到磁盘日志文件时,先会使用一个 Mutex 锁住缓冲区,刷新到所需要的位置,然后移动剩下的条目到缓冲区的前面。当 Mutex 释放时,可能有超过一个事务已经准备好刷新其日志记录。InnoDB 有一个 Group Commit 功能,可以在一个 I/O 操作内提交多个事务,但是在 MySQL 5.0 中当打开二进制日志时这个功能就不能用了。我们在前一章写了一些关于 Group Commit 的东西。
日志缓冲必须被刷新到持久化存储,以确保提交的事务完全被持久化了。如果和持久相比更在乎性能,可以修改 innodb_flush_log_at_trx_commit
变量来控制日志缓冲刷新的频繁程度。可能的设置如下:
0
把日志缓冲写到日志文件,并且每秒钟刷新一次,但是事务提交时不做任何事。1
将日志缓冲写到日志文件,并且每次事务提交都刷新到持久化存储。这是默认的(并且是最安全的)设置,该设置能保证不会丢失任何已经提交的事务,除非磁盘或者操作系统是"伪"刷新。2
每次提交时把日志缓冲写到日志文件,但是并不刷新。InnoDB 每秒钟做一次刷新。0
与2
最重要的不同是(也是为什么2
是更合适的设置),如果 MySQL 进程"挂了",2
不会丢失任何事务。如果整个服务器"挂了"或者断电了,则还是可能会丢失一些事务。
了解清楚"把日志缓冲写到日志文件"和"把日志刷新到持久化存储"之间的不同是很重要的。在大部分操作系统中,把缓冲写到日志只是简单地把数据从 InnoDB 的内存缓冲转移到了操作系统的缓存,也是在内存里,并没有真的把数据写到了持久化存储。
因此,如果 MySQL 崩溃了或者电源断电了,设置0
和2
通常会导致最多一秒的数据丢失,因为数据可能只存在于操作系统的缓存。我们说"通常",因为不论如何 InnoDB 会每秒尝试刷新日志文件到磁盘,但是在一些场景下也可能丢失超过1秒的事务,例如当刷新被推迟了。
与此相反,把日志刷新到持久化存储意味着 InnoDB 请求操作系统把数据刷出缓存,并且确认写到磁盘了。这是一个阻塞 I/O 的调用,直到数据被完全写回才会完成。因为写数据到磁盘比较慢,当 innodb_flush_log_at_trx_commit
被设置为1
时,可能明显地降低 InnoDB 每秒可以提交的事务数。今天的高速驱动器( 机械硬盘) 可能每秒只能执行一两百个磁盘事务,受限于磁盘旋转速度和寻道时间。
有时硬盘控制器或者操作系统假装做了刷新,其实只是把数据放到了另一个缓存,例如磁盘自己的缓存。这更快但是很危险,因为如果驱动器断电,数据依然可能丢失。这甚至比设置 innodb_flush_log_at_trx_commit
为不为1的值更糟糕,因为这可能导致数据损坏,不仅仅是丢失事务。
设置 innodb_flush_log_at_trx_commit
为不为1的值可能导致丢失事务。然而,如果不在意持久性( ACID
中的 D
), 那么设置为其他的值也是有用的。也许你只是想拥有 InnoDB 的其他一些功能,例如聚簇索引、防止数据损坏,以及行锁。但仅仅因为性能原因用 InnoDB 替换 MyISAM 的情况也并不少见。
高性能事务处理需要的最佳配置是把 innodb_flush_log_at_trx_commit
设置为1且把日志文件放到一个有电池保护的写缓存的 RAID 卷中。这兼顾了安全和速度。事实上,我们敢说任何希望能扛过高负荷工作负载的产品数据库服务器,都需要有这种类型的硬件。
Percona Server 扩展了 innodb_fush_log_at_trx_commit
变量,使得它成为一个会话级变量,而不是一个全局变量。这允许有不同的性能和持久化要求的应用,可以使用同样的数据库,同时又避免了标准 MySQL 提供的一刀切的解决方案。
InnoDB 怎样打开和刷新日志以及数据文件
使用 innodb_fush_method
选项可以配置 InnoDB 如何跟文件系统相互作用。从名字来看,会以为只能影响 InnoDB 怎么写数据,实际上还影响了 InnoDB 怎么读数据。Windows 和非 Windows 的操作系统对这个选项的值是互斥的: async_unbuffered
(MySQL 8.x 中已移除)、unbuffered
和 normal
只能在 Windows 下使用,并且 Windows 下不能使用其他的值。在 Windows 下默认值是 unbuffered
, 其他操作系统都是 fdatasync
(MySQL 8.x 为 fsync
)。( 如果 SHOW GLOBAL VARIABLES
显示这个变量为空,意味着它被设置为默认值了。)
改变 InnoDB 执行 I/O 操作的方式可以显著地影响性能,所以请确认你明白了在做什么后再去做改动!
这是个有点难以理解的选项,因为它既影响日志文件,也影响数据文件,而且有时候对不同类型的文件的处理也不一样。如果有一个选项来配置日志,另一个选项来配置数据文件,这样最好了,但实际上它们混合在同一个配置项中。
下面是一些可能的值:
-
fdatasync
这在非 Windows 系统上是默认值: InnoDB 用
fsync()
来刷新数据和日志文件。InnoDB 通常用
fsync()
代替fdatasync()
, 即使这个值似乎表达的是相反的意思。fdatasync()
跟fsync()
相似,但是只刷新文件的数据,而不包括元数据(最后修改时间,等等)。因此,fsync()
会导致更多的 I/O。 然而 InnoDB 的开发者都很保守,他们发现某些场景下fdatasync()
会导致数据损坏。InnoDB 决定了哪些方法可以更安全地使用,有一些是编译时设置的,也有一些是运行时设置的。它使用尽可能最快的安全方法。(详见innodb_use_fdatasync
系统变量)使用
fsync()
的缺点是操作系统至少会在自己的缓存中缓冲一些数据。理论上,这种双重缓冲是浪费的,因为 InnoDB 管理自己的缓冲比操作系统能做的更加智能。然而,最后的影响跟操作系统和文件系统非常相关。如果能让文件系统做更智能的 I/O 调度和批量操作,双重缓冲可能并不是坏事。有的文件系统和操作系统可以积累写操作后合并执行,通过对 I/O 重新排序来提升效率,或者并发写入多个设备。它们也可能做预读优化,例如,若连续请求了几个顺序的块,它会通知硬盘预读下一个块。有时这些优化有帮助,有时没有。如果你好奇你的系统中的
fsync()
会做哪些具体的事,可以阅读系统的帮助手册,看下 fsync(2)。innodb_file_per_table
选项会导致每个文件独立地做fsync()
, 这意味着写多个表不能合并到一个 I/O 操作。这可能导致 InnoDB 执行更多的fsync()
操作。 -
O_DIRECT
InnoDB 对数据文件使用
O_DIRECT
标记或directio()
函数,这依赖于操作系统。这个设置并不影响日志文件并且不是在所有的类 UNIX 系统上都有效。但至少 GNU/Linux、FreeBSD, 以及 Solaris(5.0 以后的新版本)是支持的。不像O_DSYNC
标记,它同时会影响读和写。这个设置依然使用
fsync()
来刷新文件到磁盘,但是会通知操作系统不要缓存数据,也不要用预读。这个选项完全关闭了操作系统缓存,并且使所有的读和写都直接通过存储设备,避免了双重缓冲。在大部分系统上,这个实现用
fcntl()
调用来设置文件描述符的O_DIRECT
标记,所以可以阅读 fcntl(2) 的手册页来了解系统上这个函数的细节。在 Solaris 系统,这个选项用directio()
。如果 RAID 卡支持预读,这个设置不会关闭 RAID 卡的预读。 这个设置只能关闭操作系统和文件系统的预读。
如果使用
O_DIRECT
选项,通常需要带有写缓存的 RAID 卡,并且设置为Write-Back
策略 (就是写入会在 RAID 卡缓存上进行缓冲,不直接写到硬盘), 因为这是典型的唯一能保持好性能的方法。当 InnoDB 和实际存储设备之间没有缓冲时使用O_DIRECT
, 例如当 RAID 卡没有写缓存时,可能导致严重的性能下降。现在有了多个写线程,这个问题稍微小一点(并且 MySQL 5.5 提供了原生异步 I/O), 但是通常还是有问题。这个选项可能导致服务器预热时间变长,特别是操作系统的缓存很大的时候。也可能导致小容量的缓冲池(例如,默认大小的缓冲池)比缓冲 I/O(Buffered IO) 方式操作要慢的多。这是因为操作系统不会通过保持更多数据在自己的缓存中来"帮助"(提升性能)。如果需要的数据不在缓冲池,InnoDB 将不得不直接从磁盘读取。
这个选项不会对 innodb_file_per_table
产生任何额外的损失。相反,如果不用 innodb_file_per_table
, 当使用 O_DIRECT
时,可能由于一些顺序 I/O 而遭受性能损失。这种情况的发生是因为一些文件系统(包括 Linux 所有的 ext 文件系统)每个 inode 有一个 Mutex。 当在这些文件系统上使用 O_DIRECT
时,确实需要打开 innodb_file_per_table
。 我们下一章会更深入地探究文件系统。
ALL_O_DIRECT
这个选项在 Percona Server 和 MariaDB 中可用。它使得服务器在打开日志文件时,也能使用标准 MySQL 中打开数据文件的方式(O_DIRECT
)。
-
O_DSYNC
这个选项使日志文件 调用
open()
函数时设置O_SYNC
标记。它使得所有的写同步------换个说法,只有数据写到磁盘后写操作才返回。这个选项不影响数据文件。O_SYNC
标记和O_DIRECT
标记的不同之处在于O_SYNC
没有禁用操作系统层的缓存。因此,它没有避免双重缓冲,并且它没有使写操作直接操作到磁盘。用了O_SYNC
标记,在缓存中写数据,然后发送到磁盘。使用
O_SYNC
标记做同步写操作,听起来可能跟fsync()
做的事情非常相似,但是它们两个的实现无论在操作系统层还是在硬件层都非常不同。用了O_SYNC
标记后,操作系统可能把"使用同步 I/O" 标记下传给硬件层,告诉设备不要使用缓存。另一方面,fsync()
告诉操作系统把修改过的缓冲数据刷写到设备上,如果设备支持,紧接着会传递一个指令给设备刷新它自己的缓存,所以,毫无疑问,数据肯定记录在了物理媒介上。另一个不同是,用了O_SYNC
的话,每个write()
或pwrite()
操作都会在函数完成之前把数据同步到磁盘,完成前函数调用是阻塞的。相对来看,不用O_SYNC
标记的写入调用fsync()
允许写操作积累在缓存(使得每个写更快),然后一次性刷新所有的数据。再一次吐槽下这个名称,这个选项设置
O_SYNC
标记,不是O_DSYNC
标记,因为 InnoDB 开发者发现了O_DSYNC
的 Bug。O_SYNC
和O_DSYNC
类似于fysnc()
和fdatasync()
:O_SYNC
同时同步数据和元数据,但是O_DSYNC
只同步数据。 -
async_unbuffered
这是 Windows 下的默认值。这个选项让 InnoDB 对大部分写使用没有缓冲的 I/O; 例外是当
innodb_flush_log_at_trx_commit
设置为2
的时候,对日志文件使用缓冲 I/O。这个选项使得 InnoDB 在 Windows 2000、XP, 以及更新版本中对数据读写都使用操作系统的原生异步(重叠的) I/O。 在更老的 Windows 版本中,InnoDB 使用自己用多线程模拟的异步 I/O。
-
unbuffered
只对 Windows 有效。这个选项与
async_unbuffered
类似,但是不使用原生异步 I/O。 -
normal
只对 Windows 有效。这个选项让 InnoDB 不要使用原生异步 I/O 或者无缓冲 I/O。
-
Nosync
和littlesync
只为开发使用。这两个选项在文档中没有并且对生产环境来说不安全,不应该使用这个。
如果这些看起来像是一堆不带建议的说明,那么下面是一些建议:如果使用类 UNIX 操作系统并且 RAID 控制器带有电池保护的写缓存,我们建议使用 O_DIRECT
。 如果不是这样,默认值或者 O_DIRECT
都可能是最好的选择,具体要看应用类型。
InnoDB表空间
InnoDB 把数据保存在表空间内,本质上是一个由一个或多个磁盘文件组成的虚拟文件系统。InnoDB 用表空间实现很多功能,并不只是存储表和索引。它还保存了回滚日志(旧版本行)、插入缓冲(Insert Buffer)、双写缓冲(Doublewrite Buffer,后面的章节里就会描述),以及其他内部数据结构。
MySQL 8.x 的变化:
- 使用 Undo 表空间保存回滚日志。
- 插入缓冲(现叫做"更改缓冲")位于内存中的更改缓冲区(change buffer)。
- MySQL 8.0.20 以后,双写缓冲存储在独立的双写缓冲文件中。
配置表空间。通过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的表空间。有时人们并不清楚可以使用多个文件分散驱动器的负载,像这样:
innodb_data_file_path = /disk1/ibdata1:1G;/disk2/ibdata2:1G;...
在这个例子中,表空间文件确实放在代表不同驱动器的不同目录中,InnoDB 把这些文件首尾相连组合起来。因此,通常这种方式并不能获得太多收益。InnoDB 先填满第一个文件,当第一个文件满了再用第二个,如此循环;负载并没有真的按照希望的高性能方式分布。用 RAID 控制器是分布负载更聪明的方式。
为了允许表空间在超过了分配的空间时还能增长,可以像这样配置最后一个文件自动扩展:
...ibdata3:1G:autoextend
默认的行为是创建单个 10MB 的自动扩展文件。如果让文件可以自动扩展,那么最好给表空间大小设置一个上限,别让它扩展得太大,因为一旦扩展了,就不能收缩回来。例如,下面的例子限制了自动扩展文件最多到 2 GB :
...ibdata3:1G:autoextend:max:2G
管理一个单独的表空间可能有点麻烦,尤其是如果它是自动扩展的,并且希望回收空间时(因为这个原因,我们建议关闭自动扩展功能,至少设置一个合理的空间范围)。回收空间唯一的方式是导出数据,关闭 MySQL,删除所有文件,修改配置,重启,让 InnoDB 创建新的数据文件,然后导入数据 。InnoDB 这种表空间管理方式很让人头疼------不能简单地删除文件或者改变大小。如果表空间损坏了,InnoDB 会拒绝启动。对日志文件也一样的严格。如果像 MyISAM 一样随便移动文件,千万要谨慎!
innodb_file_per_table
选项让 InnoDB 为每张表使用一个文件,MySQL 4.1 和之后的版本都支持。它在数据字典存储为"表名.ibd
"的数据。这使得删除一张表时回收空间简单多了,并且可以容易地分散表到不同的磁盘上。然而,把数据放到多个文件,总体来说可能导致更多的空间浪费,因为把单个 InnoDB 表空间的内部碎片浪费分布到了多个.ibd
文件。对于非常小的表,这个问题更大,因为 InnoDB 的页大小是 16 KB 。即使表只有 1 KB 的数据,仍然需要至少 16 KB 的磁盘空间。
一些人喜欢使用 innodb_file_per_table
, 只是因为特别容易管理,并且可以看到每个表的文件。例如,可以通过查看文件的大小来确认表的大小,这比用 SHOW TABLE STATUS
来看快多了,这个命令需要执行很多复杂的工作来判断给一个表分配了多少页面。
设置 innodb_file_per_table
也有不好的一面:更差的 DROP TABLE
性能。这可能足以导致显而易见的服务器端阻塞。因为有如下两个原因:
- 删除表需要从文件系统层去掉(删除)文件,这可能在某些文件系统( ext3, 说的就是你)上会很慢 。可以通过欺骗文件系统来缩短这个过程:把
.ibd
文件链接到一个 0 字节的文件,然后手动删除这个文件,而不用等待 MySQL 来做。 - 当打开这个选项,每张表都在 InnoDB 中使用自己的表空间。结果是,移除表空间实际上需要 InnoDB 锁定和扫描缓冲池,查找属于这个表空间的页面,在一个有庞大的缓冲池的服务器上做这个操作是非常慢的 。如果打算删除很多 InnoDB 表(包括临时表)并且用了
innodb_file_per_table
, 可能会从 Percona Server 包含的一个修复中获益,它可以让服务器慢慢地清理掉属于被删除表的页面。只需要设置innodb_lazy_drop_table
这个选项。
什么是最终的建议?我们建议使用innodb_file_per_table
并且给共享表空间设置大小范围,这样可以过得舒服点(不用处理那些空间回收的事)。如果遇到任何头痛的场景,就像上面说的,考虑用下 Percona 的那个修复。
提醒一下,事实上没有必要把 InnoDB 文件放在传统的文件系统上。像许多的传统数据库服务器一样,InnoDB 提供使用裸设备的选项------例如,一个没有格式化的分区------作为它的存储。然而,今天的文件系统已经可以存放足够大的文件,所以已经没有必要使用这个选项。使用裸设备可能提升几个百分点的性能,但是我们不认为这点小提升足以抵消这样做带来的坏处,我们不能直接用文件管理数据。当把数据存在一个裸设备分区时,不能使用 mv、cp 或其他任何工具来操作它。最终,这点小的性能收益显然不值得。
行的旧版本和表空间
在一个写压力大的环境下,InnoDB 的表空间可能增长得非常大。如果事务保持打开状态很久(即使它们没有做任何事),并且使用默认的 REPEATABLE READ
事务隔离级别,InnoDB 将不能删除旧的行版本,因为没提交的事务依然需要看到它们。InnoDB 把旧版本存在共享表空间,所以如果有更多的数据在更新,共享表空间会持续增长。有时这个问题并非是没提交的事务的原因,也可能是工作负载的问题:清理过程只有一个线程处理,直到最近的 MySQL 版本才改进,这可能导致清理线程处理速度跟不上旧版本行数增加的速度。
无论发生何种情况,SHOW INNODB STATUS
的数据都可以帮助定位问题。查看历史链表的长度会显示了回滚日志的大小,以页为单位。
分析 TRANSACTIONS
部分的第一行和第二行可以证实这个观点,这部分展示了当前事务号以及清理线程完成到了哪个点。如果这个差距很大,可能有大量的没有清理的事务。
如果有个很大的回滚日志并且表空间因此增长很快,可以强制 MySQL 减速来使 InnoDB 的清理线程可以跟得上。这听起来不怎么样,但是没办法。否则,InnoDB 将保持数据写入,填充磁盘直到最后磁盘空间爆满,或者表空间大于定义的上限。
为了控制写入速度,可以设置 innodb_max_purge_lag
变量为一个大于 0 的值。这个值表示 InnoDB 开始延迟后面的语句更新数据之前,可以等待被清除的最大的事务数量。你必须知道工作负载以决定一个合理的值。例如,事务平均影响 1KB 的行,并且可以容许表空间里有 100 MB 的未清理行,那么可以设置这个值为 100000。
牢记,没有清理的行版本会对所有的查询产生影响,因为它们事实上使得表和索引更大了 。如果清理线程确实跟不上,性能可能显著的下降。设置innodb_max_purge_lag
变量也会降低性能,但是它的伤害较少。
在更新版本的 MySQL 中,甚至在更早版本的 Percona Server 和 MariaDB,清理过程已经显著地提升了性能,并且从其他内部工作任务中分离出来。甚至可以创建多个专用的清理线程来更快地做这个后台工作。如果可以利用这些特性,会比限制服务器的服务能力要好得多。
双写缓冲( Doublewrite Buffer)
InnoDB 用双写缓冲来避免页没写完整所导致的数据损坏。当一个磁盘写操作不能完整地完成时,不完整的页写入就可能发生,16 KB 的页可能只有一部分被写到磁盘上。有多种多样的原因(崩溃、 Bug, 等等)可能导致页没有写完整。双写缓冲在这种情况发生时可以保证数据完整性。
双写缓冲是表空间一个特殊的保留区域,在一些连续的块中足够保存 100 个页。本质上是一个最近写回的页面的备份拷贝。当 InnoDB 从缓冲池刷新页面到磁盘时,首先把它们写(或者刷新)到双写缓冲,然后再把它们写到其所属的数据区域中。这可以保证每个页面的写入都是原子并且持久化的。
这意味着每个页都要写两遍?是的,但是因为 InnoDB 写页面到双写缓冲是顺序的,并且只调用一次 fsync() 刷新到磁盘,所以实际上对性能的冲击是比较小的------通常只有几个百分点,肯定没有一半那么多,尽管这个开销在 SSD 上更明显,我们下一章会讨论这个问题。更重要的是,这个策略允许日志文件更加高效。因为双写缓冲给了 InnoDB 一个非常牢固的保证,数据页不会损坏,InnoDB 日志记录没必要包含整个页,它们更像是页面的二进制变化量。
如果有一个不完整的页写到了双写缓冲,原始的页依然会在磁盘上它的真实位置。当 InnoDB 恢复时,它将用原始页面替换掉双写缓冲中的损坏页面。然而,如果双写缓冲成功写入,但写到页的真实位置失败了,InnoDB 在恢复时将使用双写缓冲中的拷贝来替换。InnoDB 知道什么时候页面损坏了,因为每个页面在末尾都有校验值( Checksum)。 校验值是最后写到页面的东西,所以如果页面的内容跟校验值不匹配,说明这个页面是损坏的。因此,在恢复的时候,InnoDB 只需要读取双写缓冲中每个页面并且验证校验值。如果一个页面的校验值不对,就从它的原始位置读取这个页面。
有些场景下,双写缓冲确实没必要------例如,你也许想在备库上禁止双写缓冲。此外一些文件系统(例如 ZFS) 做了同样的事,所以没必要再让 InnoDB 做一遍 。可以通过设置 innodb_doublewrite
为0
来关闭双写缓冲。在 Percona Server 中,可以配置双写缓冲存到独立的文件中,所以可以把这部分工作压力分离出来放在单独的盘上。
其他的 I/O 配置项
sync_binlog
选项控制 MySQL 怎么刷新二进制日志到磁盘。默认值是 0
(MySQL 8 中默认值为 1
), 意味着 MySQL 并不刷新,由操作系统自己决定什么时候刷新缓存到持久化设备。如果这个值比0
大,它指定了两次刷新到磁盘的动作之间间隔多少次二进制日志写操作(如果 autocommit
被设置了,每个独立的语句都是一次写,否则就是一个事务一次写)。把它设置为0
和1
以外的值是很罕见的。
如果没有设置 sync_binlog
为 1
, 那么崩溃以后可能导致二进制日志没有同步事务数据。这可以轻易地导致复制中断,并且使得及时恢复变得不可能。无论如何,可以把这个值设置为1
来获得安全的保障。这样就会要求 MySQL 同步把二进制日志和事务日志这两个文件刷新到两个不同的位置。这可能需要磁盘寻道,相对来说是个很慢的操作。
像 InnoDB 日志文件一样,把二进制日志放到一个带有电池保护的写缓存的 RAID 卷,可以极大地提升性能。事实上,写和刷新二进制日志缓存其实比 InnoDB 事务日志要昂贵多了,因为不像 InnoDB 事务日志,每次写二进制日志都会增加它们的大小。这需要每次写入文件系统都更新元信息。所以,设置 sync_binlog=1
可能比 innodb_fush_log_at_trx_commit=1
对性能的损害要大得多,尤其是网络文件系统,例如 NFS。
一个跟性能无关的提示,关于二进制日志:如果希望使用 expire_logs_days
选项来自动清理旧的二进制日志,就不要用 rm
命令去删。服务器会感到困惑并且拒绝自动删除它们,并且 PURGE MASTER LOGS
也将停止工作。解决的办法是,如果发现了这种情况,就手动重新同步"主机名-bin.index
" 文件,可以用磁盘上现有日志文件的列表来更新。
我们将在下一章更深入地涉及 RAID, 但是值得在这里重复一下,把带有电池保护写缓存的高质量 RAID 控制器设置为使用写回( Writeback) 策略,可以支持每秒数千的写入,并且依然会保证写到持久化存储。数据写到了带有电池的高速缓存,所以即使系统断电它也能存在。但电源恢复时,RAID 控制器会在磁盘被设置为可用前,把数据从缓存中写到磁盘。因此,一个带有电池保护写缓存的 RAID 控制器可以显著地提升性能,这是非常值得的投资。当然,SSD 存储是另一个选择,我们也会在下一章讲到。
8.5.2 MyISAM的I/O配置(略)
8.6 配置 MySQL 并发
8.6.1 InnoDB 并发配置
InnoDB 是为高性能设计的,在最近几年它的提升非常明显,但依然不完美。InnoDB 架构在有限的内存、单 CPU、 单磁盘的系统中仍然暴露出一些根本性问题。在高并发场景下, InnoDB 的某些方面的性能可能会降低,唯一的办法是限制并发。可以参考第3章中使用的技巧来诊断并发问题。
如果在 InnoDB 并发方面有问题,解决方案通常是升级服务器。相比当前的版本,像 MySQL 5.0 和早期的 MySQL 5.1 这样的旧版本,在高并发下完全是个悲剧。所有的东西都在全局 Mutex( 例如,缓冲池 Mutex) 上排队,导致服务器几乎陷入停顿。如果升级到某个更新版本的 MySQL, 在大部分场景都不再需要限制并发。
如果需要这么做,这里会介绍它是怎么工作的。InnoDB 有自己的"线程调度器"控制线程怎么进入内核访问数据,以及它们在内核中一次可以做哪些事。最基本的限制并发的方式是使用 innodb_thread_concurrency
变量,它会限制一次性可以有多少线程进入内核,0
表示不限制。如果在旧的 MySQL 版本里有 InnoDB 并发问题,这个变量是最重要的配置之一。
在任何架构和业务压力下,给这个变量设置个"靠谱"的值都很重要,理论上,下面的公式可以给出一个这样的值:
并发值= CPU 数量*磁盘数量*2
但是在实践中,使用更小的值会更好一点。必须做实验来找出适合系统的最好的值。
如果已经进入内核的线程超过了允许的数量,新的线程就无法再进入内核。InnoDB 使用两段处理来尝试让线程尽可能高效地进入内核。两段策略减少了因操作系统调度引起的上下文切换。线程第一次休眠 innodb_thread_sleep_delay
微秒,然后再重试。如果它依然不能进入内核,则放入一个等待线程队列,让操作系统来处理。
第一阶段默认的休眠时间是 10000 微秒。当 CPU 有大量的线程处在"进入队列前的休眠"状态,因而没有被充分利用时,改变这个值在高并发环境里可能会有帮助。如果有大量的小查询,默认值可能也太大了,因为这增加了 10 毫秒的查询延时。
一旦线程进入内核,它会有一定数量的"票据( Tickets)", 可以让它"免费"返回内核,不需再做并发检查。这限制了一个线程回到其他等待线程之前可以做多少事。innodb_concurrency_tickets
选项控制票据的数量。它很少需要修改,除非有很多运行时间极长的查询。票据是按查询授权的,不是按事务。一旦查询完成,它没用完的票据就销毁了。除了缓冲池和其他结构的瓶颈,还有另一个提交阶段的并发瓶颈,这个时候 I/O 非常密集,因为需要做刷新操作。innodb_commit_concurrency
变量控制有多少个线程可以在同一时间提交。如果 innodb_thread_concurrency
配置得很低也有大量的线程冲突,那么配置这个选项可能会有帮助。
最后,有一个新的解决方案值得考虑:使用线程池( Thread Pool) 来限制并发。
8.6.2 MyISAM并发配置(略)
8.7 基于工作负载的配置
配置服务器的一个目标是把它定制得符合特定的工作负载。这需要精通所有类型的服务器活动的数量、类型,以及频率------不仅仅是查询语句,也包括其他的活动,例如连接服务器以及刷新表。
第一件应该做的事情是熟悉你的服务器,如果还没做就赶紧。了解什么样的查询跑在上面。用例如 innotop
这样的工具来监控它,用 pt-query-digest
来创建查询报告。这不仅帮助你全面地了解服务器正在做什么,还可以知道查询花费大量时间做了哪些事。第3章阐明了怎么把这些东西找出来。
当服务器在满载情况下运行时,请尝试记录所有的查询语句,因为这是最好的方式来查看哪种类型的查询语句占用资源最多 。同时,创建 processlist
快照,通过 state
或者 command
字段来聚合它们(innotop
可以实现,或者可以使用第 3 章展示的脚本)。例如,是否大量地在复制数据到临时表,或者排序数据?如果有,也许需要优化查询语句,以及查看临时表和排序缓冲配置项。
8. 7.1 优化 BLOB 和 TEXT 的场景
BLOB
和 TEXT
列对 MySQL 来说是特殊类型的场景(我们把所有 BLOB
和 TEXT
都简单称为 BLOB
类型,因为它们属于相同类型的数据)。BLOB
值有几个限制使得服务器对它的处理跟其他类型不一样。一个最重要的注意事项是,服务器不能在内存临时表中存储 BLOB
值 , 因此,如果一个查询涉及 BLOB
值,又需要使用临时表------不管它多小------它都会立即在磁盘上创建临时表。这样效率很低,尤其是对小而快的查询。临时表可能是查询中最大的开销。
有两种办法来减轻这个不利的情况:通过 SUBSTRING()
函数(第4章有更多关于这个函数的细节)把值转换为 VARCHAR
, 或者让临时表更快一些。
让临时表运行更快的最好方式是,把它们放在基于内存的文件系统( GNU/Linux 上是 tmpfs
) 。 这会降低一些开销,尽管这依然比内存表慢许多。因为操作系统会避免把数据写到磁盘,所以内存文件系统可以帮助提升性能(如果操作系统把它交换(Swap)出内存,数据依然会到磁盘)。 一般的文件系统也会在内存中缓存,但是操作系统会每隔几秒就刷新一次。tmpfs
文件系统从来不会刷新,它就是为低开销和简单起见而设计的。例如,没必要为这个文件系统预备任何恢复方案。这使得它更快。
服务器设置里控制临时表文件放在哪的是 tmpdir
。 建议监控文件系统使用率以保证有足够的空间存放临时表。如果需要,可以指定多个临时表存放位置,MySQL 将会轮询使用。
如果 BLOB
列非常大,并且用的是 InnoDB, 也许可以调大 InnoDB 日志缓冲大小。在这一章前面有更多关于这方面的内容。
对于很长的变长列(例如,BLOB
、TEXT
, 以及长字符列),InnoDB 存储一个 768 字节的前缀在行内 (这个长度足够在列上创建一个 255 字符的索引,即使是 utf8
的(每个字符可能需要三个字节))。 如果列的值比前缀长,InnoDB 会在行外分配扩展存储空间来存剩下的部分。它会分配一个完整的 16KB 的页,像其他所有的 InnoDB 页面一样,每个列都有自己的页面(不同的列不会共享扩展存储空间)。InnoDB 一次只为一个列分配一个页的扩展存储空间,直到使用了超过 32 个页以后,就会一次性分配 64 个页面。
注意,我们说过 InnoDB 可能会分配扩展存储空间。如果总的行长(包括大字段的完整长度)比 InnoDB 的最大行长限制要短(比 8 KB 小一些),InnoDB 将不会分配扩展存储空间,即使大字段( Long column) 的长度超过了前缀长度。
最后,当 InnoDB 更新存储在扩展存储空间中的大字段时,将不会在原来的位置更新。而是会在扩展存储空间中写一个新值到一个新的位置,并且不会删除旧的值。
所有这一切都有以下后果:
- 大字段在 InnoDB 里可能浪费大量空间。例如,若存储字段值只是比行的要求多了一个字节,也会使用整个页面来存储剩下的字节,浪费了页面的大部分空间。同样的,如果有一个值只是稍微超过了 32 个页的大小,实际上就需要使用 96 个页面。
- 扩展存储禁用了自适应哈希,因为需要完整地比较列的整个长度,才能发现是不是正确的数据(哈希帮助 InnoDB 非常快速地找到"猜测的位置",但是必须检查"猜测的位置"是不是正确)。因为自适应哈希是完全的内存结构,并且直接指向 Buffer Pool 中访问"最"频繁的页面,但对于扩展存储空间却无法使用自适应哈希。
- 太长的值可能使得在查询中作为
WHERE
条件不能使用索引,因而执行很慢 。在应用WHERE
条件之前,MySQL 需要把所有的列读出来,所以可能导致 MySQL 要求 InnoDB 读取很多扩展存储,然后检查WHERE
条件,丢弃所有不需要的数据。查询不需要的列绝不是好主意,在这种特殊的场景下尤其需要避免这样做。如果发现查询正遇到这个限制带来的问题,可以尝试通过覆盖索引来解决部分问题。 - 如果一张表里有很多大字段,最好是把它们组合起来单独存到一个列里面,比如说用 XML 文档格式存储。这让所有的大字段共享一个扩展存储空间,这比每个字段用自己的页要好。
- 有时候可以把大字段用
COMPRESS()
压缩后再存为BLOB
, 或者在发送到 MySQL 前在应用程序中进行压缩,这可以获得显著的空间优势和性能收益。
8.7.2 优化排序(Filesorts)
从第6章我们知道 MySQL 有两种排序算法。如果查询中所有需要的列和 ORDER BY
的列总大小超过 max_length_for_sort_data
字节,则采用 two-pass 算法。或者当任何需要的列------即使没有被 ORDER BY
使用的列------是 BLOB
或者 TEXT
, 也会采用这个算法 。(可以用 SUBSTRING()
把这些列转换一下,就可以用 single-pass 算法了。)
MySQL 有两个变量可以控制排序怎样执行。通过修改 max_length_for_sort_data
变量的值,可以影响 MySQL 选择哪种排序算法。因为 single-pass 算法为每行需要排序的数据创建一个固定大小的缓冲,对于 VARCHAR
列,在和 max_length_for_sort_data
比较时,使用的是其定义的最大长度,而不是所存储数据的实际长度。这也是为什么我们建议只选择必要的列的一个原因。
当 MySQL 必须排序 BLOB
或 TEXT
字段时,它只会使用前缀,然后忽略剩下部分的值 。这是因为缓冲只能分配固定大小的结构体来保存要排序的值,然后从扩展存储空间中复制前缀到这个结构体中。使用 max_sort_length
变量可以指定这个前缀有多大。
可惜,MySQL 无法查看它用了哪个算法。如果增加了 max_length_for_sort_data
变量的值,磁盘使用率上升了,CPU 使用率下降了,并且 Sort_merge_passes
状态变量相对于修改之前开始很快地上升,也许是强制让很多的排序使用了 single-pass 算法。
8.8 完成基本配置
我们已经完成了服务器内核的旅程------希望你喜欢这个旅程!现在让我们回到示例配置,并且看下怎样修改剩下的配置。
我们已经讨论了怎样设置一般的选项,例如数据目录、 InnoDB 和 MyISAM 缓存、日志,还有其他的一些。让我们重温剩下的那些:
-
tmp_table_size
和max_heap_table_size
这两个设置控制使用 Memory 引擎的内存临时表能使用多大的内存。如果隐式内存临时表的大小超过这两个设置的值,将会被转换为磁盘 MyISAM 表,所以它的大小可以继续增长。(隐式临时表是一种并非由自己创建,而是服务器创建,用于保存执行中的查询的中间结果的表。)
应该简单地把这两个变量设为同样的值。我们的示例配置文件中选择了 32 M。 这可能不够,但是要谨防这个变量太大了。临时表最好呆在内存里,但是如果它们被撑得很大,实际上还是让它们使用磁盘比较好,否则可能会让服务器内存溢出。
假设查询语句没有创建庞大的临时表(通常可以通过合理的索引和查询设计来避免),那把这些变量设大一点,免得需要把内存临时表转换为磁盘临时表。这个过程可以在
SHOW PROCESSLIST
中看到。可以查看服务器的
SHOW STATUS
计数器在某段时间内的变化,以此来查看创建临时表的频率以及是否是磁盘临时表 。你不能判断一张(临时)表是先创建为内存表然后被转换为了磁盘表,还是一开始就创建的磁盘表(可能因为有BLOB
字段),但是至少可以看到创建磁盘临时表有多频繁。仔细检查Created_tmp_disk_tables
和Created_tmp_tables
变量。 -
max_connections
这个设置的作用就像一个紧急刹车,以保证服务器不会因应用程序激增的连接而不堪重负。如果应用程序有问题,或者服务器遇到如连接延迟的问题,会创建很多新连接。但是如果不能执行查询,那打开一个连接没有好处,所以被"太多的连接"的错误拒绝是一种快速而代价小的失败方式。
把
max_connections
设置得足够高,以容纳正常可能达到的负载,并且要足够安全,能保证允许你登录和管理服务器。例如,若认为正常情况将有 300 或者更多连接,则可以设置为 500 或者更多。如果不知道将会有多少连接,500 也算是是一个合理的起点。默认值是100
, 对大部分应用来说这都不够 (MySQL 8 中默认值为151
)。要时时小心可能遇到连接限制的突然袭击。例如,若重新启动应用服务器,可能没有把它的连接关闭干净,同时 MySQL 可能没有意识到它们已经被关闭了。当应用服务器重新开始运转,并试图打开到数据库的连接,就可能由于挂起的连接还没有超时,而使新连接被拒绝。
观察
Max_used_connections
状态变量随着时间的变化。这个是高水位标记,可以告诉你服务器连接是不是在某个时间点有个尖峰。如果这个值达到了max_connections
, 说明客户端至少被拒绝了一次,并且当它重现的时候,应该使用第3章中的技巧来抓取服务器的活动状态。 -
thread_cache_size
设置这个变量,可以通过观察服务器一段时间的活动,来计算一个有理有据的值 。观察
Threads_connected
状态变量并且找到它在一般情况下的最大值和最小值。你也许希望把线程缓存设置得足够大,以在高峰和低谷时都足够,甚至可能更大方一些,因为就算设置得有点太大了,一般也不是大问题。你也许可以设置为波动范围两到三倍的大小 。例如,若Threads_connected
状态从 150 变化到 175, 可以设置线程缓存为 75。 但是也不用设置得非常大,因为保持大量等待连接的空闲线程并没有什么真正的用处。250 的上限是个不错的估算值(或者 256, 如果你喜欢 2 的次方)。也可以观察Threads_created
状态随着时间的变化。如果这个值很大或者一直增长,这是另一个线索,告诉你可能需要调大thread_cache_size
变量。查看Threads_cached
来看有多少线程已经在缓存中了。一个相关的状态变量是
Slow_launch_threads
。 这个状态如果是个很大的值,那么意味着某些情况延迟了连接分配新线程。这也是个线索,可能服务器有些问题了,但是不能明确地指出是哪出问题了。一般来说,可能是系统过载了,导致操作系统不能为新创建的线程调度 CPU。 这不是说你就需要增加线程缓存的大小了。你应该诊断这个问题并且修复它,而不是用缓存来掩盖问题,因为这还可能导致其他问题。 -
table_cache_size
这个缓存(或者在 MySQL 5.1 中被分成两个缓存区)应该被设置得足够大,以避免总是需要重新打开和重新解析表的定义 。你可以通过观察
Open_tables
的值及其在一段时间的变化来检查该变量。如果你看到Opened_tables
每秒变化很大,那么table_cache
值可能不够大。隐式临时表也可能导致打开表的数量不断增长,即使表缓存并没有用满,所以这可能也没什么问题。该问题的线索应该是
Opened_tables
不断地增长,即使Open_tables
并不跟table_cache_size
一样大。虽然表缓存很有用,也不应该把这个变量设置得太大。表缓存可能在两种情况下适得其反。
首先,MySQL 没有一个很有效的方法来检查缓存,所以如果真的太大了,可能效率会下降。在大部分情况下,不应该把它设置得大于 10000, 或者是 10240, 如果喜欢使用 2 的 N 次方的话。
第二个原因是有些类型的工作负载是不能缓存的。如果工作负载不是可缓存的,不管把缓存设置得多大,任何访问都无法在缓存命中,忘记缓存吧,把它设置为 0 ! 这可以避免情况变得更糟糕,缓存不命中比昂贵的缓存检查后再不命中还是要好的。什么类型的工作负载不是可缓存的?如果有几万或几十万张表,并且它们都很均匀地被使用,就不可能把它们全缓存了,最好把这个变量设得小一点。当系统上有数量非常多的并行应用而其中没有一个是非常忙碌的,有时候这是适当的。
这个值从
max_connections
的 10 倍开始设置是比较有道理的,但是再次说明,在大部分场景下最好保持在 10000 以下甚至更低。
还有其他一些类型的设置可能经常会包含在配置文件中,包括二进制日志以及复制设置。二进制日志对恢复到某个时间点,以及复制是非常有用的,另外复制还有一些它自己的设置。我们会在本书后面的章节中覆盖复制和备份的重要设置。
8.9 安全和稳定的设置
基本配置设置到位后,可能希望启用一些使服务器更安全和更可靠的设置。它们中的一些会影响性能,因为保证安全性和可靠性往往要付出一些代价。有些人意识到了:他们能阻止愚蠢的错误发生,比如把无意义的数据插入服务器,以及一些变动在日常操作中没有啥区别,只是在很边缘的情况防止糟糕的事情发生。
让我们首先来看看收集的一些对一般服务器都有用的配置项:
-
expire_logs_days
如果启用了二进制日志,应该打开这个选项,可以让服务器在指定的天数之后清理旧的二进制日志。如果不启用,最终服务器的空间会被耗尽,导致服务器卡住或崩溃。我们建议把这个选项设置得足够从两个备份之前恢复(在最近的备份失败的情况下)。即使每天都做备份,还是建议留下7~ 14 天的二进制日志。从我们的经验来看,当遇到一些不常见的问题时,你会感谢有这一两个星期的二进制日志。例如重搭一个备机再次尝试赶上主库。应该保持足够多的二进制日志,遇到这些情况时可以给自己一些呼吸的空间。
-
max_allowed_packet
这个设置防止服务器发送太大的包,也会控制多大的包可以被接收。默认值可能太小了,但设置得太大也可能有危险。如果设置得太小,有时复制上会出问题,通常表现为备库不能接收主库发过来的复制数据。你也许需要增加这个设置到 16 MB 或者更大 。这些文档里没有,但这个选项也控制在一个用户定义的变量的最大值,所以如果需要非常大的变量,要小心------如果超过这个变量的大小,它们可能被截断或者设置为
NULL
。 -
max_connect_errors
如果有时网络短暂抽风了,或者应用配置出现错误,或者有另外的问题,如权限,在短暂的时间内不断地尝试连接,客户端可能被列入黑名单,然后将无法连接,直到再次刷新主机缓存。这个选项的默认设置太小了,很容易导致问题。你也许希望增加这个值,实际上,如果知道服务器可以充分抵御蛮力攻击,可以把这个值设得非常大,以有效地禁用主机黑名单。
-
skip_name_resolve
这个选项禁用了另一个网络相关和鉴权认证相关的陷阱: DNS 查找 。DNS 是 MySQL 连接过程中的一个薄弱环节。当连接服务器时,默认情况下,它试图确定连接和使用的主机的主机名,作为身份验证凭据的一部分。(就是说,你的凭据是用户名,主机名、以及密码------并不只是用户名和密码)但是验证主机来源,服务器需要执行 DNS 的正向和反向查找。要是 DNS 有问题就悲剧了,在某些时间点这是必然的事。当发生这样的情况时,所有事都会堆积起来,最终导致连接超时。为了避免这种情况,我们强烈建议设置这个选项,在验证时关闭 DNS 查找。然而,如果这么做,需要把基于主机名的授权改为用 IP 地址、通配符,或者特定主机名"
localhost
", 因为基于主机名的账号会被禁用。 -
sql_mode
这个设置可以接受多种多样的值来改变服务器行为 。我们不建议只是为了好玩而改变这个值;最好在大多数情况下让 MySQL 像 MySQL, 不要尝试让它的行为像其他数据库服务器。(许多客户端和图形界面工具,除了 MySQL 还有它们自己的 SQL 方言,例如,若修改它用更符合 ANSI 的 SQL, 有些操作会没法做。)然而,有些选项值是很有用的,有些在具体情况可能是值得考虑的。建议查看文档中下面这些选项,并且考虑使用它们:
STRICT_TRANS_TABLES
、ERROR_FOR_DIVISION_BY_ZERO
、NO_AUTO_CREATE_USER
、NO_AUTO_VALUE_ON_ZERO
、NO_ENGINE_SUBSTITUTION
、NO_ZERO_DATE
、NO_ZERO_IN_DATE
和ONLY_FULL_GROUP_BY
。然而,要意识到对已经存在的应用修改这些设置值可不是个好主意,因为这么做可能让服务器跟应用预期不兼容。人们不经意间写的查询中应用的列不在
GROUP BY
中,或者使用聚合函数,这种情况非常常见,例如,若想打开ONLY_FULL_GROUP_BY
选项,最好首先在开发或未上线服务器上做一下测试,一旦要在生产环境部署则必须确认所有地方都可以工作。 -
sysdate_is_now
这是另一个可能导致与应用预期向后不兼容的选项。但如果不是明确需要
SYSDATE()
函数的非确定性行为(非确定性行为可能会导致复制中断或者使得基于时间点的备份恢复结果不可信),那么你可能希望打开该选项以确保SYSDATE()
函数有确定的行为。
下面的选项可以控制复制行为,并且对防止备库出问题非常有帮助:
-
read_only
这个选项禁止没有特权的用户在备库做变更,只接受从主库传输过来的变更,不接受从应用来的变更。我们强烈建议把备库设置为只读模式。
-
skip_slave_start
这个选项阻止 MySQL 试图自动启动复制。因为在不安全的崩溃或其他问题后,启动复制是不安全的,所以需要禁用自动启动,用户需要手动检查服务器,并确定它是安全的之后再开始复制。
-
slave_net_timeout
这个选项控制备库发现跟主库的连接已经失败并且需要重连之前等待的时间。默认值是一个小时,太长了。设置为一分钟或更短。
-
sync_master_info
、sync_relay_log
、sync_relay_log_info
这些选项,在 MySQL 5.5 以及更新版本中可用,解决了复制中备库长期存在的问题:不把它们的状态文件同步到磁盘,所以服务器崩溃后可能需要人来猜测复制的位置实际上在主库是哪个位置,并且可能在中继日志( Relay Log) 里有损坏 。这些选项使得备库崩溃后,更容易从崩溃中恢复。这些选项默认是不打开的,因为它们会导致备库额外的
fsync()
操作,可能会降低性能。如果有很好的硬件,我们建议打开这些选项,如果复制中出现fsync()
造成的延时问题,就应该关闭它们。
Percona Server 中有一种侵入性更小的方式来做这些工作,即打开 innodb_overwrite_relay_log_info
选项。这可以让 InnoDB 在事务日志中存储复制的位置,这是完全事务化的,并且不需要任何额外的 fsync()
操作。在崩溃恢复期间, InnoDB 会检查复制的元信息文件,如果文件过期了就更新为正确的位置。
8.10 高级InnoDB设置
回到第 1 章我们讨论的 InnoDB 历史:首先是内建( built-in) 的版本,然后有了两个有效版本,现在更新的版本再次变成了一个。更新的 InnoDB 代码有更多的功能和非常好的扩展性。如果正在使用 MySQL 5.1, 应该明确地配置 MySQL 忽略旧版本的 InnoDB 而使用新版的。这将极大地提升服务器性能。需要打开 ignore_builtin_innodb
选项,然后配置 plugin_load 选项把 InnoDB 作为插件打开。建议参考 InnoDB 文档中对应平台上的扩展语法。
对于新版本的 InnoDB ,有一些新的选项可以用。如果启用,它们中有些对服务器性能相当重要,也有一些安全性和稳定性的选项,如下所示。
-
innodb
这个看似平淡无奇的选项实际上非常重要,如果把这个值设置为
FORCE
,只有在 InnoDB 可以启动时,服务器才会启动。如果使用 InnoDB 作为默认存储引擎,这一定是你期望的结果。你应该不会希望在 InnoDB 失败(例如因为错误的配置而导致的不可启动)的情况下启动服务器,因为写的不好的应用可能之后会连接到服务器,导致一些无法预知的损失和混乱。最好是整个服务器都失败,强制你必须查看错误日志,而不是以为服务器正常启动了。 -
innodb_autoinc_lock_mode
这个选项控制 InnoDB 如何生成自增主键值,某些情况下,例如高并发插入时,自增主键可能是个瓶颈。如果有很多事务等待自增锁(可以在
SHOW ENGINE INNODB STATUS
里看到),应该审视这个变量的设置。手册上已经详细解释了该选项的行为,在此我们就不再重复了。 -
innodb_buffer_pool_instances
这个选项在 MySQL 5.5 和更新的版本中出现,可以把缓冲池切分为多段,这可能是在高负载的多核机器上提升 MySQL 可扩展性最重要的一个方式了。多个缓冲池分散了工作压力,所以一些全局 Mutex 竞争就没有那么大了。
目前尚不清楚什么情况下应该选择多个缓冲池实例。我们运行过八个实例的基准,但是直到 MySQL 5.5 已经广泛部署了很长一段时间,我们依然不明白多个缓冲池实例的一些微妙之处。
我们不是暗示 MySQL 5.5 没有在生产环境广泛部署。只是对我们已经帮助解决过的大部分互斥锁相互争用的极端场景的用户来说,升级可能需要很多个月的时间来计划、验证,并执行。这些用户有时运行着高度定制化的 MySQL 版本,使得更加倍谨慎地对待升级。当越来越多的这类用户升级到 MySQL 5.5, 并以他们独特的方式进行压力验证,我们可能会学到关于多缓冲池的一些我们没见过的有趣的事情。也许直到那时,我们才可以说运行八个缓冲池实例是非常有益的。
值得注意的是 Percona Server 用了不同的方法来解决 InnoDB 互斥锁争用问题。相对于把缓冲池分成多个------一个在许多像 InnoDB 的系统下经过检验无可否认的方法------我们选择把一些全局 Mutex 拆分为更细、更专用的 Mutex。 我们的测试显示最好的方式是结合这两种方法,在 Percona Server 5.5 版本中已经可用了:多缓冲区和更细粒度的锁。
-
innodb_io_capacity
InnoDB 曾经在代码里写死了假设服务器运行在每秒 100 个 I/O 操作的单硬盘上。默认值很糟糕。现在可以告诉 InnoDB 服务器有多大的 I/O 能力。InnoDB 有时需要把这个设置得相当高(在像 PCI-E SSD 这样极快的存储设备上需要设置为上万)才能稳定地刷新脏页,原因解释起来相当复杂。
-
innodb_read_io_threads
和innodb_write_io_threads
这些选项控制有多少后台线程可以被 I/O 操作使用。最近版本的 MySQL 里,默认值是 4 个读线程和 4 个写线程,对大部分服务器这都足够了,尤其是 MySQL 5.5 里面可以用操作系统原生的异步 I/O 以后。如果有很多硬盘并且工作负载并发很大,可以发现这些线程很难跟上,这种情况下可以增加线程数,或者可以简单地把这个选项的值设置为可以提供 I/O 能力的磁盘数量(即使后面是一个 RAID 控制器)。
-
innodb_strict_mode
这个设置让 MySQL 在某些条件下把警告改成抛错,尤其是无效的或者可能有风险的
CREATE TABLE
选项 。如果打开这个设置,就必然会检查所有CREATE TABLE
选项,因为它不会让你创建一些用起来比较爽(但是有隐患)的表。有时这有点悲观,过于严格了。当尝试恢复备份时可能就不希望打开这个选项了。 -
innodb_old_blocks_time
InnoDB 有个两段缓冲池 LRU( 最近最少使用)链表,设计目的是防止换出长期使用很多次的页面。像
mysqldump
产生的这种一次性的(大)查询,通常会读取页面到缓冲池的 LRU 列表,从中读取需要的行,然后移动到下一页。理论上,两段 LRU 链表将阻止此页取代很长一段时间内都需要用到的页面被放入"年轻( Young)" 子链表,并且只在它已被浏览过多次后将其移动到"年老( Old)" 子链表。但是 InnoDB 默认没有配置为防止这种情况,因为页内有很多行,所以从页面读取的行的多次访问,会导致它立即被转移到"年老( Old)" 子链表,对那些需要长时间缓存的页面带来换出的压力。这个变量指定一个页面从 LRU 链表的"年轻"部分转移到"年老"部分之前必须经过的毫秒数。默认情况下它设置为
0
, 将它设为诸如 1000 毫秒(一秒)这样的小一点的值,在我们的基准测试中已被证明非常有效。
8.11 总结
在阅读完这一章节之后,你应该有了一个比默认设置好得多的服务器配置。服务器应该更快更稳定了,并且除非运行出现了罕见的状况,都应该没有必要再去做优化配置的工作了。
复习一下,我们建议从参考示例配置文件开始,设置符合服务器和工作负载的基本选项,增加安全性和完整性所需的选项,并且,如果合适的话,在 MySQL 5.5 中配置新版的 InnoDB Plugin 才有的配置项。这就是关于优化服务器配置所需要做的全部的事情。
如果使用的是 InnoDB ,最重要的选项是下面这两个:
innodb_buffer_pool_size
innodb_log_file_size
恭喜你------你解决了我们见过的真实存在的配置问题中的绝大部分!如果使用我们的在线配置工具 http://tools.percona.com ,对这些问题和其他配置选项的使用,会得到很好的建议。
我们也提出了很多关于不要做什么的建议。其中最重要的是不要"调优"服务器;不要使用比率、公式或"调优脚本"作为设置配置变量的基础;不要信任来自互联网上的不明身份的人的意见;不要为了看起来很糟糕的事情去不断地刷 SHOW STATUS
。如果有些设置其实是错误的,在剖析服务器性能时也会展现出来。
有几个重要的设置没有在本章讨论,主要是因为它们是为特定类型的硬件和工作负载服务的。我们暂不讨论这些设置,因为我们相信,任何关于怎样设置的意见,都需要与内部流程的解释工作一起来做。这给我们带来了下一章,它会告诉你如何优化 MySQL 的硬件和操作系统,反之亦然。