高性能 MySQL 第四版(GPT 重译)(一)

前言

由 Oracle 维护的官方文档为您提供了安装、配置和与 MySQL 交互所需的知识。本书作为该文档的伴侣,帮助您了解如何最好地利用 MySQL 作为强大的数据平台来满足您的用例需求。

本版还扩展了合规性和安全性在操作数据库足迹中的日益重要作用。隐私法律和数据主权等新现实已经改变了公司产品构建的方式,这自然地引入了技术架构如何演变的新复杂性。

本书适合对象

本书首先面向希望提升在运行 MySQL 方面的专业知识的工程师。本版假设读者熟悉为什么要使用关系数据库管理系统(RDBMS)的基本原则。我们还假设读者具有一些一般系统管理、网络和操作系统的经验。

我们将为您提供在现代架构和更为更新的工具和实践下运行 MySQL 的经验丰富的策略。

最终,我们希望您从本书中对 MySQL 内部和扩展策略的知识中获益,帮助您在组织中扩展数据存储层。我们希望您新获得的见解将帮助您学习和实践一种系统化的方法来设计、维护和故障排除基于 MySQL 构建的架构。

这个版本有何不同

高性能 MySQL已经成为数据库工程社区多年的一部分,之前的版本分别在 2004 年、2008 年和 2012 年发布。在这些先前的版本中,目标始终是通过专注于深度内部设计,解释各种调整设置的含义,并为用户提供改变这些设置的知识,教导开发人员和管理员如何优化 MySQL 以获得最佳性能。本版保持了相同的目标,但侧重点不同。

自第三版以来,MySQL 生态系统发生了许多变化。发布了三个新的主要版本。工具景观大大扩展,超越了 Perl 和 Bash 脚本,进入了完整的工具解决方案。全新的开源项目已经建立,改变了组织如何管理扩展 MySQL 的方式。

甚至传统的数据库管理员(DBA)角色也发生了变化。行业中有一个老笑话说 DBA 代表"别烦问"。DBA 因为数据库没有像周围的软件开发生命周期(SDLC)一样快速发展而被认为是软件开发生命周期中的绊脚石,这并不是因为他们有什么脾气暴躁的态度,而只是因为数据库的发展速度没有跟上周围 SDLC 的步伐。

像 Laine Campbell 和 Charity Majors(O'Reilly)合著的数据库可靠性工程:设计和运营弹性数据库系统这样的书籍,已经成为技术组织将数据库工程师视为业务增长的推动者而不是所有数据库的唯一运营者的新现实。曾经 DBA 的主要日常工作涉及模式设计和查询优化,现在他们负责教导开发人员这些技能,并管理允许开发人员快速安全地部署自己模式更改的系统。

通过这些变化,重点不再是优化 MySQL 以获得几个百分点的速度。我们认为高性能 MySQL 现在是为人们提供他们需要的信息,以便就如何最好地使用 MySQL 做出明智决策。这始于理解 MySQL 的设计,然后理解 MySQL 擅长和不擅长的地方。¹ 现代版本的 MySQL 提供合理的默认设置,除非您遇到非常具体的扩展问题,否则几乎不需要进行调整。现代团队现在正在处理模式更改、合规问题和分片。我们希望高性能 MySQL成为现代公司如何大规模运行 MySQL 的全面指南。

本书使用的约定

本书使用以下排版约定:

斜体

表示新术语、URL、电子邮件地址、文件名和文件扩展名。

常量宽度

用于程序清单,以及段落内引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。

常量宽度粗体

显示用户应按照字面意义输入的命令或其他文本。

常量宽度斜体

显示应替换为用户提供的值或由上下文确定的值的文本。

提示

此图标表示提示或建议。

注意

此图标表示一般说明。

警告

此图标表示警告或注意事项。

第四版致谢

来自 Silvia

首先,我要感谢我的家人。我的父母为了把我和我的兄弟带到美国,牺牲了在埃及的稳定工作和生活。我的丈夫 Armea,在我接受挑战的过去几年中一直支持我,最终实现了这一成就。

我作为一个移民从中东的大学时代离开,实现了移居美国的梦想。在加利福尼亚州的一所州立大学获得学位后,我在纽约市找到了一份工作,我记得这本书的第二版是我用自己的钱买的第一本不是教科书的技术书籍。我要感谢前几版的作者教给我许多基本的经验,为我在职业生涯中管理数据库做好准备。

我感激我职业生涯中与之合作过的许多人的支持。他们的鼓励让我写下了这本书的这一版,这本书在我职业生涯的早期教会了我很多。我要感谢 Tim Jenkins,SendGrid 的前首席技术官,他雇佣了我这份终身职位,尽管我在面试中告诉他他错误地使用了 MySQL 复制,但他还是信任我,结果证明这是一艘火箭。

我要感谢所有在科技领域的了不起的女性,她们是我的支持网络和啦啦队。特别感谢 Camille Fournier 和 Nicole Forsgren 博士,因为她们写的两本书影响了我过去几年的职业生涯,改变了我对日常工作的看法。

感谢我在 Twilio 团队的同事们。感谢 Sean Kilgore 让我成为一个更优秀的工程师,关心的不仅仅是数据库。感谢 John Martin 是我曾经合作过的最乐观的人。感谢 Laine Campbell 及其 PalominoDB 团队(后来被 Pythian 收购)在我最艰难的岁月中给予的支持和教导,以及 Baron Schwartz 鼓励我写下我的经历。

最后,感谢 Virginia Wilson 是一位出色的编辑,帮助我将我的思绪转化为通顺的句子,并在整个过程中给予我如此多的支持和优雅。

来自 Jeremy

当 Silvia 找我帮忙写这本书时,正值大多数人生活中异常紧张的时期------全球大流行,始于 2020 年。我不确定是否想要给自己的生活增加更多压力。我的妻子 Selena 告诉我,如果我不接受,我会后悔的,而我知道不应该和她争论。她一直支持我,鼓励我成为我能成为的最好的人。我将永远爱她,感激她为我所做的一切。

致我的家人、同事和社区朋友们:没有你们,我绝对不可能走到今天这一步。你们教会了我如何成为今天的自己。我的职业生涯是与你们共同经历的总和。你们教会了我如何接受批评,如何以身作则领导,如何失败并重新站起,最重要的是,团队的力量胜过个人的能力。

最后,我要感谢 Silvia,她信任我为这本书带来共同的理解但不同的视角。我希望我达到了你的期望。

致技术审阅者

作者还要感谢帮助将这本书推向今天这一地步的技术审阅者们:Aisha Imran、Andrew Regner、Baron Schwartz、Daniel Nichter、Hayley Anderson、Ivan Mora Perez、Jam Leoni、Jaryd Remillard、Jennifer Davis、Jeremy Cole、Keith Wells、Kris Hamoud、Nick Vyzas、Shubheksha Jalan、Tom Krouper 和 Will Gunty。感谢你们的时间和努力。

¹ 众所周知,人们经常将 MySQL 用作队列,然后才发现这样做是错误的。最常见的原因是轮询新队列操作的开销、锁定记录以进行处理的管理,以及随着数据增长而变得笨重的队列表格。

第一章:MySQL 架构

MySQL 的架构特点使其适用于各种用途。虽然它并非完美,但足够灵活,可以在小型和大型环境中都能很好地运行。从个人网站到大型企业应用程序都适用。要充分利用 MySQL,您需要了解其设计,以便与之合作,而不是对抗它。

本章概述了 MySQL 服务器架构的高层概述,存储引擎之间的主要区别以及这些区别的重要性。我们试图通过简化细节并展示示例来解释 MySQL。这个讨论对于那些对数据库服务器新手以及对其他数据库服务器是专家的读者都将有用。

MySQL 的逻辑架构

对 MySQL 组件如何协同工作有一个清晰的心理图像将有助于您理解服务器。图 1-1 展示了 MySQL 架构的逻辑视图。

最顶层的层级是客户端,包含的服务并非 MySQL 独有。这些服务是大多数基于网络的客户端/服务器工具或服务器所需的服务:连接处理、身份验证、安全等。

第二层是事情变得有趣的地方。MySQL 的大部分智慧都在这里,包括查询解析、分析、优化以及所有内置功能的代码(例如日期、时间、数学和加密)。在这个层面提供的任何功能都跨存储引擎:存储过程、触发器和视图,例如。

第三层包含存储引擎。它们负责存储和检索 MySQL 中存储的所有数据。就像 GNU/Linux 中提供的各种文件系统一样,每个存储引擎都有其自己的优点和缺点。服务器通过存储引擎 API 与它们通信。该 API 隐藏了存储引擎之间的差异,并在查询层面上使它们基本透明。它还包含几十个低级函数,执行诸如"开始事务"或"获取具有此主键的行"等操作。存储引擎不解析 SQL¹,也不相互通信;它们只是响应服务器的请求。

图 1-1 MySQL 服务器架构的逻辑视图

连接管理和安全

默认情况下,每个客户端连接在服务器进程内部都有自己的线程。连接的查询在该单个线程内执行,该线程又位于一个核心或 CPU 上。服务器维护一个准备好使用的线程缓存,因此它们不需要为每个新连接创建和销毁。²

当客户端(应用程序)连接到 MySQL 服务器时,服务器需要对其进行身份验证。身份验证基于用户名、来源主机和密码。也可以在传输层安全(TLS)连接中使用 X.509 证书。一旦客户端连接,服务器会验证客户端是否对其发出的每个查询具有权限(例如,客户端是否被允许发出访问 world 数据库中 Country 表的 SELECT 语句)。

优化和执行

MySQL 解析查询以创建内部结构(解析树),然后应用各种优化。这些优化包括重写查询、确定读取表的顺序、选择使用哪些索引等。您可以通过查询中的特殊关键字向优化器传递提示,影响其决策过程。您还可以要求服务器解释优化的各个方面。这让您了解服务器正在做出的决策,并为重新调整查询、模式和设置提供参考,使一切尽可能高效地运行。在第八章中有更详细的内容。

优化器并不真正关心特定表使用的存储引擎是什么,但存储引擎确实会影响服务器优化查询的方式。优化器向存储引擎询问一些能力以及某些操作的成本,还会请求表数据的统计信息。例如,某些存储引擎支持对某些查询有帮助的索引类型。您可以在第六章和第七章中了解更多关于模式优化和索引的内容。

在旧版本中,MySQL 利用内部查询缓存来查看是否可以从中提供结果。然而,随着并发性的增加,查询缓存成为一个臭名昭著的瓶颈。截至 MySQL 5.7.20,查询缓存正式被废弃为 MySQL 的一个特性,并在 8.0 版本中,查询缓存被完全移除。尽管查询缓存不再是 MySQL 服务器的核心部分,但缓存频繁提供的结果集是一个好的实践。虽然超出了本书的范围,但一个流行的设计模式是在 memcached 或 Redis 中缓存数据。

并发控制

每当多个查询需要同时更改数据时,就会出现并发控制问题。对于本章的目的,MySQL 必须在两个级别进行并发控制:服务器级别和存储引擎级别。我们将为您简要介绍 MySQL 如何处理并发读取和写入,以便您在本章的其余部分中获得所需的背景知识。

为了说明 MySQL 如何处理对同一组数据的并发工作,我们将以传统的电子表格文件为例。电子表格由行和列组成,就像数据库表一样。假设文件在您的笔记本电脑上,并且只有您可以访问它。没有潜在的冲突;只有您可以对文件进行更改。现在,想象您需要与同事共同使用该电子表格。它现在位于您和同事都可以访问的共享服务器上。当您和同事同时需要对此文件进行更改时会发生什么?如果我们有一个整个团队的人正在积极尝试编辑、添加和删除此电子表格中的单元格,会发生什么?我们可以说他们应该轮流进行更改,但这并不高效。我们需要一种允许高容量电子表格并发访问的方法。

读/写锁

从电子表格中读取并不那么麻烦。多个客户端同时读取同一文件没有问题;因为他们没有进行更改,所以不太可能出错。如果有人尝试在其他人正在读取电子表格时删除A25单元格会发生什么?这取决于情况,但读者可能会得到损坏或不一致的数据视图。因此,即使从电子表格中读取也需要特别小心。

如果您将电子表格视为数据库表,很容易看出在这种情况下问题是相同的。在许多方面,电子表格实际上只是一个简单的数据库表。修改数据库表中的行与删除或更改电子表格文件中的单元格内容非常相似。

解决这个经典的并发控制问题相当简单。处理并发读/写访问的系统通常实现由两种锁类型组成的锁定系统。这些锁通常被称为共享锁排他锁,或读锁和写锁。

不用担心实际的锁定机制,我们可以描述概念如下。对于资源的读锁 是共享的,或者说是相互非阻塞的:许多客户端可以同时从资源中读取,而不会相互干扰。另一方面,写锁是排他的------也就是说,它们会阻止读锁和其他写锁------因为唯一安全的策略是在给定时间内只允许单个客户端向资源写入,并在客户端写入时阻止所有读取。

在数据库世界中,锁定一直在发生:MySQL 必须防止一个客户端在另一个客户端更改数据时读取数据。如果数据库服务器表现得符合要求,那么锁定的管理速度足够快,以至于客户端几乎察觉不到。我们将在第八章中讨论如何调整查询以避免由锁定引起的性能问题。

锁定粒度

提高共享资源并发性的一种方法是更加选择性地锁定你要锁定的内容。而不是锁定整个资源,只锁定包含你需要更改的数据的部分。更好的是,只锁定你计划更改的确切数据片段。在任何给定时间最小化你锁定的数据量,让对给定资源的更改可以同时发生,只要它们不相互冲突。

不幸的是,锁并不是免费的------它们会消耗资源。每个锁操作------获取锁、检查锁是否空闲、释放锁等------都有开销。如果系统花费太多时间管理锁而不是存储和检索数据,性能可能会受到影响。

锁定策略是锁定开销和数据安全之间的一种折衷,这种折衷会影响性能。大多数商用数据库服务器并不给你太多选择:在你的表中,你得到的是所谓的行级锁定,有各种复杂的方式来提供许多锁的良好性能。锁是数据库如何实现一致性保证的方式。一个数据库的专家操作员必须深入阅读源代码,以确定最适合的一组调整配置,以优化速度与数据安全之间的这种权衡。

另一方面,MySQL 提供了选择。它的存储引擎可以实现自己的锁定策略和锁定粒度。锁管理是存储引擎设计中非常重要的决定;将粒度固定在某个级别可以提高某些用途的性能,但使该引擎不太适合其他用途。因为 MySQL 提供了多个存储引擎,它不需要一个单一的通用解决方案。让我们看看两种最重要的锁定策略。

表锁

MySQL 中最基本的锁定策略,也是开销最低的策略是表锁。表锁 类似于前面描述的电子表格锁:它锁定整个表。当客户端希望写入表(插入、删除、更新等)时,它会获取写锁。这会阻止所有其他读取和写入操作。当没有人在写入时,读者可以获取读锁,这些读锁不会与其他读锁冲突。

表锁在特定情况下有改进性能的变体。例如,READ LOCAL 表锁允许某些类型的并发写操作。写锁和读锁队列是分开的,写队列完全比读队列的优先级高。³

行锁

提供最大并发性(并带来最大开销)的锁定风格是使用行锁。回到电子表格的类比,行锁 就像只锁定电子表格中的行一样。这种策略允许多人同时编辑不同的行,而不会相互阻塞。这使服务器能够进行更多并发写入,但代价是需要跟踪谁拥有每个行锁,它们已经打开多久,以及它们是什么类型的行锁,以及在不再需要时清理锁。

行锁是在存储引擎中实现的,而不是在服务器中。服务器大部分时间⁴ 对在存储引擎中实现的锁定是不知情的,正如你将在本章和整本书中看到的,存储引擎都以自己的方式实现锁定。

事务

在深入研究数据库系统的更高级功能之前,你会发现事务的存在。事务是一组 SQL 语句,被视为一个原子单元的工作。如果数据库引擎可以将整组语句应用到数据库中,它会这样做,但如果由于崩溃或其他原因无法完成其中任何一个,那么所有语句都不会被应用。要么全部成功,要么全部失败。

这一部分与 MySQL 无关。如果你已经熟悉 ACID 事务,请随时跳到"MySQL 中的事务"。

银行应用程序是为什么需要事务的经典例子。想象一个银行的数据库有两个表:支票和储蓄。要将$200 从简的支票账户转移到她的储蓄账户,你需要至少执行三个步骤:

  1. 确保她的支票账户余额大于$200。

  2. 从她的支票账户余额中减去$200。

  3. 给她的储蓄账户余额加上$200。

整个操作应该包裹在一个事务中,这样如果任何一个步骤失败,已完成的步骤可以被回滚。

你可以使用START TRANSACTION语句开始一个事务,然后使用COMMIT使其更改永久化,或者使用ROLLBACK放弃更改。因此,我们示例事务的 SQL 可能如下所示:

sql 复制代码
1  START  TRANSACTION;
2  SELECT balance FROM checking WHERE customer_id = 10233276;
3  UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
4  UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
5  COMMIT;

事务本身并不是全部。如果数据库服务器在执行第 4 行时崩溃会发生什么?谁知道呢?客户可能刚刚损失了$200。如果另一个进程在第 3 行和第 4 行之间出现并移除整个支票账户余额会发生什么?银行已经给客户提供了$200 的信用,甚至自己都不知道。

在这个操作序列中还有很多失败的可能性。你可能会遇到连接中断、超时,甚至在操作中途数据库服务器崩溃。这通常是为什么高度复杂和缓慢的两阶段提交系统存在的原因:以减轻各种故障场景。

事务并不足够,除非系统通过 ACID 测试。ACID 代表原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。这些是数据安全事务处理系统必须满足的紧密相关标准:

原子性

事务必须作为一个单一不可分割的工作单元运行,以便整个事务要么被应用,要么永远不被提交。当事务是原子的时,不存在部分完成的事务:要么全部成功,要么全部失败。

一致性

数据库应该始终从一个一致的状态转移到下一个一致的状态。在我们的示例中,一致性确保在第 3 行和第 4 行之间发生崩溃时,支票账户中不会消失$200。如果事务从未提交,事务的任何更改都不会反映在数据库中。

隔离性

事务的结果通常对其他事务是不可见的,直到事务完成。这确保如果在我们的示例中的第 3 行和第 4 行之间运行银行账户摘要,它仍然会看到支票账户中的$200。当我们在本章后面讨论隔离级别时,你会明白为什么我们说"通常不可见"。

持久性

一旦提交,事务的更改就是永久的。这意味着更改必须被记录,以防止在系统崩溃时丢失数据。然而,持久性是一个稍微模糊的概念,因为实际上有许多级别。一些持久性策略提供比其他更强的安全保证,而且没有什么是 100%持久的(如果数据库本身真的是持久的,那么备份如何增加持久性呢?)。

ACID 事务和 InnoDB 引擎特别提供的保证是 MySQL 中最强大和最成熟的功能之一。虽然它们会带来一定的吞吐量折衷,但当适当应用时,它们可以避免在应用层实现大量复杂逻辑。

隔离级别

隔离性比看起来更复杂。ANSI SQL 标准定义了四个隔离级别。如果您是数据库领域的新手,我们强烈建议您在阅读有关具体 MySQL 实现之前熟悉 ANSI SQL 的一般标准⁶。该标准的目标是定义更改在事务内外何时可见和何时不可见的规则。较低的隔离级别通常允许更高的并发性并具有较低的开销。

注意

每个存储引擎对隔离级别的实现略有不同,并且不一定与您习惯于其他数据库产品时所期望的相匹配(因此,在本节中我们不会详细介绍)。您应该阅读您决定使用的任何存储引擎的手册。

让我们快速看一下四个隔离级别:

READ UNCOMMITTED

READ UNCOMMITTED隔离级别中,事务可以查看未提交事务的结果。在这个级别,除非您真的非常了解自己在做什么并且有充分的理由这样做,否则可能会发生许多问题。这个级别在实践中很少使用,因为其性能并不比其他级别好多少,而其他级别有许多优势。读取未提交的数据也被称为脏读

READ COMMITTED

大多数数据库系统(但不包括 MySQL!)的默认隔离级别是READ COMMITTED。它满足先前使用的隔离的简单定义:事务将继续看到在其开始后提交的事务所做的更改,并且其更改在提交之前对其他人不可见。这个级别仍然允许所谓的不可重复读。这意味着您可以两次运行相同的语句并看到不同的数据。

REPEATABLE READ

REPEATABLE READ解决了READ UNCOMMITTED允许的问题。它保证事务读取的任何行在同一事务内的后续读取中"看起来相同",但理论上仍允许另一个棘手的问题:幻读。简而言之,当您选择某些行的范围时,另一个事务将新行插入到该范围中,然后再次选择相同范围时,您将看到新的"幻影"行。InnoDB 和 XtraDB 通过多版本并发控制解决了幻读问题,我们稍后在本章中解释。

REPEATABLE READ是 MySQL 的默认事务隔离级别。

SERIALIZABLE

最高级别的隔离是SERIALIZABLE,通过强制事务按顺序排列以避免可能发生冲突来解决幻读问题。简而言之,SERIALIZABLE在读取每一行时都会放置一个锁。在这个级别,可能会发生很多超时和锁争用。我们很少看到人们使用这种隔离级别,但您的应用程序需求可能迫使您接受降低的并发性以换取数据安全性。

表 1-1 总结了各种隔离级别及与每个级别相关的缺点。

表 1-1. ANSI SQL 隔离级别

隔离级别 是否可能出现脏读 是否可能出现不可重复读 是否可能出现幻读 锁定读取
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

死锁

死锁 是指两个或多个事务相互持有并请求相同资源上的锁,从而创建了依赖循环。当事务尝试以不同顺序锁定资源时,就会发生死锁。无论何时多个事务锁定相同资源,都可能发生死锁。例如,考虑这两个针对StockPrice表运行的事务,该表具有主键(stock_id, date)

事务 1

sql 复制代码
START TRANSACTION;
UPDATE StockPrice SET close = 45.50 WHERE stock_id = 4 and date = '2020-05-01';
UPDATE StockPrice SET close = 19.80 WHERE stock_id = 3 and date = '2020-05-02';
COMMIT;

事务 2

sql 复制代码
START TRANSACTION;
UPDATE StockPrice SET high = 20.12 WHERE stock_id = 3 and date = '2020-05-02';
UPDATE StockPrice SET high = 47.20 WHERE stock_id = 4 and date = '2020-05-01';
COMMIT;

每个事务将执行其第一个查询并更新一行数据,将该行在主键索引中锁定,并在此过程中锁定其所属的任何其他唯一索引。然后,每个事务将尝试更新其第二行,只能发现它已被锁定。除非有某种干预来打破死锁,否则这两个事务将永远等待对方完成。我们在第七章中进一步介绍索引如何在架构演变过程中影响查询的性能。

为了解决这个问题,数据库系统实现了各种形式的死锁检测和超时。更复杂的系统,如 InnoDB 存储引擎,将注意到循环依赖关系并立即返回错误。这可能是一件好事------否则,死锁将表现为非常慢的查询。其他系统在查询超过锁等待超时后会放弃,这并不总是好事。InnoDB 目前处理死锁的方式是回滚具有最少独占行锁的事务(这是一个近似指标,哪个事务最容易回滚)。

锁行为和顺序是存储引擎特定的,因此一些存储引擎可能会在某些语句序列上发生死锁,即使其他存储引擎不会。死锁具有双重性质:一些是由于真实数据冲突而不可避免的,一些是由存储引擎的工作方式引起的。⁷

一旦发生死锁,就无法在不部分或完全回滚其中一个事务的情况下解除死锁。在事务系统中,死锁是生活中的一个事实,您的应用程序应设计为处理它们。许多应用程序可以简单地从头开始重试它们的事务,除非遇到另一个死锁,否则它们应该成功。

事务日志

事务日志有助于使事务更高效。存储引擎可以在每次更改发生时更新磁盘上的表之前更改其内存中的数据副本。这是非常快的。然后,存储引擎可以将更改记录写入事务日志,该日志位于磁盘上,因此是持久的。这也是一个相对快速的操作,因为追加日志事件涉及磁盘上一个小区域的顺序 I/O,而不是在许多地方进行随机 I/O。然后,在以后的某个时间,一个进程可以更新磁盘上的表。因此,大多数使用这种技术(称为预写式日志记录)的存储引擎最终会将更改写入磁盘两次。

如果在更新写入事务日志后但在更改数据本身之前发生崩溃,则存储引擎仍然可以在重新启动时恢复更改。恢复方法因存储引擎而异。

MySQL 中的事务

存储引擎是驱动数据如何存储和从磁盘检索的软件。虽然 MySQL 传统上提供了许多支持事务的存储引擎,但 InnoDB 现在是金标准和推荐使用的引擎。这里描述的事务基元将基于 InnoDB 引擎中的事务。

理解 AUTOCOMMIT

默认情况下,单个INSERTUPDATEDELETE语句会隐式包装在一个事务中并立即提交。这被称为AUTOCOMMIT模式。通过禁用此模式,您可以在事务中执行一系列语句,并在结束时COMMITROLLBACK

你可以通过使用SET命令为当前连接启用或禁用AUTOCOMMIT变量。值1ON是等效的,0OFF也是如此。当你运行时AUTOCOMMIT=0,你总是处于一个事务中,直到你发出COMMITROLLBACK。然后 MySQL 立即开始一个新的事务。此外,启用AUTOCOMMIT后,你可以使用关键字BEGINSTART TRANSACTION开始一个多语句事务。改变AUTOCOMMIT的值对非事务表没有影响,这些表没有提交或回滚更改的概念。

在打开事务期间发出某些命令会导致 MySQL 在执行之前提交事务。这些通常是进行重大更改的 DDL 命令,如ALTER TABLE,但LOCK TABLES和其他一些语句也有这种效果。查看你版本的文档以获取自动提交事务的完整命令列表。

MySQL 允许你使用SET TRANSACTION ISOLATION LEVEL命令设置隔离级别,该命令在下一个事务开始时生效。你可以在配置文件中为整个服务器设置隔离级别,也可以仅为你的会话设置:

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

最好在服务器级别设置你最常用的隔离级别,并仅在明确的情况下更改它。MySQL 识别所有四个 ANSI 标准隔离级别,而 InnoDB 支持它们全部。

在事务中混合存储引擎

MySQL 不会在服务器级别管理事务。相反,底层存储引擎自己实现事务。这意味着你不能可靠地在单个事务中混合不同的引擎。

如果你在一个事务中混合使用事务表和非事务表(例如,InnoDB 和 MyISAM 表),如果一切顺利,事务将正常工作。然而,如果需要回滚,对非事务表的更改无法撤消。这将使数据库处于一个不一致的状态,可能很难恢复,并使事务的整个目的变得无意义。这就是为什么非常重要为每个表选择正确的存储引擎,并尽量避免在应用逻辑中混合存储引擎。

如果你在非事务表上执行事务操作,MySQL 通常不会警告你或引发错误。有时回滚事务会生成警告:"一些非事务更改的表无法回滚",但大多数情况下,你不会得到任何指示你正在使用非事务表。

警告

最好的做法是不要在应用程序中混合存储引擎。失败的事务可能导致不一致的结果,因为某些部分可以回滚,而其他部分则无法回滚。

隐式和显式锁定

InnoDB 使用两阶段锁定协议。它可以在事务期间的任何时候获取锁,但直到COMMITROLLBACK才会释放锁。它同时释放所有锁。前面描述的锁定机制都是隐式的。InnoDB 根据你的隔离级别自动处理锁。

然而,InnoDB 也支持显式锁定,SQL 标准根本没有提到:⁸^,⁹

sql 复制代码
SELECT ... FOR SHARE
SELECT ... FOR UPDATE

MySQL 还支持LOCK TABLESUNLOCK TABLES命令,这些命令在服务器中实现,而不是在存储引擎中。如果你需要事务,请使用事务性存储引擎。LOCK TABLES是不必要的,因为 InnoDB 支持行级锁定。

提示

LOCK TABLES和事务之间的交互是复杂的,在某些服务器版本中存在意外行为。因此,我们建议无论使用哪种存储引擎,都不要在事务中使用LOCK TABLES

多版本并发控制

MySQL 的大多数事务性存储引擎不使用简单的行级锁定机制。相反,它们与一种称为*多版本并发控制(MVCC)*的增加并发性技术结合使用行级锁定。MVCC 并不是 MySQL 独有的:Oracle、PostgreSQL 和一些其他数据库系统也使用它,尽管存在重大差异,因为 MVCC 应如何工作没有标准。

您可以将 MVCC 视为对行级锁定的一种变通方法;在许多情况下,它避免了锁定的需要,并且开销要低得多。根据实现方式,它可以允许非锁定读取,同时仅在写入操作期间锁定必要的行。

MVCC 通过使用数据在某个时间点存在的快照来工作。这意味着事务可以看到数据的一致视图,无论它们运行多长时间。这也意味着不同的事务可以同时在相同的表中看到不同的数据!如果您以前从未经历过这种情况,可能会感到困惑,但随着熟悉度的增加,您会更容易理解。

每个存储引擎都以不同方式实现 MVCC。一些变体包括乐观和悲观并发控制。我们通过解释 InnoDB 的行为来说明 MVCC 的一种工作方式,形式为图 1-2 中的序列图。

InnoDB 通过为每个启动的事务分配事务 ID 来实现 MVCC。该 ID 是在事务第一次读取任何数据时分配的。当在该事务内修改记录时,将向撤销日志写入解释如何撤消该更改的撤销记录,并且事务的回滚指针指向该撤销日志记录。这就是事务可以找到回滚的方法的方式。

图 1-2。处理不同事务中一行的多个版本的序列图

当不同会话读取群集键索引记录时,InnoDB 会比较记录的事务 ID 与该会话的读取视图。如果记录在当前状态下不应可见(更改它的事务尚未提交),则会跟随并应用撤销日志记录,直到会话达到可以可见的事务 ID。这个过程可以一直循环到一个完全删除此行的撤销记录,向读取视图发出此行不存在的信号。

通过在记录的"信息标志"中设置"删除"位来删除事务中的记录。这也在撤销日志中跟踪为"删除标记"。

值得注意的是,所有撤销日志写入也都会被重做记录,因为撤销日志写入是服务器崩溃恢复过程的一部分,并且是事务性的。[¹¹] 这些重做和撤销日志的大小也在高并发事务执行中扮演着重要角色。我们将在第五章中更详细地介绍它们的配置。

所有这些额外的记录保留的结果是,大多数读取查询从不获取锁。它们只是尽可能快地读取数据,确保只选择符合条件的行。缺点是存储引擎必须在每行存储更多数据,在检查行时做更多工作,并处理一些额外的管理操作。

MVCC 仅适用于REPEATABLE READREAD COMMITTED隔离级别。READ UNCOMMITTED不兼容 MVCC,因为查询不会读取适合其事务版本的行版本;无论如何,它们都会读取最新版本。SERIALIZABLE不兼容 MVCC,因为读取会锁定它们返回的每一行。

复制

MySQL 设计用于在任何给定时间接受一个节点上的写入。这在管理一致性方面具有优势,但在需要将数据写入多个服务器或多个位置时会产生折衷。MySQL 提供了一种本地方法来将一个节点接受的写入分发到其他节点。这被称为复制 。在 MySQL 中,源节点每个副本都有一个线程作为复制客户端登录,当发生写入时会唤醒,发送新数据。在图 1-3 中,我们展示了这种设置的简单示例,通常称为源和副本设置中的多个 MySQL 服务器的拓扑树

图 1-3. MySQL 服务器复制拓扑的简化视图

对于在生产环境中运行的任何数据,您应该使用复制,并至少有三个以上的副本,最好分布在不同位置(在云托管环境中称为区域)以进行灾难恢复规划。

多年来,MySQL 中的复制变得更加复杂。全局事务标识符、多源复制、副本上的并行复制和半同步复制是一些主要更新。我们在第九章中详细介绍了复制。

数据文件结构

在 8.0 版本中,MySQL 将表元数据重新设计为包含在表的*.ibd文件中的数据字典。这使得关于表结构的信息支持事务和原子数据定义更改。在操作期间检索表定义和元数据不再仅依赖于information_schema,我们引入了字典对象缓存,这是一个基于最近最少使用(LRU)的内存缓存,其中包含分区定义、表定义、存储程序定义、字符集和校对信息。服务器访问表的元数据的这一重大变化减少了 I/O,尤其是如果一部分表是最活跃的并且因此最常见于缓存中的话,这是有效的。 .ibd* 和 .frm 文件被替换为每个表的序列化字典信息(.sdi)。

InnoDB 引擎

InnoDB 是 MySQL 的默认事务存储引擎,也是最重要和最广泛使用的引擎。它设计用于处理许多短暂事务,这些事务通常会完成而不是回滚。其性能和自动崩溃恢复使其在非事务性存储需求中也很受欢迎。如果您想研究存储引擎,深入学习 InnoDB 是值得的,以尽可能多地了解它,而不是平等地研究所有存储引擎。

注意

最佳实践是将 InnoDB 存储引擎作为任何应用程序的默认引擎。MySQL 通过几个主要版本之前将 InnoDB 设为默认引擎,使这一点变得容易。

InnoDB 是默认的 MySQL 通用存储引擎。默认情况下,InnoDB 将其数据存储在一系列数据文件中,这些文件统称为表空间。表空间本质上是 InnoDB 自行管理的一个黑盒。

InnoDB 使用 MVCC 实现高并发,并实现了所有四个 SQL 标准隔离级别。它默认使用REPEATABLE READ隔离级别,并具有防止在此隔离级别中出现幻读的 next-key 锁定策略:InnoDB 不仅锁定您在查询中触及的行,还锁定索引结��中的间隙,防止插入幻影。

InnoDB 表是建立在聚簇索引上的,我们将在第八章中详细讨论架构设计时进行介绍。InnoDB 的索引结构与大多数其他 MySQL 存储引擎非常不同。因此,它提供非常快速的主键查找。但是,次要索引(非主键的索引)包含主键列,因此如果您的主键很大,其他索引也会很大。如果您将在表上有许多索引,应该努力使主键尽可能小。

InnoDB 具有各种内部优化。这些包括从磁盘读取数据的预测性读取,自适应哈希索引自动在内存中构建哈希索引以进行非常快速的查找,以及插入缓冲区以加快插入速度。我们将在本书的第四章中介绍这些内容。

InnoDB 的行为非常复杂,如果你正在使用 InnoDB,我们强烈建议阅读 MySQL 手册中的"InnoDB 锁定和事务模型"部分。由于其 MVCC 架构,建议在使用 InnoDB 构建应用程序之前,您应该了解许多微妙之处。

作为事务性存储引擎,InnoDB 通过各种机制支持真正的"热"在线备份,包括 Oracle 的专有 MySQL 企业备份和开源 Percona XtraBackup。我们将在第十章中详细讨论备份和恢复。

从 MySQL 5.6 开始,InnoDB 引入了在线 DDL,在最初的版本中有限的用例在 5.7 和 8.0 版本中得到扩展。原地模式更改允许进行特定表更改而无需完全锁定表,也无需使用外部工具,这极大地提高了 MySQL InnoDB 表的操作性。我们将在第六章中涵盖在线模式更改的选项,包括本机和外部工具。

JSON 文档支持

JSON 类型是作为 5.7 版本的一部分首次引入 InnoDB 的,它具有 JSON 文档的自动验证以及优化的存储,可以快速读取访问,这对于旧式二进制大对象(BLOB)存储工程师过去常常使用的权衡来说是一个重大改进。除了新的数据类型支持外,InnoDB 还引入了支持 JSON 文档的 SQL 函数。MySQL 8.0.7 中的进一步改进增加了在 JSON 数组上定义多值索引的能力。这个功能可以通过将常见访问模式与能够映射 JSON 文档值的函数匹配,进一步加快对 JSON 类型的读取访问查询。我们将在第六章中的"JSON 数据"中讨论 JSON 数据类型的使用和性能影响。

数据字典更改

MySQL 8.0 的另一个重大变化是删除基于文件的表元数据存储,并转而使用 InnoDB 表存储的数据字典。这一变化将 InnoDB 的所有崩溃恢复事务性优势带到了表更改等操作中。这一变化虽然极大地改进了 MySQL 中数据定义的管理,但也需要对 MySQL 服务器的操作进行重大更改。特别值得注意的是,以前依赖表元数据文件的备份过程现在必须查询新数据字典以提取表定义。

原子 DDL

最后,MySQL 8.0 引入了原子数据定义更改。这意味着数据定义语句现在要么完全成功完成,要么完全回滚。这通过创建一个专门用于 DDL 的撤销和重做日志成为可能,InnoDB 依赖于此来跟踪变化------这是 InnoDB 成熟设计被扩展到 MySQL 服务器操作的另一个地方。

摘要

MySQL 有分层架构,顶部是服务器范围的服务和查询执行,底部是存储引擎。尽管有许多不同的插件 API,但存储引擎 API 是最重要的。如果您理解 MySQL 通过在存储引擎 API 上来回传递行来执行查询,那么您已经掌握了服务器架构的基本原理。

在过去几个主要版本中,MySQL 已将 InnoDB 定为其主要开发重点,并在多年后将其内部账务处理、身份验证和授权移至 MyISAM。Oracle 对 InnoDB 引擎的增加投资导致了诸如原子 DDL、更强大的在线 DDL、更好的抗崩溃能力以及更适合安全部署的操作性等重大改进。

InnoDB 是默认存储引擎,几乎可以覆盖所有用例。因此,在谈论功能、性能和限制时,以下章节将重点关注 InnoDB 存储引擎,很少会涉及其他存储引擎。

¹ 唯一的例外是 InnoDB,因为 MySQL 服务器尚未实现外键定义,所以 InnoDB 解析外键定义。

² MySQL 5.5 及更新版本支持一个可以接受线程池插件的 API,尽管并不常用。线程池的常见做法是在访问层完成的,我们在第五章中讨论过。

³ 我们强烈建议阅读关于独占锁与共享锁、意向锁和记录锁的文档

⁴ 在处理表名更改或模式更改时会使用元数据锁,而在 8.0 中我们引入了"应用级锁定功能"。在日常数据更改过程中,内部锁定留给了 InnoDB 引擎。

⁵ 尽管这是一个常见的学术练习,但大多��银行实际上依赖每日对账,而不是在白天依赖严格的事务操作。

⁶ 欲了解更多信息,请阅读 Adrian Coyler 撰写的ANSI SQL 摘要和 Kyle Kingsbury 撰写的一篇关于一致性模型的解释

⁷ 正如您将在本章后面看到的,一些存储引擎锁定整个表,而其他一些实现更复杂的基于行的锁定。所有这些逻辑在很大程度上存在于存储引擎层。

⁸ 这些锁定提示经常被滥用,通常应该避免使用。

SELECT...FOR SHARE 是 MySQL 8.0 的一个新特性,取代了之前版本中的 SELECT...LOCK IN SHARE MODE

¹⁰ 我们建议阅读 Jeremy Cole 的这篇博文,以更深入地了解 InnoDB 中的记录结构。

¹¹ 想要了解 InnoDB 如何处理其记录的多个版本,建议阅读 Jeremy Cole 的这篇博文

¹² 没有正式的标准定义 MVCC,因此不同的引擎和数据库实现方式大不相同,没有人能说其中任何一种是错误的。

第二章:在可靠性工程世界中进行监控

监控系统是一个广泛的主题,在过去几年中受到了《站点可靠性工程:谷歌如何运行生产系统》(O'Reilly)及其后续作品*《站点可靠性工作手册:实施 SRE 的实用方法》*(O'Reilly)的重要工作的��响。自这两本书出版以来,站点可靠性工程(SRE)已成为开放职位招聘中的热门趋势。一些公司甚至已经将现有员工的职称更改为某种"可靠性工程"。

站点可靠性工程改变了团队对运营工作的看法。这是因为它包含一组原则,使我们更容易回答诸如以下问题:

  • 我们是否提供了可接受的客户体验?

  • 我们应该专注于可靠性和弹性工作吗?

  • 我们如何在新功能和琐事之间取得平衡?

本章希望读者了解这些原则是什么。如果您没有阅读上述任何一本书,我们建议从*《站点可靠性工作手册》*中的这些章节作为速成课程:

  • 第一章提供了更深入理解如何朝着在生产中实现服务水平性能管理的哲学的方向发展。

  • 第二章涵盖了如何实施服务水平目标(SLO)。

  • 第五章涵盖了对 SLO 的警报。

有人可能会认为 SRE 实施并不严格属于高性能 MySQL 的一部分,但我们不同意。在她的书*《加速》*中,尼古拉·福斯格伦博士说:"我们的衡量应该关注结果,而不是产出。"有效 MySQL 管理的一个关键方面是对数据库健康状况进行良好的监控。传统监控是一条相对铺好的道路。由于 SRE 是一个新领域,如何实施 SRE 原则来应对 MySQL 还不太清楚。随着 SRE 原则的不断获得认可,DBA 的传统角色将发生变化,包括 DBA 如何考虑监控他们的系统。

可靠性工程对 DBA 团队的影响

多年来,监控数据库性能依赖于对单个服务器性能的深入研究。这仍然具有很大的价值,但更多地倾向于是关于反应性测量,比如对性能不佳的服务器进行分析。在门户守卫 DBA 团队的时代,这是标准操作程序,当时其他人不被允许知道数据库的运行方式。

进入谷歌对可靠性工程的介绍。DBA 的角色变得更加复杂,演变成了更多的站点可靠性工程师(SRE)或数据库可靠性工程师(DBRE)。团队必须优化他们的时间。服务水平帮助您定义客户何时感到不满意,并允许您更好地平衡您的时间,解决性能问题和扩展挑战,以及处理内部工具的工作。让我们讨论您需要监视 MySQL 的不同方式,以确保成功的客户体验。

定义服务水平目标

在进入如何衡量客户对数据库集群性能是否满意之前,我们必须首先了解我们的目标是什么,并就描述这些目标的共同语言达成一致。以下是一些问题,可以作为组织中的对话开端,以定义这些目标:

  • 什么是适合衡量成功的指标?

  • 这些指标的哪些值对客户和我们的业务需求是可接受的?

  • 在何时我们被认为处于降级状态?

  • 何时我们完全处于失败状态并需要尽快进行补救?

有些问题有明显的答案(例如,源数据库宕机,我们不接受任何写入,因此业务停滞)。有些问题则不那么明显,比如定期任务有时会占用所有数据库磁盘 I/O,突然其他所有操作变慢。在整个组织中对我们正在衡量的内容和原因有共享理解,可以帮助指导优先级对话。通过组织内持续对话达成共识,有助于指导您是否可以将工程工作投入新功能,或者是否需要更多投入于性能改进或稳定性。

在 SRE 实践中,关于客户满意度的讨论将使团队对于服务水平指标(SLIs)、SLOs 和服务水平协议(SLAs)在业务方面的健康状况达成一致。让我们首先定义这些术语的含义:

服务水平指标(SLI)

用非常简单的术语来说,SLI 回答了这个问题,"我如何衡量我的客户是否满意?"答案代表了用户角度的健康系统。SLIs 可以是业务级别的指标,比如"面向客户的 API 的响应时间",或者更基本的"服务是否正常"。您可能会发现,根据数据的上下文以及与产品的关系,您需要不同的指标或度量标准。

服务水平目标(SLO)

SLO 回答了这个问题,"为了确保我的客户满意,我可以允许我的 SLI 的最低值是多少?" SLO 是我们希望在给定 SLI 下达到的目标范围,以被视为健康服务。如果您认为正常运行时间是 SLI,那么您希望在给定时间段内正常运行的次数就是 SLO。必须将 SLO 定义为在给定时间范围内的值,以确保每个人对 SLO 的含义达成一致。 SLI 加上 SLO 形成了了解客户是否满意的基本方程。

服务水平协议(SLA)

SLA 提供了这个问题的答案,"我愿意同意什么样的 SLO 并承担后果?" SLA 是一个 SLO,已包含在与业务的一个或多个客户(付费客户,而不是内部利益相关者)的协议中,如果未达到该 SLA 则会有财务或其他惩罚。重要的是要注意,SLA 是可选的。

我们在本章中不会过多涉及 SLAs,因为它们往往需要更多的业务讨论而不是工程讨论。这种决定主要取决于业务期望如果在合同中承诺 SLA 会得到什么销售额,以及如果 SLA 被违反是否值得冒险损失收入。希望这样的决定是基于我们在这里涵盖的关于选择 SLIs 和匹配 SLOs 的内容。

定义这些 SLI、SLO 和 SLA 不仅指导业务的健康状况,还指导工程团队内的规划。如果一个团队没有达到其约定的 SLO,那么就不应继续进行新功能的工作。对于数据库工程团队也是如此。如果我们在本章讨论的潜在 SLO 之一没有达到,那就应该引发为什么没有达到的讨论。当您拥有数据来解释为什么客户体验不佳时,您可以就团队优先事项进行更有意义的对话。

使客户满意需要什么?

在选择一组指标作为您的 SLIs 后,可能会有诱惑将目标设定为 100%。然而,您必须抵制这种冲动。请记住,选择指标和目标的目的是随时通过客观指标评估您的团队是否可以通过新功能进行创新,或者稳定性是否有可能降至客户可接受水平以下,因此需要更多关注和资源。目标是定义使客户满意的绝对最低要求。如果客户对您的页面在两秒内加载感到满意,那么没有必要设定页面在 750 毫秒内加载的目标。这可能会给工程团队带来不合理的负担。

以正常运行时间作为指标和目标值的例子,我们可以宣称"我们不会有任何停机时间",但在实施和跟踪是否达到目标时,这意味着什么?达到三个九的可用性并不是一件小事。一整年的三个九仅相当于八个多小时,换算成每周仅为 10 分钟。你承诺的九越多,这就越困难,团队将不得不花费更多昂贵的工程时间来实现这样的承诺。表 2-1 是亚马逊网络服务展示挑战的有用数据表。

表 2-1. 各种可用时间

可用性 每年停机时间 每月停机时间 每周停机时间 每日停机时间
99.999% 5 分钟,15.36 秒 26.28 秒 6.06 秒 0.14 秒
99.995% 26 分钟,16.8 秒 2 分钟,11.4 秒 30.3 秒 4.32 秒
99.990% 52 分钟,33.6 秒 4 分钟,22.8 秒 1 分钟,0.66 秒 8.64 秒
99.950% 4 小时,22 分钟,48 秒 31 分钟,54 秒 5 分钟,3 秒 43 秒
99.900% 8 小时,45 分钟,36 秒 43 分钟,53 秒 10 分钟,6 秒 1 分钟,26 秒
99.500% 43 小时,48 分钟,36 秒 3 小时,39 分钟 50 小时,32 分钟,17 秒 7 分钟,12 秒
99.250% 65 小时,42 分钟 5 小时,34 分钟,30 秒 1 小时,15 分钟,48 秒 10 分钟,48 秒
99.000% 3 天,15 小时,54 分钟 7 小时,18 分钟 1 小时,41 分钟,5 秒 14 分钟,24 秒

因为工程时间是有限资源,选择服务水平目标时不要追求完美。产品中并非所有功能都需要这些九来满足客户,因此随着产品功能集的增长,你会发现根据特定功能影响或其带来的收入,你将有不同的服务水平指标和目标。这是可以预期的,也是一个深思熟虑过程的标志。你在这里有一个关键任务:检测数据集何时成为不同利益相关者的瓶颈,危及性能。这也意味着找到一种方法来区分这些不同利益相关者的需求,以便为他们提供合理的服务水平指标和目标。

这些指标和目标也��产品和工程之间具有统一语言的有效方式,指导在"将工程时间花在新功能上"与"将时间花在弹性和解决问题上"之间做出决策。这也是一种决定,从我们想要实现的事情清单中,基于客户体验来确定哪个最重要的方式。你可以使用服务水平指标和目标来指导工作优先级的对话,否则很难达成一致。

应该衡量什么

假设有一家公司,其产品是一个在线商店。由于增加了在线购物,公司看到了更多的流量,基础设施团队需要确保数据库层能够处理增加的需求。在本节中,我们将讨论作为虚构基础设施团队时应该如何衡量的内容。

定义服务水平指标和目标

定义一个良好的服务水平指标和相匹配的服务水平目标的核心在于简洁地解释如何为客户提供愉快的用户体验。我们不会花费大量时间在抽象层面上解释如何创建有意义的服务水平指标和目标。² 在 MySQL 的背景下,它需要是一个定义了三个主要主题的表示:可用性、延迟和关键错误缺失。

对于我们的在线商店示例,这意味着页面加载速度要快,至少在一个月内 99.5% 的时间内快于几百毫秒。这还意味着一个可靠的结账流程,在给定日历月内只允许 1% 的时间发生间歇性故障。请注意这些指标和目标的定义。我们没有将 100% 定义为要求,因为我们生活在一个失败不可避免的世界中。我们使用时间跨度,以便团队可以准确平衡其在新功能和弹性之间的工作。

"我期望我的数据库请求中有 99.5% 在两毫秒内无错误地提供服务"既是一个具有明确 SLO 的充分 SLI,又不简单。您无法通过一个指标来确认所有这些。这是对数据库层行为的单句表述,以提供可接受的客户体验。

那么在我们的在线商店中,可以构建这种客户体验画面的度量标准是什么?从在生产环境中对页面加载进行采样负载率的合成测试开始。这对于作为一个一致的信号表明"一切正常"是有用的。但这只是一个开始。让我们讨论跟踪不同信号的各个方面以构建画面。随着我们通过这些示例,我们将把它与我们的在线商店联系起来,帮助您可视化这些不同的度量标准如何创建一个良好的客户体验画面。首先,让我们谈谈跟踪查询响应时间。

监控解决方案

在 SLIs 和 SLOs 的背景下进行查询分析和监控查询延迟需要关注客户体验。这意味着依赖可以在查询响应时间超过约定阈值时尽快向您发出警报的工具。让我们讨论一下您可以采取的几种路径来实现这种监控水平。

商业选项

这是一个例子,支付一个竞争优势在于 MySQL 性能分析的供应商可以让您的组织获得丰厚回报。像SolarWinds 数据库性能管理这样的工具可以大大简化查询性能分析的自动化,并让您的工程团队中的大部分人都能够访问。

开源选项

一个成熟的开源选项是Percona 监控与管理,简称 PMM。它作为一个客户端/服务器对运行。您在数据库实例上安装客户端,它收集并发送指标到服务器部分。服务器端还有一组仪表板,允许您查看与性能相关的图表。PMM 的一个主要优点是,仪表板的组织受到 Percona 社区长期监控 MySQL 性能经验的指导。这��其成为一个极好的资源,让新手工程师熟悉如何监控 MySQL 性能。

您可以采取的另一种方法是将数据库慢日志和 MySQL 性能模式输出发送到一个集中位置,您可以使用像pt-query-digest这样的知名工具,它是 Percona Toolkit 包的一部分,来分析日志并更深入地了解数据库实例花费时间的情况。虽然有效,但这个过程可能会很慢,并且如果使用不当可能会影响客户。理想情况下,您希望在客户注意到问题之前发现问题。在发生问题后被动地检查日志,您会面临因为发现性能退化需要花费很长时间以及挖掘各种事后证据的风险,从而磨损客户信任。

最后,使用性能模式来分析 MySQL 性能可能非常有帮助,正如您将在第三章中看到的那样。您可以使用它来找出瓶颈,使您的实例在相同规格下做更多事情,节省基础设施成本,或回答"为什么这个操作花费这么长时间?"这不是一个确定您是否符合服务可靠性承诺的工具,因为它深入到 MySQL 的内部。对于服务水平性能评估,我们需要一种新的关于性能的思考方式。

现在让我们深入了解一些额外的指标,帮助您进一步了解您在线商店的客户体验。您应该考虑从 MySQL 中获取的指标,而不是输出。我们还将涵盖一些单凭 MySQL 指标无法衡量的事例。

监控可用性

一个间歇性下线的在线商店会冒着侵蚀购物者信心的风险。这就是为什么可用性作为一个独立的指标,以及作为您对客户体验的看法的一部分,是如此重要。

可用性是能够在没有错误的情况下响应客户请求。用标准的 HTTP 术语来表述,这可能是一个明确成功的响应,比如 200 响应代码,或者是成功接受请求并承诺异步完成相关工作的响应,比如 202 accepted。在单体主机系统时代,可用性曾经是一个简单的指标。如今,大多数架构要复杂得多。可用性的概念也演变成对分布式系统故障的更微妙的反映。在尝试将可用性转化为数据库架构的 SLI 和 SLO 时,考虑进一步讨论更多细节(以及我们在线商店的示例),例如以下内容:

  • 在处理不可避免的灾难性故障时,哪些功能是不可妥协的,哪些功能是"nice to have"(例如,客户是否可以继续使用现有的购物车并结账,但在此故障期间可能无法添加新商品)?

  • 我们将哪些类型的故障定义为"灾难性"(例如,列表搜索失败可能不是灾难性,但结账操作失败就是)?

  • "降级功能"是什么样子的(例如,我们是否可以在需要时加载通用推荐而不是基于过去购买历史的定制推荐)?

  • 在一组可能的故障场景中,我们可以为核心功能承诺的最短平均恢复时间(MTTR)是多少(例如,如果支持购物车结账系统的数据库写入失败,我们可以安全地多快地切换到新的源节点)?

在选择一组代表可用性的指标时,您希望与客户支持团队设定期望,"100%的正常运行时间"是不合理的,重点是在一个理解和接受组件故障不可避免的世界中提供尽可能最佳的客户体验。

验证可用性的首选方法是来自客户端或远程端点。如果您可以访问客户端的数据库访问日志,这可以被动地完成。明确地说,这意味着如果您的应用程序是 PHP 并且在 Apache 下运行,您需要访问 Apache 日志以确定 PHP 是否发出任何连接到数据库的错误。您也可以主动验证可用性。如果您的环境被隔离并且无法访问客户端日志,考虑设置远程代码来执行对数据库的操作以确保其可用性。这可以是一些简单的操作,比如一个SELECT 1查询,用于验证 MySQL 是否接收并解析您的查询,但不访问存储层。或者这可以更复杂,比如从表中读取实际数据或执行写入和随后读取以验证写入是否成功。来自网络中其他位置的这种合成事务可以让您了解您的应用程序是否可用。

远程验证可用性对于跟踪可用性目标非常有用。它无法帮助您在问题出现之前 获得洞察。用作可用性问题的领先指标的一个 MySQL 指标是Threads_running。它跟踪当前在给定数据库主机上正在运行的查询数量。当运行的线程数量快速增长且没有显示任何下降迹象时,这表明查询完成速度不够快,因此正在堆积并消耗资源。允许这个指标增长通常会导致数据库主机在源节点上��起完全的 CPU 锁定或强烈的内存负载,这可能导致操作系统关闭整个 MySQL 进程。如果这种情况发生在源节点上,显然会导致重大故障,因此您应该努力获得领先指标。监视的起点是检查您有多少 CPU 核心,如果Threads_running超过了这个值,这可能表明您的服务器正处于危险状态。除此之外,您还可以监视接近max_connections时的情况,这是另一个检查工作进度过载的数据点。

"安全设置"部分在第五章中提供了关于如何设置 MySQL 线程的制动器的见解。

监控查询延迟

MySQL 引入了许多长期需要的增强功能来跟踪查询运行时间,随着应用代码的变化,您应该绝对使用监控堆栈来跟踪这些趋势。然而,这仍然不能完全反映客户体验,特别是考虑到现代软件架构的设计方式。除了内部跟踪的延迟之外,您还需要了解应用程序如何感知延迟以及当感知延迟增加时会发生什么。这意味着除了直接跟踪数据库服务器的查询延迟之外,您还应该通过工具让客户端报告查询完成时间,以便尽可能接近客户体验。从客户端摄取所有这些样本指标(特别是当您的基础设施规模扩大时)可以使用付费工具如 Datadog 或 SolarWinds Database Performance Monitor,甚至使用开源工具如 PMM。这是一个需要与组织的应用开发人员密切合作的领域。您需要了解应用团队如何从应用程序角度衡量这一点,并使用跟踪工具如 Honeycomb 或 Lightstep 来增加对异常值的洞察力。

监控错误

您是否需要跟踪并警报每次发生的错误?这取决于情况。

在运行服务中的 MySQL 客户端存在错误并不意味着一定有什么东西出了问题。在分布式系统的世界中,有许多情况下客户端可能遇到间歇性错误,并且在许多情况下,通过简单重试失败的查询可以解决。然而,发生错误的速率,跨越处理基础设施中数据库查询的服务群体,可能是潜在问题的关键指标。以下是一些客户端错误的例子,通常可能只是噪音,但如果它们的速率加快,则可能是问题的迹象:

锁等待超时

你的客户报告这种错误急剧增加可能是源节点上的行锁争用升级的迹象,事务不断重试仍然失败。这可能是写入停机的前兆。

中止连接

客户报告突然出现大量中止连接可能是你在客户端和数据库实例之间的任何访问层存在问题的指标。不追踪这些问题可能导致大量客户端重试,消耗资源。

MySQL 服务器跟踪的一个可以帮助你的东西是名为Connection_errors_xxx的服务器变量集,其中xxx是不同类型的连接错误。任何这些计数器的突然增加都可能是一个强烈的指示,告诉你当前有一些新的异常情况。

是否有错误,一个单个实例意味着有问题需要处理?是的。

例如,收到 MySQL 实例正在以只读模式运行的错误是一个问题的迹象,即使这些错误并不经常发生。这可能意味着你刚刚将一个复制实例提升为源,但它仍在只读模式下运行(你确实在只读模式下运行复制实例,对吧?),这意味着对于你的集群来说写入的停机时间。或者这可能意味着在发送写流量到复制实例的访问层中存在一些问题。在这两种情况下,这不是一个通过重试解决的间歇性问题的迹象。

另一个服务器端错误,表明存在重大问题的标志是"连接过多"或操作系统级别的"无法创建新线程"。这些是你的应用层创建并保留的连接数超过了数据库服务器配置允许的数量的迹象,无论是在服务器max_connections变量还是 MySQL 进程允许打开的线程数方面。这些错误会立即转换为 5xx 错误传递给你的应用程序,并且根据你的应用程序设计,也可能对你的客户产生影响。

正如你所看到的,衡量性能并选择围绕 SLIs 构建哪些错误,这既是一个技术问题,也是一个沟通和社交问题,所以你应该做好准备。

主动监控

正如我们所说,SLO 监控侧重于你的客户是否满意。这有助于让你专注于在客户不满意时改善他们的体验,以及在其他任务上,比如减少重复劳动时。这忽略了一个关键领域:主动监控。

如果我们回到我们的在线商店示例以及我们如何设想监控客户体验,我们可以进一步阐述。想象一下,你没有遇到任何组件的重大故障,但你注意到有越来越多的客户支持票证报告"缓慢"或偶尔出现的错误,似乎会自行消失。你如何追踪这样的行为?如果你不已经对多个信号的基准性能有一个很好的想法,这可能是一项非常困难的任务。你用来触发值班警报的仪表板和脚本可以称为稳态监控 。这让你知道在给定系统中是否发生了意外情况,无论是否有变化。它们是在客户经历故障之前为你提供领先指标的重要工具。

在监控中需要取得平衡的一点是,它始终需要是可操作的,同时也需要是真正的领先指标。对于数据库磁盘空间已满时发出警报已经太晚了,因为服务已经停止了,但是在 80%时发出警报可能太慢,或者如果增长速率不那么快,则可能不够可操作。

让我们谈谈你可以监控的有用信号,这些信号与实际客户影响没有直接关联。

磁盘增长

跟踪磁盘增长是一种你可能不会考虑直到它成为问题的指标。一旦它成为问题,解决问题可能会耗费时间并影响你的业务。了解如何跟踪它,制定缓解计划,并知道适当的警报阈值肯定是更好的选择。

有许多策略可以用来监控磁盘增长。让我们从最理想到最低限来分解它们。

如果你的监控工具允许的话,跟踪磁盘空间使用量的增长速率可能非常有用。总会有一些情况,可用磁盘空间会相对迅速减少,使你的可用性受到威胁。长时间运行的具有大型撤消日志或更改表的事务是为什么你可能会迅速接近磁盘满的例子。有许多事故故事表明,过多的日志记录或给定数据集的插入模式的更改直到"数据库"耗尽磁盘空间才被发现。然后才会触发各种警报。

如果跟踪增长速率不可行(并非所有监控工具都提供此功能),你可以设置多个阈值,较低的警告只在工作时间触发,较高的更关键值作为非工作时间值班的警报。这使团队在工作时间之前有一个预警,以避免事情变得严重到需要叫醒某人。

如果你既不能监控增长速率,也不能为同一指标定义多个阈值,那么你至少需要确定一个磁盘空间使用量的单一阈值,在达到该阈值时向你的值班工程师发出警报。这个阈值需要足够低,以便在团队评估触发原因并考虑长期缓解措施时采取一些行动并释放磁盘空间。考虑评估磁盘的最大吞吐量(MB/s),并利用这一点来帮助计算在最大流量吞吐量下填满磁盘需要多长时间。你需要这么长的前期时间来避免事件发生。

我们在第四章中讨论了与 MySQL 如何使用磁盘空间以及在这些决策中考虑磁盘空间增长相关的操作系统和硬件配置。应该预期,希望你的业务会发展到某个程度,以至于你无法将所有数据存储在一组服务器集群中。即使你在一个可以为你扩展卷的云环境中运行,你仍然需要对此进行规划,因此你总是希望有一个空闲磁盘空间的阈值,以便你有时间进行规划并进行所需的扩展而不会惊慌。

这里的要点是确保你对磁盘空间增长有一些监控,即使你认为现在还为时过早,还不需要。这是几乎每个人都准备不足的增长轴之一。

连接增长

随着业务的增长,一个常见的线性增长层是你的应用层。你将需要更多的实例来支持登录、购物车、处理请求,或者产品背景可能是什么。所有这些添加的实例开始向数据库主机打开越来越多的连接。你可以通过添加副本、使用复制作为扩展措施,甚至使用中间件层如 ProxySQL 来将前端的增长与直接在数据库上的连接负载分离来缓解这种增长。

在流量增长的同时,数据库服务器可以支持有限数量的连接池,这是通过服务器设置max_connections配置的。一旦连接到服务器的总数达到最大值,你的数据库将不允许任何新连接,这是导致无法再向数据库打开新连接的常见原因之一,从而增加用户的错误。

监控连接增长是为了确保资源不会耗尽,从而危及数据库的可用性。这种风险可能以两种不同的方式出现:

  • 应用层打开了很多未使用的连接,无故增加连接风险。这种情况的明显迹象是看到连接计数(threads_connected)很高,但threads_running仍然很低。

  • 应用层正在积极使用大量连接,有可能过载数据库。你可以通过看到threads_connectedthreads_running都很高(数百?数千?)且不断增加来区分这种状态。

在设置连接计数监控时需要考虑的一个有用的事情是依赖于百分比而不是绝对数字。threads_connected/max_connections的百分比显示了你的应用节点数量增长将带你接近数据库允许的最大连接池。这有助于监控连接增长问题的第一个阶段。

另外,你应该跟踪和警报数据库主机的繁忙程度,正如我们之前解释的,可以通过threads_running的值来看到。通常,如果这个值增长到一百以上的线程,你会开始看到增加的 CPU 使用率和内存使用率,这是数据库主机负载高的一般迹象。这对于数据库的可用性是一个直接的关注点,因为它可能升级到 MySQL 进程被操作系统杀死。一个常见的快速解决方案是使用 kill 进程命令或自动化使用它的工具,比如pt-kill,战术性地减轻负载,然后通过查询分析来查明数据库陷入这种状态的原因,这是我们之前描述过的。

警告

连接风暴是生产系统中的情况,应用层感知到查询延迟增加,并响应地向数据库层打开更多连接。这可能会给数据库增加大量负载,因为它处理大量新连接的涌入,这会消耗资源,无法满足查询请求。连接风暴可能导致max_connections中可用连接数量突然减少,增加数据库可用性风险。

复制延迟

MySQL 具有一种本地复制功能,可以将数据从一个服务器(源)发送到一个或多个额外的服务器,称为副本。数据在源上写入和在副本上可用之间的延迟称为复制延迟。如果你的应用从副本读取数据,延迟可能会导致数据看起来不一致,因为你向尚未赶上所有更改的副本发送读取请求。在社交媒体的例子中,用户可能会评论其他人发布的内容。这些数据被写入源,然后复制到副本。当用户尝试查看他们的回复时,如果应用发送请求到一个滞后的服务器,副本可能尚未有数据。这可能会让用户感到困惑,认为他们的评论没有保存。我们在第九章中更详细地介绍了对抗复制延迟的策略。

延迟是那些可能触发事件的急性 SLI 指标之一。它也是需要更多架构变化的长期趋势指示。在长期情况下,即使你从未遇到影响客户体验的复制延迟,它仍然表明,至少间歇性地,源节点的写入量超过了副本在当前配置下的写入量。它可以成为你写入容量的煤矿中的警报。如果听取建议,它可以防止未来发生全面事件。

警告

警惕将有关复制延迟的信息告知他人。立即可行的纠正措施并不总是可能的。同样,如果你不从副本中读取数据,请考虑监控系统对此情况如何积极地提醒他人。尤其是在非工作时间接收到的警报应始终是可操作的。

复制延迟是那些既影响即时又战术决策的指标之一,但长期关注其趋势可以帮助你避免更大的业务影响,并使你走在增长曲线的前面。

I/O 利用率

数据库工程师永无止境的努力之一是"尽可能多地在内存中完成工作,因为这样更快。"虽然这确实是准确的,但我们也知道我们不可能 100%地做到这一点,因为那意味着我们的数据完全适合内存,而在这种情况下,"规模"还不是我们需要花费精力的事情。

随着数据库基础设施的扩展和数据不再适合内存,你会意识到下一个最好的方法是不要从磁盘中读取太多数据,以至于查询被卡在那些宝贵的 I/O 周期中等待它们的轮次。即使在几乎所有东西都运行在固态驱动器上的这个时代,这仍然是真实的。随着数据规模的增长和查询需要扫描更多数据来满足请求,你会发现 I/O 等待可能会成为你的流量增长的瓶颈。

监控磁盘 I/O 活动有助于在影响客户之前提前了解性能下降。有一些事项可以监控以实现这一目标。像 iostat 这样的工具可以帮助你监控 I/O 等待。你希望监控并提醒如果你的数据库服务器有很多线程处于 IOwait 中,这表明它们在排队等待某些磁盘资源可用。你可以通过跟踪 IOutil 作为一个有意义的时间段的运行图,比如一天或两天,甚至一周。IOutil 报告为整个系统磁盘访问容量的百分比。在一个不运行备份的主机上,如果持续时间接近 100%,这可能表明存在全表扫描和低效查询。你还想监控你的磁盘 I/O 容量的整体利用率作为一个百分比,因为这可以预警你的磁盘访问成为未来数据库性能的瓶颈。

自增空间

在使用 MySQL 时较少为人知的一个雷区是,自增主键默认创建为有符号整数,并且可能会用尽键空间。当你进行了足够多的插入操作时,自增键达到了其数据类型的最大可能值。在长期基础上计划应该监控哪些指标时,监控使用自增作为主键的任何表的剩余整数空间是一个简单的操作,几乎肯定会在未来为你节省一些重大事件的痛苦,因为你可以提前预测到需要更大的键空间。

如何监控这个关键空间?您有几个选项。如果您已经使用 PMM 及其 Prometheus 导出器,这是内置的,您只需要打开标志-collect.auto_increment.columns。如果您的团队不使用 Prometheus,您可以使用以下查询,可以将其修改为度量生产者或警报,告诉您何时任何表接近可能的最大关键空间。此查询依赖于information_schema,其中包含有关数据库实例中表的所有元数据:

sql 复制代码
SELECT
    t.TABLE_SCHEMA AS `schema`,
    t.TABLE_NAME AS `table`,
    t.AUTO_INCREMENT AS `auto_increment`,
    c.DATA_TYPE AS `pk_type`,
    (
        t.AUTO_INCREMENT /
        (CASE DATA_TYPE
            WHEN 'tinyint'
                THEN IF(COLUMN_TYPE LIKE '%unsigned',
                    255,
                    127
                )
            WHEN 'smallint'
                THEN IF(COLUMN_TYPE LIKE '%unsigned',
                    65535,
                    32767
                )
            WHEN 'mediumint'
                THEN IF(COLUMN_TYPE LIKE '%unsigned',
                    16777215,
                    8388607
                )
            WHEN 'int'
                THEN IF(COLUMN_TYPE LIKE '%unsigned',
                    4294967295,
                    2147483647
                )
            WHEN 'bigint'
                THEN IF(COLUMN_TYPE LIKE '%unsigned',
                    18446744073709551615,
                    9223372036854775807
                )
        END / 100)
    ) AS `max_value`
    FROM information_schema.TABLES t
    INNER JOIN information_schema.COLUMNS c
        ON t.TABLE_SCHEMA = c.TABLE_SCHEMA
        AND t.TABLE_NAME = c.TABLE_NAME
    WHERE
        t.AUTO_INCREMENT IS NOT NULL
        AND c.COLUMN_KEY = 'PRI'
        AND c.DATA_TYPE LIKE '%int'
;

在一般情况下以及专门管理自动增量时,选择主键时需要考虑很多微妙和上下文,我们将在第六章中进行讨论。

备份创建/恢复时间

长期规划不仅涉及业务正常运行时的增长,还涉及在可接受的时间范围内进行恢复。我们将在第十章中更深入地讨论如何考虑灾难恢复,以及在第十三章中讨论它如何成为您的合规控制职责的一部分,但我们在这里提到它是为了指出一个好的灾难恢复计划只有在您重新审视并调整其目标时才能起作用。

如果您的数据库达到一个恢复备份所需时间超过可接受时间以恢复业务的关键功能的大小,即使其他一切都正常运行,您也需要考虑调整 MTTR 目标,更改"关键功能"的定义,或找到缩短备份恢复时间的方法。在制定灾难恢复计划时需要考虑的一些事项:

  • 对于这个恢复目标,要非常具体,并且如果需要的话,要查看支持该功能子集的数据是否需要在一个单独的集群中,以实现这一期望的现实性。

  • 如果将数据在多个较小的实例中进行功能分区不可行,则整个数据集现在都处于通过备份恢复的目标下。从备份中恢复所需时间最长的数据集将驱动此恢复过程完成时间。

  • 确保有自动化的测试方法(我们将在第十章中涵盖一些示例)。监视从文件恢复备份到已经赶上自备份创建以来的所有更改的运行数据库所需的时间,并将该指标存储在一个足够长的保留期内,以查看长期(至少一年)的趋势。如果不自动化监控,这是一个可能被忽视并且变得令人惊讶地长的指标之一。

在我们即将描述的许多示例长期指标中,您会发现我们几乎总是指出需要对数据进行功能分片或水平分片。这里的目标是明确指出,如果在容量问题是主要贡献原因的情况下考虑分片,那么您很可能考虑得太晚了。将数据分解为可管理的部分的工作并不是在您的数据对一个集群来说太大时才开始,而是在您仍在确定为提供成功的客户体验而设定目标时。

了解恢复数据所需的时间可以帮助设定在真正灾难发生时该做什么的期望。它还可以让您意识到可能需要比业务希望的时间更长。这是需要分片的前兆。

测量长期性能

选择日常运营的服务水平指标(SLIs)和服务水平目标(SLOs)只是一个开始。你需要确保自己没有把森林误认为是树木,而是专注于具体的主机指标而不是检查整体系统性能和客户体验结果。在这一部分,我们将介绍您可以使用的策略来思考系统的整体长期健康状况。

了解您的业务节奏

了解您的业务流量节奏非常重要,因为这将始终是您的所有 SLOs 都经受最严格测试并受到最重要客户最严格审查的时候。业务节奏可能意味着高峰流量时间比"平均"高出数倍,如果您的数据库基础设施没有准备好,这将产生许多后果。在数据库基础设施的背景下,这可能意味着每秒要处理数倍的请求,应用服务器的连接负载更大,或者如果写操作间歇性失败,收入影响更大。以下是一些业务节奏的示例,这些示例应该帮助您了解您的公司所处的业务周期:

电子商务网站

11 月底至年底是许多国家最繁忙的时期,在线商店可能会看到销售额增加数倍。这意味着更多的购物车,更多的同时销售,以及同一年的任何其他时间相比更多的收入影响。

人力资源软件

在美国,11 月通常是许多员工在被称为"开放选项"的时间内进行福利选举的时候,这将带来更多的流量。

在线鲜花供应商

情人节将是一年中最忙碌的时候,会有更多人订购花束送货。

正如您所看到的,这些业务周期可以根据业务填充的客户需求而有很大的变化。对于您的业务周期和其对业务收入、声誉以及您应该做出多少准备以满足需求而不影响您负责运行的系统的稳定性的影响,您必须意识到这一点至关重要。

当衡量支撑业务的数据库基础设施的性能时,重要的是不要将性能测量与工程组织正在跟踪的其他重要指标分开。数据库性能应该是关于技术堆栈性能的更大对话的一部分,而不应被视为特例。尽可能使用与您的工程组织其余部分相同的工具。您希望依赖于确定数据库层性能的指标和仪表板与应用层指标一样易于访问,甚至在同一仪表板上。无论您使用什么技术或供应商,这种思维方式都将在创造一个每个人都投入到整个堆栈性能并减少工程师可能感受到的功能编写和支持它们的数据库之间的隔阂的环境中发挥作用。

有效跟踪您的指标

在进行业务的长期规划时,有许多事情需要考虑,其中包括但不限于:

  • 规划未来的容量

  • 预见何时需要进行重大改进以及何时足够进行渐进式变化

  • 规划运行基础设施的成本增加

您需要能够不仅在某一特定时间点测量数据存储基础设施的健康状况,还要在长期基础上趋势性能的改善或恶化。这意味着不仅要确定 SLIs 和 SLOs,还要找出哪些 SLIs 和 SLOs 在长期趋势中仍然是有价值的、高信号的指标。您可能会发现,并非所有可用于短期值班决策的指标也适用于长期业务规划。

在深入讨论哪些指标对长期规划至关重要之前,让我们谈谈一些能够支持长期趋势监控的工具。

使用监控工具检查性能

在即时"我们当前是否处于事故"意义上和长期跟踪和趋势意义上,衡量性能都很重要。保存您关心的指标的工具与指标本身一样重要。如果选择了一个好的 SLI,但随后无法适当地查看其随时间的趋势,以一种与组织其他指标相关的方式,那又有什么用呢?

监控工具领域正在迅速发展,对于如何进行监控有很多不同的看法。这里的目标是增加透明度,关注跟踪结果而不是产出。在确保基础架构成功的领域中,追踪成功是一个团队运动。

在这里不讨论具体的工具,而是列出一些在考虑一个工具是否适合这种长期趋势时需要考虑的重要特性和方面。

拒绝平均值

无论您是作为工程组织自行管理指标解决方案,还是使用软件即服务(SaaS),都要注意您的指标解决方案如何对长期存储的数据进行归一化处理。许多解决方案默认将长期数据聚合为平均值(Graphite 是最早这样做的之一),这是一个大问题。如果您需要查看一个指标在几周以上时间段内的趋势,平均值将会平滑下降峰值,这意味着如果您想知道您的磁盘 I/O 利用率是否会在接下来的一年内翻倍,平均数据点的图表很可能会给您一种虚假的安全感。在趋势化几个月的数据时,始终查看峰值,这样您就可以保持偶发性峰值在视图中的准确性。

百分位数是您的朋友

百分位数依赖于对给定时间跨度内的数据点进行排序,并根据目标百分位数(即,如果您寻找第 95 个百分位数,则删除前 5%)来删除最高值。这是使您查看的数据在视��上更类似于我们查看 SLIs 和 SLOs 的一种绝佳方式。如果您可以使显示您的查询响应时间的图表显示第 95 个百分位数,那么您可以更容易地将其与您希望实现的应用请求完成的 SLO 相匹配,并使数据库指标对您的客户支持团队和工程师团队等人员有意义,而不仅仅是对数据库工程团队有意义。

长期保留期和性能

这似乎是显而易见的,但是当尝试显示长时间跨度时,监控工具的性能很重要。如果您正在评估用于业务指标趋势的解决方案,您需要确保在要求越来越长时间跨度的数据时,用户体验如何变化。一个指标解决方案只有在能够提供数据的可用性方面才算好,而不仅仅是摄入速度或数据保留时间。

现在我们已经描述了长期监控工具应该是什么样子,让我们讨论一下到目前为止我们所涵盖的所有内容如何指导您的数据架构选择 SLIs 和 SLOs。

使用 SLOs 指导您的整体架构

在您的业务不断增长的同时保持一致且良好的客户体验绝非易事。随着业务规模的增长,即使保持相同的 SLOs,更不用说设定更雄心勃勃的目标,也变得越来越困难。以可用性为例:每个人都希望数据的读写都能保持尽可能多的连续运行时间。但是,您想要实现的 SLOs 越严格,工作就会变得越昂贵,因为您的数据库每秒事务数或其规模也会成倍增长。

使用我们已经讨论过的 SLIs 和 SLOs,您可以找到增长点,从而有意义地开始将数据分割为功能性分片或数据分区。我们将在第十一章中更详细地讨论使用分片来扩展 MySQL,但这里要强调的重要一点是,告诉您系统当前表现如何的相同 SLIs 和 SLOs 也可以指导您知道何时是投资扩展 MySQL 的时机,以便个别集群在保持维护客户体验的 SLOs 范围内仍然可管理。

拥有一个可以处理短期和长期指标,并能以有用的方式趋势变化的度量解决方案是跟踪战术绩效指标以及数据库基础设施长期影响趋势的一个非常重要的部分。

摘要

在将可靠性工程概念应用于监控数据库基础设施的过程中,不断改进和重新审视您的指标和目标非常重要。它们并不是在您第一次定义一些 SLIs 和 SLOs 后就一成不变的。随着业务的增长,您将更深入地了解客户的体验,这应该推动您改进 SLIs 和 SLOs。

在选择指标并为其分配目标时,请意识到您始终专注于代表客户体验。此外,不要将所有精力都集中在显示事故发生时的指标上,而是花一些时间监控可以帮助您预防事故的事项。这一切都是为了积极主动地保护客户体验。

我们建议在三个关键领域提前设定目标:延迟、可用性和错误。这三个领域可以很好地表明您的客户是否满意。此外,请确保您还在连接增长、磁盘空间、磁盘 I/O 和延迟方面进行积极监控。

我们希望本章能帮助您成功地将可靠性工程应用于监控 MySQL,随着公司规模的扩大。

¹ Nicole Forsgren,加速:精益软件和 DevOps 的科学 (IT Revolution Press,2018)。https://oreil.ly/Bfvda

² 我们强烈推荐阅读实施服务水平目标 by Alex Hidalgo(O'Reilly)。

第三章. 性能模式

由 Sveta Smirnova 贡献

在高负载下调整数据库性能是一个迭代循环。每次您进行更改以调整数据库性能时,您需要了解更改是否产生了影响。您的查询是否比以前运行得更快?锁是否减慢了应用程序,或者它们完全消失了?内存使用量是否改变?等待磁盘的时间是否改变?一旦您了解如何回答这些问题,您将能够更快速、更自信地评估和应对日常情况。

性能模式是一个存储回答这些问题所需数据的数据库。本章将帮助您了解性能模式的工作原理、其局限性以及如何最好地使用它------以及其伴随的sys模式------来揭示 MySQL 内部发生的常见信息。

性能模式简介

性能模式提供了 MySQL 服务器内部运行操作的低级度量标准。为了解释性能模式的工作原理,我需要提前介绍两个概念。

第一个是工具 。工具指的是我们想要捕获信息的 MySQL 代码的任何部分。例如,如果我们想要收集关于元数据锁的信息,我们需要启用wait/lock/meta​data/sql/mdl工具。

第二个概念是消费者,它只是一个存储有关哪些代码被检测的信息的表。如果我们检测查询,消费者将记录关于执行次数、未使用索引次数、花费的时间等信息。消费者是大多数人与性能模式紧密相关的内容。

性能模式的一般功能如图 3-1 所示。

图 3-1. 数据库中运行查询的流程,展示了performance_schema如何收集和聚合数据,然后呈现给数据库管理员

当应用用户连接到 MySQL 并执行一个被检测的指令时,performance_schema将每个检查的调用封装成两个宏,然后将结果记录在相应的消费者表中。这里的要点是启用工具会调用额外的代码,这意味着工具会消耗 CPU。

工具元素

performance_schema中,setup_instruments表包含所有支持的工具列表。所有工具的名称都由斜杠分隔的部分组成。我将使用以下示例来帮助您理解这些名称是如何命名的:

  • statement/sql/select

  • wait/synch/mutex/innodb/autoinc_mutex

工具名称的最左边部分表示工具的类型。因此,statement表示该工具是一个语句,wait表示它是一个等待,依此类推。

名称字段中的其余元素从左到右表示从一般到具体的子系统。在上面的示例中,selectsql子系统的一部分,属于statement类型。或者autoinc_mutex属于innodb,是更通用的mutex类的一部分,而mutex又是wait类型的更通用的sync工具的一部分。

大多数工具名称都是自描述的。如示例中所示,statement/sql/select是一个SELECT查询,而wait/synch/mutex/innodb/autoinc_mutex是 InnoDB 在自增列上设置的互斥体。setup_instruments表中还有一个DOCUMENTATION列,其中可能包含更多细节:

sql 复制代码
mysql> SELECT * FROM performance_schema.setup_instruments
    -> WHERE DOCUMENTATION IS NOT NULL LIMIT 5, 5\G
*************************** 1\. row ***************************
 NAME: statement/sql/error
 ENABLED: YES
 TIMED: YES
 PROPERTIES:
 VOLATILITY: 0
 DOCUMENTATION: Invalid SQL queries (syntax error).
*************************** 2\. row ***************************
 NAME: statement/abstract/Query
 ENABLED: YES
 TIMED: YES
 PROPERTIES: mutable
 VOLATILITY: 0
 DOCUMENTATION: SQL query just received from the network. At this point, the 
 real statement type is unknown, the type will be refined after SQL parsing.
*************************** 3\. row ***************************
 NAME: statement/abstract/new_packet
 ENABLED: YES
 TIMED: YES
 PROPERTIES: mutable
 VOLATILITY: 0
 DOCUMENTATION: New packet just received from the network. At this point, 
the real command type is unknown, the type will be refined after reading
the packet header.
*************************** 4\. row ***************************
 NAME: statement/abstract/relay_log
 ENABLED: YES
 TIMED: YES
 PROPERTIES: mutable
 VOLATILITY: 0
 DOCUMENTATION: New event just read from the relay log. At this point, the 
real statement type is unknown, the type will be refined after parsing the event.
*************************** 5\. row ***************************
 NAME: memory/performance_schema/mutex_instances
 ENABLED: YES
 TIMED: NULL
 PROPERTIES: global_statistics
 VOLATILITY: 1
 DOCUMENTATION: Memory used for table performance_schema.mutex_instances
5 rows in set (0,00 sec)

不幸的是,对于许多工具,DOCUMENTATION列可能为NULL,因此您需要使用工具名称、直觉和对 MySQL 源代码的了解来理解特定工具检查的内容。

消费者组织

正如我之前提到的,消费者是仪器发送信息的目的地。性能模式将仪器结果存储在许多表中;事实上,MySQL Community 8.0.25 中包含了 110 个performance_schema表。要理解它们的用途,最好将它们分组。

当前和历史数据

事件被放入以以下方式结尾的表中:

*_current

目前在服务器上发生的事件

*_history

每个线程的最后 10 个已完成事件

*_history_long

每个线程全局最后 10,000 个已完成事件

*_history*_history_long表的大小是可配置的。

可用的当前和历史数据包括:

events_waits

低级服务器等待,例如获取互斥锁

events_statements

SQL 语句

events_stages

概要信息,例如创建临时表或发送数据

events_transactions

事务

摘要表和摘要

摘要表包含有关表建议的聚合信息。例如,memory_summary_by_thread_by_event_name表包含每个 MySQL 线程的用户连接或任何后台线程的聚合内存使用情况。

摘要是通过消除查询中的变体来聚合查询的一种方式。看以下查询示例:

sql 复制代码
SELECT user,birthdate FROM users WHERE user_id=19;
SELECT user,birthdate FROM users WHERE user_id=13;
SELECT user,birthdate FROM users WHERE user_id=27;

此查询的摘要将是:

sql 复制代码
SELECT user,birthdate FROM users WHERE user_id=?

这使得性能模式能够跟踪摘要的延迟等指标,而无需保留每个查询的各种变体。

实例

实例指的是 MySQL 安装中可用的对象实例。例如,file_instances表包含文件名以及访问这些文件的线程数。

设置

设置表用于运行时设置performance_schema

其他表

还有其他表的名称不遵循严格的模式。例如,metadata_locks表保存有关元数据锁的数据。在讨论performance_schema可以帮助解决的问题时,我将在本章稍后介绍其中的一些。

资源消耗

性能模式收集的数据保存在内存中。您可以通过设置消费者的最大大小来限制其使用的内存量。performance_schema中的一些表支持自动缩放。这意味着它们在启动时分配最小内存量,并根据需要调整其大小。但是,一旦分配了内存,即使禁用了特定仪器并截断了表,也永远不会释放这些内存。

正如我之前提到的,每个仪器调用都会添加两个宏调用来存储数据��perform​ance_​schema中。这意味着您仪器化越多,CPU 使用率就会越高。对 CPU 利用率的实际影响取决于具体的仪器。例如,与查询期间仅调用一次的与语句相关的仪器不同,等待仪器可能会更频繁地调用。例如,要扫描具有一百万行的 InnoDB 表,引擎将需要设置并释放一百万行锁。如果您仪器化锁定,CPU 使用率可能会显著增加。但是,如果启用语句仪器,同一查询将需要一个调用来确定它是statement/sql/select。因此,如果启用语句仪器,您不会注意到 CPU 负载的增加。内存或元数据锁仪器也是如此。

限制

在讨论如何设置和使用performance_schema之前,了解其局限性是很重要的:

它必须由 MySQL 组件支持。

例如,假设您正在使用内存仪器来计算哪个 MySQL 组件或线程使用了大部分内存。您发现使用最多内存的组件是一个不支持内存仪器的存储引擎。在这种情况下,您将无法找到内存去向。

仅在特定仪器和消费者启用后才收集数据。

例如,如果您启动了一个禁用了所有仪器的服务器,然后决定对内存使用进行仪器化,您将无法知道由全局缓冲区(例如 InnoDB 缓冲池)分配的确切数量,因为在启用内存仪器化之前它已经被分配。

释放内存很困难。

您可以在启动时限制消费者的大小,或者让它们自动调整大小。在后一种情况下,它们在启动时不分配内存,而只有在启用数据收集时才分配内存。然而,即使您稍后禁用特定的工具或消费者,除非重新启动服务器,否则内存不会被释放。

在本章的其余部分,我将假设您已经了解这些限制,因此我不会特别关注它们。

系统模式

自 MySQL 5.7 版本以来,标准 MySQL 发行版包括一个名为sys模式的performance_schema数据的伴随模式。该模式仅由performance_schema上的视图和存储过程组成。虽然它旨在使您与performance_schema的体验更加顺畅,但它本身不存储任何数据。

注意

sys模式非常方便,但您需要记住它只访问存储在performance_schema表中的数据。如果您需要sys模式中不可用的数据,请检查它是否存在于performance_schema中的基础表中。

理解线程

MySQL 服务器是多线程软件。它的每个组件都使用线程。例如,可能��由主线程或存储引擎创建的后台线程,也可能是为用户连接创建的前台线程。每个线程至少有两个唯一标识符:一个操作系统线程 ID,例如,在 Linux 的ps -eLf命令的输出中可见,以及一个内部 MySQL 线程 ID。在performance_schema的大多数表中,这个内部 MySQL 线程 ID 称为THREAD_ID。此外,每个前台线程都有一个分配的PROCESSLIST_ID:连接标识符,在SHOW PROCESSLIST命令输出中可见,或者在使用 MySQL 命令行客户端连接时的"Your MySQL connection id is"字符串中可见。

警告

THREAD_ID不等于PROCESSLIST_ID

performance_schema中的threads表包含服务器中存在的所有线程:

sql 复制代码
mysql> SELECT NAME, THREAD_ID, PROCESSLIST_ID, THREAD_OS_ID 
    -> FROM performance_schema.threads;
+------------------------+-----------+----------------+--------------+
| NAME                   | THREAD_ID | PROCESSLIST_ID | THREAD_OS_ID |
+------------------------+-----------+----------------+--------------+
| thread/sql/main        |         1 |           NULL |       797580 |
| thread/innodb/io_ib... |         3 |           NULL |       797583 |
| thread/innodb/io_lo... |         4 |           NULL |       797584 |
...
| thread/sql/slave_io    |        42 |              5 |       797618 |
| thread/sql/slave_sql   |        43 |              6 |       797619 |
| thread/sql/event_sc... |        44 |              7 |       797620 |
| thread/sql/signal_h... |        45 |           NULL |       797621 |
| thread/mysqlx/accep... |        46 |           NULL |       797623 |
| thread/sql/one_conn... |     27823 |          27784 |       797695 |
| thread/sql/compress... |        48 |              9 |       797624 |
+------------------------+-----------+----------------+--------------+
44 rows in set (0.00 sec)

除了线程编号信息外,threads表包含与SHOW PROCESSLIST输出相同的数据以及一些附加列,例如RESOURCE_GROUPPARENT_THREAD_ID

警告

性能模式在各处使用THREAD_ID,而PROCESSLIST_ID仅在threads表中可用。如果您需要获取PROCESSLIST_ID,例如为了终止持有锁的连接,您需要查询threads表以获取其值。

threads表可以与许多其他表连接,以提供有关正在运行的查询的附加信息(例如,查询数据,锁定,互斥锁或打开的表实例)。

在本章的其余部分,我希望您熟悉这个表以及THREAD_ID的含义。

配置

性能模式的一些部分只能在服务器启动时更改:启用或禁用性能模式本身以及与收集数据的内存使用和限制相关的变量。性能模式仪器和消费者可以动态启用或禁用。

提示

您可以启动性能模式,所有消费者和仪器都被禁用,并且只在您期望问题发生之前启用那些需要解决特定问题的仪器。这样,您将不会在不需要的地方花费任何资源在性能模式上,也不会因为过度仪器化而使系统陷入困境。

启用和禁用性能模式

要启用或禁用性能模式,请将变量performance_schema相应地设置为ONOFF。这是一个只读变量,只能在配置文件中或在 MySQL 服务器启动时通过命令行参数更改。

启用和禁用仪器

仪器可以启用或禁用。要查看仪器的状态,可以查询setup_instruments表:

sql 复制代码
mysql> SELECT * FROM performance_schema.setup_instruments 
    -> WHERE NAME='statement/sql/select'\G
*************************** 1\. row ***************************
 NAME: statement/sql/select
 ENABLED: NO
 TIMED: YES
 PROPERTIES:
 VOLATILITY: 0
 DOCUMENTATION: NULL
1 row in set (0.01 sec)

正如我们所见,ENABLEDNO;这告诉我们我们目前没有对SELECT查询进行仪器化。

有三种选项可以启用或禁用performance_schema仪器:

  • 使用setup_instruments表。

  • 调用sys模式中的ps_setup_enable_instrument存储过程。

  • 使用启动参数performance-schema-instrument

更新语句

第一种方法是使用UPDATE语句更改列值:

sql 复制代码
mysql> UPDATE performance_schema.setup_instruments
    -> SET ENABLED='YES' WHERE NAME='statement/sql/select';
Query OK, 1 rows affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

由于这是标准 SQL,您也可以使用通配符来启用所有 SQL 语句的仪器:

sql 复制代码
mysql> UPDATE performance_schema.setup_instruments
    -> SET ENABLED='YES' WHERE NAME LIKE statement/sql/%';
Query OK, 167 rows affected (0.00 sec)
Rows matched: 167 Changed: 167 Warnings: 0

此方法在重新启动之间不会持久化。

存储过程 sys

sys模式提供了两个存储过程---ps_setup_enable_instrumentps_setup_disable_instrument---它们通过参数传递启用和禁用仪器。这两个例程都支持通配符。如果要启用或禁用所有支持的仪器,请使用通配符'%'

sql 复制代码
mysql> CALL sys.ps_setup_enable_instrument('statement/sql/select');
+----------------------+
| summary              |
+----------------------+
| Enabled 1 instrument |
+----------------------+
1 row in set (0.01 sec)

此方法实际上与前一种方法完全相同,包括在重新启动之间不会持久化。

启动选项

如前所述,这两种方法都允许您在线更改performance_schema配置,但不会在服务器重新启动之间存储该更改。如果要在重新启动之间保存特定仪器的选项,请使用配置参数performance-schema-instrument

此变量支持performance-schema-instrument=​'instrument_​name=​value'语法,其中instrument_name是仪器名称,value为启用仪器的ONTRUE1;禁用的为OFFFALSE0;对于计数而不是TIMED的为COUNTED。您可以多次指定此选项以启用或禁用不同的仪器。该选项还支持通配符:

sql 复制代码
performance-schema-instrument='statement/sql/select=ON'
警告

如果指定了多个选项,则较长的仪器字符串优先于较短的,无论顺序如何。

启用和禁用消费者

与仪器一样,消费者可以通过以下方式启用或禁用:

  • 更新性能模式中的setup_consumers

  • sys模式中使用存储过程ps_setup_enable_consumerps_setup_disable_consumer

  • 设置performance-schema-consumer配置参数

有 15 个可能的消费者。其中一些具有相当自明的名称,但有一些消费者的名称需要更多解释,列在表 3-1 中。

表 3-1. 消费者及其目的

消费者 描述
events_stages_[current|history|history_long] 分析详细信息,如"创建临时表","统计"或"缓冲池加载"
events_statements_[current|history|history_long] 语句统计
events_transactions_[current|history|history_long] 事务
events_waits_[current|history|history_long] 等待
global_instrumentation 启用或禁用全局仪器化。如果禁用,则不会检查任何单独的参数,也不会维护全局或每个线程的数据。不会收集任何单独的事件。
thread_instrumentation 每个线程的仪器化。仅在全局仪器化已启用时才会检查。如果禁用,则不会收集每个线程或单个事件数据。
statements_digest 语句摘要

为仪器给出的示例对于消费者是可重复的,使用所述方法。

为特定对象调整监控

性能模式允许您为特定对象类型、模式和名称启用和禁用监控。这是在setup_objects表中完成的。

OBJECT_TYPE列可能具有五个值之一:EVENTFUNCTIONPROCEDURETABLETRIGGER。此外,您可以指定OBJECT_SCHEMAOBJECT_NAME。支持通配符。

例如,要禁用test数据库中触发器的performance_schema,请使用以下语句:

sql 复制代码
mysql> INSERT INTO performance_schema.setup_objects
    -> (OBJECT_TYPE, OBJECT_SCHEMA, OBJECT_NAME, ENABLED) 
    -> VALUES ('TRIGGER', 'test', '%', 'NO');

如果要为名为my_trigger的触发器添加异常,请使用以下语句:

sql 复制代码
mysql> INSERT INTO performance_schema.setup_objects
    -> (OBJECT_TYPE, OBJECT_SCHEMA, OBJECT_NAME, ENABLED) 
    -> VALUES ('TRIGGER', 'test', 'my_trigger', 'YES');

performance_schema决定是否需要对特定对象进行仪器化时,首先搜索更具体的规则,然后退而求其次。例如,如果用户在触发test.my_trigger的表上运行查询,它将检查触发器触发的语句。但如果用户在触发名为test.some_other_trigger的触发器的表上运行查询,则不会检查触发器。

对于对象没有配置文件选项。如果需要在重新启动期间保留对此表的更改,您需要编写这些INSERT语句到一个 SQL 文件中,并使用init_file选项在启动时加载 SQL 文件。

调整线程监视

setup_threads表包含一个可以监视的后台线程列表。ENABLED列指定特定线程的仪器化是否已启用。HISTORY列指定特定线程的仪器化事件是否也应存储在_history_history_long表中。

例如,要禁用事件调度程序(thread/sql/event_scheduler)的历史记录,请运行:

sql 复制代码
mysql> UPDATE performance_schema.setup_threads SET HISTORY='NO' 
    -> WHERE NAME='thread/sql/event_scheduler';

setup_threads表不存储用户线程的设置。为此,存在setup_actors表,其中包含表 3-2 中描述的列。

表 3-2。setup_actors表中包含的列

列名 描述
HOST 主机,例如 localhost,%,my.domain.com 或 199.27.145.65
USER 用户名,例如sveta
ROLE 未使用
ENABLED 如果线程已启用
HISTORY 如果启用在_history_history_long表中存储数据

要为特定帐户指定规则,请使用以下命令:

sql 复制代码
mysql> INSERT INTO performance_schema.setup_actors 
    -> (HOST, USER, ENABLED, HISTORY) 
    -> VALUES ('localhost', 'sveta', 'YES', 'NO'), 
    -> ('example.com', 'sveta', 'YES', 'YES'), 
    -> ('localhost', '%', 'NO', 'NO');

此语句启用了sveta@localhostsveta@example.com的仪器化,禁用了sveta@localhost的历史记录,并禁用了所有其他从localhost连接的用户的仪器化和历史记录。

与对象监视一样,线程和参与者没有配置文件选项。如果需要在重新启动期间保留对此表的更改,您需要将这些INSERT语句写入 SQL 文件,并使用init_file选项在启动时加载 SQL 文件。

调整 Performance Schema 的内存大小

Performance Schema 将数据存储在使用PERFORMANCE_SCHEMA引擎的表中。此引擎将数据存储在内存中。默认情况下,performance_schema表中的一些表是自动调整大小的;其他表具有固定数量的行。您可以通过更改启动变量来调整这些选项。变量的名称遵循模式perform​ance_schema_object_[size|instances|classes|length|handles],其中object可以是消费者、设置表或特定事件的仪器化实例。例如,配置变量perform​ance_​schema_��events_​stages_history_size定义了perform​ance_​schema_events_stages_history表将存储的每个线程的阶段数。变量perform​ance_​schema_max_memory_classes定义了可以使用的内存仪器的最大数量。

默认值

MySQL 不同部分的默认值随版本而变化;因此,在依赖于此处描述的值之前,最好先查阅用户参考手册。但是,对于 Performance Schema,它们会影响服务器的整体性能,因此我想涵盖重要的部分。

自版本 5.7 以来,默认情况下启用了 Performance Schema,大多数仪器被禁用。只有全局、线程、语句和事务仪器被启用。自版本 8.0 以来,默认还额外启用了元数据锁和内存仪器。

mysqlinformation_schemaperformance_schema数据库未被仪器化。所有其他对象、线程和执行者都被仪器化。

大多数实例、句柄和设置表都是自动调整大小的。对于_history表,每个线程存储最后的 10 个事件。对于_history_long表,每个线程存储最新的 10,000 个事件。最大存储的 SQL 文本长度为 1,024 字节。最大的 SQL 摘要长度也是 1,024 字节。超出长度的部分会被右侧修剪。

使用性能模式

现在我已经介绍了性能模式的配置方式,我想提供一些示例来帮助您解决常见的故障排除情况。

检查 SQL 语句

正如我在"仪器元素"中提到的,性能模式支持一套丰富的仪器,用于检查 SQL 语句的性能。您将找到用于标准准备语句和存储例程的工具。通过performance_schema,您可以轻松找到哪个查询导致性能问��以及原因。

要启用语句仪表化,您需要启用类型为statement的仪器,如表 3-3 中所述。

表 3-3。Statement工具及其描述

仪器类 描述
statement/sql SQL 语句,如SELECTCREATE TABLE
statement/sp 存储过程控制
statement/scheduler 事件调度器
statement/com 命令,如quitKILLDROP DATABASEBinlog Dump。有些命令对用户不可用,由mysqld进程自身调用。
statement/abstract 四个命令的类:cloneQuerynew_packetrelay_log

常规 SQL 语句

性能模式将语句指标存储在events_statements_currentevents_statements_historyevents_statements_history_long表中。这三个表具有相同的结构。

直接使用 performance_schema

这是一个event_statement_​hist⁠ory条目的示例:

sql 复制代码
 THREAD_ID: 3200
 EVENT_ID: 22
 END_EVENT_ID: 23
 EVENT_NAME: statement/sql/select
 SOURCE: init_net_server_extension.cc:94
 TIMER_START: 878753511280779000
 TIMER_END: 878753544491277000
 TIMER_WAIT: 33210498000
 LOCK_TIME: 657000000
 SQL_TEXT: SELECT film.film_id, film.description FROM sakila.film INNER JOIN
( SELECT film_id FROM sakila.film ORDER BY title LIMIT 50, 5 ) 
AS lim USING(film_id)
 DIGEST: 2fdac27c4a9434806da3b216b9fa71aca738f70f1e8888a581c4fb00a349224f
 DIGEST_TEXT: SELECT `film` . `film_id` , `film` . `description` FROM `sakila` .
`film` INNER JOIN ( SELECT `film_id` FROM `sakila` . `film` ORDER BY 
`title` LIMIT?, ... ) AS `lim` USING ( `film_id` )
 CURRENT_SCHEMA: sakila
 OBJECT_TYPE: NULL
 OBJECT_SCHEMA: NULL
 OBJECT_NAME: NULL
 OBJECT_INSTANCE_BEGIN: NULL
 MYSQL_ERRNO: 0
 RETURNED_SQLSTATE: NULL
 MESSAGE_TEXT: NULL
 ERRORS: 0
 WARNINGS: 0
 ROWS_AFFECTED: 0
 ROWS_SENT: 5
 ROWS_EXAMINED: 10
 CREATED_TMP_DISK_TABLES: 0
 CREATED_TMP_TABLES: 1
 SELECT_FULL_JOIN: 0
 SELECT_FULL_RANGE_JOIN: 0
 SELECT_RANGE: 0
 SELECT_RANGE_CHECK: 0
 SELECT_SCAN: 2
 SORT_MERGE_PASSES: 0
 SORT_RANGE: 0
 SORT_ROWS: 0
 SORT_SCAN: 0
 NO_INDEX_USED: 1
 NO_GOOD_INDEX_USED: 0
 NESTING_EVENT_ID: NULL
 NESTING_EVENT_TYPE: NULL
 NESTING_EVENT_LEVEL: 0
 STATEMENT_ID: 25

这些列在官方文档中有解释,所以我不会逐一介绍它们。表 3-4 列出了可用作识别需要优化查询的指标的列。并非所有这些列都是相等的。例如,大多数情况下CREATED_TMP_DISK_TABLES是一个糟糕优化查询的迹象,而四个与排序相关的列可能只是表明查询结果需要排序。列的重要性表示指标的严重程度。

表 3-4。event_statement_history中可用作优化指标的列

描述 重要性
CREATED_TMP_DISK_TABLES 查询创建了这么多基于磁盘的临时表。您有两种解决此问题的选择:优化查询或增加内存临时表的最大大小。
CREATED_TMP_TABLES 查询创建了这么多基于内存的临时表。使用内存临时表本身并不是坏事。但是,如果底层表增长,它们可能会转换为基于磁盘的表。最好提前为这种情况做好准备。
SELECT_FULL_JOIN 如果JOIN执行了全表扫描,因为没有好的索引来解决查询。除非表很小,否则您需要重新考虑您的索引。
SELECT_FULL_RANGE_JOIN 如果JOIN使用了引用表的范围搜索。
SELECT_RANGE 如果JOIN使用范围搜索来解决第一个表中的行。这通常不是一个大问题。
SELECT_RANGE_CHECK 如果JOIN没有索引,每行后都会检查键。这是一个非常糟糕的症状,如果这个值大于零,您需要重新考虑表索引。
SELECT_SCAN 如果 JOIN 对第一个表进行了全扫描。如果表很大,这是一个问题。 中等
SORT_MERGE_PASSES 排序执行的合并次数。如果值大于零且查询性能较慢,可能需要增加 sort_buffer_size
SORT_RANGE 如果排序是通过范围完成的。
SORT_ROWS 排序行数。与返回行数的值进行比较。如果排序行数较高,可能需要优化查询。 中等(见描述)
SORT_SCAN 如果排序是通过扫描表来完成的。这是一个非常糟糕的迹象,除非您��意选择表中的所有行而不使用索引。
NO_INDEX_USED 未使用索引解析查询。 高,除非表很小
NO_GOOD_INDEX_USED 用于解析查询的索引不是最佳的。如果此值大于零,则需要重新考虑索引。

要找出哪些语句需要优化,您可以选择任何列并将其与零进行比较。例如,要查找所有不使用良好索引的查询,请运行以下操作:

sql 复制代码
SELECT THREAD_ID, SQL_TEXT, ROWS_SENT, ROWS_EXAMINED, CREATED_TMP_TABLES,
NO_INDEX_USED, NO_GOOD_INDEX_USED 
FROM performance_schema.events_statements_history_long
WHERE NO_INDEX_USED > 0 OR NO_GOOD_INDEX_USED > 0;

要查找所有创建临时表的查询,请运行:

sql 复制代码
SELECT THREAD_ID, SQL_TEXT, ROWS_SENT, ROWS_EXAMINED, CREATED_TMP_TABLES,
CREATED_TMP_DISK_TABLES 
FROM performance_schema.events_statements_history_long 
WHERE CREATED_TMP_TABLES > 0 OR CREATED_TMP_DISK_TABLES > 0;

您可以使用这些列中的值来单独显示潜在问题。例如,要查找所有返回错误的查询,使用条件 WHERE ERRORS > 0;要查找执行时间超过五秒的所有查询,使用条件 WHERE TIMER_WAIT > 5000000000;等等。

或者,您可以创建一个查询,通过长条件查找所有存在问题的语句,如下所示:

sql 复制代码
WHERE ROWS_EXAMINED > ROWS_SENT 
OR ROWS_EXAMINED > ROWS_AFFECTED
OR ERRORS > 0
OR CREATED_TMP_DISK_TABLES > 0
OR CREATED_TMP_TABLES > 0
OR SELECT_FULL_JOIN > 0
OR SELECT_FULL_RANGE_JOIN > 0
OR SELECT_RANGE > 0
OR SELECT_RANGE_CHECK > 0
OR SELECT_SCAN > 0
OR SORT_MERGE_PASSES > 0
OR SORT_RANGE > 0
OR SORT_ROWS > 0
OR SORT_SCAN > 0
OR NO_INDEX_USED > 0
OR NO_GOOD_INDEX_USED > 0
使用 sys schema

sys schema 提供了可用于查找存在问题的语句的视图。例如,statements_with_errors_or_warnings 列出了所有带有错误和警告的语句,而 statements_with_full_table_scans 列出了所有需要执行全表扫描的语句。sys schema 使用摘要文本而不是查询文本,因此您将获得摘要查询文本,而不是在访问原始 performance_schema 表时获得的 SQL 或摘要文本:

sql 复制代码
mysql> SELECT query, total_latency, no_index_used_count, rows_sent,
    -> rows_examined
    -> FROM sys.statements_with_full_table_scans
    -> WHERE db='employees' AND 
    -> query NOT LIKE '%performance_schema%'\G
********************** 1\. row ********************** 
 query: SELECT COUNT ( 'emp_no' ) FROM ... 'emp_no' )
 WHERE 'title' = ?
 total_latency: 805.37 ms
 no_index_used_count: 1
 rows_sent: 1
 rows_examined: 397774
 ...

其他可用于找到需要优化的语句的视图在 Table 3-5 中有描述。

Table 3-5. 可用于找到需要优化的语句的视图

视图 描述
statement_analysis 一个带有聚合统计信息的标准化语句视图,按照标准化语句的总执行时间排序。类似于 events_statements_summary_by_digest 表,但更简略。
statements_with_errors_or_warnings 所有引发错误或警告的标准化语句。
statements_with_full_table_scans. 所有执行全表扫描的标准化语句。
statements_with_runtimes_in_95th_percentile 所有平均执行时间位于前 95% 的标准化语句。
statements_with_sorting 所有执行排序的标准化语句。该视图包括所有类型的排序。
statements_with_temp_tables 所有使用临时表的标准化语句。

预处理语句

prepared_statements_instances 表包含服务器中存在的所有预处理语句。它具有与 events_statements_[current|history|history_long] 表相同的统计信息,此外还包含拥有预处理语句的线程信息以及语句执行次数。与 events_statements_[current|history|history_long] 表不同,统计数据是累加的,表中包含所有语句执行的总次数。

警告

COUNT_EXECUTE列包含语句执行的次数,因此您可以通过将总值除以此列中的数字来获得每个语句的平均统计信息。但请注意,任何平均统计信息可能是不准确的。例如,如果您执行了 10 次语句,而列SUM_SELECT_FULL_JOIN中的值为 10,则平均值将是每个语句一个完全连接。如果您然后添加一个索引并再次执行该语句,SUM_SELECT_FULL_JOIN将保持为 10,因此平均值将为 10/11 = 0.9。这并不表明问题现在已解决。

要启用准备语句的仪器,您需要启用表 3-6 中描述的仪器。

表 3-6. 用于准备语句仪器的启用

仪器类别 描述
statement/sql/prepare_sql 在文本协议中的PREPARE语句(通过 MySQL CLI 运行时)
statement/sql/execute_sql 在文本协议中的EXECUTE语句(通过 MySQL CLI 运行时)
statement/com/Prepare 在二进制协议中的PREPARE语句(如果通过 MySQL C API 访问)
statement/com/Execute 在二进制协议中的EXECUTE语句(如果通过 MySQL C API 访问)

一旦启用,您可以准备一个语句并执行几次:

sql 复制代码
mysql> PREPARE stmt FROM 
    -> 'SELECT COUNT(*) FROM employees WHERE hire_date > ?';
Query OK, 0 rows affected (0.00 sec)
Statement prepared

mysql1> SET @hd='1995-01-01';
Query OK, 0 rows affected (0.00 sec)

mysql1> EXECUTE stmt USING @hd;
+----------+
| count(*) |
+----------+
| 34004    |
+----------+
1 row in set (1.44 sec)

-- Execute a few more times with different values

然后您可以检查诊断信息:

sql 复制代码
mysql2> SELECT statement_name, sql_text, owner_thread_id, 
     -> count_reprepare, count_execute, sum_timer_execute 
     -> FROM prepared_statements_instances\G
*************************** 1\. row ***************************
 statement_name: stmt
 sql_text: select count(*) from employees where hire_date > ?
 owner_thread_id: 22
 count_reprepare: 0
 count_execute: 3
 sum_timer_execute: 4156561368000
1 row in set (0.00 sec)

请注意,只有在服务器中存在时,您才会在prepared_statements_instances表中看到语句。一旦它们被删除,您将无法再访问它们的统计信息:

sql 复制代码
mysql1> DROP PREPARE stmt;
Query OK, 0 rows affected (0.00 sec)

mysql2> SELECT * FROM prepared_statements_instances\G
Empty set (0.00 sec)

存储过程

使用performance_schema,您可以检索有关存储过程执行情况的信息:例如,IF ... ELSE流程控制语句的哪个分支被选择,或者是否调用了错误处理程序。

要启用存储过程仪器,您需要启用遵循模式'statement/sp/%'的仪器。statement/sp/stmt仪器负责例程内调用的语句,而其他仪器负责跟踪事件,例如进入或离开过程、循环或任何其他控制指令。

为了演示存储过程仪器的工作原理,使用存储过程:

sql 复制代码
CREATE DEFINER='root'@'localhost' PROCEDURE 'sp_test'(val int)
BEGIN
  DECLARE CONTINUE HANDLER FOR 1364, 1048, 1366
  BEGIN
    INSERT IGNORE INTO t1 VALUES('Some string');
    GET STACKED DIAGNOSTICS CONDITION 1 @stacked_state = RETURNED_SQLSTATE;
    GET STACKED DIAGNOSTICS CONDITION 1 @stacked_msg = MESSAGE_TEXT;
  END;
  INSERT INTO t1 VALUES(val);
END

然后用不同的值调用它:

sql 复制代码
mysql> CALL sp_test(1);
Query OK, 1 row affected (0.07 sec)

mysql> SELECT THREAD_ID, EVENT_NAME, SQL_TEXT 
    -> FROM EVENTS_STATEMENTS_HISTORY
    -> WHERE EVENT_NAME LIKE 'statement/sp%';
+-----------+-------------------------+----------------------------+
| THREAD_ID | EVENT_NAME              | SQL_TEXT                   |
+-----------+-------------------------+----------------------------+
|        24 | statement/sp/hpush_jump | NULL                       |
|        24 | statement/sp/stmt       | INSERT INTO t1 VALUES(val) |
|        24 | statement/sp/hpop       | NULL                       |
+-----------+-------------------------+----------------------------+
3 rows in set (0.00 sec)

在这种情况下,错误处理程序没有被调用,而存储过程将参数值(1)插入到表中:

sql 复制代码
mysql> CALL sp_test(NULL);
Query OK, 1 row affected (0.07 sec)

mysql> SELECT THREAD_ID, EVENT_NAME, SQL_TEXT 
    -> FROM EVENTS_STATEMENTS_HISTORY
    -> WHERE EVENT_NAME LIKE 'statement/sp%';
+-----------+-------------------------+------------------------------+
| THREAD_ID | EVENT_NAME              | SQL_TEXT                     |
+-----------+-------------------------+------------------------------+
|        24 | statement/sp/hpush_jump | NULL                         |
|        24 | statement/sp/stmt       | INSERT INTO t1 VALUES(val)   |
|        24 | statement/sp/stmt       | INSERT IGNORE INTO t1 
                                                 VALUES('Some str... |
|        24 | statement/sp/stmt       | GET STACKED DIAGNOSTICS 
                                                 CONDITION 1 @s...   |
|        24 | statement/sp/stmt       | GET STACKED DIAGNOSTICS 
                                                 CONDITION 1 @s...   |
|        24 | statement/sp/hreturn    | NULL                         |
|        24 | statement/sp/hpop       | NULL                         |
+-----------+-------------------------+------------------------------+
7 rows in set (0.00 sec)

然而,在第二次调用中,events_statements_history表的内容不同:它包含了来自错误处理程序的调用以及替换错误语句的 SQL 语句。

虽然存储过程本身的返回值没有改变,但我们清楚地看到它已经以不同的方式执行。了解例程执行流程中的这些差异可以帮助理解为什么同一个例程如果被调用一次几乎立即完成,而另一次调用时可能需要更长的时间。

语句分析

events_stages_[current|history|history_long]表包含了诸如 MySQL 在创建临时表、更新或等待锁时花费的时间等分析信息。要启用分析,您需要启用相应的消费者以及遵循模式'stage/%'的仪器。一旦启用,您可以找到答案,比如"查询执行的哪个阶段花费了非常长的时间?"以下示例搜索了花费超过一秒的阶段:

sql 复制代码
mysql> SELECT eshl.event_name, sql_text, 
    ->        eshl.timer_wait/10000000000 w_s
    -> FROM performance_schema.events_stages_history_long eshl
    -> JOIN performance_schema.events_statements_history_long esthl
    -> ON (eshl.nesting_event_id = esthl.event_id)
    -> WHERE eshl.timer_wait > 1*10000000000\G
*************************** 1\. row ***************************
 event_name: stage/sql/Sending data
 sql_text: SELECT COUNT(emp_no) FROM employees JOIN salaries 
 USING(emp_no) WHERE hire_date=from_date
 w_s: 81.7
1 row in set (0.00 sec)

使用events_stages_[current|history|history_long]表的另一种技术是关注那些在已知会导致性能问题的阶段中花费超过一定阈值的语句。表 3-7 列出了��些阶段。

表 3-7. 表现问题的指标阶段

阶段类别 描述
stage/sql/%tmp% 与临时表相关的所有内容。
stage/sql/%lock% 与锁相关的所有内容。
stage/%/Waiting for% 一切等待资源的内容。
stage/sql/Sending data 这个阶段应该与语句统计中的 ROWS_SENT 数量进行比较。如果 ROWS_SENT 很小,一个在这个阶段花费大量时间的语句可能意味着它必须创建一个临时文件或表来解决中间结果。这通常会在向客户端发送数据之前对行进行过滤。这通常是一个查询优化不佳的症状。
stage/sql/freeing items``stage/sql/cleaning up``stage/sql/closing tables``stage/sql/end 这些是清理资源的阶段。不幸的是,它们的细节不够详细,每个阶段包含的任务不止一个。如果你发现你的查询在这些阶段花费了很长时间,很可能是由于高并发导致资源争用。你需要检查 CPU、I/O 和内存使用情况,以及你的硬件和 MySQL 选项是否能够处理应用程序创建的并发。

非常重要的一点是,性能分析仅适用于一般服务器阶段。存储引擎不支持使用 performance_schema 进行性能分析。因此,诸如 stage/sql/update 这样的阶段意味着作业在存储引擎内部,并且可能包括不仅仅是更新本身,还包括等待存储引擎特定锁或其他争用问题。

检查读写性能

在 Performance Schema 中的语句仪表化非常有用,可以帮助理解你的工作负载是读取还是写入受限。你可以从统计语句的类型开始:

sql 复制代码
mysql> SELECT EVENT_NAME, COUNT(EVENT_NAME) 
    -> FROM events_statements_history_long 
    -> GROUP BY EVENT_NAME;
+----------------------+-------------------+
| EVENT_NAME           | COUNT(EVENT_NAME) |
+----------------------+-------------------+
| statement/sql/insert |               504 |
| statement/sql/delete |               502 |
| statement/sql/select |              6987 |
| statement/sql/update |              1007 |
| statement/sql/commit |               500 |
| statement/sql/begin  |               500 |
+----------------------+-------------------+
6 rows in set (0.03 sec)

在这个示例中,SELECT 查询的数量大于任何其他查询的数量。这表明在这个设置中,大多数查询都是读取查询。

如果想了解语句的延迟,可以按 LOCK_TIME 列进行聚合:

sql 复制代码
mysql> SELECT EVENT_NAME, COUNT(EVENT_NAME), 
    -> SUM(LOCK_TIME/1000000) AS latency_ms
    -> FROM events_statements_history 
    -> GROUP BY EVENT_NAME ORDER BY latency_ms DESC;
+----------------------------------+-------------------+------------+
| EVENT_NAME                       | COUNT(EVENT_NAME) | latency_ms |
+----------------------------------+-------------------+------------+
| statement/sql/select             |               194 |  7362.0000 |
| statement/sql/update             |                33 |  1276.0000 |
| statement/sql/insert             |                16 |   599.0000 |
| statement/sql/delete             |                16 |   470.0000 |
| statement/sql/show_status        |                 2 |   176.0000 |
| statement/sql/begin              |                 4 |     0.0000 |
| statement/sql/commit             |                 2 |     0.0000 |
| statement/com/Ping               |                 2 |     0.0000 |
| statement/sql/show_engine_status |                 1 |     0.0000 |
+----------------------------------+-------------------+------------+
9 rows in set (0.01 sec)

你可能还想了解读取和写入的字节数和行数。为此,使用全局状态变量 Handler_*

sql 复制代码
mysql> WITH rows_read AS (SELECT SUM(VARIABLE_VALUE) AS rows_read
    -> FROM global_status
    -> WHERE VARIABLE_NAME IN ('Handler_read_first', 'Handler_read_key',
    -> 'Handler_read_next', 'Handler_read_last', 'Handler_read_prev',
    -> 'Handler_read_rnd', 'Handler_read_rnd_next')), 
    -> rows_written AS (SELECT SUM(VARIABLE_VALUE) AS rows_written
    -> FROM global_status
    -> WHERE VARIABLE_NAME IN ('Handler_write')) 
    -> SELECT * FROM rows_read, rows_written\G
*************************** 1\. row ***************************
rows_read: 169358114082
rows_written: 33038251685
1 row in set (0.00 sec)

检查元数据锁

元数据锁用于保护数据库对象定义免受修改。任�� SQL 语句都会设置共享元数据锁:SELECTUPDATE 等。它们不会影响其他需要共享元数据锁的语句。但是,它们会阻止那些改变数据库对象定义的语句(如 ALTER TABLECREATE INDEX)启动,直到锁被释放。虽然大多数由元数据锁冲突引起的问题影响表,但锁本身是为任何数据库对象设置的,如 SCHEMAEVENTTABLESPACE 等。

元数据锁会一直保持直到事务结束。如果使用多语句事务,这会使故障排除变得更加困难。哪个语句正在等待锁通常是明确的:DDL 语句会隐式提交事务,因此它们是新事务中唯一的语句,并且你会在进程列表中找到它们处于"等待元数据锁"状态。然而,持有锁的语句可能会在进程列表中消失,如果它是仍然打开的多语句事务的一部分。

performance_schema 中的 metadata_locks 表保存了不同线程当前设置的锁的信息,还保存了等待锁的锁请求信息。这样,你可以轻松地识别哪个线程不允许你的 DDL 请求启动,并决定是否要终止此语句或等待其执行完成。

要启用元数据锁仪表化,需要启用 wait/lock/metadata/sql/mdl 仪表。

以下示例显示了一个线程,在进程列表中以 ID 5 可见,持有了线程 processlist_id=4 正在等待的锁:

sql 复制代码
mysql> SELECT processlist_id, object_type, 
    -> lock_type, lock_status, source
    -> FROM metadata_locks JOIN threads ON (owner_thread_id=thread_id)
    -> WHERE object_schema='employees' AND object_name='titles'\G
*************************** 1\. row ***************************
 processlist_id: 4
 object_type: TABLE
 lock_type: EXCLUSIVE
 lock_status: PENDING -- waits
 source: mdl.cc:3263
*************************** 2\. row ***************************
 processlist_id: 5
 object_type: TABLE
 lock_type: SHARED_READ
 lock_status: GRANTED -- holds
 source: sql_parse.cc:5707

检查内存使用情况

要在 performance_schema 中启用内存仪表化,需要启用 memory 类的仪表。一旦启用,你可以找到有关 MySQL 内部结构如何使用内存的详细信息。

直接使用 performance_schema

Performance Schema 将内存使用统计信息存储在以memory_summary_前缀开头的摘要表中。内存使用聚合在 Table 3-8 中描述。

表 3-8. 内存使用的聚合参数

聚合参数 描述
global 每个事件名称的全局
thread 每个线程:包括后台线程和用户线程
account 用户账户
host 主机
user 用户名

例如,要找到使用大部分内存的 InnoDB 结构,请执行以下查询:

sql 复制代码
mysql> SELECT EVENT_NAME, 
    -> CURRENT_NUMBER_OF_BYTES_USED/1024/1024 AS CURRENT_MB, 
    -> HIGH_NUMBER_OF_BYTES_USED/1024/1024 AS HIGH_MB 
    -> FROM performance_schema.memory_summary_global_by_event_name 
    -> WHERE EVENT_NAME LIKE 'memory/innodb/%' 
    -> ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC LIMIT 10;
+----------------------------+--------------+--------------+
| EVENT_NAME                 | CURRENT_MB   | HIGH_MB      |
+----------------------------+--------------+--------------+
| memory/innodb/buf_buf_pool | 130.68750000 | 130.68750000 |
| memory/innodb/ut0link_buf  |  24.00006104 |  24.00006104 |
| memory/innodb/buf0dblwr    |  17.07897949 |  24.96951294 |
| memory/innodb/ut0new       |  16.07891273 |  16.07891273 |
| memory/innodb/sync0arr     |   6.25006866 |   6.25006866 |
| memory/innodb/lock0lock    |   4.85086060 |   4.85086060 |
| memory/innodb/ut0pool      |   4.00003052 |   4.00003052 |
| memory/innodb/hash0hash    |   3.69776917 |   3.69776917 |
| memory/innodb/os0file      |   2.60422516 |   3.61988068 |
| memory/innodb/memory       |   1.23812866 |   1.42373657 |
+----------------------------+--------------+--------------+
10 rows in set (0,00 sec)

使用 sys 模式

sys模式具有视图,允许您以更好的方式获取内存统计信息。它们还支持按hostuserthreadglobal进行聚合。视图memory_global_total包含一个单一值,显示了被检测内存的总量:

sql 复制代码
mysql> SELECT * FROM sys.memory_global_total;
+-----------------+
| total_allocated |
+-----------------+
| 441.84 MiB      |
+-----------------+
1 row in set (0,09 sec)

聚合视图将字节转换为需要的千字节、兆字节和千兆字节。视图memory_by_thread_by_current_bytes有一个user列,可能取以下值之一:

NAME@HOST

常规用户账户,比如sveta@oreilly.com

系统用户,比如sql/maininnodb/*

此类"用户名"的数据来自threads表,当您需要了解特定线程在做什么时非常方便。

视图memory_by_thread_by_current_bytes中的行按照当前分配的内存量降序排序,因此您将轻松找到占用大部分内存的线程:

sql 复制代码
mysql> SELECT thread_id tid, user, 
    -> current_allocated ca, total_allocated 
    -> FROM sys.memory_by_thread_by_current_bytes LIMIT 9;
+-----+----------------------------+------------+-----------------+
| tid | user                       | ca         | total_allocated |
+-----+----------------------------+------------+-----------------+
|  52 | sveta@localhost            | 1.36 MiB   | 10.18 MiB       |
|   1 | sql/main                   | 1.02 MiB   | 4.95 MiB        |
|  33 | innodb/clone_gtid_thread   | 525.36 KiB | 24.04 MiB       |
|  44 | sql/event_scheduler        | 145.72 KiB | 4.23 MiB        |
|  43 | sql/slave_sql              | 48.74 KiB  | 142.46 KiB      |
|  42 | sql/slave_io               | 20.03 KiB  | 232.23 KiB      |
|  48 | sql/compress_gtid_table    | 13.91 KiB  | 17.06 KiB       |
|  25 | innodb/fts_optimize_thread | 1.92 KiB   | 2.00 KiB        |
|  34 | innodb/srv_purge_thread    | 1.56 KiB   | 1.64 KiB        |
+-----+----------------------------+------------+-----------------+
9 rows in set (0,03 sec)

上面的示例是在笔记本电脑上进行的;因此,数字并不描述生产服务器的情况。仍然清楚的是,本地连接使用了大部分内存,其次是主服务器进程。

当你需要找到占用最多内存的用户线程时,内存工具非常方便。在下面的示例中,一个用户连接分配了 36 GB 的 RAM,即使在现代高内存系统中也相当巨大:

sql 复制代码
mysql> SELECT * FROM sys.memory_by_thread_by_current_bytes
    -> ORDER BY current_allocated desc\G
*************************** 1\. row ***************************
 thread_id: 152
 user: lj@127.0.0.1
 current_count_used: 325
 current_allocated: 36.00 GiB
 current_avg_alloc: 113.43 MiB
 current_max_alloc: 36.00 GiB
 total_allocated: 37.95 GiB
...

检查变量

Performance Schema 将变量检测提升到一个新水平。它为以下内容提供了检测:

  • 服务器变量

    • 全局

    • 会话,适用于所有当前打开的会话

    • 来源,所有当前变量值的来源

  • 状态变量

    • 全局

    • 会话,适用于所有当前打开的会话

    • 按照聚合

      • 主机

      • 用户

      • 账户

      • 线程

  • 用户变量

警告

在 5.7 版本之前,服务器和状态变量在information_schema中被检测。这种检测是有限的:它只允许跟踪全局和当前会话值。其他会话中的变量和状态信息,以及用户变量的信息是不可访问的。然而,出于向后兼容性的原因,MySQL 5.7 使用information_schema来跟踪变量。要启用对变量的performance_schema支持,您需要将配置变量show_compatibility_56设置为0。这个要求,以及information_schema中的变量表,在 8.0 版本中不再存在。

全局变量值存储在表global_variables中。当前会话的会话变量存储在表session_variables中。这两个表只有两列,列名自明:VARIABLE_NAMEVARIABLE_VALUE

variables_by_thread表有一个额外的列,THREAD_ID,指示变量所属的线程。这使您可以找到将会话变量值设置为与默认配置不同的线程。

在下面的示例中,具有THREAD_ID=84的线程将变量tx_isolation设置为SERIALIZABLE,这可能导致事务获取的锁比使用默认级别时更多:

sql 复制代码
mysql> SELECT * FROM variables_by_thread 
    -> WHERE VARIABLE_NAME='tx_isolation';
+-----------+---------------+-----------------+
| THREAD_ID | VARIABLE_NAME |  VARIABLE_VALUE |
+-----------+---------------+-----------------+
|        71 |  tx_isolation | REPEATABLE-READ |
|        83 |  tx_isolation | REPEATABLE-READ |
|        84 |  tx_isolation | SERIALIZABLE    |
+-----------+---------------+-----------------+
3 rows in set, 3 warnings (0.00 sec)

下面的示例找到所有具有与当前活动会话不同的会话变量值的线程:

sql 复制代码
mysql> SELECT vt2.THREAD_ID AS TID, vt2.VARIABLE_NAME, 
    -> vt1.VARIABLE_VALUE AS MY_VALUE, 
    -> vt2.VARIABLE_VALUE AS OTHER_VALUE 
    -> FROM performance_schema.variables_by_thread vt1 
    -> JOIN performance_schema.threads t USING(THREAD_ID) 
    -> JOIN performance_schema.variables_by_thread vt2 
    -> USING(VARIABLE_NAME) 
    -> WHERE vt1.VARIABLE_VALUE != vt2.VARIABLE_VALUE 
    -> AND t.PROCESSLIST_ID=@@pseudo_thread_id;
+-----+--------------------+-------------------+--------------------+
| TID | VARIABLE_NAME      | MY_VALUE          | OTHER_VALUE        |
+-----+--------------------+-------------------+--------------------+
|  42 | max_allowed_packet | 67108864          | 1073741824         |
|  42 | pseudo_thread_id   | 22715             | 5                  |
|  42 | timestamp          | 1626650242.678049 | 1626567255.695062  |
|  43 | gtid_next          | AUTOMATIC         | NOT_YET_DETERMINED |
|  43 | pseudo_thread_id   | 22715             | 6                  |
|  43 | timestamp          | 1626650242.678049 | 1626567255.707031  |
+-----+--------------------+-------------------+--------------------+
6 rows in set (0,01 sec)

全局和当前会话状态值分别存储在表global_statussession_status中。它们也只有两列:VARIABLE_NAMEVARIABLE_VALUE

状态变量可以按用户帐户、主机、用户和线程进行聚合。在我看来,最有趣的聚合是按线程进行的,因为它可以快速识别哪个连接在服务器上造成了大部分资源压力。例如,以下代码片段清楚地显示了THREAD_ID=83的连接正在进行大部分写操作:

sql 复制代码
mysql> SELECT * FROM status_by_thread 
    -> WHERE VARIABLE_NAME='Handler_write';
+-----------+---------------+----------------+
| THREAD_ID | VARIABLE_NAME | VARIABLE_VALUE |
+-----------+---------------+----------------+
|        71 | Handler_write | 94             |
|        83 | Handler_write | 4777777777     | -- Most writes
|        84 | Handler_write | 101            |
+-----------+---------------+----------------+
3 rows in set (0.00 sec)

用户定义变量是通过SET @my_var = 'foo'创建的,并在表user_variables_by_thread中进行跟踪:

sql 复制代码
mysql> SELECT * FROM user_variables_by_thread;
+-----------+---------------+----------------+
| THREAD_ID | VARIABLE_NAME | VARIABLE_VALUE |
+-----------+---------------+----------------+
|        71 | baz           | boo            |
|        84 | foo           | bar            |
+-----------+---------------+----------------+
2 rows in set (0.00 sec)

当您需要找出内存消耗的来源时,此工具非常有用,因为每个变量都需要字节来保存其值。您还可以使用此信息解决与持久��接、使用用户定义变量相关的棘手问题。最后但同样重要的是,此表是唯一的方法来查找您在自己会话中定义的变量。

variables_info不包含任何变量值。相反,它包含有关服务器变量来源以及其他文档的信息,例如变量的默认最小值和最大值。SET_TIME列包含最新变量更改的时间戳。SET_HOSTSET_USER列标识设置变量的用户帐户。例如,要查找自服务器启动以来动态更改的所有变量,请运行:

sql 复制代码
mysql> SELECT * FROM performance_schema.variables_info 
    -> WHERE VARIABLE_SOURCE = 'DYNAMIC'\G
*************************** 1\. row ***************************
 VARIABLE_NAME: foreign_key_checks
 VARIABLE_SOURCE: DYNAMIC
 VARIABLE_PATH:
    MIN_VALUE: 0
    MAX_VALUE: 0
    SET_TIME: 2021-07-18 03:14:15.560745
    SET_USER: NULL
    SET_HOST: NULL
*************************** 2\. row ***************************
 VARIABLE_NAME: sort_buffer_size
 VARIABLE_SOURCE: DYNAMIC
 VARIABLE_PATH:
    MIN_VALUE: 32768
    MAX_VALUE: 18446744073709551615
    SET_TIME: 2021-07-19 02:37:11.948190
    SET_USER: sveta
    SET_HOST: localhost
2 rows in set (0,00 sec)

可能的VARIABLE_SOURCE值包括:

COMMAND_LINE

在命令行上设置的变量

COMPILED

编译默认值

PERSISTED

从特定服务器的mysqld-auto.cnf选项文件设置

也有许多变量选项,设置在不同的选项文件中。我不会讨论它们全部:它们要么是自描述的,要么可以在用户参考手册中轻松查找。细节的数量也随着版本的增加而增加。

检查最频繁的错误

除了特定的错误信息,performance_schema还提供摘要表,通过用户、主机、帐户、线程以及全局按错误编号聚合错误。所有聚合表的结构与events_errors_summary_global_by_error表中使用的结构类似:

sql 复制代码
mysql> USE performance_schema;
mysql> SHOW CREATE TABLE events_errors_summary_global_by_error\G
*************************** 1\. row ***************************
    Table: events_errors_summary_global_by_error
Create Table: CREATE TABLE `events_errors_summary_global_by_error` (
 `ERROR_NUMBER` int DEFAULT NULL,
 `ERROR_NAME` varchar(64) DEFAULT NULL,
 `SQL_STATE` varchar(5) DEFAULT NULL,
 `SUM_ERROR_RAISED` bigint unsigned NOT NULL,
 `SUM_ERROR_HANDLED` bigint unsigned NOT NULL,
 `FIRST_SEEN` timestamp NULL DEFAULT '0000-00-00 00:00:00',
 `LAST_SEEN` timestamp NULL DEFAULT '0000-00-00 00:00:00',
 UNIQUE KEY `ERROR_NUMBER` (`ERROR_NUMBER`)
) ENGINE=PERFORMANCE_SCHEMA DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0,00 sec)

ERROR_NUMBERERROR_NAMESQL_STATE标识错误。SUM_ERROR_RAISED是错误被引发的次数。SUM_ERROR_HANDLED是错误被处理的次数。FIRST_SEENLAST_SEEN是错误首次和最后出现的时间戳。

特定的聚合表具有额外的列。因此,表events_errors_summary_by_thread_by_error有一个名为THREAD_ID的列,用于标识引发错误的线程,表events_errors_summary_by_host_by_error有一个名为HOST的列,依此类推。

例如,要查找所有运行导致错误超过 10 次的语句的帐户,请运行:

sql 复制代码
mysql> SELECT * FROM 
    -> performance_schema.events_errors_summary_by_account_by_error 
    -> WHERE SUM_ERROR_RAISED > 10 AND USER IS NOT NULL 
    -> ORDER BY SUM_ERROR_RAISED DESC\G
*************************** 1\. row ***************************
    USER: sveta
    HOST: localhost
    ERROR_NUMBER: 3554
    ERROR_NAME: ER_NO_SYSTEM_TABLE_ACCESS
    SQL_STATE: HY000
    SUM_ERROR_RAISED: 60
    SUM_ERROR_HANDLED: 0
    FIRST_SEEN: 2021-07-18 03:14:59
    LAST_SEEN: 2021-07-19 02:50:13
1 row in set (0,01 sec)

错误摘要表对于查找哪些用户帐户、主机、用户或线程发送了最多错误查询并执行操作可能很有用。它们还可以帮助处理像ER_DEPRECATED_UTF8_ALIAS这样的错误,这可能表明一些经常使用的查询是为以前的 MySQL 版本编写的,需要更新。

检查性能模式本身

您可以使用与自己模式相同的工具和消费者检查性能模式本身。只需注意,默认情况下,如果将performance_schema设置为默认数据库,则不会跟踪对其的查询。如果需要检查对performance_schema的查询,首先需要更新setup_actors表。

一旦更新了setup_actors表,所有工具都可以使用。例如,要查找在performance_schema中分配了大部分内存的前 10 个消费者,请运行:

sql 复制代码
mysql> SELECT SUBSTRING_INDEX(EVENT_NAME, '/', -1) AS EVENT,
    -> CURRENT_NUMBER_OF_BYTES_USED/1024/1024 AS CURRENT_MB, 
    -> HIGH_NUMBER_OF_BYTES_USED/1024/1024 AS HIGH_MB 
    -> FROM performance_schema.memory_summary_global_by_event_name 
    -> WHERE EVENT_NAME LIKE 'memory/performance_schema/%'
    -> ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC LIMIT 10;
+----------------------------------------+-------------+-------------+
| EVENT                                  | CURRENT_MB  | HIGH_MB     |
+----------------------------------------+-------------+-------------+
| events_statements_summary_by_digest    | 39.67285156 | 39.67285156 |
| events_statements_history_long         | 13.88549805 | 13.88549805 |
| events_errors_summary_by_thread_by_... | 11.81640625 | 11.81640625 |
| events_statements_summary_by_thread... |  9.79296875 |  9.79296875 |
| events_statements_history_long.dige... |  9.76562500 |  9.76562500 |
| events_statements_summary_by_digest... |  9.76562500 |  9.76562500 |
| events_statements_history_long.sql_... |  9.76562500 |  9.76562500 |
| memory_summary_by_thread_by_event_name |  7.91015625 |  7.91015625 |
| events_errors_summary_by_host_by_error |  5.90820313 |  5.90820313 |
| events_errors_summary_by_account_by... |  5.90820313 |  5.90820313 |
+----------------------------------------+-------------+-------------+
10 rows in set (0,00 sec)

或使用sys模式:

sql 复制代码
mysql> SELECT SUBSTRING_INDEX(event_name, '/', -1), current_alloc 
    -> FROM sys.memory_global_by_current_bytes 
    -> WHERE event_name LIKE 'memory/performance_schema/%' LIMIT 10;
+---------------------------------------------------+---------------+
| SUBSTRING_INDEX(event_name, '/', -1)              | current_alloc |
+---------------------------------------------------+---------------+
| events_statements_summary_by_digest               | 39.67 MiB     |
| events_statements_history_long                    | 13.89 MiB     |
| events_errors_summary_by_thread_by_error          | 11.82 MiB     |
| events_statements_summary_by_thread_by_event_name | 9.79 MiB      |
| events_statements_history_long.digest_text        | 9.77 MiB      |
| events_statements_summary_by_digest.digest_text   | 9.77 MiB      |
| events_statements_history_long.sql_text           | 9.77 MiB      |
| memory_summary_by_thread_by_event_name            | 7.91 MiB      |
| events_errors_summary_by_host_by_error            | 5.91 MiB      |
| events_errors_summary_by_account_by_error         | 5.91 MiB      |
+---------------------------------------------------+---------------+
10 rows in set (0,00 sec)

performance_schema还支持SHOW ENGINE PERFORMANCE_SCHEMA STATUS语句:

sql 复制代码
mysql> SHOW ENGINE PERFORMANCE_SCHEMA STATUS\G
*************************** 1\. row ***************************
 Type: performance_schema
 Name: events_waits_current.size
 Status: 176
*************************** 2\. row ***************************
 Type: performance_schema
 Name: events_waits_current.count
 Status: 1536
*************************** 3\. row ***************************
 Type: performance_schema
 Name: events_waits_history.size
 Status: 176
*************************** 4\. row ***************************
 Type: performance_schema
 Name: events_waits_history.count
 Status: 2560
...
*************************** 244\. row ***************************
 Type: performance_schema
 Name: (pfs_buffer_scalable_container).count
 Status: 17
*************************** 245\. row ***************************
 Type: performance_schema
 Name: (pfs_buffer_scalable_container).memory
 Status: 1904
*************************** 246\. row ***************************
 Type: performance_schema
 Name: (max_global_server_errors).count
 Status: 4890
*************************** 247\. row ***************************
 Type: performance_schema
 Name: (max_session_server_errors).count
 Status: 1512
*************************** 248\. row ***************************
 Type: performance_schema
 Name: performance_schema.memory
 Status: 218456400
248 rows in set (0,00 sec)

在其输出中,您将找到诸如存储在消费者中的特定事件数量或特定指标的最大值等细节。最后一行包含性能模式当前占用的字节数。

总结

性能模式是一个经常受到批评的功能。MySQL 的早期版本实现不够优化,导致资源消耗较高。通常建议只需关闭它。

它也被认为难以理解。启用一个仪器只是在服务器中启用一个额外的代码片段,记录数据并将其提交给消费者。消费者只是存储在内存中的表,您需要使用标准 SQL 向表提出正确的问题,以找到您要查找的内容。了解性能模式如何管理自己的内存将帮助您意识到 MySQL 并非内存泄漏;它只是将消费者数据保留在内存中,并且只在重新启动时释放该内存。

我在这里的建议很简单:您应该保持性能模式启用,动态启用仪器和消费者,以帮助您解决可能存在的任何问题------查询性能、锁定、磁盘 I/O、错误等。您还应该利用sys模式作为解决最常见问题的捷径。这样做将为您提供一种直接从 MySQL 内部测量性能的可访问方式。

相关推荐
xjjeffery2 分钟前
MySQL 基础
数据库·mysql
恒辉信达18 分钟前
hhdb数据库介绍(8-4)
服务器·数据库·mysql
小兜全糖(xdqt)3 小时前
mysql数据同步到sql server
mysql·adb
Karoku0663 小时前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
周全全3 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
白云如幻3 小时前
MySQL的分组函数
数据库·mysql
秋意钟6 小时前
MySQL日期类型选择建议
数据库·mysql
ac-er88886 小时前
MySQL如何实现PHP输入安全
mysql·安全·php
桀桀桀桀桀桀7 小时前
数据库中的用户管理和权限管理
数据库·mysql
瓜牛_gn12 小时前
mysql特性
数据库·mysql