InnoDB-集群入门指南-全-

InnoDB 集群入门指南(全)

原文:Introducing InnoDB Cluster

协议:CC BY-NC-SA 4.0

一、高可用性简介

管理基础设施的数据库管理员和系统架构师了解在尽量减少维护工作量的同时构建冗余的必要性。用于实现这一点的工具之一是一类使服务器或服务尽可能可用的特性。我们称之为高可用性

高可用性不仅是建立健壮的、始终就绪的基础设施的关键因素,也是健壮的、企业级数据库系统的质量的关键因素。Oracle 一直在开发和改进 MySQL 中的高可用性特性。事实上,这些功能已经成熟,包括详细的管理和配置、状态报告,甚至主服务器的自动故障转移,以确保即使主服务器出现故障,您的数据仍然可用。最重要的是,Oracle 在 MySQL 的社区版中包含了这些特性,因此全世界都可以使用它们。

通过新的 InnoDB 集群特性实现的 MySQL 高可用性是建立在 MySQL 复制的长期稳定性基础上的组件集合。这些组件包括对服务器的修改和新组件,如组复制、用于路由连接的路由以及用于简化设置和配置的 MySQL Shell。这些组件共同构成了 MySQL 高可用性的新范例。

在本章中,您将了解什么是高可用性,以及如何通过使用 MySQL 高可用性功能组合的第一个构建块来实现高可用性。您还将看到一个关于如何使用 MySQL 复制的简短教程。了解如何通过使用 InnoDB Cluster 之前的可用特性在 MySQL 中实现高可用性,将有助于阐明 InnoDB Cluster 如何改进这些特性。

让我们从一个关于高可用性的简短教程开始。

MYSQL——这意味着什么?

名称 MySQL 是一个专有名称和一个缩写的组合。 SQL 代表结构化查询语言。我的部分不是所有格形式——它是一个名字。在这种情况下,我的是创始人女儿的名字。至于发音,MySQL 专家发音为“My-S-Q-L”而不是“my sequel”

什么是高可用性?

高可用性是最容易理解的,如果你认为它是可靠性的松散同义词——使解决方案尽可能易于使用,并在商定的时间内容忍计划内或计划外的故障。也就是说,这是用户对系统可操作性的期望值。系统越可靠,运行时间越长,就相当于可用性水平越高。

高可用性可以通过多种方式实现,从而产生不同级别的可用性。这些级别可以表示为达到某种更高可靠性状态的目标。本质上,您使用技术和工具来提高可靠性,并使解决方案尽可能长时间地保持运行和数据可用(也称为正常运行时间)。正常运行时间表示为解决方案运行时间的比率或百分比。

您可以通过实践以下工程原则来实现高可用性:

  • 消除单点故障:设计您的解决方案时,尽可能减少组件数量,以免在组件出现故障时导致解决方案无法使用。

  • 通过冗余增加恢复能力:设计您的解决方案,允许多个活动冗余机制,以便从故障中快速恢复。

  • 实施容错:设计您的解决方案,通过切换到冗余或替代机制,主动检测故障并自动恢复。

这些原则是实现更高级别的可靠性和高可用性的基础或步骤。即使您不需要实现最大的高可用性(解决方案几乎一直处于运行状态),通过实施这些原则,您至少会使您的解决方案更加可靠,这是一个很好的目标。

既然您已经理解了高可用性可以解决的目标或需求,那么让我们讨论一下在 MySQL 解决方案中实现高可用性的一些选项。以下部分讨论了实现高可用性目标的四个选项。通过实现所有这些,您将获得一定程度的高可用性。您的成就不仅取决于您如何实现这些选项,还取决于您满足可靠性目标的程度。

可靠性与高可用性:区别是什么?

可靠性是对解决方案随时间推移的可操作性的度量,涵盖了高可用性的主要目标之一。事实上,你可以说最终的可靠性水平——解决方案总是可操作的——是高可用性的定义。要使您的解决方案成为高可用性解决方案,您应该专注于提高可靠性。

恢复

最容易实现的可靠性是从故障中恢复的能力。这可能是组件、应用服务器、数据库服务器或解决方案的任何其他部分的故障。恢复因此,就是如何在尽可能短的时间内以尽可能低的成本让解决方案恢复运行。

但是,可能无法从所有类型的故障中恢复。例如,如果您的一台或多台服务器发生灾难性磁盘故障,恢复可能需要更换硬件,并且在停机期间会丢失数据。对于其他类型的故障,恢复选项可能允许更快地恢复运行。此外,有些组件更重要,必须是可恢复的,所以您应该努力保护那些更重要的组件,其中数据库是主要的。

例如,如果您的数据由于硬件故障而损坏或丢失,您需要一种尽可能少丢失数据的方法来恢复数据。实现这一点的一种方法是保留数据的频繁备份副本,以后可以恢复这些副本以防止数据丢失。

已经有许多关于备份和恢复数据的各种策略的书籍问世。我没有试图解释每一个细微差别、技术和最佳实践,而是向您推荐了许多可用的文本。对于本书和 MySQL 可用的解决方案,理解有两种类型的备份方法(逻辑和物理)就足够了,每种方法都有自己的优点。

逻辑备份

逻辑备份通过遍历数据、逐行复制数据以及通常将数据从二进制形式转换为 SQL 语句来复制数据。逻辑备份的优点是数据是可读的,甚至可以在恢复数据之前对其进行修改或更正。缺点是,对于大量数据,逻辑备份往往很慢,并且可能需要比实际数据更多的存储空间(取决于数据类型、索引数量等)。

物理备份

物理备份从磁盘存储层制作数据的二进制副本。备份通常是特定于应用的;您必须使用制作备份的同一应用来恢复它。这样做的好处是,备份速度更快,大小更小。此外,执行物理备份的应用具有一些高级功能,如增量备份(仅备份自上次备份以来发生变化的数据)和其他高级功能。对于小型解决方案,逻辑备份可能已经足够了,但是随着您的解决方案(您的数据)的增长,您将需要使用物理备份解决方案。

裁员

可靠性更具挑战性的实现之一是冗余——让两个或更多组件在系统中扮演相同的角色。冗余的目标可能只是在需要替换主要组件的情况下准备一个组件。这可能是一个热备用:该组件主动与主组件并行工作,当检测到故障时,您的系统会自动切换到冗余组件。冗余最常见的目标是数据库服务器。MySQL 在这方面有几个突出的特性。MySQL 中最古老的冗余特性之一叫做复制

对于最基本的用例,即热备用和备份,MySQL 复制并不难设置。为此,您设置了第二个数据库服务器,它可以获得在原始服务器上所做的所有更改的副本。原来的服务器叫做,或,第二个服务器叫做,或。MySQL 复制是一个非常大的主题,在本章的后面我将专门用一节来讨论它。

等等,为什么我们要讨论 MYSQL 复制?

您可能想知道为什么我们在讨论 MySQL 复制,而这本书是关于 InnoDB 集群的。理解 MySQL 复制非常重要,因为它是构建 InnoDB 集群的基础组件之一。尽管 MySQL 复制版本较旧,功能较少,并且需要手动管理,但是理解 MySQL 复制会让您更好地了解 InnoDB Cluster 的工作方式。它还将帮助您了解 InnoDB Cluster 与它所基于的组件相比有多复杂。

冗余也可以通过使用额外的专用硬件来实现。您可以实现冗余电源选项(例如,第二个电源或替代电源,如太阳能或备用电池),使用多个应用服务器,使用多个数据采集节点,等等。没有任何理由不能在您的解决方案中构建冗余。但是,只有您,即设计者或管理员,知道哪些节点是最关键的,因此知道在出现故障时您希望复制哪些节点。

冗余机制的复杂程度是你可以控制的,取决于你想投入多少。事实上,冗余的复杂程度与实现的工作量或费用有关。

例如,您可以使用一个备用的离线组件,当原始组件出现故障时,可以手动激活该组件,这是一个缓慢的过程,需要手动干预。或者,您可以使用备用的在线组件来代替主组件,主组件仍然需要手动干预,但恢复速度更快。或者您可以编写应用代码来自动检测故障,并切换到第二个,这是最好的(最快的),但需要更多的编程,因此需要更多的工作(可能要多得多)。

您可以调整冗余以满足您的需求或能力。您可以从简单的离线备件开始,并随着解决方案的发展增加更多的复杂性。

那么,五个九是多少?

你可能听说过或读过一个叫做“五个九”的概念,即一年 99.999%的正常运行时间。因此,五个九的解决方案每年最多只允许 5.26 分钟的停机时间。但是“五个九”只是可靠性的一个等级,还包括其他类别,每个类别都与正常运行时间或可靠性的百分比有关。有关可用类的更多信息,请参见 https://en.wikipedia.org/wiki/High_availability#Percentage_calculation

缩放比例

另一个可靠性实现与性能有关。在这种情况下,您希望最大限度地减少存储和检索数据的时间。MySQL 复制是实现可伸缩性的一种很好的方式。您可以通过设计解决方案将数据写入(保存)到主设备(主设备)并从从设备(辅助设备)读取数据来实现这一点。随着应用的增长,您可以添加从属服务器来帮助最小化读取数据的时间。拥有额外的从属服务器允许您的应用同时运行多个实例甚至多个连接(每个从属服务器至少一个)。因此,可伸缩性建立在 MySQL 的冗余特性之上。

通过拆分写和读,可以减轻主机执行许多语句的负担。考虑到大多数应用的读操作比写操作多得多,使用不同的服务器(或几个服务器)来提供读操作的数据并将写操作留给一个主服务器是有意义的。

理解横向扩展有两种形式很重要:读取和写入。您可以通过使用像 MySQL 复制中看到的冗余读取器来实现读取扩展,但是实现写入扩展需要一个可以在两台或更多服务器上协商和处理更新的解决方案。幸运的是,MySQL InnoDB Cluster 通过使用一种称为 MySQL 组复制的高级复制形式实现了这一点。您将在后面的章节中看到更多关于这个特性的内容。

当然,还有其他不需要实现 MySQL 复制就能提高性能的方法,但是从长远来看,您可能不会获得太多好处。

容错

可靠性的最后一个实现,实际上也是大多数高可用性解决方案在正常运行时间方面的区别在于容错,即检测故障并从事件中恢复的能力。容错是通过利用恢复和冗余以及添加检测机制和主动切换来实现的。

例如,您可以在数据库中实现容错。我们再次利用 MySQL 复制来实现切换。当主服务器关闭时,我们使用 MySQL 中的复制命令将主服务器的角色切换到一个从服务器。使用 MySQL 时,有两种类型的主角色改变:切换,即当主服务器仍在运行时,将主服务器的角色切换到从服务器,以及故障转移,即当主服务器不再运行时,选择从服务器来承担主服务器的角色。切换是有意的,而故障转移是被动的。

Oracle 提供了一些工具来帮助您设置自动故障转移。您可以使用 MySQL 实用程序(mysqlfailover)来监控您的主服务器,并在主服务器离线时切换到从服务器。对于包含许多服务器的大型解决方案,可能还需要写入扩展,您可以使用 MySQL 组复制,自动执行故障转移以及其他更复杂的高可用性操作。还有 MySQL 路由,它是 MySQL 的连接路由,允许您设置路由使用的一组特定服务器,以便路由在当前服务器离线(变得不可访问)时自动切换到另一台服务器。幸运的是,组复制和路由都是 InnoDB 集群的一部分。

您还可以在您的应用中实现某种形式的容错,但是像冗余一样,这需要专门的代码,构建和维护这些代码的成本可能会更高。甚至使用路由也可能需要修改您的应用,以使用某些端口和其他连接信息。然而,与在应用中编写自己的容错能力相比,这些都是小变化。

MYSQL 真的能达到高可用性吗?

您不仅可以使用 MySQL 实现高可用性,还可以使用 MySQL 实现高可用性,有些来自第三方供应商,还有 Oracle 的一些工具。甚至 MySQL 本身也是用高可用性的基本构件设计的。然而,MySQL 的特性以及用于高可用性的工具和解决方案允许您定制 MySQL 来提供您所需要的可靠性。

现在,您对什么是高可用性以及如何实现高可用性原则有了更广泛的理解,让我们来看看 MySQL 的高可用性特性。

MySQL 高可用性特性概述

MySQL 拥有高可用性特性已经有一段时间了。事实上,MySQL 复制在很多年前的 3.23.15 版本中首次引入,并经历了许多层的改进和完善。从那时起,MySQL 高可用性特性的组合已经扩展到涵盖许多用例,从简单的冗余(热备用),到横向扩展,再到高度可靠的系统。

以下是 MySQL 中主要高可用性特性的列表。包括与高可用性相关的每个功能的简要概述。在服务器和外部工具中,有许多小的改进、工具和增强来补充这些特性。例如,您可以使用 MySQL Enterprise Monitor(https://dev.mysql.com/doc/mysql-monitor/4.0/en/)来监控这些特性。

  • MySQL 复制:第一个高可用性特性,允许将数据从一个实例(服务器)复制到一个或多个额外的实例(服务器)。MySQL 复制中实现的主要高可用性特性包括冗余、热备用(恢复)、备份和读取可伸缩性。

  • MySQL 组复制:组复制建立在 MySQL 复制的基础上,提供了高级的服务器交互,允许更好的冗余,具有更好的同步、自动故障转移和写入可扩展性。因此,组复制比 MySQL 复制提供了更好的恢复和可靠性。

  • MySQL InnoDB Cluster:InnoDB Cluster 建立在 MySQL Group Replication 的基础上,通过应用编程接口(API)、应用故障转移和路由以及简化的配置,增加了额外的管理功能,便于利用新的客户端进行管理。因此,InnoDB 集群提供了比组复制更高的可用性。

  • MySQL NDB 集群:经常与 InnoDB 集群混淆,NDB 集群是一款独立于 Oracle 的产品,它提供了一个适用于分布式计算环境的高可用性、高冗余版本的 MySQL,使用内存中的 NDB 存储引擎(也称为 NDB Cluster)在一个集群中运行多台装有 MySQL 服务器和其他软件的计算机。

如您所见,MySQL 中的高可用性特性令人印象深刻。虽然这个清单并不长,但这些特性的重要性是不可低估的。许多组织已经使用 MySQL 复制和 NDB 集群很多年了。组复制和后来的 InnoDB 集群的加入证明了 Oracle 对 MySQL 服务器中企业级高可用性的承诺。

注意

NDB 集群仅作为 MySQL NDB 集群分发版的一部分提供。有关 MySQL NDB 集群的更多信息,请参见 https://dev.mysql.com/doc/refman/5.7/en/mysql-cluster.html

为了充分理解使用 InnoDB Cluster 的重要性和改进收益,我们从 MySQL 复制入门开始。您将在第 2 章了解更多关于 InnoDB 集群的信息,并在第 3 章了解组复制。

MySQL 复制入门

MySQL 复制是一个易于使用的特性,也是 MySQL 服务器的一个复杂和主要的组件。本节提供了复制的鸟瞰图,以解释它是如何工作的以及如何设置一个简单的复制拓扑。 1 虽然本节讨论的是 8.0 版本下的 MySQL 复制,但是在 MySQL 的早期版本中,MySQL 复制的配置方式是相同的。本节中显示的设置和配置复制的步骤可以用于 MySQL 的旧版本。如前所述,了解 MySQL 复制的工作原理将让您更好地了解 InnoDB 集群的工作原理。

复制需要两台或更多服务器。一台服务器必须被指定为源服务器或主服务器(称为主服务器)。主角色意味着对数据的所有数据更改(写入)都发送到主服务器,并且只发送到主服务器。拓扑中的所有其他服务器维护主数据的副本,并且根据设计和要求是只读服务器(称为从服务器)。因此,当你的应用存储或更新数据时,它们会将数据发送给主程序。您编写的使用数据的应用可以从从属服务器读取数据。

注意

术语专门用于 MySQL 复制,代表只有一个服务器可以被写入,因此拥有副本。其余的服务器是只读的,包含数据的副本。在后来的高可用性特性中,这些术语被更改为辅助,以便更好地描述新特性中的角色。

MySQL 复制支持两种复制方法。最初的(有时称为 MySQL 5.7 复制经典复制二进制日志文件复制,或日志文件和位置复制)方法涉及使用二进制日志文件名称和位置来执行事件或应用更改,以在主服务器和从服务器之间同步数据。使用全局事务标识符(GTIDs)的新方法是事务性的,因此不需要处理日志文件或这些文件中的位置,这大大简化了许多常见的复制任务。使用 GTIDs 的复制保证了主设备和从设备之间的一致性。

什么是 GTID?

GTIDs 使服务器能够为每个事件集或组分配一个唯一的标识符,从而可以知道每个从服务器上应用了哪些事件。要使用 GTIDs 执行故障转移,您可以选择最好的从设备(丢失事件最少且硬件与主设备最匹配的设备),并使其成为所有其他从设备的从设备。我们称这个从设备为候选从设备。GTID 机制将确保只应用那些没有在候选从设备上执行的事件。通过这种方式,候选从模块成为最新的,因此成为主模块的替代。

复制机制通过使用一种称为二进制日志的技术来工作,该技术以一种特殊的格式存储更改,从而保留所有更改的记录。然后,这些更改被复制到从属服务器,并在那里重新执行。在从机重新执行更改(称为事件)后,从机拥有数据的精确副本。我们将在后面的章节中看到更多关于二进制日志的内容。

主设备维护一个二进制日志,从设备维护该二进制日志的副本,称为中继日志,其格式与二进制日志相同。当从设备向主设备请求数据更改时,它从主设备读取事件并将它们写入其中继日志;然后,从属线程中的另一个线程执行中继日志中的那些事件。

在最低级别,主设备和从设备之间的二进制日志交换支持三种格式:

  • 基于语句的复制(SBR) :复制整个 SQL 语句

  • 基于行的复制(RBR) :仅复制已更改的行

  • 基于混合的复制(MBR):RBR 的混合体,使用 SQL 语句记录一些事件

可以想象,从主服务器上发生更改到从服务器上发生更改之间会有一点延迟。幸运的是,除了在高流量(大量变化)的拓扑中,这种延迟几乎是不明显的。出于您的目的,当您从从属服务器读取数据时,它可能是最新的。您可以使用命令SHOW SLAVE STATUS来检查从设备的进度;在许多其他事情中,它向你展示了奴隶已经落后于主人有多远。在后面的部分中,您将看到这个命令的运行。

MySQL 复制还支持两种类型的同步。最初的类型异步是单向的:在主服务器上执行的事件被传输到从服务器,并在它们到达时执行(或应用),没有检查来确保从服务器与主服务器都处于相同的同步点(当有许多事务时,从服务器的更新可能会延迟)。另一种类型是半同步:在主设备上执行的提交在返回到执行事务的会话之前被阻塞,直到至少一个从设备确认它已经接收并记录了事务的事件。

MySQL NDB 集群支持同步复制,在这种情况下,所有节点在全有或全无的提交场景中保证拥有相同的数据。有关同步复制的信息,请参见在线参考手册中的 MySQL NDB 集群部分。

小费

有关 MySQL 复制的更多信息,请参见在线参考手册中的“复制”部分( https://dev.mysql.com/doc/refman/8.0/en/replication.html )。

现在您已经对复制及其工作原理有了一些了解,让我们来看看如何设置它。下一节讨论如何设置复制,将一台服务器作为主服务器,另一台作为从服务器。您将看到两种类型的复制都被使用。正如您将看到的,在配置服务器和启动复制的方式上只有一些差异。

MySQL 复制教程

本节演示如何设置从一台服务器(主服务器)到另一台服务器(从服务器)的复制。这些步骤包括通过启用二进制日志记录和创建用于读取二进制日志的用户帐户来准备主服务器,通过将从服务器连接到主服务器来准备从服务器,以及启动从服务器进程。这一部分以对复制系统的测试结束。

如果您想自己体验本教程,您应该准备两台服务器——两台物理机或两台虚拟机。然而,试验 MySQL 复制最简单的方法是在一个测试系统上设置两个 MySQL 实例。更具体地说,您将看到如何在同一台机器上运行多个 MySQL 服务器。为此,您应该已经在系统上安装了 MySQL。如果您的系统上没有安装 MySQL,您可以按照在线参考手册( https://dev.mysql.com/doc/refman/8.0/en/installing.html )中的说明进行操作。在下一章中,你会看到关于安装 MySQL 8.0 的更深入的演示。

注意

使用二进制日志文件和位置设置复制的步骤与使用 GTIDs 的步骤相同,但是在某些步骤中命令略有不同。本教程展示了这两种方法。

设置和配置 MySQL 复制的步骤包括:

  1. 初始化数据目录。

  2. 配置主服务器。

  3. 配置从机。

  4. 启动 MySQL 实例。

  5. 创建复制用户帐户。

  6. 将从设备连接到主设备。

  7. 开始复制。

  8. 验证复制状态。

可能还有其他同样可行的步骤来设置复制,但是前面的步骤可以在任何机器上完成,并且不会影响 MySQL 的任何现有安装。也就是说,建议在开发机器上执行这些步骤,以消除中断生产系统的风险。

以下部分将更详细地演示这些步骤。虽然本教程使用多个本地实例来演示如何使用复制,但是在生产环境中设置复制的过程是相同的。使用特定主机、驱动器、文件夹、端口等的各个命令的详细信息是在生产中使用该过程时唯一需要更改的内容。

注意

本教程中的步骤是在 Ubuntu 16.04 平台上运行的。尽管有特定于平台的命令和一些特定于平台的选项,但本教程只需稍加修改就可以在 macOS 和 Windows 平台上运行。

初始化数据目录

第一步是为使用的每台机器初始化一个数据目录。在这种情况下,我们将在本地计算机上创建一个文件夹来包含所有数据目录。我们将使用 MySQL 的两个实例来表示一个主服务器和一个从服务器。下面演示了如何创建所需的文件夹。请注意,我在我使用的用户帐户可以访问的本地文件夹中创建这些文件,而不是系统或管理帐户。这是因为我们将在本地运行实例,不需要额外的权限或访问此类帐户的许可。

$ mkdir rpl
$ cd rpl
$ mkdir data

现在我们有了一个文件夹<user_home>/rpl/data,我们可以使用 MySQL 服务器的初始化选项来设置我们的数据目录。 3 我们通过使用服务器可执行文件的特殊--initialize-insecure--datadir选项来做到这一点。--initialize-insecure选项告诉服务器创建数据目录并用系统数据填充它,但是跳过任何身份验证的使用。这是安全的,因为还没有创建用户(没有数据目录!).

--datadir选项指定数据目录主文件夹的位置。因为我们是作为本地用户运行的,所以我们需要--user选项。我们还需要知道安装在本地机器上的 MySQL 服务器的基目录(名为basedir)。您可以从服务器配置文件中获取这些信息,或者使用 MySQL 客户端(名为mysql)并向其传递一个 show 命令。下面演示了如何做到这一点。在这里,我们看到的基本目录是/usr/。我们将使用这个值,这样mysqld可执行文件就可以找到它所依赖的库和文件。

$ mysql -uroot -proot -e "SHOW VARIABLES LIKE 'basedir'"
mysql: [Warning] Using a password on the command line interface can be insecure.
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| basedir
       | /usr/ |
+---------------+-------+

最后,我们使用--no-defaults选项(它必须首先出现在参数列表中)跳过对 MySQL 配置文件的读取。如果我们已经在机器上运行了一个 MySQL 实例,或者机器上已经安装了 MySQL,那么这是必要的。

下面显示了初始化主机和从机的数据目录所需的命令。注意,我用slave1来表示奴隶。这样,如果您想尝试添加额外的从属对象,就可以将教程扩展到多个从属对象。

mysqld --no-default --user=cbell --initialize-insecure --basedir=/usr/ --datadir=<user_home>/rpl/data/master
mysqld --no-default --user=cbell --initialize-insecure --basedir=/usr/ --datadir=<user_home>/rpl/data/slave1

注意

您可以很容易地扩展本教程,使用两个或更多的奴隶。只需对从机重复这些命令,替换正确的端口。

当您运行这些命令时,您将看到如下几条消息。您可以放心地忽略这些警告,但是请注意,最后一个警告告诉我们没有为 root 用户分配密码。这对于我们的教程来说是可以的,但是对于生产安装来说,您绝对不希望这样做。幸运的是,在启动实例后,我们可以很容易地修复这个问题。

$ mysqld --no-defaults --user=cbell --initialize-insecure --basedir=/usr/ --datadir=/home/cbell/rpl/data/master
2018-03-05T16:44:44.746906Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2018-03-05T16:44:44.948910Z 0 [Warning] InnoDB: New log files created, LSN=45790
2018-03-05T16:44:45.027466Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.
2018-03-05T16:44:45.096708Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: 82783ccc-2094-11e8-b3e3-10bf4850c554.
2018-03-05T16:44:45.100255Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.
2018-03-05T16:44:45.101415Z 1 [Warning] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.

现在我们已经创建并填充了数据目录,我们可以配置主服务器和从服务器了。

配置主服务器

复制要求主服务器启用二进制日志记录。在 MySQL 8.0.11 中它是默认打开的,但是如果您有一个旧版本,您必须在配置文件中添加这个选项。事实上,我们需要为每个想要启动的实例配置一个配置文件。在这一节中,我们集中讨论主设备,在下一节中,我们将看到从设备的配置文件。

我们还需要为实例选择端口。对于本教程,我们将使用从 13001 开始的主端口号和 13002+开始的从端口号。此外,我们需要选择唯一的服务器标识号。我们将用 1 代表主设备,用 2+代表从设备。

我们还需要做一些其他的设置。与其列出它们,不如让我们来看一个典型的主服务器的基本配置文件,它使用带有二进制日志和文件位置的复制。清单 1-1 显示了我们将在本教程中用于主服务器的配置文件。

[mysqld]
datadir="/home/cbell/rpl/data/master"
basedir="/usr/"
port=13001
socket="/home/cbell/rpl/master.sock"

server_id=1
master_info_repository=TABLE
relay_log_info_repository=TABLE
log_bin=master_binlog
binlog_format=row

Listing 1-1Master Configuration File (Log File and Position)

注意,配置文件有一个名为mysqld的部分,它只适用于 MySQL 服务器可执行文件。只有mysqld和相关的可执行文件会读取这个部分的值。这些值包括datadirbasedirportsocket(适用于*nix 风格平台)的常用必需设置。请注意,这些值与我们之前讨论过的设置相匹配。

下一节设置服务器 ID,打开用于存储复制信息的TABLE选项,这使得复制可以从崩溃中恢复,并打开二进制日志并设置其位置。最后,我们对二进制日志使用ROW格式,这是一种二进制格式,是 MySQL 复制最新版本的默认格式。

如果我们想使用基于 GTID 的复制,必须设置附加选项。对于主服务器,只有三个:打开 GTIDs、设置一致性强制和记录从服务器更新。清单 1-2 中显示了启用 GTID 的主服务器的配置文件。请注意,文件的第一部分与前面的示例相同。只添加最后几行来启用 GTIDs。

[mysqld]
datadir="/home/cbell/rpl/data/master"
basedir="/usr/"
port=13001
socket="/home/cbell/rpl/master.sock"

server_id=1
master_info_repository=TABLE
relay_log_info_repository=TABLE
log_bin=master_binlog
binlog_format=row

# GTID VARIABLES
gtid_mode=on
enforce_gtid_consistency=on
log_slave_updates=on

Listing 1-2Master Configuration File (GTIDs)

对于本教程,我们将使用支持 GTID 的复制,所以您应该在我们之前创建的文件夹中创建一个名为master.cnf的文件;比如/home/cbell/rpl/master.cnf。在后面的步骤中,我们将使用该文件启动主服务器的实例。

小费

如果配置文件是全球可读的,一些平台可能无法启动 MySQL。如果您的服务器没有启动,请检查日志中有关文件权限的消息。

现在,让我们来看看从属服务器的配置文件。

配置从机

日志文件和位置复制要求主服务器启用二进制日志记录,而从服务器则不需要。但是,如果您想使用从属服务器来生成备份或进行崩溃恢复,那么打开从属服务器的二进制日志是一个好主意。如果您想使用支持 GTID 的复制,也需要二进制日志记录。在本节中,我们将在从属服务器上使用二进制日志记录。

正如我们为 master 所做的那样,我们需要设置几个变量,包括datadirbasedirportsocket(对于*nix 风格的平台)。清单 1-3 显示了第一个从机(名为slave1)的配置文件。

[mysqld]
datadir="/home/cbell/rpl/data/slave1"
basedir="/usr/"
port=13002
socket="/home/cbell/rpl/slave1.sock"

server_id=2
master_info_repository=TABLE
relay_log_info_repository=TABLE

log_bin=slave1_binlog
binlog_format=row
report-port=13002
report-host=localhost

Listing 1-3Slave Configuration File (Log File and Position)

注意,设置了两个额外的变量:report-portreport-host。这些对于确保像SHOW SLAVE HOSTS这样的命令报告正确的信息是必要的;该视图的信息来自这些变量。因此,正确设置它们总是一个好主意。

还要注意,我们将数据目录设置为一个为这个从服务器留出的目录,服务器 ID 也发生了变化。最后,我们还更改了二进制日志的名称,以确保我们知道日志来自哪个服务器(如果将来需要的话)。

如果我们想要使用基于 GTID 的复制,我们将添加与主服务器相同的一组变量,如清单 1-4 所示。

[mysqld]
datadir="/home/cbell/rpl/data/slave1"
basedir="/usr/"
port=13002
socket="/home/cbell/rpl/slave1.sock"

server_id=2
master_info_repository=TABLE
relay_log_info_repository=TABLE
log_bin=slave1_binlog
binlog_format=row
report-port=13002
report-host=localhost

# GTID VARIABLES
gtid_mode=on
enforce_gtid_consistency=on
log_slave_updates=on

Listing 1-4Slave Configuration File (GTIDs)

对于本教程,我们将使用支持 GTID 的复制,所以您应该在我们之前创建的文件夹中创建一个名为slave1.cnf的文件;比如/home/cbell/rpl/slave1.cnf。如果您想添加更多的从服务器,可以使用相同的数据创建额外的配置文件,只需更改数据目录、套接字、端口、服务器 ID 和二进制日志文件。

启动 MySQL 实例

现在我们已经准备好启动 MySQL 实例了。这很容易做到,因为我们已经用我们需要的所有参数创建了配置文件。我们需要提供只带有--defaults-file选项的配置文件。下面显示了启动两个服务器实例的命令:

mysqld --defaults-file=master.cnf
mysqld --defaults-file=slave1.cnf

运行这些命令时,应该从包含配置文件的文件夹中运行它们。否则,您必须提供配置文件的完整路径。使用单独的终端窗口启动每个实例或者将输出(消息记录)重定向到一个文件也是一个好主意,如清单 1-5 所示。但是,您可能希望在第一次启动服务器时使用单独的终端,以确保不会出现错误。清单 1-5 显示了启动主程序时打印的信息摘录。

$ mysqld --defaults-file=master.cnf
2018-03-05T18:45:18.544588Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2018-03-05T18:45:18.545466Z 0 [Note] mysqld (mysqld 5.7.21-0ubuntu0.16.04.1-log) starting as process 8477 ...
...
2018-03-05T18:45:18.697423Z 0 [Note] Server hostname (bind-address): '*'; port: 13001
2018-03-05T18:45:18.697500Z 0 [Note] IPv6 is available.
2018-03-05T18:45:18.697523Z 0 [Note]   - '::' resolves to '::';
2018-03-05T18:45:18.697569Z 0 [Note] Server socket created on IP: '::'.
2018-03-05T18:45:18.735880Z 0 [Note] Event Scheduler: Loaded 0 events
2018-03-05T18:45:18.736092Z 0 [Note] mysqld: ready for connections.
Version: '5.7.21-0ubuntu0.16.04.1-log'  socket: '/home/cbell/rpl/master.sock'port: 13001  (Ubuntu)

Listing 1-5Starting the Master Instance

如果您计划使用单个终端,建议将输出重定向到名为master_log.txt的文件,并使用选项在另一个进程中启动应用(例如,&符号)。服务器生成消息时会更新日志文件,因此如果遇到问题,您可以参考这些日志文件。这也有助于保持您的终端会话清除额外的消息。下面显示了如何构建上述命令,使其作为单独的进程启动,并将消息记录到文件中:

$ mysqld --defaults-file=master.cnf > master_output.txt 2>&1 &

如果您还没有这样做,请继续启动从属服务器。以下是我用来启动从机的命令(slave1):

$ mysqld --defaults-file=slave1.cnf > slave1_output.txt 2>&1 &

创建复制用户帐户

MySQL 实例启动后,您必须创建一个用户,供从属服务器连接到主服务器并读取二进制日志,然后才能设置复制。这个有一个特殊的特权叫做REPLICATION SLAVE。下面显示了创建用户和添加权限的正确的GRANT语句。记住您在这里使用的用户名和密码,因为您将需要它来连接从设备。

下面显示了创建复制用户所需的命令。在所有服务器上执行这些命令。尽管从属服务器不需要用户,但现在创建用户将允许您使用从属服务器进行恢复、切换或故障转移,而无需创建用户。事实上,这一步是允许自动故障转移所必需的。

SET SQL_LOG_BIN=0;
CREATE USER rpl_user@'localhost' IDENTIFIED BY 'rpl_pass';
GRANT REPLICATION SLAVE ON *.* TO rpl_user@'localhost';
FLUSH PRIVILEGES;
SET SQL_LOG_BIN=1;

请注意第一个和最后一个命令。这些命令告诉服务器暂时禁止记录二进制日志的更改。每当我们不想在拓扑中的其他机器上复制命令时,我们就这样做。具体来说,不应复制维护和管理命令,如创建用户。关闭二进制日志是确保您不会意外发出无法在其他机器上执行的事务的好方法。

执行这些命令的最佳方式是将它们保存到名为create_rpl_user.sql的文件中,并使用mysql客户端的源命令从文件中读取命令并执行它们。您可以使用以下命令在所有实例上快速创建复制用户:

mysql -uroot -h 127.0.0.1 -e "source /home/cbell/rpl/create_rpl_user.sql" --port=13001
mysql -uroot -h 127.0.0.1 -e "source /home/cbell/rpl/create_rpl_user.sql" --port=13002

现在,我们准备将从设备连接到主设备,并开始复制数据。

将从设备连接到主设备

下一步是将从设备连接到主设备。根据您使用的复制形式,有多种方法可以做到这一点。具体来说,与 GTID 复制相比,使用日志文件和位置时,将从设备连接到主设备的命令是不同的。还有两个步骤:配置从机进行连接和开始复制。让我们先来看看用日志文件和位置配置从属服务器。

连接日志文件并定位

要使用日志文件和位置将从设备连接到主设备,我们需要一些信息。完成指示从机与主机建立连接的CHANGE MASTER命令需要这些信息。表 1-1 显示了所需信息的完整列表。该表包括信息的来源之一,以及本教程中使用的值的示例。

表 1-1

连接从属设备所需的信息(日志文件和位置)

|

主文件中的项目

|

来源

|

例子

|
| --- | --- | --- |
| IP 地址或主机名 | master.cnf | 本地主机 |
| 港口 | master.cnf | Thirteen thousand and one |
| 二进制日志文件 | 显示主状态 | master_binlog.000002 |
| 二进制日志文件位置 | 显示主状态 | One hundred and fifty-four |
| 复制用户 ID | 创建一个用户 | rpl _ 用户 |
| 复制用户密码 | 创建一个用户 | rpl_pass |

主二进制日志文件的信息可以通过SHOW MASTER STATUS命令找到。下面展示了如何使用mysql客户端执行命令并返回:

$ mysql -uroot -h 127.0.0.1 --port=13001 -e "SHOW MASTER STATUS\G"
*************************** 1\. row ***************************
             File: master_binlog.000002
         Position: 154
     Binlog_Do_DB:
 Binlog_Ignore_DB:
Executed_Gtid_Set:

请注意,该命令还显示任何活动的过滤器以及最新执行的 GTID 集的特定于 GTID 的值。在本教程中我们不需要它,但是如果您需要恢复启用了 GTID 的拓扑,将它归档是一个好主意。

小费

对于宽结果,使用\G选项将列视为行(称为垂直格式)。

现在您已经有了主服务器的二进制日志文件名和位置以及复制用户和密码,您可以访问您的从服务器并使用CHANGE MASTER命令将其连接到主服务器。该命令可以由表 1-1 中的信息构成,如下所示(格式化以使其更容易阅读——如果你按照本教程进行,请删除\):

CHANGE MASTER TO MASTER_USER="rpl_user", MASTER_PASSWORD="rpl_pass", \
    MASTER_HOST='localhost', MASTER_PORT=13001, \
    MASTER_LOG_FILE='master_binlog.000002', MASTER_LOG_POS=154;

您必须在所有从属服务器上运行该命令。将它保存到一个文件并使用mysql客户机执行它可能更容易,就像我们对复制用户所做的那样。例如,将它保存到一个名为change_master.sql的文件中,并如下所示执行它:

mysql -uroot -h 127.0.0.1 -e "source /home/cbell/rpl/change_master.sql" --port=13002

启动从服务器还需要一个步骤,但是让我们首先看看如何为启用 GTID 的复制配置CHANGE MASTER命令。

用 GTIDs 连接

要使用 GTIDs 将从设备连接到主设备,我们需要一些信息。完成指示从机与主机建立连接的CHANGE MASTER命令需要这些信息。表 1-2 显示了所需信息的完整列表。该表包括信息的来源之一,以及本教程中使用的值的示例。

表 1-2

连接从机所需的信息(GTIDs)

|

主文件中的项目

|

来源

|

例子

|
| --- | --- | --- |
| IP 地址或主机名 | master.cnf | 本地主机 |
| 港口 | master.cnf | Thirteen thousand and one |
| 复制用户 ID | 创建一个用户 | rpl _ 用户 |
| 复制用户密码 | 创建一个用户 | rpl_pass |

请注意,我们需要的信息比日志文件和位置复制少。我们不需要知道主二进制日志文件或位置,因为 GTID 握手过程将为我们解析该信息。我们需要的只是主服务器和复制用户的主机连接信息和密码。对于启用 GTID 的复制,我们使用一个特殊的参数MASTER_AUTO_POSITION,来指示复制自动协商连接信息。可以从表 1-2 中的信息构造CHANGE MASTER命令,如下所示(格式化以使其更容易阅读——如果您跟随本教程,请删除\):

CHANGE MASTER TO MASTER_USER="rpl_user", MASTER_PASSWORD="rpl_pass", \
    MASTER_HOST='localhost', MASTER_PORT=13001, MASTER_AUTO_POSITION = 1;

您必须在所有从属服务器上运行该命令。将它保存到一个文件并使用mysql客户机执行它可能更容易,就像我们对复制用户所做的那样。例如,将它保存到一个名为change_master.sql的文件中,并如下所示执行它:

mysql -uroot -h 127.0.0.1 -e "source /home/cbell/rpl/change_master.sql" --port=13002

如果您希望能够将该文件用于任何一种形式的复制,您可以简单地将这两个命令放在文件中,并注释掉一个您不需要的命令。例如,下面显示了带有两个CHANGE MASTER命令的示例文件。请注意,GTID 变体是用#符号注释掉的:

CHANGE MASTER TO MASTER_USER="rpl_user", MASTER_PASSWORD="rpl_pass", MASTER_HOST="localhost", MASTER_PORT=13001, MASTER_LOG_FILE='master_binlog.000001', MASTER_LOG_POS=150;
# GTID option:
# CHANGE MASTER TO MASTER_USER="rpl_user", MASTER_PASSWORD="rpl_pass", MASTER_HOST="localhost", MASTER_PORT=13001, MASTER_AUTO_POSITION = 1;

既然我们已经配置了从属设备进行连接,我们必须通过告诉从属设备启动连接并开始复制来完成这个过程。

开始复制

下一步是启动从属进程。这个命令简单来说就是START SLAVE。我们将在所有从机上运行这个命令,就像我们对CHANGE MASTER命令所做的那样。下面显示了启动从机的命令:

mysql -uroot -h 127.0.0.1 -e "START SLAVE" --port=13002

START SLAVE命令通常不报告任何错误;您必须使用SHOW SLAVE STATUS才能看到它们。清单 1-6 展示了该命令的实际应用。为了安全和放心,您可能希望在您启动的任何从属服务器上运行这个命令。

$ mysql -uroot -h 127.0.0.1 -e "SHOW SLAVE STATUS \G" --port=13002
*************************** 1\. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: localhost
                  Master_User: rpl_user
                  Master_Port: 13001
                Connect_Retry: 60
              Master_Log_File: master_binlog.000002
          Read_Master_Log_Pos: 154
               Relay_Log_File: oracle-pc-relay-bin.000002
                Relay_Log_Pos: 375
        Relay_Master_Log_File: master_binlog.000002
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 154
              Relay_Log_Space: 586
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0

               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: 1
                  Master_UUID: 82783ccc-2094-11e8-b3e3-10bf4850c554
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind:
      Last_IO_Error_Timestamp:
     Last_SQL_Error_Timestamp:
               Master_SSL_Crl:
           Master_SSL_Crlpath:
           Retrieved_Gtid_Set:
            Executed_Gtid_Set:
                Auto_Position: 1
         Replicate_Rewrite_DB:
                 Channel_Name:
           Master_TLS_Version:

Listing 1-6Checking SLAVE STATUS

花点时间费力地读完所有这些行。你需要注意几个关键领域。这些包括名称中带有error的任何内容,以及 state 列。例如,第一行(Slave_IO_State)显示了指示从机 I/O 线程状态的文本消息。I/O 线程负责从主服务器的二进制日志中读取事件。还有一个 SQL 线程负责从中继日志中读取事件并执行它们。

对于这个例子,您只需要确保两个线程都在运行(YES)并且没有错误。关于SHOW SLAVE STATUS命令中所有字段的详细解释,请参见在线 MySQL 参考手册“用于控制从服务器的 SQL 语句”( https://dev.mysql.com/doc/refman/5.7/en/replication-slave-sql.html )一节。

既然从服务器已经连接并正在运行,让我们通过检查主服务器并创建一些数据来检查复制。

验证复制状态

使用SHOW SLAVE STATUS命令检查从属状态是验证复制健康的第一步。下一步是使用SHOW SLAVE HOSTS命令检查主机。清单 1-7 显示了本教程中拓扑设置的SHOW SLAVE HOSTS的输出。该命令显示连接到主设备的从设备及其 UUIDs。需要注意的是,这些信息是一个视图,不是实时的。从属连接可能会失败,但仍会显示在报告中,直到进程超时,服务器终止它们。因此,这个命令最好用作健全性检查。

$ mysql -uroot -h 127.0.0.1 -e "SHOW SLAVE HOSTS \G" --port=13001
*************************** 1\. row ***************************
 Server_id: 2
      Host: localhost
      Port: 13002
 Master_id: 1
Slave_UUID: 7e71cad7-20a6-11e8-a12b-10bf4850c554

Listing 1-7SHOW SLAVE HOSTS Command (Master)

这里我们看到从机已连接,从上一部分我们知道从机状态良好。

接下来,让我们在主服务器上创建一些简单的数据,然后看看这些数据是否被复制到从服务器上。在这种情况下,我们将创建一个数据库、一个表和一个单独的行,然后在主服务器上运行。清单 1-8 显示了在主机上执行的样本数据。

$ mysql -uroot -h 127.0.0.1 --port=13001
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 7
Server version: 8.0.11 MySQL Community Server - GPL

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> CREATE DATABASE test;
Query OK, 1 row affected (0.01 sec)

mysql> USE test;
Database changed
mysql> CREATE TABLE test.t1 (c1 INT PRIMARY KEY, c2 TEXT NOT NULL);
Query OK, 0 rows affected (0.03 sec)

mysql> INSERT INTO test.t1 VALUES (1, 'Chuck');
Query OK, 1 row affected (0.03 sec)

Listing 1-8Creating Sample Data for Testing Replication (Master)

为了验证数据是否被复制,我们需要做的就是在一个从服务器(或者所有从服务器,如果您愿意的话)的表上发出一个SELECT SQL 命令。下面显示了我们期望在每个从机上看到的示例:

$ mysql -uroot -h 127.0.0.1 --port=13002 -e "SELECT * FROM test.t1"
+----+-------+
| c1 | c2    |
+----+-------+
|  1 | Chuck |
+----+-------+

关于设置 MySQL 复制的简短教程到此结束。本节用最简洁的术语简要介绍了 MySQL 复制。现在,让我们看看如何编写 MySQL 复制的示例设置。

MySQL 复制脚本示例

如果您从事系统工作已经有一段时间了,那么几乎可以肯定的是,您将任何重复的操作视为编写脚本的机会,以使该过程更容易、更快地重复。正如我们在教程中所做的,在测试环境中设置 MySQL 复制是自动化成为可能的一个很好的例子。我们将在本节中探索一个示例脚本。

虽然有许多方法可以编写脚本来自动化复制环境,但让我们保持简单,将所有配置文件和数据目录放在用户帐户的单个文件夹中。例如,如果我们使用 Linux,我们可以创建一个名为rpl的文件夹,并在该文件夹中创建一个名为data的文件夹,其中包含实例的数据目录。下面显示了这可能是什么样子:

/home/cbell/rpl
                |
                +---/data
                      |
                      +---/master
                      |
                      +---/slave1
                      |
                      +---/slave2
                      |
                      +---/slave3

我们还放置配置文件、自动化脚本和任何包含用于命令的数据的附加文件,等等。例如,我们的文件夹内容可能如下所示(在开始复制之前):

/home/<user>/rpl $ ls
change_master.sql  create_rpl_user.sql  sample_data.sql  shutdown.sh  slave2.cnf
check_rpl.sql      master.cnf           setup.sh         slave1.cnf   slave3.cnf

清单 1-9 展示了一个可以用来在典型的 Linux 平台上启动复制的脚本。正如您将看到的,该脚本使用了几个附加文件,如教程中提到的.sql文件。要使用这个脚本,只需按照前面教程中的说明创建这些文件。您还需要更改路径以匹配您的系统。脚本文件被命名为setup.sh,但是您可以随意命名。

#
#!/bin/bash
#
# Introducing MySQL InnoDB Cluster - Chapter 1 : Setup Replication (Linux)
#
...
#
# Dr. Charles Bell, 2018
#
BIN='/usr/sbin'
BASEDIR='/usr/'
DATADIR='/home/<user>/rpl'
echo
echo Introduction to MySQL InnoDB Cluster - Ch01 : Setup MySQL Replication
echo
echo ====== Step 1 of 6: INITIALIZE DATA DIRECTORIES ======
echo "> Creating data directory root ..."
cd "$DATADIR"
rm -rf "$DATADIR/data"
mkdir "$DATADIR/data"
echo "> Initializing the master ..."
echo
$BIN/mysqld --no-defaults --user=<user> --initialize-insecure --basedir=$BASEDIR --datadir="$DATADIR/data/master"

echo
echo "> Initializing slave1 ..."
echo
$BIN/mysqld --no-defaults --user=<user> --initialize-insecure --basedir=$BASEDIR --datadir="$DATADIR/data/slave1"
echo
echo "> Initializing slave2 ..."
echo
$BIN/mysqld --no-defaults --user=<user> --initialize-insecure --basedir=$BASEDIR --datadir="$DATADIR/data/slave2"
echo
echo "> Initializing slave3 ..."
echo
$BIN/mysqld --no-defaults --user=<user> --initialize-insecure --basedir=$BASEDIR --datadir="$DATADIR/data/slave3"
echo
echo ====== Step 2 of 6: START ALL INSTANCES ======
echo "> Removing old socket file ..."
cd $DATADIR
rm *.sock*
echo "> Starting master ..."
$BIN/mysqld --defaults-file="$DATADIR/master.cnf" > master_output.txt 2>&1 &
echo "> Starting slave1 ..."
$BIN/mysqld --defaults-file="$DATADIR/slave1.cnf" > slave1_output.txt 2>&1 &
echo "> Starting slave2 ..."
$BIN/mysqld --defaults-file="$DATADIR/slave2.cnf" > slave2_output.txt 2>&1 &
echo "> Starting slave3 ..."
$BIN/mysqld --defaults-file="$DATADIR/slave3.cnf" > slave3_output.txt 2>&1 &
sleep 5
echo
echo ====== Step 3 of 6: CREATE THE REPLICATION USER ======
echo "> Creating replication user on the master ..."
mysql -uroot -h 127.0.0.1 -e "source /home/<user>/rpl/create_rpl_user.sql" --port=13001
echo "> Creating replication user on slave1 ..."
mysql -uroot -h 127.0.0.1 -e "source /home/<user>/rpl/create_rpl_user.sql" --port=13002
echo "> Creating replication user on slave2 ..."
mysql -uroot -h 127.0.0.1 -e "source /home/<user>/rpl/create_rpl_user.sql" --port=13003
echo "> Creating replication user on slave3 ..."
mysql -uroot -h 127.0.0.1 -e "source /home/<user>/rpl/create_rpl_user.sql" --port=13004
echo
echo ====== Step 4 of 6: START RPL ======
echo "> Executing CHANGE MASTER on slave1 ..."
mysql -uroot -h 127.0.0.1 -e "source /home/<user>/rpl/change_master.sql" --port=13002
echo "> Executing START SLAVE on slave1 ..."
mysql -uroot -h 127.0.0.1 -e "START SLAVE" --port=13002
mysql -uroot -h 127.0.0.1 -e "source /home/<user>/rpl/change_master.sql" --port=13003
echo "> Executing START SLAVE on slave2 ..."
mysql -uroot -h 127.0.0.1 -e "START SLAVE" --port=13003
mysql -uroot -h 127.0.0.1 -e "source /home/<user>/rpl/change_master.sql" --port=13004
echo "> Executing START SLAVE on slave3 ..."

mysql -uroot -h 127.0.0.1 -e "START SLAVE" --port=13004

echo
echo ====== Step 5 of 6: CHECK RPL ======
echo "> Checking replication setup ..."
mysql -uroot -h 127.0.0.1 -e "source /home/<user>/rpl/check_rpl.sql" --port=13001
echo
echo ====== Step 6 of 6: CREATE SOME DATA ======
echo "> Creating data ..."
mysql -uroot -h 127.0.0.1 -e "source /home/<user>/rpl/sample_data.sql" --port=13001
sleep 3
mysql -uroot -h 127.0.0.1 -e "SELECT * FROM test.t1" --port=13003
echo Done.
echo

Listing 1-9Example Replication Startup Script

清单 1-10 是这个脚本运行时的输出示例。服务器启动步骤中的一些消息已被删除。在您的系统上的执行可能会有所不同,但是清单显示了您应该看到的语句的正确顺序。

$ ./setup.sh
Introduction to MySQL InnoDB Cluster - Ch01 : Setup MySQL Replication
====== Step 1 of 6: INITIALIZE DATA DIRECTORIES ======
> Creating data directory root ...
> Initializing the master ...
> Initializing slave1 ...
> Initializing slave2 ...
> Initializing slave3 ...
====== Step 2 of 6: START ALL INSTANCES ======
> Removing old socket file ...
> Starting master ...
> Starting slave1 ...
> Starting slave2 ...
> Starting slave3 ...
====== Step 3 of 6: CREATE THE REPLICATION USER ======
> Creating replication user on the master ...
> Creating replication user on slave1 ...
> Creating replication user on slave2 ...
> Creating replication user on slave3 ...
====== Step 4 of 6: START RPL ======
> Executing CHANGE MASTER on slave1 ...
> Executing START SLAVE on slave1 ...
> Executing CHANGE MASTER on slave2 ...
> Executing START SLAVE on slave2 ...
> Executing CHANGE MASTER on slave3 ...
> Executing START SLAVE on slave3 ...
====== Step 5 of 6: CHECK RPL ======

> Checking replication setup ...
+-----------+-----------+-------+-----------+-----------------------------+
| Server_id | Host      | Port  | Master_id | Slave_UUID                  |
+-----------+-----------+-------+-----------+-----------------------------+
|         2 | localhost | 13002 |         1 | d2a4b096-254f-11e8-8694-                                              8086f28ecc6d                |
|         4 | localhost | 13004 |         1 | d774c619-254f-11e8-894f-                                              8086f28ecc6d                |
|         3 | localhost | 13003 |         1 | d507b376-254f-11e8-8882-                                              8086f28ecc6d                |
+-----------+-----------+-------+-----------+-----------------------------+
====== Step 6 of 6: CREATE SOME DATA ======
> Creating data ...
+----+-------+
| c1 | c2    |
+----+-------+
|  1 | Chuck |
+----+-------+
Done.

Listing 1-10Example Replication Script Output

注意,在脚本的最后,我们看到了SHOW SLAVE HOSTS的输出,显示了所有连接的从机(您应该看到所有三个从机)。我们还看到了在一个从机上执行的测试数据的SELECT查询的结果。

如果您尝试这个脚本并遇到错误,请确保检查脚本中的路径,以确保您已经正确地更改了它们。此外,一定要检查*_output.txt日志文件,因为服务器可能会发出错误消息,帮助您修复导致错误的原因。

您可能还对一种快速简单的关闭复制的方法感兴趣。清单 1-11 显示了一个示例脚本,您可以使用该脚本来关闭您的示例复制拓扑。请注意,我们按照精确的顺序关闭,首先停止从属线程,然后关闭 MySQL 实例,最后删除数据目录。

#!/bin/bash
#
# Introducing MySQL InnoDB Cluster - Chapter 1 : Shutdown Replication
#
...
#
# Dr. Charles Bell, 2018
#
DATADIR='/home/<user>/rpl'
echo
echo Introduction to MySQL InnoDB Cluster - Ch01 : Shutdown MySQL Replication
echo
echo ====== Step 1 of 3: STOP REPLICATION ON SLAVES ======
echo "> Stopping the slave threads on slave1 ..."
mysql -uroot -h 127.0.0.1 --port=13002 -e "STOP SLAVE"
echo "> Stopping the slave threads on slave2 ..."

mysql -uroot -h 127.0.0.1 --port=13003 -e "STOP SLAVE"

echo "> Stopping the slave threads on slave3 ..."
mysql -uroot -h 127.0.0.1 --port=13004 -e "STOP SLAVE"
echo
echo ====== Step 2 of 3: SHUTDOWN mysqld INSTANCES ======
echo "> Stopping the MySQL instance for slave1 ..."
mysql -uroot -h 127.0.0.1 --port=13002 -e "SHUTDOWN"
echo "> Stopping the MySQL instance for slave2 ..."
mysql -uroot -h 127.0.0.1 --port=13003 -e "SHUTDOWN"
echo "> Stopping the MySQL instance for slave3 ..."
mysql -uroot -h 127.0.0.1 --port=13004 -e "SHUTDOWN"
echo "> Stopping the MySQL instance for the master ..."
mysql -uroot -h 127.0.0.1 --port=13001 -e "SHUTDOWN"
echo
echo ====== Step 3 of 3: DESTROY THE DATA DIRECTORIES ======
echo "> Removing data directories and the root ..."
cd "$DATADIR"
rm -rf "$DATADIR/data"
echo Done.

Listing 1-11Example Replication Shutdown Script

如果您试图诊断示例复制拓扑的问题,您可能希望注释掉最后一步,以防从属线程或查询中出现问题。删除数据目录也将删除任何有助于诊断问题的日志文件。清单 1-12 显示了运行这个脚本的输出示例。

$ ./shutdown.sh

Introduction to MySQL InnoDB Cluster - Ch01 : Shutdown MySQL Replication
====== Step 1 of 3: STOP REPLICATION ON SLAVES ======
> Stopping the slave threads on slave1 ...
> Stopping the slave threads on slave2 ...
> Stopping the slave threads on slave3 ...
====== Step 2 of 3: SHUTDOWN mysqld INSTANCES ======
> Stopping the MySQL instance for slave1 ...
> Stopping the MySQL instance for slave2 ...
> Stopping the MySQL instance for slave3 ...
> Stopping the MySQL instance for the master ...
====== Step 3 of 3: DESTROY THE DATA DIRECTORIES ======
> Removing data directories and the root ...
Done.

Listing 1-12Example Replication Shutdown Script Output

小费

您可能需要对这些脚本进行更改,以匹配您的平台和帐户设置。一些变化可能需要一些工作。然而,本书的示例代码包括了这个适用于 Linux、macOS 和 Windows 10 的脚本。

既然您对 MySQL 复制和组复制有了更多的了解,让我们回顾一下数据库管理员在管理这些特性时面临的一些挑战。

MySQL 数据库管理员面临的挑战

从这些关于 MySQL 复制和组复制的简短讨论中,您可能已经了解了一些事情。其中之一可能是当你意识到设置过程是严格的并且需要神秘的命令时典型的压倒性的感觉。当然,这同样适用于任何系统或主要特性,比如高可用性。幸运的是,像这本书和其他的参考资料可以帮助缓解一些学习曲线。

然而,众所周知,MySQL 中的这些高可用性特性给管理员带来了挑战——包括系统和数据库。下面给出了几类挑战的概述,目的是让您做好准备,了解 InnoDB Cluster 如何简化其中的许多挑战。

设置

此类别包括基于复制的高可用性解决方案的规划和安装。考虑对现有数据、服务器、应用、用户和管理员的需求。

  • 为复制设置变量:了解如何调整复制和服务器以获得最佳操作。

  • 安装的推广:在整个基础架构中可能有数百台服务器上安装和设置复制,并以自动化方式完成。

  • 集成应用和第三方解决方案:找出如何将其他高可用性组件与复制和 MySQL 集成。

  • 读取扩展:为更快的应用读取构建拓扑;从多个从设备/辅助设备读取数据。

  • 写入扩展:为更快的应用写入构建拓扑;将数据写入多个主服务器。

解决纷争

此类别包括与检测和纠正错误、停机、计划维护等异常相关的任务。

  • 数据未复制:当数据未到达一台或多台服务器时发现并纠正。

  • 辅助服务器因错误(或意外)而停止:对停止复制的服务器进行故障排除和修复。

  • 复制过程中的错误:从错误中恢复,包括恢复服务器、同步数据、重新建立复制等任务。

  • 二级更新滞后:确定为什么一些服务器更新数据不如其他服务器快。

保持

此类别包括与保持高可用性解决方案以最高效率运行相关的预防性任务,有时还包括纠正性任务。这一类别经常被缺乏经验的管理员忽略,并且在大型安装中经常需要高级的付费工具。

  • 检查服务器性能:确保所有服务器都以最高效率运行。

  • 监控拓扑中的服务器:及早发现问题,如性能、错误和负载。

  • 升级数据库、MySQL 或平台:了解如何升级拓扑结构中的服务器(在大型组织中通常是一项主要工作)。

  • 检查数据一致性:检查给定服务器的数据不一致性。

  • 同步数据:确保所有服务器与主服务器拥有相同的数据。

管理

此类别涵盖作为高可用性解决方案的一部分而出现的典型或预期任务。这些可能包括管理服务器、复制和数据,以应对计划或预期的条件或事件。

  • 进行切换:手动将主要角色转换为次要角色。

  • 自动故障转移:主(写)角色切换到候选辅助角色,自动确保数据访问不中断。

  • 恢复拓扑中的服务器:恢复发生故障或数据损坏的服务器。

  • 备份和恢复:利用辅助节点卸载备份,使用辅助节点恢复数据。

  • Provisioning :向拓扑中添加更多的辅助节点,目标是最大限度地减少复制更改的时间。通常使用另一个辅助节点的备份来完成。

MySQL 高可用性参考资料

如果您仔细阅读了在 InnoDB Cluster 之前关于 MySQL 高可用性的一些较长的作品,您可能会发现一些针对特定用例的额外的、更复杂的挑战。如果您想在使用 InnoDB Cluster 之前了解有关 MySQL 复制和 MySQL 高可用性特性的更多信息,以下是一些优秀的资源:

  • 查尔斯·贝尔等人的《MySQL 高可用性:构建强大数据中心的工具》第二版(奥赖利,2014 年)

  • MySQL 复制简化版:Sribatsa Das (Business Compass,2014)提供的建立、故障排除和监控复制的简单分步示例

  • 高性能 MySQL:优化、备份、复制等Baron Schwartz 等人(O'Reilly,2012 年)

  • 亚历克斯·戴维斯的《高可用性 MySQL 食谱》( Packt,2010)

对于想了解二进制日志及其格式的更多细节的人来说,有一些相当晦涩的资源是他们感兴趣的。当然,在线参考手册有大量的文档,应该是您的主要来源。但是,以下包含其他来源中没有的关键信息:

小费

本书的源代码包含脚本,您可以使用这些脚本来设置 MySQL 复制和 MySQL 组复制,并根据您的需要对它们进行修改。你可以在该书的网站上找到这些( www.apress.com/9781484238844 )。

摘要

使用 MySQL 复制可以在 MySQL 中实现高可用性。事实上,您可以通过复制创建健壮的数据中心。更好的是,复制已经存在了很长时间,并且被认为是稳定的。许多组织已经并将继续成功地在生产中使用复制—从小规模安装到大规模安装。

即便如此,使用 MySQL 复制也有一些限制,例如,如果主服务器出现故障,如何将主服务器角色切换到另一台机器(从服务器),如何自动执行这一操作,如何处理多个写入场景,以及一般的故障排除和维护。其中许多在组复制中得到了改进。然而,正如您所看到的,复制的设置需要努力和维护,这可能是规划者(例如,架构师)和管理员都关心的问题。

对于那些不熟悉类似高可用性解决方案的人来说,启动和运行复制并随着时间的推移对其进行管理是一个巨大的挑战。事实上,对 MySQL 复制知识的需求非常高,因为这是一个独特的技能组合,很难找到。

因此,Oracle 对 MySQL 复制进行了改进,在其功能集和成功的基础上,结合了其他几个功能,以提供一个更易于学习和维护的高可用性解决方案。组复制和后来 InnoDB 集群就是答案,且已经证明它是 MySQL 高可用性的首选解决方案。

在下一章中,您将了解关于 InnoDB 集群的更多信息,以及如何使用它来实现高可用性。

二、什么是 MySQL InnoDB 集群?

现在,您已经了解了什么是高可用性以及如何通过 MySQL 复制实现高可用性,让我们看看 MySQL 中高可用性特性的最新发展:InnoDB Cluster。

这是对 Oracle MySQL 工程师(以及 Oracle 本身)奉献精神的证明,MySQL 将继续通过新功能进行改进。MySQL 工程部门的动力是继续为互联网开发颠覆性的数据库技术。甲骨文不仅培养了这种进取精神,还继续兑现其投资和发展 MySQL 业务的承诺。这个最新版本 MySQL 8 最终证明了 Oracle 已经实现了它的承诺,确保 MySQL 仍然是世界上最受欢迎的开源数据库系统。

MySQL 的这个新版本打破了以前版本的许多模式,增加了新的、革命性的功能,改变了一些人使用 MySQL 的方式。事实上,仅版本号就从 5。x 到 8.0,标志着技术复杂性的飞跃,最终脱离了 5 的持续发展。持续了超过 13 年的 x 代码库。

8 . 0 . 5–8 . 0 . 10 版本发生了什么变化?

您可能已经注意到 8.0 版本系列不是连续的,并且已经跳过了一些临时编号的版本。这在很大程度上是为了稳定和协调单一产品发布口号,以便用户可以一眼就知道哪些组件的哪些版本可以协同工作。很明显,知道你所有的 MySQL 组件都有 8.0.11 版本,可以确保它们能一起工作。我们需要路线图和夏尔巴人指南来发现哪个组件的哪个版本可以一起工作的日子已经一去不复返了!

最令人兴奋的新特性之一是 InnoDB 集群。这代表了 MySQL 在高可用性方面的巨大飞跃。最重要的是,它是 MySQL 8.0 所有版本的标准配置。让我们看看是什么让 InnoDB 集群成为大大小小的企业的重要功能。

概观

InnoDB 集群的核心组件是 InnoDB 存储引擎。自 MySQL 5.6 以来,InnoDB 一直是 MySQL 的旗舰存储引擎(也是默认引擎)。Oracle 已经慢慢地脱离了多存储引擎模型,专注于现代数据库服务器应该做的事情:支持事务存储机制。InnoDB 是满足这一需求的答案。

为了更好地理解我们是如何实现 InnoDB 集群的,让我们简短地浏览一下 MySQL 8.0 和早期版本中的其他存储引擎。

什么是存储引擎?

存储引擎是一种以各种方式存储数据的机制。例如,有一种存储引擎允许您与逗号分隔值(文本)文件(CSV)进行交互,另一种为写日志文件(归档)进行了优化,一种只在内存中存储数据(内存),甚至还有一种根本不存储任何东西(黑洞)。除了 InnoDB,MySQL 服务器还附带了几个存储引擎。以下部分描述了一些更常用的替代存储引擎。请注意,随着 MySQL 的发展,一些存储引擎已经不再受支持,包括 Berkeley Database (BDB)存储引擎。

小费

如果您想查看 MySQL 服务器上有哪些可用的存储引擎,可以使用SHOW ENGINES命令。请参见( https://dev.mysql.com/doc/refman/8.0/en/create-table.html )了解有关使用CREATE TABLE命令指定存储引擎的更多信息。

我的天

MyISAM 存储引擎最初是 MySQL 中的默认引擎,被大多数 LAMP 堆栈、数据仓库、电子商务和企业应用使用。MyISAM 文件是索引顺序访问方法(ISAM)的扩展,具有额外的优化,如高级缓存和索引机制。这些表是使用压缩特性和索引优化来提高速度的。

此外,MyISAM 存储引擎通过提供表级锁定来支持并发操作。MyISAM 存储机制为各种应用提供可靠的存储,同时提供快速的数据检索。当考虑读取性能时,MyISAM 是首选的存储引擎。

记忆

内存存储引擎(有时称为堆表)是一个内存中的表,它使用哈希机制来快速检索经常使用的数据。这些表比那些从磁盘存储和引用的表要快得多。它们的访问方式与其他存储引擎相同,但是数据存储在内存中,只有在 MySQL 重新启动后才有效。数据在关机(或崩溃)时被刷新和删除。

内存存储引擎通常用于静态数据被频繁访问且很少被更改的情况。这种情况的例子包括邮政编码、州、县、类别和其他查找表。堆表也可以用于利用快照技术进行分布式或历史数据访问的数据库中。

合并

合并存储引擎(有时称为MRG_MYISAM)是使用一组具有相同结构(元组布局或模式)的 MyISAM 表构建的,这些表可以作为单个表引用。这些表是根据各个表的位置进行分区的,但是没有使用额外的分区机制。所有表必须驻留在同一台机器上(由同一台服务器访问)。使用单个操作或语句来访问数据,例如SELECTUPDATEINSERTDELETE。幸运的是,当在合并表上发出一个DROP时,只有合并规范被删除。原始表格没有改变。

这种表类型最大的好处就是速度。可以将一个大表分割成不同磁盘上的几个小表,使用合并表规范将它们组合起来,并同时访问它们。搜索和排序将执行得更快,因为每个表中需要操作的数据更少。例如,如果按谓词划分数据,则可以只搜索包含要搜索的类别的特定部分。同样,对表的修复更有效,因为修复几个较小的单个文件比修复单个大表更快更容易。据推测,大多数错误将局限于一个或两个文件内的区域,因此不需要重建和修复所有数据。不幸的是,这种配置有几个缺点:

  • 您只能使用相同的 MyISAM 表或架构来形成一个合并表。这限制了合并存储引擎在 MyISAM 表中的应用。如果合并存储引擎接受任何存储引擎,合并存储引擎将更加通用。

  • 不允许替换操作。

  • 已经证明索引访问比单个表的效率低。

合并存储机制最适用于大型数据库(VLDB)应用,例如数据驻留在一个或多个数据库的多个表中的数据仓库。

档案馆

档案存储引擎设计用于以压缩格式存储大量数据。存档存储机制最适合用于存储和检索大量很少访问的存档或历史数据。这些数据包括安全访问数据日志。虽然这不是您想要搜索甚至日常使用的东西,但是如果发生安全事故,关心安全的数据库专业人员会希望拥有它。没有为归档存储机制提供索引,唯一的访问方法是通过表扫描。归档存储引擎不应用于正常的数据库存储和检索。

联邦的

联邦存储引擎被设计为从多个 MySQL 数据库系统创建单个表引用。因此,联邦存储引擎的工作方式类似于合并存储引擎,但是它允许您跨数据库服务器将数据(表)链接在一起。这种机制在目的上类似于其他数据库系统中可用的链接数据表。联邦存储机制最适合在分布式或数据集市环境中使用。

联邦存储引擎最有趣的方面是它不移动数据,也不要求远程表是同一个存储引擎。这说明了可插拔存储引擎层的真正威力。数据在存储和检索过程中被转换。

战斗支援车

CSV 存储引擎被设计为以表格形式创建、读取和写入逗号分隔值(CSV)文件。虽然 CSV 存储引擎不会将数据复制为另一种格式,但图纸布局或元数据会与服务器上指定的文件名一起存储在数据库文件夹中。这允许数据库专业人员快速导出存储在电子表格中的结构化业务数据。CSV 存储引擎不提供任何索引机制,因此对于大量数据来说不切实际。它旨在用作存储数据和在电子表格应用中可视化数据之间的链接。

黑洞

黑洞存储引擎,一个具有惊人效用的有趣特性,被设计成允许系统写入数据,但数据永远不会被保存。但是,如果启用了二进制日志记录,SQL 语句将被写入日志。这允许数据库管理员和开发者通过切换表类型来临时禁用数据库中的数据接收。当您想要测试一个应用以确保它正在写入您不想存储的数据时,例如当创建一个用于过滤复制的中继从属时,这可能会很方便。

InnoDB

InnoDB 是一个通用存储引擎,平衡了高可靠性和高性能。InnoDB 成为 MySQL 5.7 中的默认存储引擎,这意味着所有没有使用ENGINE子句创建的表都作为 InnoDB 表空间中的 InnoDB 表创建。使用 InnoDB 存储引擎的决定是在多次尝试为 MySQL 构建一个健壮、高性能的存储引擎之后做出的。考虑到 InnoDB 的成熟性和复杂性,使用已经存在的东西更有意义。另外,甲骨文同时拥有 MySQL 和 InnoDB。

为什么叫 InnoDB?

在早期,InnoDB 存储引擎是由一家名为芬兰 InfoBase Oy 的独立公司构建和拥有的,该公司将其数据库引擎命名为 InnoDB。这是一个独立的产品,不属于 MySQL,也不归 MySQL AB 所有(MySQL 的原所有者现在完全归 Oracle 所有)。最终,甲骨文在 2005 年拥有了 InnoDB,在 2010 年拥有了 MySQL,将两者结合起来是有意义的,因为它们有相互包容的目标。尽管独立的 InnoDB 工程团队仍然存在,但他们已经与核心服务器开发团队完全集成。

当您需要使用事务时,可以使用 InnoDB 存储引擎。InnoDB 支持传统的 ACID 事务(参见附带的侧栏)和外键约束。InnoDB 中的所有索引都是 B 树,索引记录存储在树的叶页中。InnoDB 是高可靠性和事务处理环境的首选存储引擎。

酸是什么?

代表原子数稠度隔离度耐久性。也许是数据库理论中最重要的概念之一,它定义了数据库系统必须表现出的行为,才能被认为是可靠的事务处理。

原子性意味着对于包含多个命令的事务,数据库必须允许在“全有或全无”的基础上修改数据。每个事务都是原子的。如果命令失败,则整个事务失败,并且事务中到该点为止的所有更改都将被丢弃。这对于在高事务环境(如金融市场)中运行的系统尤其重要。考虑一下资金转移的后果。通常,借记一个账户和贷记另一个账户需要多个步骤。如果在借记步骤后事务失败,并且没有将钱贷记回第一个帐户,该帐户的所有者将会生气。在这种情况下,从借方到贷方的整个事务必须成功,否则都不会成功。

一致性意味着只有有效的数据才会存储在数据库中。如果事务中的命令违反了一致性规则之一,则整个事务将被丢弃,数据将返回到事务开始前的状态。相反,如果事务成功完成,它将以遵守数据库一致性规则的方式更改数据。

隔离意味着同时执行的多个事务不会相互干扰。这是并发性的真正挑战最明显的地方。数据库系统必须处理事务不能违反另一个事务正在使用的数据(更改、删除等)的情况。有很多方法可以解决这个问题。大多数系统使用一种叫做锁定的机制,在第一个事务完成之前防止数据被另一个事务使用。虽然隔离属性没有规定先执行哪个事务,但它确实确保了它们不会相互干扰。

持久性意味着事务不会导致数据丢失,也不会丢失事务期间创建或更改的任何数据。耐用性通常由强大的备份和恢复维护功能提供。一些数据库系统使用日志记录来确保任何未提交的数据可以在重启时恢复。

与 MySQL 中的旧存储引擎相比,InnoDB 提供了几个关键优势,包括:

  • 数据操作语言(DML)操作遵循 ACID 模型,事务具有提交、回滚和崩溃恢复功能来保护用户数据。

  • 行级锁定和 Oracle 风格的一致读取提高了多用户并发性和性能。

  • InnoDB 表在磁盘上排列您的数据,以优化基于主键的查询。每个 InnoDB 表都有一个名为聚簇索引的主键索引,它组织数据以最小化主键查找的 I/O。

  • 为了保持数据的完整性,InnoDB 支持外键约束。使用外键时,会检查插入、更新和删除,以确保它们不会导致不同表之间的不一致。

对 InnoDB 进行了许多改进,包括许多性能增强,甚至支持微调等等。InnoDB 在 MySQL 的每个版本中不断改进,这一点显而易见。事实上,自 5.6 版本以来,改进的列表已经增长了很长时间。虽然大多数改进都很细微,从某种意义上说,你不会注意到它们(除了通过更好的性能和可靠性,这是不可轻视的),但大多数都显示出致力于使 InnoDB 成为最好的事务存储机制,并通过扩展,MySQL 成为强大的事务数据库系统。

下面列出了 MySQL 8 中对 InnoDB 的一些更有趣的改进。其中一些可能看起来非常深奥,但是那些已经优化或调整了 InnoDB 安装的人在计划迁移到 MySQL 8 时可能需要注意这些。这里没有列出的是几十个小缺陷修复和可靠性、性能的提升。

  • 改进的表空间支持:包括使用新数据字典、重新定位表空间等增强功能。

  • 新的 innodb_dedicated_server 配置选项:(默认禁用)用于让 InnoDB 根据服务器上检测到的内存量自动配置以下选项。

    • innodb_buffer_pool_size

    • innodb_log_file_size

    • innodb_flush_method

  • 崩溃恢复:如果索引树损坏,InnoDB 会将损坏标志写入重做日志。这使得损坏标志崩溃安全(它不会在强制重启时丢失)。类似地,InnoDB 还会在每个检查点上写一个内存损坏标志。当启动崩溃恢复时,InnoDB 可以读取这些标志,并使用它们来调整恢复操作。

  • InnoDB memcached 插件:通过允许在单个 memcached 查询中提取多个键/值对而得到改进。

  • 死锁检测:有几个新选项,但是最有前途的包括一个动态配置死锁检测的选项(innodb_deadlock_detect)。这可以为高使用率系统提供额外的调优控制,因为死锁检测会降低系统的性能。

  • INFORMATION_SCHEMA 视图:InnoDB 有新的视图。

  • AUTO_INCREMENT:自动递增字段有几处小的改进,包括以下内容:

    • 当前最大自动增量值现在在服务器重新启动后保持不变。

    • 重启不再取消AUTO_INCREMENT = N工作台选项的效果。

    • 紧随ROLLBACK操作之后的服务器重启不再导致分配给回滚事务的自动增量值的重用。

    • 将一个AUTO_INCREMENT列值设置为大于当前最大值的值是持久的,并且以后的新值(比如,在重启之后)以新的、更大的值开始。

  • 临时表:默认情况下,所有临时表都创建在名为ibtmp1的共享临时表空间中。

虽然这个列表似乎只关注一些小的改进,但是其中一些对于寻求帮助来调整和规划数据库服务器安装的系统管理员来说是非常重要的。如果您想了解更多关于这些改进的信息或查看所有最新变化的列表,请参阅在线 MySQL 8.0 参考手册( http://downloads.mysql.com/docs/refman-8.0-en.pdf )。

也许 InnoDB 区别于 MySQL 早期存储引擎的最重要的特性是它的可配置性。尽管一些早期的存储引擎是可配置的,但没有一个能达到配置 InnoDB 的规模。您可以使用几十个参数来调整 InnoDB,以满足您独特的存储需求。

警告

修改 InnoDB 参数时要小心。有可能会使您的系统降级到损害性能的程度。与任何调优练习一样,总是先查阅文档(和专家),然后计划针对特定的参数。确保一次调优一个参数,并在继续之前进行测试、确认或恢复。

尽管 InnoDB 使用精心选择的缺省值,开箱即可正常工作,并且对大多数人来说可能不需要太多的调优,但是那些需要调优 MySQL 的人会发现他们需要的一切,甚至更多,以便让他们的数据库系统以最高效率运行。参见 https://dev.mysql.com/doc/refman/8.0/en/innodb-introduction.html 了解有关 InnoDB 存储引擎的更多信息,包括其众多的配置和调优选项。

小费

另一个关于配置 MySQL 和 InnoDB 的技巧和建议的极好来源是 Baron Schwartz 等人的High Performance MySQL:Optimization,Backups,Replication and More(O ' Reilly,2012)。

现在,您已经对 InnoDB 的谱系和发展有了更好的了解,让我们来看看 InnoDB Cluster 提供了什么。

InnoDB 集群简介

MySQL 8.0 1 中最令人兴奋的新特性之一是 InnoDB Cluster。它旨在使高可用性更易于设置、使用和维护。InnoDB Cluster 通过 MySQL Shell 和 AdminAPI、组复制和 MySQL 路由与 X DevAPI 一起工作,将高可用性和读取可伸缩性提升到一个新的水平。InnoDB Cluster 将 InnoDB 中用于克隆数据的新功能与组复制和 MySQL 路由相结合,提供了一种设置和管理高可用性的新方法。下面的列表描述了构成 InnoDB 集群的组件。在下一节中,您将了解到关于这些的更多信息,并且在后面的章节中,当您通过教程探索 InnoDB 时,您将看到如何配置和使用这些信息的细节。

  • 组复制:一种新的复制形式,建立在 MySQL 复制的基础上,增加了一个活动的通信协议(组成员),允许更高级别的可用性,包括自动故障转移的容错。

  • MySQL Shell :一个新的 MySQL 客户端,允许几种接口模式,包括传统的 SQL 以及 JavaScript 和 Python 脚本语言。

  • X DevAPI :一个特殊的应用编程接口,供应用以编程方式与数据进行交互。

  • AdminAPI :通过 MySQL Shell 提供的特殊 API,用于配置 InnoDB 集群并与之交互。AdminAPI 具有旨在简化 InnoDB 集群工作的特性。

  • MySQL 路由:轻量级的中间件,在你的应用和后端 MySQL 服务器之间提供透明的路由。

你可能想知道所有的大惊小怪是什么。从表面上看,InnoDB Cluster 是 MySQL 中现有特性的捆绑包。虽然这可能是一个有效的结论,但事实是,该产品不仅仅是简单地用一个新名字将东西捆绑在一起。在这种情况下,Oracle 在 InnoDB Cluster 中添加了专门的管理层和内部特性,以改进这些产品。正如您将看到的,使用 InnoDB Cluster 通常隐藏了单独使用组件的许多细节(和繁琐)。

让我们看一个概念性的配置,以了解组件是如何交互的。图 2-1 显示了这些组件如何在概念上排列以形成 InnoDB 集群。

img/460910_1_En_2_Fig1_HTML.jpg

图 2-1

InnoDB 集群的典型配置(由 Oracle 提供)

在这个用例中,一个包含三个服务器的集群设置有一个主服务器(按照标准复制的说法,认为是主服务器),它是所有写入(更新)的目标。多个辅助服务器(从属服务器)维护数据的副本,可以从这些副本中读取数据;这些服务器支持读取数据,而不会加重主服务器的负担,因此支持读取可伸缩性(但所有服务器都参与协商和协调)。组复制的引入意味着集群是容错的,并且组成员是自动管理的。MySQL 路由缓存 InnoDB 集群的元数据,并执行到 MySQL 服务器实例的高可用性路由,从而更容易编写应用来与集群进行交互。

您可能想知道这与标准复制的读取可伸缩性(有时称为读出可伸缩性)设置有何不同。从高层次来看,这些解决方案似乎正在解决同一个用例。但是,使用 InnoDB Cluster,您可以从 MySQL Shell 创建、部署和配置集群中的服务器,从而提供一个易于管理的完整的高可用性解决方案。您可以通过 shell 使用 X AdminAPI(也称为 AdminAPI)使用 JavaScript 或 Python 以编程方式创建和管理 InnoDB 集群。

要了解有关 InnoDB 集群的更多信息,包括最新的特性、使用案例等,请参阅位于 https://dev.mysql.com/doc/refman/8.0/en/mysql-innodb-cluster-userguide.html 的在线文档。

InnoDB 集群和 MySQL 文档库

如果没有讨论 InnoDB Cluster 如何与 MySQL 的其他新特性很好地协同工作,关于 InnoDB Cluster 的书将是不完整的。MySQL 文档存储就是这样一个特性。MySQL 文档存储是 MySQL 的一个新动态,它允许通过应用编程接口(实际上是 X DevAPI)存储和检索 JSON 数据(称为文档)。文档存储允许您创建处理非结构化数据的 NoSQL 应用。

如果您使用过关系数据库系统,那么您肯定熟悉结构化查询语言(SQL ),它使用特殊的语句(命令)与数据进行交互。事实上,大多数数据库系统都有自己的 SQL 版本,包括操作数据的命令(DML)以及定义存储数据的对象的命令(DDL),甚至还有管理服务器的管理命令。

要在 SQL 接口中检索数据,您必须使用特殊的命令来搜索数据,然后将结果转换为内部编程结构,使数据看起来像是一个辅助组件,而不是解决方案的一个组成部分。NoSQL 接口打破了这种模式,它允许您使用 API 来处理数据。更具体地说,您使用编程接口,而不是基于命令的接口。

为什么叫“NoSQL”?

遗憾的是,根据你的观点,NoSQL 可以有几种意思,包括非 SQL不仅仅是 SQL ,或者非关系。但是它们都表明你正在使用的机制不是基于命令的接口,这个术语的大多数用法表明你正在使用编程接口。

在 MySQL 8 中,可以使用 X 协议通过 SQL 或 NoSQL 访问 JSON 文档,通过 X 插件访问 X DevAPI。因此,尽管 InnoDB Cluster 将增强为传统 SQL 数据库编写的高可用性应用,但 InnoDB Cluster 也可以与 NoSQL 应用无缝协作。

小费

要了解更多关于 MySQL 文档存储的信息,请参阅我的书,介绍 MySQL 8 文档存储(2018 年出版)。

InnoDB 集群和 NDB 集群

如果您仔细阅读 MySQL 网站,您会发现另一个名称中带有 cluster 的产品。它被诱人地命名为 NDB 集群。NDB 集群是一个独立于 MySQL 服务器的产品,它采用了一种技术,能够在一个无共享的系统中实现内存数据库的集群。无共享架构使系统能够与廉价的硬件一起工作,并且对硬件或软件的特定要求最小。

NDB 集群的设计不存在任何单点故障。在无共享系统中,每个组件都有自己的内存和磁盘,不建议或不支持使用共享存储机制,如网络共享、网络文件系统和存储区域网络(San)。参见https: https://dev.mysql.com/doc/refman/5.7/en/mysql-cluster-compared.html 了解更多关于 NDB 集群及其与 InnoDB 的关系。

小费

要了解更多关于 NDB 集群的信息,请参阅 Jesper Krogh 和 Mikiya Okuno 撰写的优秀的 Pro MySQL NDB 集群(a press,2017)。这本书涵盖了 NDB 集群的各个方面,是任何对部署和管理 NDB 集群感兴趣的人的必读之作。

成分

现在,您已经对 InnoDB 集群有了更好的了解,包括它是如何发展的以及它对高可用性的好处,让我们了解更多关于组成 InnoDB 集群的组件。以下部分从较高的层面描述了每个组件。同样,在后面的章节中,你会发现更多关于它们的内容。我将只介绍每个组件的基础知识,让您了解它的复杂性和好处。

组复制

如果您使用过 MySQL 复制,那么您无疑熟悉如何在构建高可用性解决方案时利用它。事实上,您很可能已经发现了许多使用 MySQL 复制来提高应用可用性的方法。

什么是复制,它是如何工作的?

MySQL 复制是一个易于使用的特性,也是 MySQL Server 的一个复杂和主要的组件。本节提供了复制的鸟瞰图,目的是解释它是如何工作的以及如何设置一个简单的复制拓扑。有关复制及其众多特性和命令的更多信息,请参见在线 MySQL 参考手册( http://dev.mysql.com/doc/refman/8.0/en/replication.html )。

复制需要两台或更多服务器。必须将一台服务器指定为源服务器或主服务器。主角色意味着对数据的所有数据更改(写入)都发送到主服务器,并且只发送到主服务器。拓扑中的所有其他服务器维护主数据的副本,并且根据设计和要求是只读服务器。因此,当您的应用发送数据进行存储时,它们会将数据发送到主服务器。您编写的使用传感器数据的应用可以从从属服务器读取这些数据。

复制机制通过使用一种称为二进制日志的技术来工作,该技术以一种特殊的格式存储更改,从而保留所有更改的记录。然后,这些更改被复制到从属服务器并在那里执行。在从机执行更改(称为事件)后,从机拥有数据的精确副本。

主设备维护一个二进制日志,从设备维护该二进制日志的副本,称为中继日志。当从设备向主设备请求数据更改时,它从主设备读取事件并将它们写入其中继日志;然后,从属线程中的另一个线程执行中继日志中的那些事件。可以想象,从主服务器上发生更改到从服务器上发生更改之间会有一点延迟。幸运的是,除了在高流量(大量变化)的拓扑中,这种延迟几乎是不明显的。

很可能您的高可用性需求越大,您的解决方案扩展得越多(复杂性增加),您就越需要采用更好的方法来管理节点丢失、数据完整性和集群的一般维护(复制数据的服务器组—有时称为副本集)。事实上,大多数高可用性解决方案已经超越了基本的主-从拓扑结构,演变成由服务器集群组成的层;有些复制一部分数据以获得更快的吞吐量,甚至用于分区存储。

所有这些导致许多人发现他们需要更多的 MySQL 复制。Oracle 通过组复制满足了这些需求以及更多需求。组复制允许您的 MySQL 高可用性解决方案远远超出原始 MySQL 复制功能的限制,从而使 MySQL 8 成为高可用性数据库解决方案的重要组成部分。

组复制于 2016 年 12 月作为 GA 发布(从 5.7.17 版本开始),以插件的形式与 MySQL 服务器捆绑。因为组复制是作为服务器插件实现的,所以您可以安装插件并开始使用组复制,而不必重新安装服务器,这使得试验新功能变得容易。

组复制也使得同步复制(在属于同一个组的节点之间)成为现实,而现有的 MySQL 复制特性是异步的(或者最多是半同步的)。因此,始终提供了更强的数据一致性(数据在所有成员上都可用,没有因等待副本跟上而导致的延迟)。

这可以通过在分配给一个组的服务器之间进行强协调的分布式状态机来实现。这种通信允许服务器在组内自动协调复制。更具体地说,组维护成员关系,以便服务器之间的数据复制在任何时间点都是一致的。即使从组中删除了服务器,当它们被重新添加时,一致性也会自动启动。此外,对于离线或变得不可达的服务器,还有一个故障检测机制。图 2-2 展示了如何在应用中使用组复制来实现高可用性。

img/460910_1_En_2_Fig2_HTML.jpg

图 2-2

对应用使用组复制以实现高可用性(由 Oracle 提供)

请注意,组复制可以与 MySQL 路由一起使用,以允许您的应用拥有一个与集群隔离的层。您将在后面的章节中看到一些关于路由的内容。

组复制和标准复制的另一个重要区别是,组中的所有服务器都可以参与更新数据,并自动解决冲突。是的,您不再需要精心设计您的应用来发送写入(更新)到特定的服务器!但是,您可以配置组复制,只允许一台服务器(称为主服务器)进行更新,其他服务器充当辅助服务器或备份服务器(用于故障转移)。

使用组复制中内置的三种特定技术(组成员、故障检测和容错)可以实现以下功能及更多功能:

  • 组成员身份:管理服务器是否活动(在线)并加入组。此外,还确保组中的每台服务器都有一致的成员集视图。每个服务器都知道组中服务器的完整列表。当服务器添加到组中时,组成员资格服务会自动重新配置成员资格。

  • 故障检测:一种能够发现并报告哪些服务器离线(不可达)并被认为是死的机制。故障检测器是一种分布式服务,它允许组中的所有服务器测试假定失效服务器的状况,通过这种方式,组决定服务器是否不可达(失效)。这允许该组通过协调排除故障服务器的过程来自动重新配置。

  • 容错:该服务使用 Paxos 分布式算法的实现来提供服务器之间的分布式协调。简而言之,该算法允许自动提升组内的角色,以确保即使一个(或几个)服务器出现故障或离开组,组也保持一致(数据一致且可用)。像类似的容错机制一样,失败(失败的服务器)的数量是有限的。目前,组复制容错被定义为 n = 2f + 1,其中 n 是容忍 f 个故障所需的服务器数量。例如,如果您希望容忍多达 5 台服务器出现故障,则该组中至少需要 11 台服务器。

有关组复制的更多信息,请参见位于 https://dev.mysql.com/doc/refman/8.0/en/replication.html 的在线参考手册中的“组复制”部分。

MySQL Shell

旧的 MySQL 客户端(名为mysql)最大的缺失之一是没有任何形式的脚本功能。但是,可以使用旧客户端处理一批 SQL 命令,并且客户端对编写存储例程(过程和函数)的支持有限。对于那些想要创建和使用脚本来管理他们的数据库(和服务器)的人来说,过去有一些外部工具,比如 MySQL Workbench,但是没有专门用于合并多种脚本语言的工具。

MySQL Workbench 是 Oracle 的一款非常受欢迎的产品。MySQL Workbench 是一个 GUI 工具,设计为基于工作站的管理工具。它提供了许多特性,包括数据库设计和建模工具、SQL 开发、数据库管理、数据库迁移和 Python 脚本支持。有关 MySQL Workbench 的更多信息,请参见 https://dev.mysql.com/doc/workbench/en/

MySQL Shell 是 MySQL 产品组合中令人激动的新成员。MySQL Shell 代表了第一个连接到 MySQL 并与之交互的现代高级客户端。shell 可以用作脚本环境,用于开发处理数据的新工具和应用。尽管它支持 SQL 模式,但它的主要目的是允许用 JavaScript 和 Python 语言访问数据。没错,您可以编写 Python 脚本,并在 shell 中以交互方式或批处理方式执行它们。酷!图 2-3 展示了一个启动 MySQL Shell 的例子。请注意显示 MySQL 徽标、连接信息和模式的漂亮提示符。很好!

img/460910_1_En_2_Fig3_HTML.jpg

图 2-3

MySQL Shell

MySQL Shell 被设计成使用新的 X 协议通过 X 插件与服务器通信。然而,shell 也可以通过使用旧的协议连接到服务器,尽管在脚本模式中功能有限。这意味着,shell 允许您使用关系数据(SQL)、JSON 文档(NoSQL)或两者。

SQL 模式的加入为学习如何使用脚本管理数据提供了一个很好的基础。您可以继续使用您的 SQL 命令(或批处理),直到您将它们转换为 JavaScript 或 Python。此外,您可以使用这两者来确保您的迁移是完整的。

MySQL Shell 有很多特性,包括支持传统的 SQL 命令处理、脚本原型,甚至支持定制 Shell。大多数特性都可以通过命令行选项或特殊的 shell 命令来控制。在后面的章节中,您将更深入地了解一些更重要的特性。现在,下面列出了 shell 的一些主要特性:

  • 日志:你可以为你的会话创建一个日志,用于以后的分析或者保存消息的记录。您可以使用-option设置详细程度,范围从 1(无记录)到 8(最大调试)。

  • 输出格式:shell 支持三种格式选项。这些是您在启动 shell 时指定的命令行选项。

    • 表格 ( --table):传统的网格格式,您已经习惯了旧客户端的格式

    • 制表符:用制表符分隔显示信息,用于批量执行

    • JSON ( --json):以更易于阅读的方式格式化 JSON 文档

  • 交互codeeexecution:使用 shell 的默认模式是交互模式,它的工作方式就像一个传统的客户端:你输入一个命令,得到一个响应。

  • 批处理代码执行:如果您想在没有交互会话的情况下运行您的脚本,您可以使用 shell 以批处理模式运行脚本。但是,输出仅限于非格式化输出(但可以用--interactive选项覆盖)。

  • 脚本语言:shell 支持 JavaScript 和 Python,尽管你一次只能使用一种。

  • 会话:会话本质上是到服务器的连接。shell 允许您存储和删除会话。

  • 启动脚本:您可以定义一个脚本在 shell 启动时执行。您可以用 JavaScript 或 Python 编写脚本。

  • 命令历史:shell 保存您输入的命令,允许您使用上下箭头键调用它们。

  • 全局变量:shell 提供了一些在交互模式下可以访问的全局变量。其中包括以下内容。

    • session:全局会话对象,如果已建立

    • db:模式,如果通过连接建立

    • dba:用于使用 InnoDB 集群的 AdminAPI 对象

    • shell:使用 Shell 的通用功能

    • util:与服务器一起工作的实用功能

  • 自定义提示:您也可以通过使用特殊格式更新名为~/.mysqlsh/prompt.json的配置文件或定义名为MYSQLSH_PROMPT_THEME的环境变量来更改默认提示。

  • 自动完成关键字、类和方法:从 8.0.4 开始,shell 允许用户在 SQL 模式下按 Tab 键自动完成关键字,在 JavaScript 和 Python 模式下自动完成主要的类和方法。shell 还为已知的关键字、API 函数和 SQL 关键字提供代码补全。

小费

有关 MySQL Shell 的更多信息,请参见在线 MySQL 参考手册( https://dev.mysql.com/doc/refman/8.0/en/mysql-shell.html )中标题为“MySQL Shell 用户指南”的部分。

X DevAPI

X Developer application programming interface,简称 X DevAPI,是一个类库和方法库,为 MySQL 实现了一个新的 NoSQL 接口。具体来说,X DevAPI 旨在允许与 JSON 文档和关系数据轻松交互。X DevAPI 有专门支持这两个概念的类,允许开发者在他们的应用中使用其中一个(或两个)。X DevAPI 与 X 协议、X 插件以及为公开 X DevAPI 而编写的客户端一起构成了 MySQL 8 文档库。X DevAPI 有几个强大的特性,包括:

  • MySQLX :一个模块,用于获取 X 协议连接到 MySQL 服务器产生的 session 对象。

  • 会话:连接到 MySQL 服务器。

  • 集合:存储 JSON 文档的组织抽象。

  • 文档 : JSON 文档是集合中数据的主要存储机制。

  • CRUD 操作:创建、读取、更新和删除操作的简单方法。读操作简单易懂。

  • 关系数据:实现传统关系数据的 CRUD 操作,包括 SQL 语句执行和结果处理。

  • 表达式:现代实践和语法风格用于摆脱传统的 SQL 字符串构建,以便在您的集合和文档中查找内容。

  • 并行执行:非阻塞、异步调用遵循常见的宿主语言模式。

  • 方法链接:构建 API 是为了让创建或检索(获取)对象的方法返回该对象的实例。这允许我们将几种方法结合在一起(称为方法链)。尽管方法链接既不是新概念,也不是 X DevAPI 所独有的,但它是一种强大的机制,使我们的代码更具表现力,更易于阅读。

注意

X DevAPI 仅在使用 X 插件时可用。如果没有安装 X 插件,就不能使用 X DevAPI,只能通过支持 X 协议的客户机或数据库连接器。

X DevAPI 只能通过实现 X 协议的一个客户机使用,具体来说,就是下面的任何一个客户机。此外,要使用这些客户端中的任何一个,您还必须在您的服务器上安装和配置 X 插件。

小费

有关 X DevAPI 的更多信息,请参见 https://dev.mysql.com/doc/x-devapi-userguide/en/

AdminAPI

管理应用编程接口(AdminAPI)是一个类库和方法库,为 InnoDB 集群实现了一个新的管理接口。具体来说,AdminAPI 旨在通过使用 MySQL Shell 中的脚本语言来实现与 InnoDB Cluster 的轻松交互。MySQL Shell 包含 AdminAPI,它使您能够部署、配置和管理 InnoDB 集群。AdminAPI 包含两个用于访问 InnoDB 集群功能的类:

  • dba:使您能够使用 AdminAPI 管理 InnoDB 集群。dba类使您能够管理集群;例如,创建一个新集群,使用沙箱配置(一种在同一台机器上使用几个 MySQL 实例来试验 InnoDB 集群的方法),以及检查实例和集群的状态。

  • cluster:InnoDB 集群的管理句柄。cluster类使您能够使用集群来添加实例、删除实例、获取集群的状态(健康)等等。

因为我们将在本书中直接使用 AdminAPI,所以你将在第 5 章中看到关于每个类可用方法的更多细节。

小费

参见 https://dev.mysql.com/doc/dev/mysqlsh-api-python/8.0/group___admin_a_p_i.html 了解更多关于 AdminAPI 的信息。

路由

MySQL 路由是 MySQL 中相对较新的组件。它最初是为现已过时的 MySQL Fabric 产品而构建的,经过了显著的改进和修改,可用于 InnoDB Cluster。事实上,它是 InnoDB 集群的重要组成部分。

MySQL 路由是一个轻量级的中间件组件,在应用和 MySQL 服务器之间提供透明的路由。虽然它可以用于各种各样的用例,但它的主要目的是通过有效地将数据库流量路由到适当的 MySQL 服务器来提高高可用性和可伸缩性。

对于处理故障转移的客户端应用,它们需要了解 InnoDB 集群拓扑,并知道哪个 MySQL 实例是主(写)服务器。虽然应用可以实现这种逻辑,但是 MySQL 路由可以为您提供和处理这种功能。

此外,当与 InnoDB Cluster 一起使用时,MySQL Router 充当代理来隐藏网络上的多个 MySQL 实例,并将数据请求映射到集群中的一个实例。如果有足够多的在线副本,并且组件之间的通信完好无损,应用将能够(重新)连接到其中一个副本。MySQL 路由通过简单地重新打印应用来连接到路由而不是直接连接到 MySQL,也使这种情况成为可能。

小费

关于 MySQL 路由的更多信息,请参见 https://dev.mysql.com/doc/mysql-router/8.0/en/ .

安装 InnoDB 集群

回想一下,Oracle 选择将 InnoDB 集群构建到 MySQL 服务器中,并致力于为社区和企业客户提供 InnoDB 集群。当您安装 MySQL 时,您也安装了 InnoDB Cluster 和使其工作所需的所有组件。

在本节中,您将通过使用 MySQL Windows Installer 在 Windows PC 上安装 MySQL。如果你使用的是另一个平台,可以查看在线 MySQL 参考手册了解更多细节( https://dev.mysql.com/doc/refman/8.0/en/installing.html )。

MySQL Windows Installer(也称为 MySQL Installer )为您的所有 MySQL 软件需求提供了易于使用的、基于向导的安装体验。虽然某些特定于开发者的组件可能需要额外安装,但安装程序是您安装最新版本 MySQL 产品的一站式站点,包括:

  • MySQL 服务器

  • MySQL 连接器

  • MySQL 工作台和示例模型

  • 示例数据库

  • MySQL for Excel

  • MySQL 通告程序

  • 面向 Visual Studio 的 MySQL

  • 文件

小费

在 Windows 上安装时,Windows 可能会要求您批准升级安装。

安装程序允许您选择要安装的产品,并且您可以根据需要多次运行安装程序来更新、添加或删除产品。此外,您可以选择基于 web 的安装程序或独立安装程序。如果您在运行 MySQL 安装程序时有在线连接,并且您不想安装所有 MySQL 产品(从而只下载您需要的产品),请选择 web 安装程序。如果在运行 MySQL 安装程序时没有联机连接,或者希望现在或将来安装所有产品,请选择独立安装程序。

您可以在 https://dev.mysql.com/downloads/installer/ 下载 MySQL 安装程序。在该网站上,您将看到最新版本的安装程序,包括安装 32 位或 64 位二进制文件的选项。但是,请注意,安装程序本身是一个 32 位应用。

现在,让我们看一个在 Windows 机器上安装 MySQL 的快速、典型的演练。因为安装程序遵循的脚本会根据您的选择而变化,所以在您的 PC 上安装 MySQL 可能会有所不同。但是,下面显示了从头开始安装 MySQL、安装 InnoDB Cluster 和 MySQL 文档存储所需的所有产品的进度。我们从下载安装程序并启动它开始。图 2-4 显示了初始许可协议屏幕。请注意,这是安装程序的社区版(这意味着它只安装 GNUv2 许可下的社区版产品)。

img/460910_1_En_2_Fig4_HTML.jpg

图 2-4

许可协议(MySQL 安装程序)

阅读并同意许可协议后,您可以勾选“我接受许可条款”复选框,然后单击“下一步”以显示选择安装类型面板,如图 2-5 所示。该面板为您提供了多种选择,包括以开发者为中心的安装、仅安装服务器、仅安装客户端、完全安装所有产品,以及允许您选择要安装哪些组件的自定义选项。选择自定义选项,然后单击下一步。

img/460910_1_En_2_Fig5_HTML.jpg

图 2-5

选择安装类型(MySQL 安装程序)

下一个面板允许您选择要安装的产品。图 2-6 显示了带有 MySQL 服务器的面板和选择安装的文档。要选择要安装的产品,请展开左侧列表中的类别,然后选择要安装的产品,并单击向右箭头将其移动到右侧列表中。右侧列表中显示的产品将在您继续操作时安装。对于本章,您只需要 MySQL 服务器和文档。您将在后面的章节中添加其他产品。当您要安装的产品列在右边时,单击“下一步”按钮。

img/460910_1_En_2_Fig6_HTML.jpg

图 2-6

选择产品和功能(MySQL 安装程序)

下一个面板是安装摘要,允许您确认是否有正确的产品可供安装。图 2-7 所示的面板也显示了状态,以便您可以观察安装进度。当您准备好开始安装所选产品时,请单击执行。

img/460910_1_En_2_Fig7_HTML.jpg

图 2-7

安装对话框–暂存(MySQL 安装程序)

当安装开始时,你会看到每个产品的进度,如图 2-8 所示。

img/460910_1_En_2_Fig8_HTML.jpg

图 2-8

安装对话框–安装进度(MySQL 安装程序)

所有产品安装完成后,安装面板将显示所有安装的状态为完成,并将底部的按钮改为显示下一步,如图 2-9 所示。准备好后,单击下一步。

img/460910_1_En_2_Fig9_HTML.jpg

图 2-9

安装对话框–安装完成(MySQL 安装程序)

安装的下一步是配置任何具有安装后选项的产品。MySQL Server 就是这样一种产品,它允许您设置几个配置项来完成安装。图 2-10 显示了产品配置面板。准备好后,单击“下一步”开始配置。

img/460910_1_En_2_Fig10_HTML.jpg

图 2-10

产品配置(MySQL 安装程序)

MySQL 8.0.11 的 MySQL 安装程序新增了从安装程序设置测试 InnoDB 集群配置的功能。如果您想在测试服务器上安装 MySQL 进行开发或实验,这可以节省大量时间。在第 6 章中,您将采取一种更加谨慎的方式来设置 InnoDB 集群。现在,你可以保留默认选择来安装一个独立的 MySQL 服务器实例,如图 2-11 所示。然后单击下一步按钮。

img/460910_1_En_2_Fig11_HTML.jpg

图 2-11

在沙箱中设置组复制或 InnoDB 集群(MySQL 安装程序)

配置服务器时,第一步是设置网络和配置类型。图 2-12 显示了允许您选择配置类型(包含所选用例典型设置的基线)和任何您希望选择的连接选项的类型和网络面板。

img/460910_1_En_2_Fig12_HTML.jpg

图 2-12

类型和网络–配置服务器(MySQL 安装程序)

在这种情况下,我们将选择开发机器的配置类型(默认)。我们还将选择在端口 3306 上使用 TCP/IP,并打开 Windows 防火墙以允许与网络之间的通信;为此,我们勾选了每个选项旁边的复选框,如图所示。还建议勾选显示高级选项复选框进行进一步配置。选择选项后,单击“下一步”继续。

回想一下,MySQL 在 8.0.4 版中更改了默认的身份验证插件。因为许多安装仍然使用传统的身份验证方法,所以 Oracle 提供了一个选项,在安装过程中选择旧的方法,以便您的应用可以继续使用最新版本。如果您想使用新的身份验证和强密码加密,请保留默认选择,如图 2-13 所示。单击“下一步”按钮继续。

img/460910_1_En_2_Fig13_HTML.jpg

图 2-13

选择验证方法(MySQL 安装程序)

我们需要做的下一件事是为 root 用户帐户选择密码。这是 MySQL 用于所有管理操作的帐户。您应该明智地选择密码,以保护您的服务器免受意外访问。图 2-14 显示了账户和角色面板。请注意,此时您还可以设置其他用户帐户,这样可以节省安装后的时间。准备好后,请单击“下一步”按钮继续。

img/460910_1_En_2_Fig14_HTML.jpg

图 2-14

帐户和角色–配置服务器(MySQL 安装程序)

接下来,我们可以选择设置一个 Windows 服务来管理服务器的启动和停止。这是推荐给任何安装服务器的人的,他们希望服务器从启动开始运行。事实上,您可以选择创建服务,但不能将其设置为自动启动。图 2-15 显示了为服务器设置 Windows 服务的典型选择。选择所需选项后,单击“下一步”按钮继续。

img/460910_1_En_2_Fig15_HTML.jpg

图 2-15

Windows 服务–配置服务器(MySQL 安装程序)

接下来,我们可以选择通过 X 协议插件来启用和配置 MySQL 文档存储。图 2-16 显示了用于启用插件的插件和扩展面板。我们希望启用插件,将端口设置为 33060(默认),并允许端口通过防火墙。选择选项后,单击下一步。

img/460910_1_En_2_Fig16_HTML.jpg

图 2-16

插件和扩展–配置服务器(MySQL 安装程序)

接下来,因为我们选择了高级配置选项,所以我们可以设置各种日志选项,包括错误、查询和二进制日志。您应该考虑打开(选择)常规日志和查询日志。我们还必须启用二进制日志,以便与 InnoDB 集群一起使用。图 2-17 显示了高级选项面板。请注意,您还可以选择命名每个日志并重新定位它们。但是,对于本书,您可以保留默认名称,这些名称基于机器名称。例如,本演练在名为 OPTIPLEX-7010 的计算机上运行,因此文件名具有该前缀。选择选项后,单击下一步。

img/460910_1_En_2_Fig17_HTML.jpg

图 2-17

高级选项(MySQL 安装程序)

此时,安装程序将向您显示要执行的步骤摘要,并允许您开始该过程或返回并进行更改,如图 2-18 所示。准备就绪后,单击执行按钮继续。

img/460910_1_En_2_Fig18_HTML.jpg

图 2-18

应用配置-暂存(MySQL 安装程序)

当配置过程运行时,面板将变灰执行和返回按钮,并在每个步骤旁边的点上显示绿色复选标记,如图 2-19 所示。如果出现错误,面板将显示一个红色 X,如果错误禁止继续,可能会显示一条错误消息。

img/460910_1_En_2_Fig19_HTML.jpg

图 2-19

应用配置-进行中(MySQL 安装程序)

当产品配置完成时,面板将改变为在所有步骤旁边显示绿点。底部的按钮将变为一个标记为完成的按钮,如图 2-20 所示。准备就绪后,单击“完成”按钮继续。

img/460910_1_En_2_Fig20_HTML.jpg

图 2-20

应用配置-完成(MySQL 安装程序)

下一个面板显示了产品配置操作的摘要,如图 2-21 所示。

img/460910_1_En_2_Fig21_HTML.jpg

图 2-21

产品配置–摘要(MySQL 安装程序)

确认无误后,点击下一步按钮进入最后一个面板,如图 2-22 所示。您可以单击“完成”按钮来完成安装并关闭安装程序。

img/460910_1_En_2_Fig22_HTML.jpg

图 2-22

安装完成(MySQL 安装程序)

第一次在 Windows PC 上安装 MySQL 的演练到此结束。如上所述,您可以再次运行安装程序来安装其他产品。还记得,根据所选的产品,显示的面板顺序会有所不同。

摘要

除了添加组复制之外,InnoDB Cluster 可能是那些寻求用 MySQL 构建高可用性解决方案的人最重要的新特性。服务器中从未有过如此多的功能被设计为提供一个健壮、强大、可靠且易于管理的高可用性 MySQL。

在本章中,您了解了有关 InnoDB 存储引擎的更多信息,包括最新的新功能和构成 InnoDB 集群的许多组件,包括组复制、MySQL Shell 和 AdminAPI 以及用于应用连接路由的 MySQL 路由。

在下一章中,您将尝试组复制,并了解更多关于这个奇妙且高度复杂的特性。虽然您将看到 AdminAPI 使配置组复制变得很容易,但是优秀的系统和数据库管理员必须知道组复制是如何工作的,以便他们能够继续改进和扩展他们的高可用性解决方案,并做好排除组复制故障的准备。

三、MySQL 组复制

InnoDB 集群的核心是组复制。正如您在前一章中了解到的,组复制是 MySQL 复制的最新发展,旨在使数据复制更加健壮和可靠。与对 InnoDB 存储引擎的修改(全部隐藏在罩下)一起,组复制实现了高可用性功能,而在过去,这需要专门的、有时是定制的产品、中间件和定制的应用才能实现。

在本章中,您将探索从设置到活动组的组复制。这将演示并强化组复制的概念。您可能想知道为什么要在最低的手动配置级别研究这样一个特性,尤其是如果您已经尝试过 InnoDB Cluster 和 MySQL Shell(通过 AdminAPI)。

答案是知识的稳健性。每一个成功的系统或数据库管理员都必须知道产品是如何工作的,而不仅仅是如何设置和运行它们。这是容易的部分,由于有了 AdminAPI,任何人都可以设置 InnoDB 集群。但是,如果出错了,或者需要以不同的方式使用 InnoDB 集群(组复制)怎么办?有了在最低级别设置组复制的经验,您将对将来要做的事情有更深入的了解。

在本章中,您将深入了解组复制的组成部分。之后,您将在导游的带领下参观组复制。跟随我们探索 MySQL 历史上最伟大的进步之一!但是,让我们从组成描述组复制的语言的概念和术语列表开始。

概念、术语和行话

很可能除了最精通或者掌握 MySQL 最新知识的人之外,所有人都将完全理解描述组复制的所有术语和概念。在本节中,您将后退一步,花一点时间关注您将在本章和本书(或任何关于 MySQL 高可用性的书)的其余部分中遇到的一些术语和概念。本节提供了与组复制相关的术语表,您可以随时参考。

  • 二进制日志:服务器产生的文件,包含所有执行的事务的二进制形式。二进制日志文件也用于复制,以便在两台服务器之间交换事务。当在主服务器(master)上使用时,它形成了所有更改的记录,可以发送到辅助服务器(slave)执行以创建副本(有时有点不准确地称为副本)。

  • 多主节点:一个组,其中的写入可以发送到多个主节点,并在组内复制。

  • 故障转移:允许组从主服务器上的故障中恢复的事件,自动选举新的主服务器。

  • 容错:从组中检测到的故障或错误中恢复而不丢失数据或功能的能力。请注意,组复制中的容错能力受到组中服务器数量的限制。参见侧栏“我如何计算一个组可以处理的故障数量?”学习如何计算一个群体可以容忍的错误数量。

  • Group :参与同一个组复制通信设置的一组 MySQL 服务器。

  • 组通信:一种特殊的机制,使用状态机和通信协议来保持组内服务器的协调,包括事务执行的同步和角色的选择/选举。

  • 实例:正在运行的 MySQL 服务器。通常用于指在同一台机器上运行的一个或多个 MySQL 服务器。这和 MySQL 服务器不一样,后者往往指的是硬件和 MySQL 执行的集合。

  • Primary :组中的服务器,被分配收集所有数据写入(更新)的角色。

  • 中继日志:二进制日志文件,在辅助(从)上使用,记录从主(主)二进制日志中读取的事务,缓存以供执行。它的格式与二进制日志相同。

  • 辅助服务器:组中被分配了 reader 角色的服务器,这意味着应用可以从辅助服务器读取数据,但不能写入辅助服务器。

  • 单主:由一台主服务器和一台或多台从服务器组成的组。这类似于旧的 MySQL 复制特性中的主/从配置。

  • 切换:管理员主动改变主服务器角色的受控维护事件,将主服务器从一台服务器上移除,并将其分配给另一台服务器(使新服务器成为主服务器)。这不会自动发生,通常与故障无关。

  • 事务:一组数据更改,在将该组数据应用到数据之前,必须全部成功。失败的事务不会写入数据库。

  • 拓扑:复制组中服务器的逻辑布局。例子包括如下:

    • 单主服务器:单个服务器,与每个从服务器径向连接。

    • 分层:单主服务器组的连接,其中每个辅助服务器都是另一组辅助服务器的主服务器。

    • 多主服务器:每个主服务器连接到组中的所有其他主服务器以及组中的辅助服务器。

注意

回想一下,在 MySQL 复制中,主服务器被命名为主服务器,辅助服务器被命名为从服务器。虽然不完全是同义词,但您可以这样认为,但是在使用 InnoDB 集群和组复制时,一定要重新训练您的思维过程,以使用

概观

MySQL 组复制是 MySQL 复制的一种高级形式,用于实现容错系统。复制组(拓扑)是一组通过消息传递相互交互的服务器。通信层提供了一组保证,如原子消息和全序消息传递。这些强大的属性转化为有用的抽象,可以用来构建更高级的数据库复制解决方案。组复制具有以下优点:

  • 无需手动处理服务器故障转移

  • 提供分布式容错

  • 自动化重新配置(添加/删除实例、故障等)

  • 自动检测和处理冲突

  • 提供防止数据丢失的保证

组复制建立在这样的属性和抽象之上,并实现多主机、到处更新的复制协议。使组复制成为可能的技术之一是全局事务标识符(GTIDs)。因此,参与组复制的服务器将启用 GTIDs。

本质上,一个组由多个服务器组成,组中的每个服务器可以独立执行事务。但是所有读/写(RW)事务只有在得到组的批准后才会提交。只读(RO)事务不需要在组内协调,因此可以立即提交。换句话说,对于任何 RW 事务,组需要决定它是否提交,因此提交操作不是来自发起服务器的单方面决定。

准确地说,当一个事务准备在原始服务器上提交时,服务器自动广播写值(已更改的行)和相应的写集(已更新的行的唯一标识符)。然后为该事务建立全局总订单。最终,所有服务器以相同的顺序接收相同的事务集。因此,所有服务器以相同的顺序应用相同的更改集,并且它们在组内保持一致。

组复制通过在复制组之间复制系统状态来提供冗余。如果一台(或多台)服务器出现故障,系统仍然可用。如果足够多的服务器出现故障,性能或可伸缩性可能会受到影响,但系统将保持可用。

这是通过组成员服务实现的,该服务依赖于分布式故障检测器,当任何服务器通过有意的交互或由于故障而离开组时,该检测器可以发出信号。分布式恢复过程确保当服务器加入组时,它们会自动更新。不需要手动服务器故障转移,多主服务器无处不更新的特性确保了在单个服务器出现故障的情况下,甚至更新也不会被阻止。因此,MySQL 组复制保证了数据库服务的持续可用性。

设置 MySQL 组复制类似于设置 MySQL 复制的过程,这可能不会让您感到惊讶。毕竟,组复制建立在 MySQL 复制的基础上。在下一部分,您将看到组复制的演示。我们不再关注 MySQL 复制教程中的相同步骤,而是简要介绍相同的主题,深入探讨组复制特有的细微差别。

MySQL 组复制教程

本节演示如何在一组服务器之间设置组复制。如前所述,组复制对组中的角色使用不同的术语。具体来说,有一个主要角色和一个次要角色。与将一台服务器指定为主服务器的 MySQL 复制不同,组复制可以根据需要自动更改组中服务器的角色。因此,尽管我们将通过将其中一台服务器标识为主服务器来设置组复制,但是随着时间的推移,组的最终状态可能会导致其他服务器之一成为主服务器。

如果您想自己体验本教程,您应该准备四台服务器。和上一个教程一样,我们将使用当前机器上运行的几个实例。我们需要几个实例来确保该组有一个可行的集合来启用冗余和故障转移。在这种情况下,团队最多可以容忍一次失败。

我如何计算一个组可以处理的故障数量?

确定一组服务器可以容忍多少故障(同时或连续、不可恢复的故障)的公式如下,其中 S 是服务器组,f 是故障数:

S = 2f + 1

例如,一组七个服务器最多可以容忍三个故障:

7 = 2f + 1
6 = 2f
2f = 6
f = 6 / 2
f = 3

如果您想知道容忍已知数量的故障需要多少台服务器,s,F,一个小小的数学应用揭示了以下内容。请注意,您必须向下舍入任何分数。你不能让 1.5 服务器失败。 1

s = 2F + 1
(s – 1) = 2F
2F = (s – 1)
F = (s – 1)/2

例如,一组三个服务器可以容忍一个故障:

F = (3 – 1)/2
F = 2 / 2
F = 1

类似地,一组五个服务器可以容忍两个故障:

F = (5 – 1)/2
F = 4 / 2
F = 2

本教程中的四台服务器示例只能容忍一个故障:

(4 – 1) / 2 = 1.5

或者 1,向下舍入

设置和配置组复制的步骤包括以下内容。可能还有其他同样可行的步骤来设置组复制,但是这些可以在任何机器上完成,并且不会影响 MySQL 的任何现有安装。也就是说,建议在开发机器上执行这些步骤,以消除中断生产系统的风险。

注意

用于设置组复制的步骤类似于 MySQL 复制的步骤。事实上,除了术语(例如, vs. )、配置文件,以及第一次在主服务器上安装组复制插件和启动组复制的两个额外步骤,过程是相同的。

  1. 初始化数据目录。

  2. 配置主服务器。

  3. 配置辅助节点。

  4. 启动 MySQL 实例。

  5. 安装组复制插件。

  6. 创建复制用户帐户。

  7. 在主节点上启动组复制。

  8. 将辅助节点连接到主节点。

  9. 在辅助节点上启动组复制。

  10. 验证组复制状态。

下面几节将更详细地演示在安装了 MySQL 的 Linux 上运行这些步骤。对于其他平台,步骤是相同的,但路径可能略有不同。虽然本教程使用多个本地实例来演示如何使用复制,但是在生产环境中设置复制的过程是相同的。使用特定主机、驱动器、文件夹和端口的各个命令的详细信息是在生产中使用该过程时唯一需要更改的内容。

注意

我们将在示例中使用旧的 MySQL 客户端(mysql)和 SQL 命令。在第 4 章中,你将看到新的 MySQL Shell 的运行,在第 5 章中,你将看到 AdminAPI 的使用。

初始化数据目录

第一步是为使用的每台机器初始化一个数据目录。在这种情况下,我们将在本地计算机上创建一个文件夹来包含所有数据目录。我们将使用四个 MySQL 实例来代表一个主服务器和三个辅助服务器。下面演示了如何创建所需的文件夹。请注意,我在我使用的用户帐户可以访问的本地文件夹中创建这些文件,而不是系统或管理帐户。这是因为我们将在本地运行实例,不需要此类帐户允许的额外特权或访问权限。

$ mkdir gr
$ cd gr
$ mkdir data

现在我们有了一个文件夹<user_home>/gr/data,我们可以使用 MySQL 服务器的初始化选项来设置我们的数据目录,就像我们在 MySQL 复制中所做的一样。回想一下,我们使用服务器的特殊的--initialize-insecure--datadir选项。下面显示了初始化主节点和辅助节点的数据目录所需的命令:

mysqld --no-defaults --user=cbell --initialize-insecure --basedir=/usr/ --datadir=<user_home>/gr/data/primary
mysqld --no-defaults --user=cbell --initialize-insecure --basedir=/usr/ --datadir=<user_home>/gr/data/secondary1
mysqld --no-defaults --user=cbell --initialize-insecure --basedir=/usr/ --datadir=<user_home>/gr/data/secondary2
mysqld --no-defaults --user=cbell --initialize-insecure --basedir=/usr/ --datadir=<user_home>/gr/data/secondary3

现在我们已经创建并填充了数据目录,我们可以配置主服务器和从服务器了。

配置主服务器

这一步与 MySQL 复制最不同。事实上,配置文件有很大的不同。具体来说,除了必须设置的几个更常见的组复制变量之外,我们还使用启用 GTID 的复制中的相同变量。表 3-1 列出了与组复制相关的变量及其用途。控制组复制还有其他变量。参见在线参考手册中的 https://dev.mysql.com/doc/refman/8.0/en/group-replication-options.html 获取完整列表。

表 3-1

组复制变量(配置文件)

|

可变的

|

描述

|
| --- | --- |
| transaction_write_set_extraction | 定义用于对事务期间提取的写入进行哈希运算的算法。组复制必须设置为 XXHASH64。 |
| loose-group_replication_recovery_use_ssl | 确定组复制恢复连接是否应使用 SSL。通常设置为开,但默认为关。 |
| loose-group_replication_group_name | 此服务器实例所属的组的名称。必须是有效的 UUID。 |
| loose-group_replication_start_on_boot | 确定服务器是否应该在服务器启动期间启动组复制。 |
| loose-group_replication_local_address | 成员为来自其他成员的连接提供的网络地址,指定为 host:port 格式的字符串。 |
| loose-group_replication_group_seeds | 群组成员列表,用于建立新成员与群组之间的连接。该列表由种子成员的 group _ replication _ local _ address 网络地址组成,以逗号分隔的列表形式指定,例如 host1:port1,host2:port2。 |
| loose-group_replication_bootstrap_group | 将此服务器配置为引导组。此选项只能在一台服务器上设置,并且只能在第一次启动组或重新启动整个组时设置。引导组后,将此选项设置为 OFF。 |

注意最后一个变量,group_replication_bootstrap_group。这个变量是我们将在配置文件中设置为OFF的东西,但是只有在我们第一次引导组之后。这是初始主节点的用途之一,用于启动组。您将看到一个特殊步骤,您必须在第一次启动主服务器时执行该步骤,以启动该组。之后,该变量必须设置为OFF

注意

还要注意前缀loose-。这个特殊的前缀适用于变量,用于指示服务器继续启动,如果在服务器启动时插件还没有加载的话。这是一种预防措施,以避免在启动过程中出错。在生产服务器上,不应包括前缀,尤其是在依赖组复制来实现高可用性的情况下。

为了构造主服务器的配置文件,我们需要几样东西:数据目录、基本目录和端口的常用变量,以及 GTID 变量和组复制变量。添加插件目录也是一个好主意,以确保服务器可以找到组复制插件(您将在后面的步骤中看到这一点),并打开二进制日志校验和。

因为group_replication_group_seeds变量需要最初加入该组的服务器列表,所以我们必须决定每个服务器将使用的端口。组复制设置要求每台服务器有两个端口:一个用于正常连接,另一个用于组复制。在本教程中,我们将使用端口24801 +连接服务器,使用端口24901 +连接组复制端口。此外,因为我们使用本地实例,所以组中所有成员的主机名都将使用环回地址(127.0.0.1),但这通常是运行它的服务器的主机名。最后,我们还需要选择服务器 id,所以我们将使用从1开始的连续值。清单 3-1 显示了我们将在本教程中用于主服务器的配置文件。

[mysqld]
datadir=/home/cbell/gr/data/primary
basedir=/usr/
plugin_dir=/usr/lib/mysql/plugin/
port=24801
socket=/home/cbell/gr/primary.sock

server_id=1
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_checksum=NONE

transaction_write_set_extraction=XXHASH64
loose-group_replication_recovery_use_ssl=ON
loose-group_replication_group_name="bbbbbbbb-bbbb-cccc-dddd-eeeeeeeeeeee"
loose-group_replication_start_on_boot=OFF
loose-group_replication_local_address="127.0.0.1:24901"
loose-group_replication_group_seeds="127.0.0.1:24901,127.0.0.1:24902,127.0.0.1:24903,127.0.0.1:24904"
loose-group_replication_bootstrap_group=OFF

Listing 3-1Primary Configuration File (Group Replication)

您可能会注意到没有设置log-bin变量。当服务器遇到用于组复制的变量时,它将自动启用二进制日志,因为这是必需的。但是,如果您想要命名二进制日志文件或者将它们放在另一个文件夹中,您可以包含变量,但是这是一个高级配置选项,对于教程甚至开发安装来说都不是必需的。

注意

如果您在 Windows 上运行本教程,并且没有安装安全套接字层(SSL ),也没有将 MySQL 配置为使用 SSL 连接,您必须删除group_replication_recovery_use_ssl选项。

对于本教程,您应该在我们之前创建的文件夹中创建一个名为primary.cnf的文件;比如/home/cbell/gr/primary.cnf。在后面的步骤中,我们将使用该文件启动主实例。

现在,让我们看看辅助节点的配置文件。

配置辅助节点

辅助节点的配置文件类似于主节点的配置文件。唯一的变化是为特定于实例的变量(如端口、数据目录、套接字和服务器 ID)使用正确的值。然而,除了这些设置之外,还存在一些差异。transaction_write_set_extraction变量在初始初级上设置。对于辅助节点,我们添加group_replication_recovery_get_public_key并将其设置为ON。此变量确定辅助节点是否向主节点请求基于 RSA 密钥对的密码交换所需的公钥。该变量适用于使用caching_sha2_password身份验证插件进行身份验证的辅助设备。清单 3-2 显示了第一个辅助节点(名为secondary1)的配置文件。

[mysqld]
datadir=/home/cbell/gr/data/secondary1
basedir=/usr/
plugin_dir=/usr/lib/mysql/plugin/
port=24802
socket=/home/cbell/gr/secondary1.sock

server_id=2
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_checksum=NONE

loose-group_replication_recovery_get_public_key=ON
loose-group_replication_recovery_use_ssl=ON
loose-group_replication_group_name="bbbbbbbb-bbbb-cccc-dddd-eeeeeeeeeeee"
loose-group_replication_start_on_boot=OFF
loose-group_replication_local_address="127.0.0.1:24902"
loose-group_replication_group_seeds="127.0.0.1:24901,127.0.0.1:24902,127.0.0.1:24903,127.0.0.1:24904"
loose-group_replication_bootstrap_group=OFF

Listing 3-2Secondary Configuration File (Group Replication)

在本教程中,我们将使用三个辅助节点,因此您应该在我们之前创建的文件夹中为每个节点创建一个文件,并将它们命名为secondary1.cnfsecondary2.cnfsecondary3.cnf。确保更改特定于实例的变量,例如数据目录、套接字、端口、服务器 ID 等等。您必须更改两个端口:服务器端口和组复制端口。

注意

如果您在 Windows 上运行本教程,并且没有安装 SSL 和配置 MySQL 来使用 SSL 连接,您必须删除group_replication_recovery_use_ssl选项。

在本教程中,我们将使用三个辅助节点,因此您应该在我们之前创建的文件夹中为每个节点创建一个文件,并将它们命名为secondary1.cnfsecondary2.cnfsecondary3.cnf。确保更改特定于实例的变量,例如数据目录、套接字、端口、服务器 ID 等等。您必须更改两个端口:服务器端口和组复制端口。

启动 MySQL 实例

现在我们已经准备好启动 MySQL 实例了。这很容易,因为我们已经创建了包含所有所需参数的配置文件。我们只需要提供带有--defaults-file选项的配置文件。下面显示了启动本教程中使用的服务器实例的命令。请注意,添加了一个重定向,将来自服务器的消息放在一个日志文件中。

mysqld --defaults-file=primary.cnf > primary_output.txt 2>&1 &
mysqld --defaults-file=secondary1.cnf > secondary1_output.txt 2>&1 &
mysqld --defaults-file=secondary2.cnf > secondary2_output.txt 2>&1 &
mysqld --defaults-file=secondary3.cnf > secondary3_output.txt 2>&1 &

运行这些命令时,应该从包含配置文件的文件夹中运行它们。否则,您必须提供配置文件的完整路径。尽管这些命令包含重定向,但您可能希望在第一次启动服务器时使用单独的终端,以确保没有错误。清单 3-3 显示了启动主服务器时打印的消息摘录。

$ mysqld --defaults-file=primary.cnf
2018-03-06T16:54:27.768154Z 0 [System] [MY-010116] /home/cbell/source/git/mysql-bug-staging/build/bin/mysqld (mysqld 8.0.11) starting as process 6533
2018-03-06T16:54:29.212063Z 0 [Warning] [MY-010068] CA certificate ca.pem is self signed.
2018-03-06T16:54:29.226500Z 0 [Warning] [MY-011071] unknown variable 'loose-group_replication_recovery_use_ssl=ON'
2018-03-06T16:54:29.226523Z 0 [Warning] [MY-011071] unknown variable 'loose-group_replication_group_name=bbbbbbbb-bbbb-cccc-dddd-eeeeeeeeeeee'
2018-03-06T16:54:29.226530Z 0 [Warning] [MY-011071] unknown variable 'loose-group_replication_start_on_boot=OFF'
2018-03-06T16:54:29.226535Z 0 [Warning] [MY-011071] unknown variable 'loose-group_replication_local_address=127.0.0.1:24901'
2018-03-06T16:54:29.226540Z 0 [Warning] [MY-011071] unknown variable 'loose-group_replication_group_seeds=127.0.0.1:24901,127.0.0.1:24902,127.0.0.1:24903,127.0.0.1:24904'
2018-03-06T16:54:29.226549Z 0 [Warning] [MY-011071] unknown variable 'loose-group_replication_bootstrap_group=OFF'
...
2018-03-06T16:54:29.313298Z 0 [System] [MY-010931] /home/cbell/source/git/mysql-bug-staging/build/bin/mysqld: ready for connections. Version: '8.0.11'  socket: '/home/cbell/gr/primary.sock'  port: 24801  MySQL Community Server (GPL).

Listing 3-3Starting the Primary Instance

同样,如果您计划使用单个终端,建议将输出重定向到一个文件,并使用选项在另一个进程中启动应用(例如,&符号)。

如果您正在按照本教程进行操作,并且还没有这样做,请继续操作并启动辅助服务器。启动所有服务器实例后,我们可以进入下一步——安装组复制插件。

安装组复制插件

MySQL 实例启动后,您必须安装组复制插件。 2 这只需要一条命令,如果你需要重启服务器,不需要重启插件。下面显示了您用来安装插件的命令。该命令需要插件的名称以及可动态加载的可执行文件的名称。在这种情况下,插件的名称是group_replication,,可加载的可执行文件的名称是group_replication.so:

INSTALL PLUGIN group_replication SONAME 'group_replication.so'

小费

注意文件名中的.so。这是您将用于*nix 平台的扩展。在 Windows 上,文件扩展名是.dll

下面显示了在主节点上执行的命令。请注意,没有其他消息。要检查插件状态,使用SHOW PLUGINS命令或在INFORMATION_SCHEMA.PLUGINS表中搜索插件,如清单 3-4 所示。

$ mysql -uroot -h 127.0.0.1 --port=24801
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.11 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> INSTALL PLUGIN group_replication SONAME 'group_replication.so';
Query OK, 0 rows affected (0.09 sec)

mysql> SELECT * FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME = 'group_replication' \G
*************************** 1\. row ***************************
           PLUGIN_NAME: group_replication
        PLUGIN_VERSION: 1.1
         PLUGIN_STATUS: ACTIVE
           PLUGIN_TYPE: GROUP REPLICATION
   PLUGIN_TYPE_VERSION: 1.2
        PLUGIN_LIBRARY: group_replication.so
PLUGIN_LIBRARY_VERSION: 1.9
         PLUGIN_AUTHOR: ORACLE
    PLUGIN_DESCRIPTION: Group Replication (1.1.0)
        PLUGIN_LICENSE: GPL
           LOAD_OPTION: ON
1 row in set (0.01 sec)

mysql> \q
Bye

Listing 3-4Installing and Checking the Group Replication Plugin Status

请注意,当安装插件时,我们只得到一个粗略的 OK 消息。如果配置文件中的插件目录错误或者插件可执行文件丢失,那么在运行该命令时会出现错误。如果发生这种情况,使用SHOW VARIABLES LIKE 'plugin_dir';检查插件目录。然后关闭服务器,确保可执行文件在插件目录中,并且插件目录是配置文件中的正确值。

对于插件状态,我们期望从SELECT查询中获得一行,并看到状态被设置为ACTIVE。如果您看到另一个状态,请确保插件安装正确,并且是您安装的 MySQL 版本的正确插件。如果你安装了多个版本的 MySQL,很容易错误地将插件目录指向错误的插件。

注意

在开始组复制之前,必须在每个服务器实例上运行这些命令。

这是前面在比较启动组复制和 MySQL 复制时提到的额外步骤。如果您遵循本教程,现在执行这些语句在所有四个实例上安装插件。当插件加载到所有实例上时,您可以继续在所有实例上创建复制用户。

创建复制用户帐户

MySQL 实例启动后,您必须创建一个用户,供服务器相互连接使用。回想一下,在组复制中,服务器都相互“对话”。幸运的是,这些命令与我们在 MySQL 复制中使用的命令相同。我们需要在所有服务器实例上创建这个用户。下面显示了创建复制用户所需的命令。在所有服务器上执行以下命令:

SET SQL_LOG_BIN=0;
CREATE USER rpl_user@'%' IDENTIFIED BY 'rpl_pass';
GRANT REPLICATION SLAVE ON *.* TO rpl_user@'%';
FLUSH PRIVILEGES;
SET SQL_LOG_BIN=1;

注意主机名中使用了%。这样做是为了确保复制用户可以从任何服务器连接。对于生产环境,您通常不会这样做,但是对于教程或开发测试,这使事情变得简单了一些。

回想一下,set sql_log_bin=0命令告诉服务器暂时禁止记录对二进制日志的更改。每当我们不想在拓扑中的其他机器上复制命令时,我们就这样做。具体来说,不应复制维护和管理命令,如创建用户。关闭二进制日志是确保您不会意外发出无法在其他机器上执行的事务的好方法。

执行这些命令的最佳方式是将它们保存到名为create_rpl_user.sql的文件中,并使用mysql客户端的源命令从文件中读取命令并执行它们。您可以使用以下命令在所有实例上快速创建复制用户。

注意

本节中的路径使用主目录/home/cbell/。请确保替换用户目录的路径。

mysql -uroot -h 127.0.0.1 -e "source /home/cbell/gr/create_rpl_user.sql" --port=24801
mysql -uroot -h 127.0.0.1 -e "source /home/cbell/gr/create_rpl_user.sql" --port=24802
mysql -uroot -h 127.0.0.1 -e "source /home/cbell/gr/create_rpl_user.sql" --port=24803
mysql -uroot -h 127.0.0.1 -e "source /home/cbell/gr/create_rpl_user.sql" --port=24804

在主节点上启动组复制

下一步是首次在主节点上启动组复制。回想一下我们对组复制变量的讨论,变量group_replication_bootstrap_group通常被设置为OFF,除非在组的第一次启动时。因为该组从未启动过,所以我们必须在主服务器上启动。

幸运的是,变量group_replication_bootstrap_group是动态的,我们可以动态地打开和关闭它。我们可以在主节点上运行以下命令来首次启动组复制:

SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;

您可能还记得,我们在主配置文件中将group_replication_bootstrap_group设置为OFF。这样,如果我们重新启动主服务器,设置将是正确的。如果您愿意,您可以将它设置为ON,但是您必须在重新启动主服务器之前在配置文件中更改它。设置为OFF安全多了,工作量也少了。

如果您正在学习本教程,那么现在就在主服务器上运行这些命令。完成后,您就可以将辅助节点连接到主节点了。

将辅助节点连接到主节点

下一步是将辅助节点连接到主节点。我们使用你在之前的教程中看到的相同的CHANGE MASTER命令。但是,我们只需要复制用户和密码。我们告诉服务器连接到名为group_replication_recovery的特殊复制通道。下面显示了用于将每个辅助节点连接到主节点的命令:

CHANGE MASTER TO MASTER_USER="rpl_user", MASTER_PASSWORD="rpl_pass" FOR CHANNEL 'group_replication_recovery';

注意

您可能会看到关于在命令中使用密码的警告。这是为了鼓励您不要将复制用户和密码放在文件中。使用可插拔身份验证选项更安全。

请注意,我们需要的信息甚至比启用了 GTID 的复制还要少。酷!您必须在所有辅助节点上运行该命令。将它保存到一个文件并使用mysql客户机执行它可能更容易,就像我们对复制用户所做的那样。例如,将它保存到一个名为change_master.sql的文件中,并如下所示执行它:

mysql -uroot -h 127.0.0.1 -e "source /home/cbell/gr/change_master" --port=24802
mysql -uroot -h 127.0.0.1 -e "source /home/cbell/gr/change_master" --port=24803
mysql -uroot -h 127.0.0.1 -e "source /home/cbell/gr/change_master" --port=24804

现在,我们已经将辅助节点配置为连接到主节点,我们必须通过启动组复制来完成该过程。

在辅助节点上启动组复制

下一步是在辅助节点上启动组复制。组复制使用命令START GROUP_REPLICATION,而不是像 MySQL 复制那样使用START SLAVE命令。在每个辅助节点上运行此命令,如下所示:

mysql -uroot -h 127.0.0.1 -e "START GROUP_REPLICATION" --port=24802
mysql -uroot -h 127.0.0.1 -e "START GROUP_REPLICATION" --port=24803
mysql -uroot -h 127.0.0.1 -e "START GROUP_REPLICATION" --port=24804

START GROUP_REPLICATION命令通常不会报告任何错误,可能需要更长时间才能返回。这是因为当辅助节点连接到主节点并开始与主节点协商时,许多事情都在后台进行。然而,与 MySQL 复制不同,您不能使用SHOW SLAVE STATUS来检查状态。事实上,发出那个命令不会得到任何结果。你是做什么的?

验证组复制状态

组复制重新设计了我们监控复制服务的方式。组复制向performance_schema数据库添加了几个视图,您可以使用这些视图来监控组复制。那里有很多信息,如果你有兴趣,你可以查看 https://dev.mysql.com/doc/refman/8.0/en/group-replication-monitoring.html 来了解更多关于视图及其包含的内容。

检查组复制状态需要针对performance_schema视图发出查询。replication_group_members视图(table)用于监控在当前视图中被跟踪的服务器实例的状态,换句话说,这些服务器实例是组的一部分,因此由成员资格服务进行跟踪。该信息在作为复制组成员的所有服务器实例之间共享,因此可以从任何成员处查询所有组成员的信息。清单 3-5 展示了该命令的实际应用。

$ mysql -uroot -h 127.0.0.1  --port=24802
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 34
Server version: 8.0.11 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> SHOW SLAVE STATUS\G

Empty set (0.00 sec)

mysql> SELECT * FROM performance_schema.replication_group_members \G
*************************** 1\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 21e6463c-4330-11e8-bc61-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24801
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: PRIMARY
MEMBER_VERSION: 8.0.11
*************************** 2\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 2854aecd-4330-11e8-abb6-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24802
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.11
*************************** 3\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 2ecd9f66-4330-11e8-90fe-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24803
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.11
*************************** 4\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 3525b7be-4330-11e8-80b1-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24804
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.11
4 rows in set (0.01 sec)

Listing 3-5Checking Group Replication Status

注意,我们运行了SHOW SLAVE STATUS命令,但没有得到任何回报。Drat。然而,当我们查询视图时,我们得到大量信息,包括组中每个成员的当前状态。有趣的是,您可以对组中的任何成员运行这个查询。这显示了组复制如何将元数据传播到组中的所有成员。

您还可以缩小输出范围,以获得更令人满意的视图,仅包括成员主机、端口、状态和角色,如下所示:

mysql> SELECT MEMBER_HOST, MEMBER_PORT, MEMBER_STATE, MEMBER_ROLE FROM performance_schema.replication_group_members;
+-------------+-------------+--------------+-------------+
| MEMBER_HOST | MEMBER_PORT | MEMBER_STATE | MEMBER_ROLE |
+-------------+-------------+--------------+-------------+
| oracle-pc   |       24801 | ONLINE       | PRIMARY     |
| oracle-pc   |       24802 | ONLINE       | SECONDARY   |
| oracle-pc   |       24803 | ONLINE       | SECONDARY   |
| oracle-pc   |       24804 | ONLINE       | SECONDARY   |
+-------------+-------------+--------------+-------------+

如果您只想定位主节点,可以对任何组成员使用以下查询:

SELECT member_id, member_host, member_port FROM performance_schema.global_status JOIN performance_schema.replication_group_members ON VARIABLE_VALUE=member_id WHERE VARIABLE_NAME="group_replication_primary_member";

当您对组中的任何一个成员执行此查询时,您将看到主服务器的 UUID:

+--------------------------------------+-------------+-------------+
| member_id                            | member_host | member_port |
+--------------------------------------+-------------+-------------+
| 21e6463c-4330-11e8-bc61-d4258b76e981 | oracle-pc   |       24801 |
+--------------------------------------+-------------+-------------+

您应该将这些 SQL 语句放在一个名为check_gr.sql的文件中,以便我们稍后可以在脚本中使用它来自动设置组复制。

现在我们已经运行了组复制,让我们创建一些数据。我们将使用与上一教程中相同的样本数据。但是,这一次,我们将在其中一个辅助节点上执行查询。你预计会发生什么?如果您从 MySQL 复制的角度考虑,您可能希望数据只出现在一个辅助服务器上。让我们看看会发生什么。以下代码在其中一个辅助节点上执行数据查询:

$ mysql -uroot -h 127.0.0.1  --port=24802
Welcome to the MySQL monitor.  Commands end with ; or \g.
...
mysql> CREATE DATABASE test;
ERROR 1290 (HY000): The MySQL server is running with the --super-read-only option so it cannot execute this statement

为什么会出现这个错误?原来,每个辅助都是以super-read-only开始的,解决了拥有“超级”能力的用户能够写入副本的老问题;super-read-only=OFF禁止任何人写入副本。因此,(从 MySQL 复制)向从属服务器发送写操作的常见问题得到了解决。万岁。使用super-read-only还表明我们正在单主模式下运行组复制(这是默认模式)。当我们在后面的章节中探索 InnoDB 集群的细微差别时,您将看到其他模式。

回到我们创建一些数据的测试,让我们在主服务器上运行相同的命令。下面显示了预期的结果:

$ mysql -uroot -h 127.0.0.1  --port=24801
...
mysql> CREATE DATABASE test;
Query OK, 1 row affected (0.03 sec)

mysql> USE test;
Database changed
mysql> CREATE TABLE test.t1 (id INT PRIMARY KEY, message TEXT NOT NULL);
Query OK, 0 rows affected (0.04 sec)

mysql> INSERT INTO test.t1 VALUES (1, 'Chuck is here');
Query OK, 1 row affected (0.01 sec)

在这里,我们看到数据是创建的。现在,检查第二个。下面显示了在辅助节点上运行查询的结果。如您所见,数据已经被复制。

$ mysql -uroot -h 127.0.0.1  --port=24802
...
mysql> SELECT * FROM test.t1;
+----+----------------+
| id | message        |
+----+----------------+
|  1 | Chuck is here. |
+----+----------------+
1 row in set (0.00 sec)

您应该将四个 SQL 命令— CREATE DATABASECREATE TABLEINSERTSELECT—放到一个名为sample_data.sql的文件中,我们将在稍后的脚本中使用它来自动设置组复制。

故障转移演示

现在我们有了一个工作组复制设置,让我们看看自动故障转移是如何工作的。如果您还没有运行前面的教程,并且想继续学习,请确保先运行前面的步骤。

自动故障切换是组复制的内置功能。通信机制确保监控主节点(在单主节点配置中)的活动,并且当主节点不再可用或出现严重问题时,该组可以决定终止主节点连接并选举新的主节点。

让我们看看这是如何工作的。回想一下前面的教程,我们在端口 24801 上运行初始主服务器。我们可以通过终止该服务器的 MySQL 进程来模拟失败。因为我们运行在 Linux 上,所以我们可以通过检查进程 ID 文件来确定进程 ID,MySQL 在数据目录中用机器名和文件扩展名.pid创建该文件。例如,教程中显示的主文件在data/primary/oracle-pc.pid中。该文件使用机器命名,并存储在数据目录中。您系统的文件将被不同地命名。下面演示了如何找到进程 ID 并停止它。请注意,您可能需要超级用户权限来终止该进程。

$ more ./data/primary/oracle-pc.pid
18019
$ sudo kill -9 18019

小费

在 Windows 上,您可以使用任务管理器终止该进程。

既然主服务器已经关闭,我们可以使用前面的查询来查看组的运行状况。回想一下,我们使用包含查询的check_gr.sql文件。清单 3-6 显示了查询的输出。

$ mysql -uroot -h 127.0.0.1 --port=24802 -e "source check_gr.sql"
*************************** 1\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 2854aecd-4330-11e8-abb6-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24802
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: PRIMARY
MEMBER_VERSION: 8.0.11
*************************** 2\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 2ecd9f66-4330-11e8-90fe-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24803
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.11
*************************** 3\. row ***************************
  CHANNEL_NAME: group_replication_applier

     MEMBER_ID: 3525b7be-4330-11e8-80b1-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24804
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.11
+--------------------------------------+-------------+-------------+
| member_id                            | member_host | member_port |
+--------------------------------------+-------------+-------------+
| 2854aecd-4330-11e8-abb6-d4258b76e981 | oracle-pc   |       24802 |
+--------------------------------------+-------------+-------------+

Listing 3-6Checking Group Health After the Primary Goes Down

请注意,该组已经自动选择了一个新的主服务器(在端口 24802 上),现在该组中只有三台服务器。因此,写入功能不会有任何损失。但是,回想一下前面的讨论,该组只能容忍这么多的故障,达到该限制后,该组将无法再成功进行故障切换,在这种情况下,该组可能不具备容错能力。清单 3-7 显示了第二台和第三台主机停止后同一组的状态。请注意,最后一个主节点的状态未知。

$ mysql -uroot -h 127.0.0.1 --port=24804 -e "source check_gr.sql"
*************************** 1\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 2ecd9f66-4330-11e8-90fe-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24803
  MEMBER_STATE: UNREACHABLE
   MEMBER_ROLE: PRIMARY

MEMBER_VERSION: 8.0.11
*************************** 2\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 3525b7be-4330-11e8-80b1-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24804
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.11
+--------------------------------------+-------------+-------------+
| member_id                            | member_host | member_port |
+--------------------------------------+-------------+-------------+
| 2ecd9f66-4330-11e8-90fe-d4258b76e981 | oracle-pc   |       24803 |
+--------------------------------------+-------------+-------------+

Listing 3-7State of the Group When No More Primary Servers Remain

既然我们已经看到了故障转移的实际应用,那么让我们来看看如何自动化本教程,以便我们可以进一步(并且更容易地)进行实验。

DevOps 脚本

如果您和我一样,想要使您的开发生活更容易,您可能会想要使用一个脚本来协调启动和停止一个实验组复制设置。幸运的是,在这种情况下我们可以这样做,因为我们使用的是本地实例,因此确切地知道应该使用什么样的服务器 id、IP 地址和端口。清单 3-8 是为 Linux 操作系统编写的脚本。

#!/bin/sh
# This file contains the setup commands used to start the tests for using MEB'
# with GR. Specifically, this file contains commands to start (4) mysqld
# instances and establish group replication among them.
#
# Note: All of the files reside in a local directory such as /home/cbell/# gr_linux. If you wish to run these commands, substitute the correct
#       directory in the commands.
#
# Note: Change the user to your user account or appropriate surrogate.
#
# The instances are primary (primary), secondary1, secondary2, secondary3 # (secondaries). Each is started with a corresponding config file, which is
# expected to be in the base directory. Each instance uses a different
# port but runs on the local machine.
#
# The steps include:
# 1) initialize the data directories
# 2) launch all mysqld instances
# 3) install the GR plugin
# 4) create the replication user
# 5) start GR
# 6) check GR
# 7) create initial data

echo ====== Step 1 of 7: INITIALIZE DATA DIRECTORIES ======

cd /home/cbell/gr_linux
rm -rf /home/cbell/gr_linux/data
mkdir /home/cbell/gr_linux/data
mysqld --no-defaults --user=cbell --initialize-insecure --basedir=/home/cbell/source/git/mysql-bug-staging/build/ --datadir=/home/cbell/gr_linux/data/primary
mysqld --no-defaults --user=cbell --initialize-insecure --basedir=/home/cbell/source/git/mysql-bug-staging/build/ --datadir=/home/cbell/gr_linux/data/secondary1
mysqld --no-defaults --user=cbell --initialize-insecure --basedir=/home/cbell/source/git/mysql-bug-staging/build/ --datadir=/home/cbell/gr_linux/data/secondary2
mysqld --no-defaults --user=cbell --initialize-insecure --basedir=/home/cbell/source/git/mysql-bug-staging/build/ --datadir=/home/cbell/gr_linux/data/secondary3

echo ====== Step 2 of 7: START ALL INSTANCES ======

cd /home/cbell/gr_linux
rm *.sock*
mysqld --defaults-file=/home/cbell/gr_linux/primary.cnf > primary_output.txt 2>&1 &
mysqld --defaults-file=/home/cbell/gr_linux/secondary1.cnf > secondary1_output.txt 2>&1 &
mysqld --defaults-file=/home/cbell/gr_linux/secondary2.cnf > secondary2_output.txt 2>&1 &
mysqld --defaults-file=/home/cbell/gr_linux/secondary3.cnf > secondary3_output.txt 2>&1 &

sleep 5

echo ====== Step 3 of 7: INSTALL THE GR PLUGIN ======

mysql -uroot -h 127.0.0.1 -e "INSTALL PLUGIN group_replication SONAME 'group_replication.so'" --port=24801
mysql -uroot -h 127.0.0.1 -e "INSTALL PLUGIN group_replication SONAME 'group_replication.so'" --port=24802
mysql -uroot -h 127.0.0.1 -e "INSTALL PLUGIN group_replication SONAME 'group_replication.so'" --port=24803
mysql -uroot -h 127.0.0.1 -e "INSTALL PLUGIN group_replication SONAME 'group_replication.so'" --port=24804

echo ====== Step 4 of 7: CREATE THE REPLICATION USER ======

mysql -uroot -h 127.0.0.1 -e "source create_rpl_user.sql" --port=24801
mysql -uroot -h 127.0.0.1 -e "source create_rpl_user.sql" --port=24802
mysql -uroot -h 127.0.0.1 -e "source create_rpl_user.sql" --port=24803
mysql -uroot -h 127.0.0.1 -e "source create_rpl_user.sql" --port=24804

echo ====== Step 5 of 7: START GR ======

mysql -uroot -h 127.0.0.1 -e "source start_gr_primary.sql" --port=24801
mysql -uroot -h 127.0.0.1 -e "source change_master.sql" --port=24802
mysql -uroot -h 127.0.0.1 -e "START GROUP_REPLICATION" --port=24802
mysql -uroot -h 127.0.0.1 -e "source change_master.sql" --port=24803
mysql -uroot -h 127.0.0.1 -e "START GROUP_REPLICATION" --port=24803
mysql -uroot -h 127.0.0.1 -e "source change_master.sql" --port=24804
mysql -uroot -h 127.0.0.1 -e "START GROUP_REPLICATION" --port=24804

echo ====== Step 6 of 7: CHECK GR ======

echo "Waiting for GR to start and reconcile..."
sleep 5

mysql -uroot -h 127.0.0.1 -e "source check_gr.sql" --port=24801

echo ====== Step 7 of 7: CREATE SOME DATA ======

sleep 30

mysql -uroot -h 127.0.0.1 -e "source sample_data.sql" --port=24801
mysql -uroot -h 127.0.0.1 -e "SELECT * FROM test.t1" --port=24801

echo ====== SETUP COMPLETE ======

Listing 3-8DevOps Script to Start Group Replication (Linux)

花一些时间来研究这个脚本。您应该能够通过一些小的改动使它适应您自己的平台。例如,如果您使用的不是默认的 MySQL 安装,您可能需要提供可执行文件的路径,或者类似地,您可能需要修改设置各种目录的配置文件。

当您执行这个脚本时,您将看到类似于清单 3-9 的输出。请注意,这与我们在教程中遇到的输出相同。

$ ./setup_gr.sh
====== Step 1 of 7: INITIALIZE DATA DIRECTORIES ======
2018-04-18T17:44:19.902106Z 0 [System] [MY-013169] [Server] /home/cbell/source/git/mysql-bug-staging/build/bin/mysqld (mysqld 8.0.11) initializing of server in progress as process 17828
2018-04-18T17:44:24.306322Z 4 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
2018-04-18T17:44:2    9.133321Z 0 [System] [MY-013170] [Server] /home/cbell/source/git/mysql-bug-staging/build/bin/mysqld (mysqld 8.0.11) initializing of server has completed
2018-04-18T17:44:30.615437Z 0 [System] [MY-013169] [Server] /home/cbell/source/git/mysql-bug-staging/build/bin/mysqld (mysqld 8.0.11) initializing of server in progress as process 17876
2018-04-18T17:44:35.248184Z 4 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
2018-04-18T17:44:40.080563Z 0 [System] [MY-013170] [Server] /home/cbell/source/git/mysql-bug-staging/build/bin/mysqld (mysqld 8.0.11) initializing of server has completed
2018-04-18T17:44:41.589196Z 0 [System] [MY-013169] [Server] /home/cbell/source/git/mysql-bug-staging/build/bin/mysqld (mysqld 8.0.11) initializing of server in progress as process 17925
2018-04-18T17:44:45.912417Z 4 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
2018-04-18T17:44:50.810105Z 0 [System] [MY-013170] [Server] /home/cbell/source/git/mysql-bug-staging/build/bin/mysqld (mysqld 8.0.11) initializing of server has completed
2018-04-18T17:44:52.246658Z 0 [System] [MY-013169] [Server] /home/cbell/source/git/mysql-bug-staging/build/bin/mysqld (mysqld 8.0.11) initializing of server in progress as process 17971
2018-04-18T17:44:56.781545Z 4 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
2018-04-18T17:45:01.652505Z 0 [System] [MY-013170] [Server] /home/cbell/source/git/mysql-bug-staging/build/bin/mysqld (mysqld 8.0.11) initializing of server has completed
====== Step 2 of 7: START ALL INSTANCES ======
rm: cannot remove '*.sock*': No such file or directory
====== Step 3 of 7: INSTALL THE GR PLUGIN ======
====== Step 4 of 7: CREATE THE REPLICATION USER ======
====== Step 5 of 7: START GR ======
====== Step 6 of 7: CHECK GR ======
Waiting for GR to start and reconcile...
*************************** 1\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 21e6463c-4330-11e8-bc61-d4258b76e981

   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24801
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: PRIMARY
MEMBER_VERSION: 8.0.11
*************************** 2\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 2854aecd-4330-11e8-abb6-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24802
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.11
*************************** 3\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 2ecd9f66-4330-11e8-90fe-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24803
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.11
*************************** 4\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: 3525b7be-4330-11e8-80b1-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24804
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.11
+--------------------------------------+-------------+-------------+
| member_id                            | member_host | member_port |
+--------------------------------------+-------------+-------------+
| 21e6463c-4330-11e8-bc61-d4258b76e981 | oracle-pc   |       24801 |
+--------------------------------------+-------------+-------------+
====== Step 7 of 7: CREATE SOME DATA ======
+----+----------------+
| id | message        |
+----+----------------+
|  1 | Chuck is here. |
+----+----------------+
====== SETUP COMPLETE ======

Listing 3-9Executing the setup_gr.sh Script (Linux)

还有一个相应的关机脚本,如清单 3-10 所示。你可以用这个来快速关闭群。

# This file contains commands to shut down and destroy the test GR cluster as
# set up by the commands in gr_meb_setup.txt.
#
# Note: the primary is expected to be p1, but it doesn't matter as the data
#       directories are destroyed at the end
#
# The steps include:
# 1) issue STOP GROUP_REPLICATION on all secondary servers
# 2) issue STOP GROUP_REPLICATION on the primary
# 3) shut down all mysqld instances
# 4) destroy the data directories

echo ====== Step 1 of 4: STOP GROUP REPLICATION ON SECONDARIES ======
mysql -uroot -h 127.0.0.1 --port=24802 -e "STOP GROUP_REPLICATION"
mysql -uroot -h 127.0.0.1 --port=24803 -e "STOP GROUP_REPLICATION"
mysql -uroot -h 127.0.0.1 --port=24804 -e "STOP GROUP_REPLICATION"

echo ====== Step 2 of 4: STOP GROUP REPLICATION ON PRIMARY ======

mysql -uroot -h 127.0.0.1 --port=24801 -e "STOP GROUP_REPLICATION"

echo ====== Step 3 of 4: SHUTDOWN mysqld INSTANCES ======

mysql -uroot -h 127.0.0.1 --port=24802 -e "SHUTDOWN"
mysql -uroot -h 127.0.0.1 --port=24803 -e "SHUTDOWN"
mysql -uroot -h 127.0.0.1 --port=24804 -e "SHUTDOWN"
mysql -uroot -h 127.0.0.1 --port=24801 -e "SHUTDOWN"

echo ====== Step 4 of 4: DESTROY THE DATA DIRECTORIES ======

cd /home/cbell/gr_linux
rm -rf data/

echo ====== SHUTDOWN COMPLETE ======

Listing 3-10DevOps Shutdown Script (Linux)

当您运行这个脚本时,您只会看到关于进度的粗略陈述。如果您看到连接错误,这通常是正常的。这只是意味着客户端无法连接。在为已经关闭的服务器运行shutdown的情况下,这是意料之中的。

$ ./shutdown.sh
====== Step 1 of 4: STOP GROUP REPLICATION ON SECONDARIES ======
====== Step 2 of 4: STOP GROUP REPLICATION ON PRIMARY ======
====== Step 3 of 4: SHUTDOWN mysqld INSTANCES ======
====== Step 4 of 4: DESTROY THE DATA DIRECTORIES ======
====== SHUTDOWN COMPLETE ======

小费

本书的源代码包含了在 Ubuntu、macOS 和 Windows 上使用这些脚本的例子。

关于设置 MySQL 组复制的简短教程到此结束。本节用最简洁的术语简要介绍了组复制。

摘要

不可否认,组复制是 MySQL 高可用性的一次飞跃。然而,正如你在本章的教程中看到的,设置起来并不简单。虽然那些熟悉 MySQL 复制的人会认为这个过程是一样的,只是多了几个额外的步骤,但那些不熟悉 MySQL 和高可用性的人可能会觉得学习曲线非常陡峭。

本章提供了组复制的基础知识,以及在最初和一两次故障期间设置和维护组的方法。如果你也认为一定有更好的方法,那就有了——而且我们就快成功了!

在下一章中,我们将暂时停止探索 InnoDB 集群的更多技术细节,并看看令人兴奋的新 MySQL 客户端:MySQL Shell。

四、MySQL Shell

可能有些被忽视的最大特性之一是新的 MySQL Shell。回想一下,MySQL Shell 是一种与 MySQL 服务器交互的新方式。与以前与服务器捆绑在一起的客户机相比,它有许多优势,最强大的是能够直接从 shell 中使用 Python 或 JavaScript。

在这一章中,你将更详细地探索 MySQL Shell。您将了解更多关于它的主要特性和选项,以及如何使用新的 shell 来交互式地执行脚本。正如您将看到的,MySQL Shell 是 MySQL 未来的另一个关键元素。

我建议在自己尝试 MySQL Shell 之前,至少通读一遍本章中的示例部分。所提供的信息将帮助您适应使用新的命令和连接,在您理解这些概念之前,这些命令和连接有时会有点混乱。

注意

我使用术语 shell 来指代 MySQL Shell 支持的特性或对象。我用 MySQL Shell 来指代产品本身。

使用 MySQL Shell

MySQL Shell 是 MySQL 产品组合中令人激动的新成员。MySQL Shell 代表了第一个连接到 MySQL 并与之交互的现代高级客户端。shell 可以用作脚本环境,用于开发处理数据的新工具和应用。酷!图 4-1 展示了一个启动 MySQL Shell 的例子。请注意显示 MySQL 徽标、连接信息和模式的漂亮提示符。很好!

img/460910_1_En_4_Fig1_HTML.jpg

图 4-1

MySQL Shell

注意

与需要服务器连接才能启动的旧客户端不同,当您在没有指定服务器连接的情况下启动 shell 时,shell 将会运行,但不会连接到服务器。您必须使用\connect shell 命令来连接到服务器。

回想一下第 2 章的内容,MySQL Shell 被设计成使用新的 X 协议通过 X 插件与服务器通信。然而,shell 也可以通过使用旧的协议连接到服务器,尽管在脚本模式中功能有限。因此,shell 允许您处理关系文档(SQL)或 JSON 文档(NoSQL),或者两者都处理。

SQL 模式的加入为学习如何使用脚本管理数据提供了一个很好的基础。您可以继续使用您的 SQL 命令(或批处理),直到您将它们转换为 JavaScript 或 Python。此外,您可以使用这两者来确保您的迁移是完整的。

以下各节从较高的层面介绍了 shell 的主要特性。有关 MySQL Shell 的更多信息,请参见在线 MySQL 参考手册( https://dev.mysql.com/doc/mysql-shell-excerpt/8.0/en/ )中的“MySQL Shell 用户指南”一节。

Shell 命令

与最初的 MySQL 客户端一样,一些特殊的命令控制应用本身,而不是与数据交互(通过 SQL 或 X DevAPI)。要执行 shell 命令,请发出带斜线(\)的命令。例如,\help打印所有 shell 命令的帮助。表 4-1 列出了一些更常用的 shell 命令。

表 4-1

Shell 命令

|

命令

|

捷径

|

描述

|
| --- | --- | --- |
| \ |   | 开始多行输入(仅限 SQL 模式) |
| \connect | (\c) | 连接到服务器 |
| \help | (\?,\h) | 打印帮助文本 |
| \js |   | 切换到 JavaScript 模式 |
| \nowarnings | (\w) | 不显示警告 |
| \py |   | 切换到 Python 模式 |
| \quit | (\q,\exit) | 放弃 |
| \source | (\.) | 执行指定的脚本文件 |
| \sql |   | 切换到 SQL 模式 |
| \status | (\s) | 打印有关连接的信息 |
| \use | (\u) | 设置会话的模式 |
| \warnings | (\W) | 在每个语句后显示警告 |

注意,您可以使用\sql\js\py shell 命令来动态切换模式。这使得处理 SQL 和 NoSQL 数据更加容易,因为您不必退出应用来切换模式。此外,即使使用了启动选项来设置模式,也可以使用这些 shell 命令。

小费

要获得任何 shell 命令的帮助,请使用\help命令。例如,要了解更多关于\connect命令的信息,请输入\help connect

最后,注意您退出 shell 的方式(\q\quit)。如果您像在旧客户端中习惯的那样键入quit,shell 将根据您所处的模式做出不同的响应。以下是每种模式下发生的情况的示例:

MySQL  SQL > quit;
ERROR: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'quit' at line 1
 MySQL  SQL > \js
Switching to JavaScript mode...
 MySQL  JS > quit
ReferenceError: quit is not defined
 MySQL  JS > \py
Switching to Python mode...
 MySQL  Py > quit
Use quit() or Ctrl-D (i.e. EOF) to exit
 MySQL  Py > \q
Bye!

如果您习惯了旧的 MySQL 客户端,并且不小心使用了旧的客户端命令,您可能会看到类似的奇怪现象,但是只需要经常使用它就可以提醒您要使用的正确命令。现在,让我们看看 shell 的启动选项。

选择

可以使用几个控制模式、连接、行为等的启动选项来启动 shell。表 4-2 介绍了一些你可能想要使用的更常用的选项。您将在后面的部分中看到更多关于连接选项的内容。

表 4-2

常见的 MySQL Shell 选项

|

[计]选项

|

描述

|
| --- | --- |
| -f, --file=file | 用于执行的流程文件。 |
| -e, --execute=<cmd> | 执行命令并退出。 |
| --uri | 通过统一资源标识符(URI)连接。 |
| -h, --host=<value> | 用于连接的主机名。 |
| -P, --port=# | 用于连接的端口号。 |
| -S, --socket=sock | UNIX 中用于连接的套接字名称或 Windows 中的命名管道名称(仅限经典会话)。 |
| -u, --dbuser=<value> | 用于连接的用户。 |
| --user=<value> | dbuser的别名。 |
| --dbpassword=<value> | 连接到服务器时使用的密码。 |
| --password=<value> | dbpassword的别名。 |
| -p | 请求密码提示来设置密码。 |
| -D --schema=<value> | 要使用的架构。 |
| --database=<value> | --schema的别名。 |
| --sql | 以 SQL 模式启动。 |
| --sqlc | 使用经典会话在 SQL 模式下启动。 |
| --sqlx | 使用 X 协议会话在 SQL 模式下启动。 |
| --js | 以 JavaScript 模式启动。 |
| --py | 以 Python 模式启动。 |
| --json | 以 JSON 格式生成输出。 |
| --table | 以表格格式生成输出(默认为交互模式)。 |
| -i, --interactive[=full] | 为了在批处理模式下使用,它强制模拟交互模式处理。批处理中的每一行都像在交互模式下一样进行处理。 |
| --log-level=value | 日志级别。值必须是 1 到 8 之间的整数或[无、内部、错误、警告、信息、调试、调试 2、调试 3]中的任何一个。 |
| --mx --mysqlx | 创建一个 X 协议会话(简称为“会话”)。 |
| --mc --mysql | 创建经典(旧协议)会话。 |
| --ma | 创建一个带有自动协议选择的会话。 |
| --nw, --no-wizard | 禁用执行脚本的向导模式(非交互式)。 |
| --ssl-mode | 为连接启用 SSL(使用其他标志自动启用)。 |
| --ssl-key=name | PEM 格式的 X509 密钥。 |
| --ssl-cert=name | PEM 格式的 X509 证书。 |
| --ssl-ca=name | PEM 格式的 CA 文件(查看 OpenSSL 文档)。 |
| --ssl-capath=dir | CA 目录。 |
| --ssl-cipher=name | 要使用的 SSL 密码。 |
| --ssl-crl=name | 证书吊销列表。 |
| --ssl-crlpath=dir | 证书吊销列表路径。 |
| --tls-version=version | 要使用的 TLS 版本。允许值为 TLSv1、TLSv1.1。 |
| --auth-method=method | 要使用的身份验证方法。 |
| --dba=enableXProtocol | 在连接到的服务器中启用 X 协议。必须和--mysql一起使用。 |

请注意,有些选项的别名与原始客户端的用途相同。如果您有启动客户机来执行操作的脚本,这使得切换到 shell 变得更容易一些。还要注意使用安全套接字层(SSL)连接的一组选项。其中大部分都是不言自明的,你以前已经见过几个了。现在让我们看看可用的会话和连接以及如何使用它们。要获得完整的选项列表,请使用--help选项执行 shell,如清单 4-1 所示。

$ mysqlsh --help
MySQL Shell 8.0.11

Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Usage: mysqlsh [OPTIONS] [URI]
       mysqlsh [OPTIONS] [URI] -f <path> [script args...]
       mysqlsh [OPTIONS] [URI] --dba [command]
       mysqlsh [OPTIONS] [URI] --cluster

  -?, --help                  Display this help and exit.
  -e, --execute=<cmd>         Execute command and quit.
  -f, --file=file             Process file.
  --uri=value                 Connect to Uniform Resource Identifier. Format:
                              [user[:pass]@]host[:port][/db]
  -h, --host=name             Connect to host.
  -P, --port=#                Port number to use for connection.
  -S, --socket=sock           Socket name to use in UNIX, pipe name to use in
                              Windows (only classic sessions).
  -u, --dbuser=name           User for the connection to the server.
  --user=name                 see above
  -p, --password[=name]       Password to use when connecting to server.
  --dbpassword[=name]         see above
  -p                          Request password prompt to set the password
  -D, --schema=name           Schema to use.
  --database=name             see above
  --recreate-schema           Drop and recreate the specified schema.Schema
                              will be deleted if it exists!
  -mx, --mysqlx               Uses connection data to create Creating an X
                              protocol session

.
  -mc, --mysql                Uses connection data to create a Classic Session.
  -ma                         Uses the connection data to create the session
                              withautomatic protocol detection.
...

Listing 4-1Getting Help for MySQL Shell

小费

在 Windows 上,MySQL Shell 显示为一个应用,但是您可以将C:\Program Files\MySQL\MySQL Shell 8.0\bin添加到您的路径中,并从命令窗口执行它。

会话和模式

与最初的客户机以及实际上大多数 MySQL 客户机应用一样,您需要连接到 MySQL 服务器,以便可以运行命令。MySQL Shell 支持多种连接 MySQL 服务器的方式和多种与服务器交互的选项(称为会话)。在一个会话中,您可以更改 shell 接受命令的方式(称为模式,以包含 SQL、JavaScript 或 Python 命令。

考虑到使用服务器的所有不同的新概念,那些初学使用 shell 的人可能会发现其中的细微差别,甚至有时会感到困惑。事实上,在线参考手册和各种博客及其他报告有时会交替使用模式会话,但正如您将看到的,它们是不同的(无论多么微妙)。下面几节阐明了每个主要概念,包括会话、模式和连接,以便您可以更快地适应新方法。我首先用简单的例子介绍概念,然后用详细的例子讨论如何建立联系。让我们从查看可用的会话对象开始。

会话对象

关于会话 s,首先要理解的是,会话是到单个服务器的连接。第二件要理解的事情是,每个会话都可以从两个会话对象中的一个开始。会话是到服务器的连接(定义了所有参数),会话对象是 shell 用来以几种方式之一与服务器交互的对象。更具体地说,MySQL Shell 会话对象定义了您如何与服务器交互,包括支持哪些模式,甚至 Shell 如何与服务器通信。shell 支持如下两个会话对象:

  • 会话:X 协议会话用于应用开发,支持 JavaScript、Python、SQL 模式。通常用于开发脚本或执行脚本。要使用该选项启动 shell,请使用--mx ( --mysqlx)选项。

  • 经典会话:使用旧的服务器通信协议,对 DevAPI 的支持有限。对没有 X 插件或不支持 X 协议的旧服务器使用这种模式。通常用于旧服务器的 SQL 模式。要使用该选项启动 shell,请使用--mc ( --mysqlc)选项。

注意

经典会话仅在 MySQL Shell 中可用。它不是 X DevAPI 的一部分。通过 X DevAPI,只有通过 X 协议的会话连接是可用的。

当您使用\connect shell 命令时,您可以通过指定-mc用于经典会话、-mx用于 X 协议会话或-ma用于自动协议选择来指定要使用的会话对象(协议)。下面依次展示了其中的每一个。注意<URI>指定了一个统一的资源标识符。

  • \connect -mx <URI>:使用 X 协议(会话)。

  • \connect -mc <URI>:使用经典协议(经典会话)。

  • \connect -ma <URI>:使用自动协议选择。

回想一下,会话在广义上与连接同义。但是,会话不仅仅是一个连接,因为用于建立连接的所有设置(包括会话对象)以及用于与服务器通信的协议都包括在内。因此,我们有时会遇到术语协议来描述会话。在后面的部分中,您将看到更多使用会话的示例。

支持的模式

shell 支持三种模式(也称为语言支持或简称为活动语言 ): SQL、JavaScript 和 Python。回想一下,我们可以通过使用 shell 命令来启动这些模式中的任何一种。你可以随时切换模式(语言),每次都不会断线。下面列出了三种模式以及如何切换到每种模式。

  • \sql:切换到 SQL 语言。

  • \js:切换到 JavaScript 语言(默认模式)。

  • \py:切换到 Python 语言。

现在您已经了解了会话、会话对象和模式,接下来您可以看看如何连接 MySQL 服务器。

连接

在 shell 中建立连接可能需要一些时间来适应与最初的 MySQL 客户端不同的工作方式,最初的 MySQL 客户端需要在命令行中使用几个选项。您可以使用特殊格式的 URI 字符串,或者通过名称使用单个选项连接到服务器(像旧客户端一样)。也支持 SSL 连接。可以通过启动选项、shell 命令和脚本来建立连接。但是,所有连接都需要使用密码。除非您另外声明,否则如果没有给出密码,shell 将提示您输入密码。

注意

如果您想使用没有密码的连接(不推荐),您必须使用--password选项,或者,如果使用 URI,包括一个额外的冒号来代替密码。

下面的部分没有讨论所有可用的连接方式和选项,而是给出了每种连接方式的一个例子。

使用 URI

URI 是使用格式<dbuser>[:<dbpassword>]@host[:<por>t][/<schema>/]的特殊字符串,其中<>表示各种参数的字符串值。请注意,口令、端口和模式是可选的,但用户和主机是必需的。在这种情况下,模式是您希望在连接时使用的默认模式(数据库)。X 协议的默认端口是 33060。要在启动 shell 时使用命令行上的 URI 连接到服务器,请使用--uri选项指定它,如下所示:

$ mysqlsh --uri root:secret@localhost:33060

shell 假定所有连接都需要密码,如果没有提供密码,它将提示输入密码。清单 4-2 显示了没有密码的相同连接。注意 shell 是如何提示输入密码的。

$ mysqlsh --uri root@localhost:33060/world_x
Creating a session to 'root@localhost:33060/world_x'
Enter password:
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 13 (X protocol)
Server version: 8.0.11 MySQL Community Server (GPL)
Default schema `world_x` accessible through db.
MySQL Shell 8.0.11

Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type '\help' or '\?' for help; '\quit' to exit.

 MySQL  localhost:33060+  world_x  JS >

Listing 4-2Connecting with a URI

注意,我们还在 URI 中用/schema选项指定了默认模式(world_x)。world_x数据库是一个示例数据库,您可以从 https://dev.mysql.com/doc/index-other.html 下载。您将在后面的 MySQL Shell 教程中安装这个数据库。

使用单个选项

您还可以通过使用单独的选项在 shell 命令行上指定连接。可用的连接选项如表 4-1 所示。为了向后兼容(并使向 MySQL Shell 的过渡更容易),Shell 还支持用--user代替--dbuser,用--password代替--dbpassword,用--database代替--schema。清单 4-3 展示了如何使用单独的选项连接到 MySQL 服务器。注意,我用--py选项将模式(语言)改为 Python。

$ mysqlsh --dbuser root --host localhost --port 33060 --schema world_x --py -mx
Creating an X protocol session to 'root@localhost:33060/world_x'
Enter password:
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 14 (X protocol)
Server version: 8.0.11 MySQL Community Server (GPL)
Default schema `world_x` accessible through db.
MySQL Shell 8.0.11

Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective

owners.

Type '\help' or '\?' for help; '\quit' to exit.

 MySQL  localhost:33060+  world_x  Py >

Listing 4-3Connecting by Using Individual Options

在脚本中使用连接

如果您计划使用 shell 来创建脚本或者仅仅作为一个原型工具,那么您还会希望在脚本中使用会话。在这种情况下,我们将创建一个变量来包含提取后的会话。以这种方式创建的会话被称为全局会话,因为在它被创建之后,它可用于任何模式。然而,根据我们使用的会话对象(回想一下,这是经典或 X 协议),我们将使用mysqlx对象的不同方法来创建 X 或经典会话。我们对 X 协议会话对象使用getSession()方法,对经典会话对象使用getClassicSession()方法。

下面演示了在 JavaScript 中获取 X 协议会话对象。请注意,我在 URI 中将密码指定为方法参数:

 MySQL  JS > var js_session = mysqlx.getSession('root@localhost:33060', 'secret')
 MySQL  JS > print(js_session)
<Session:root@localhost:33060>
The following demonstrates getting a Classic session object in JavaScript.
 MySQL  JS > var js_session = mysql.getClassicSession('root@localhost:3306', 'secret')
 MySQL  JS > print(js_session)
<ClassicSession:root@localhost:3306>

使用 SSL 连接

您还可以创建 SSL 连接,以便安全地连接到您的服务器。要使用 SSL,您必须将服务器配置为使用 SSL。要在运行 MySQL 的同一台机器上使用 SSL,可以使用--ssl-mode=REQUIRED选项。您也可以指定 SSL 选项,如表 4-1 所示。您可以使用命令行选项在命令行上指定它们,或者将其作为\connect shell 命令的扩展。下面显示了如何通过 SSL 和命令行选项连接到服务器:

$ mysqlsh -uroot -h127.0.0.1 --port=33060 --ssl-mode=REQUIRED

小费

有关加密连接( https://dev.mysql.com/doc/refman/8.0/en/encrypted-connections.html )的更多详细信息,请参见《MySQL Shell 参考手册》中的“使用加密连接”一节。

现在您已经知道了如何连接到我们的服务器,让我们回顾一下如何设置和安装 shell,更重要的是,如何确保 X 插件设置正确。

安装 MySQL Shell

在 MySQL 的当前版本中,MySQL Shell 是作为独立于服务器的产品发布的。在除 Windows 之外的所有平台上,您可以单独安装它。本节演示了在 Windows 上安装 MySQL Shell 所需的步骤。要在其他平台上安装,请访问 http://dev.mysql.com/downloads/shell/ 并为您的平台选择最新版本和软件包,然后安装 shell。

注意

shell 中的服务器配置脚本需要 Python 2.7。如果要在另一个平台上安装 shell,必须确保要在 InnoDB 集群中使用的所有服务器上都安装了 Python 2.7。

使用 MySQL 安装程序安装 MySQL Shell 遵循与安装 MySQL Server 相同的模式。但是,由于安装程序已经在系统中,我们只需再次启动它,并添加我们想要的 MySQL 产品(如 MySQL Shell)。当您启动安装程序时,会出现一个欢迎面板,其中包含已安装产品的列表。图 4-2 显示了服务器安装后 MySQL 安装程序的欢迎面板。

img/460910_1_En_4_Fig2_HTML.jpg

图 4-2

安装程序欢迎面板-安装后(MySQL 安装程序)

请注意,我们可以选择添加(添加… )新产品,修改(修改… )已安装产品的安装或配置,从目录(安装程序)中升级(升级… )具有较新版本的已安装产品,或者移除已安装产品(移除… )。请注意,还有一个名为 Catalog… 的按钮,它允许您更新安装程序中的产品目录。这允许您用新版本更新产品。我们将在后面的步骤中看到如何做到这一点。

对于本教程,我们希望安装 MySQL Shell,因此单击 Add 继续。然后你会看到一个产品选择面板,如图 4-3 所示。导航左边的树找到 MySQL Shell,选择它,然后单击绿色箭头将其添加到右边的列表中。添加后,单击“下一步”按钮继续。

img/460910_1_En_4_Fig3_HTML.jpg

图 4-3

选择产品和功能(MySQL 安装程序)

下一个屏幕是一个安装摘要,允许您确认列出的安装产品是否正确。此面板还指示状态,以便您可以观察安装进度。图 4-4 为安装面板。当您准备好开始安装所选产品时,请单击执行。

img/460910_1_En_4_Fig4_HTML.jpg

图 4-4

安装对话框–暂存(MySQL 安装程序)

安装开始后,你会看到每个产品的进度,如图 4-5 所示。

img/460910_1_En_4_Fig5_HTML.jpg

图 4-5

安装对话框–安装进度(MySQL 安装程序)

所有产品安装完成后,安装面板将显示所有安装的状态为完成,并将底部的按钮改为显示下一步,如图 4-6 所示。准备好后,单击下一步。

img/460910_1_En_4_Fig6_HTML.jpg

图 4-6

安装对话框–安装完成(MySQL 安装程序)

确认无误后,点击下一步按钮进入最后一个面板,如图 4-7 所示。您可以单击“完成”按钮来完成安装并关闭安装程序。

img/460910_1_En_4_Fig7_HTML.jpg

图 4-7

安装完成(MySQL 安装程序)

MySQL 安装程序提供的一个非常好的附加功能是能够更新要安装的产品目录。如果有更新的包可用,MySQL 安装程序会提供更新它们的选项。回想一下欢迎面板,如果我们愿意,我们可以手动这样做。但是,在安装结束时,安装程序会提示您进行更新,如图 4-8 所示。如果要连接到互联网并更新目录,请单击执行。如果没有,您可以勾选此时不更新复选框并继续。让我们看看目录是如何更新的。

img/460910_1_En_4_Fig9_HTML.jpg

图 4-9

更新目录-完成(MySQL 安装程序)

img/460910_1_En_4_Fig8_HTML.jpg

图 4-8

更新目录-初始化(MySQL 安装程序)

安装程序将连接到互联网,下载目录更改,然后将它们集成到安装程序中,以便进行下一次安装操作。请注意,这与升级现有安装不同;它只是更新安装程序目录。更新完成后,单击“下一步”完成升级。

img/460910_1_En_4_Fig10_HTML.jpg

图 4-10

安装程序欢迎面板-安装后(MySQL 安装程序)

当所有操作完成后,安装程序将返回到欢迎面板,显示所有已安装产品的列表,如图 4-10 所示。完成后,您可以关闭安装程序,或者添加、修改、升级或移除其他产品。

现在已经安装了 MySQL Shell,我们需要配置 X 插件。

启用 X 插件

如果您在系统上安装了 MySQL,那么您已经安装了 X 插件。如果您有 8.0.11 或更高版本,X 插件已经安装并启用。但是,默认情况下,旧的安装包不会设置或启用 X 插件。因此,您可能需要启用插件来使用 shell 连接到您的服务器。尽管您仍然可以使用 shell 通过经典会话对象进行连接,但是在启用 X 插件之前,您将无法使用 X 协议会话对象。如果需要启用 X 插件,至少可以从其他两种方法中选择:可以使用新的 MySQL Shell,也可以使用旧的客户端。下面演示了每个选项。

小费

如果在新安装的 MySQL 上连接到 MySQL 服务器有问题,请确保启用 X 插件,如本节所示。

使用 MySQL Shell 启用 X 插件

要通过 MySQL Shell 启用 X 插件,通过使用用户和主机的单独选项以及指定--mysql--dba enableXProtocol选项来启动经典会话,如下所示。我们使用经典的会话对象,因为我们还没有启用 X 协议。

$ mysqlsh -uroot -hlocalhost --mysql --dba enableXProtocol
Enter password: ****
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 13
Server version: 8.0.11 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.
enableXProtocol: Installing plugin mysqlx...
enableXProtocol: done

使用 MySQL 客户端启用 X 插件

要通过旧的 MySQL 客户端启用 X 插件,您必须连接到服务器并手动安装插件。没有新的魔法命令选项来打开它。这涉及到使用INSTALL PLUGIN SQL 命令,如清单 4-4 所示。

$ mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 343
Server version: 8.0.11 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> INSTALL PLUGIN mysqlx SONAME 'mysqlx.so';
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW PLUGINS \G
*************************** 1\. row ***************************
   Name: keyring_file
 Status: ACTIVE
   Type: KEYRING
Library: keyring_file.so
License: GPL
...
*************************** 43\. row ***************************
   Name: mysqlx
 Status: ACTIVE
   Type: DAEMON
Library: mysqlx.so
License: GPL
43 rows in set (0.00 sec)

Listing 4-4Enabling the X Plugin Using the MySQL Client

注意,我使用了SHOW PLUGINS SQL 命令来列出该命令前后安装的插件。为了清楚起见,我省略了一些冗长的输出。

有趣的是,您也可以使用如下的UNINSTALL PLUGIN SQL 命令卸载插件。如果您需要通过使用 X 协议来诊断连接,或者想要通过仅使用经典会话对象来测试 MySQL Shell 的脚本,这可能会很有帮助。

mysql> UNINSTALL PLUGIN mysqlx;
Query OK, 0 rows affected (0.80 sec)

现在,让我们通过演示 MySQL Shell 的基本特性来看看它的运行情况。

辅导的

以下部分演示了如何使用 MySQL Shell。该示例使用了world_x数据库,旨在提供一个概述,而不是深入探讨。在第 5 章中,你会看到 MySQL Shell 与 InnoDB 集群一起使用。如果您对 MySQL 文档存储或 JSON 数据一无所知,不要绝望;本教程旨在演示如何使用 MySQL Shell,因为 Shell 旨在用于 JSON 文档,所以我们将这样做。然而,在第 5 章中,您将看到 AdminAPI 用于配置 InnoDB 集群。

本教程的目标是在world_x数据库中插入新数据,然后执行搜索以检索满足包含新数据的标准的行。我将使用一个关系表来说明这些概念,因为这对我们这些熟悉“普通”数据库操作的人来说更容易。

在开始我们的旅程之前,让我们花点时间安装我们将需要的示例数据库,Oracle 的world_x示例 MySQL 数据库。

安装示例数据库

Oracle 提供了几个示例数据库,供您在测试和开发应用时使用。样本数据库可以从 http://dev.mysql.com/doc/index-other.html 下载。我们想要使用的示例数据库被命名为world_x,以表明它包含 JSON 文档,并打算用 X DevAPI、shell 等进行测试。继续前进,导航到该页面并下载数据库。示例数据库包含几个关系表(countrycitycountrylanguage)以及一个集合(countryinfo)。

下载完文件后,解压缩并记下文件的位置。当您导入它时,您将需要它。接下来,启动 MySQL Shell 并连接到您的服务器。使用\sql shell 命令切换到 SQL 模式,然后使用\source shell 命令读取world_x.sql文件并处理其所有语句。

我们还将使用SHOW DATABASES命令列出服务器上的所有数据库(以确保添加了新的数据库)。我们使用USE命令选择默认数据库,使用SHOW TABLES命令查看world_x数据库中的表。最后,我们还将通过使用EXPLAIN命令来查看city表的模式(布局)。如果您自己运行这些命令,您可能会用SHOW DATABASES命令得到一个不同的数据库列表。这没关系,因为您的数据库可能比教程中使用的机器少(或多)。

清单 4-5 显示了您应该看到的命令和响应的摘录。我在输出中突出显示了命令和一行,以表明这个 world 数据库确实允许在一个表中存储 JSON 文档。

 MySQL  JS > \connect root@localhost:33060
Creating a session to 'root@localhost:33060'
Enter password:
Your MySQL connection id is 9 (X protocol)
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.
 MySQL  localhost:33060+ ssl  JS > \sql
Switching to SQL mode... Commands end with ;
 MySQL  localhost:33060+ ssl  SQL > \source /Users/cbell/Downloads/world_x-db/world_x.sql
...
Query OK, 0 rows affected (0.00 sec)

MySQL  localhost:33060+ ssl  SQL > SHOW DATABASES;

+--------------------+
| Database           |
+--------------------+
| animals            |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| test               |
| world_x            |
+--------------------+
7 rows in set (0.00 sec)
 MySQL  localhost:33060+ ssl  SQL > USE world_x;
Query OK, 0 rows affected (0.00 sec)
 MySQL  localhost:33060+ ssl  SQL > SHOW TABLES;
+-------------------+
| Tables_in_world_x |
+-------------------+
| city              |
| country           |
| countryinfo       |
| countrylanguage   |
+-------------------+
4 rows in set (0.00 sec)

MySQL  localhost:33060+ ssl  SQL > EXPLAIN city;

+-------------+----------+------+-----+---------+----------------+
| Field       | Type     | Null | Key | Default | Extra          |
+-------------+----------+------+-----+---------+----------------+
| ID          | int(11)  | NO   | PRI | NULL    | auto_increment |
| Name        | char(35) | NO   |     |         |                |
| CountryCode | char(3)  | NO   |     |         |                |
| District    | char(20) | NO   |     |         |                |
| Info        | json     | YES  |     | NULL    |                |
+-------------+----------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

Listing 4-5Installing the world_x Database (SQL Mode)

注意,\source shell 命令是一种加载文件并批量执行命令的方式。这是一种重放常用命令序列的流行方法,它也适用于 JavaScript 和 Python 命令。

小费

如果文件的路径中有空格,应该用双引号将路径括起来。

您还可以使用命令行上的--recreate-schema选项来安装示例数据库,如下所示。请注意,如果数据库已经存在,这将删除并重新创建数据库。这是批处理运行 SQL 命令的另一个例子。

$ mysqlsh -uroot -hlocalhost --sql --recreate-schema --schema=world_x < ~/Downloads/world_x-db/world_x.sql
Enter password:
Recreating schema world_x...

当然,您可以使用 similar source 命令在旧客户机上安装 sample 数据库,但是这有什么意思呢?

现在,让我们看看如何插入数据。

插入数据

我们希望通过在每一行中添加一个 JSON 文档来将两行插入到city表中,然后只从表中读取那些有额外数据的行。更具体地说,我们将向表中添加一个名胜古迹列表,以便我们稍后可以询问哪些城市有名胜古迹。你可以把它当作一种方式,来添加你自己对那些你觉得有趣并会推荐给他人的城市中你去过的地方的评论。

因为本练习是一个示例,所以您还将看到如何删除我们添加的数据,以便我们将数据库恢复到其原始状态。如果您计划按照这些示例进行操作,以便完成一个示例不会影响下一个示例的尝试,那么这样做也是有帮助的。

接下来,我们插入一些数据。我们将在表中插入两行:我最近去过的每个城市一行(北卡罗来纳州的夏洛特和佛罗里达州的代托纳)。在这一步中,我们将使用INSERT SQL 命令来插入数据。回想一下前面的内容,我们需要仔细格式化 JSON 文档,这样我们就不会遇到错误。具体来说,我们希望添加包括姓名、国家代码和地区的结构化数据,但是我们还希望添加一个 JSON 文档,其中包含人口和名胜古迹的列表(数组)。下面显示了我们将用来插入行的每个命令:

INSERT INTO world_x.city (Name, CountryCode, District, Info) VALUES ('Charlotte', 'USA', 'North Carolina', '{"Population": 792862, "Places_of_interest": [{"name": "NASCAR Hall of Fame"}, {"name": "Charlotte Motor Speedway"}]}');

INSERT INTO world_x.city (Name, CountryCode, District, Info) VALUES ('Daytona', 'USA', 'Florida', '{"Population": 590280, "Places_of_interest": [{"name": "Daytona Beach"}, {"name": "Motorsports Hall of Fame of America"}, {"name": "Daytona Motor Speedway"}]}');

警告

不要在 JSON 文档的键名中使用空格。SQL 函数无法正确识别包含空格的键。

尽管这看起来有点混乱(确实如此),但是如果您仔细阅读这些语句,您会看到 JSON 文档被编码为一个字符串。例如,第一次插入的 JSON 文档的格式良好的版本如下所示。很明显,这更容易阅读。您可以使用如下格式输入语句,但是显示的结果没有额外的格式。

注意,我们保留了表中其他行的 population 键(选择一些并查看),我们还添加了一个名为Places_of_interest的数组来列出我们可能想去的地方。

{
  "Population": 792862,
  "Places_of_interest": [
    {
      "name": "NASCAR Hall of Fame"
    },
    {
      "name": "Charlotte Motor Speedway"
    }
  ]
}

注意,为了简洁起见,我从示例中截断了表格格式行(虚线)。

选择数据

现在,让我们看看如果使用一个SELECT SQL 语句,数据会是什么样子。在这种情况下,我们将只按城市名选择两行,因为它们在表中是唯一的。以下是调查结果的摘录:

MySQL  localhost:33060+ ssl  SQL > SELECT * FROM city WHERE Name in ('Charlotte', 'Daytona') \G
*************************** 1\. row ***************************
         ID: 3818
       Name: Charlotte
CountryCode: USA
   District: North Carolina
       Info: {"Population": 540828}
*************************** 2\. row ***************************
         ID: 4080
       Name: Charlotte
CountryCode: USA
   District: North Carolina
       Info: {"Population": 792862, "Places_of_interest": [{"name": "NASCAR Hall of Fame"}, {"name": "Charlotte Motor Speedway"}]}
*************************** 3\. row ***************************
         ID: 4081
       Name: Daytona
CountryCode: USA
   District: Florida
       Info: {"Population": 590280, "Places_of_interest": [{"name": "Daytona Beach"}, {"name": "Motorsports Hall of Fame of America"}, {"name": "Daytona Motor Speedway"}]}

很有意思,但是没有回答我们想问的问题:哪些城市有名胜古迹?为此,我们需要使用一些为 JSON 数据类型设计的特殊函数。所有函数都以名称JSON_*开头。让我们依次来看每一个,从在 JSON 文档中搜索具有特定键的行开始。在这种情况下,我们选择有兴趣地点的行的所有数据。

为了确定 JSON 文档是否有特定的键,我们使用了JSON_CONTAINS_PATH()函数。回想一下,路径是文档中键的解析。在这种情况下,我们想知道 JSON 文档是否包含Places_of_interest的路径。因为函数在没有匹配时返回 0,在至少有一个匹配时返回 1,所以我们检查它是否等于 1。你可以省略等式,但是在试验新的特性和命令时最好是学究式的。我们还使用all选项告诉函数返回所有匹配(值),而one只返回第一个匹配。你也可以使用稍微正确一点的IS NOT NULL比较。

MySQL  localhost:33060+ ssl  SQL > SELECT * FROM city WHERE JSON_CONTAINS_PATH(info, 'all', '$.Places_of_interest') = 1 \G
*************************** 1\. row ***************************
         ID: 4080
       Name: Charlotte
CountryCode: USA
   District: North Carolina
       Info: {"Population": 792862, "Places_of_interest": [{"name": "NASCAR Hall of Fame"}, {"name": "Charlotte Motor Speedway"}]}
*************************** 2\. row ***************************
         ID: 4081
       Name: Daytona
CountryCode: USA
   District: Florida
       Info: {"Population": 590280, "Places_of_interest": [{"name": "Daytona Beach"}, {"name": "Motorsports Hall of Fame of America"}, {"name": "Daytona Motor Speedway"}]}
2 rows in set (0.00 sec)

现在,假设我们只想查看那些感兴趣的地方,而不是整个 JSON 文档。在这种情况下,我们需要使用JSON_EXTRACT()函数从文档中提取值。具体来说,我们希望在 info 列中搜索数组Places_of_interest中的所有值。尽管这看起来很复杂,但还不算太糟,正如你在这里看到的:

MySQL  localhost:33060+ ssl  SQL > SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest') as Sights FROM city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL \G
*************************** 1\. row ***************************
    Name: Charlotte
District: North Carolina
  Sights: [{"name": "NASCAR Hall of Fame"}, {"name": "Charlotte Motor Speedway"}]
*************************** 2\. row ***************************
    Name: Daytona
District: Florida
  Sights: [{"name": "Daytona Beach"}, {"name": "Motorsports Hall of Fame of America"}, {"name": "Daytona Motor Speedway"}]
2 rows in set (0.00 sec)

现在,如果我们只想检索Places_of_interest数组的值呢?在这种情况下,我们可以使用特殊格式的 JSON access 从数组中获取这些值。下面演示了这种技术。请注意以粗体突出显示的部分:

MySQL  localhost:33060+ ssl  SQL > SELECT Name, District, JSON_EXTRACT(info, '$.Places_of_interest[*].name') as Sights FROM city WHERE JSON_EXTRACT(info, '$.Places_of_interest') IS NOT NULL \G
*************************** 1\. row ***************************
    Name: Charlotte
District: North Carolina
  Sights: ["NASCAR Hall of Fame", "Charlotte Motor Speedway"]
*************************** 2\. row ***************************
    Name: Daytona
District: Florida
  Sights: ["Daytona Beach", "Motorsports Hall of Fame of America", "Daytona Motor Speedway"]
2 rows in set (0.00 sec)

好了,现在看起来容易多了,不是吗?这也是一个混乱的 SQL 命令。如果这一切看起来有点痛苦,你是对的,的确如此。在 SQL 中处理 JSON 数据需要借助 JSON 函数,但是这需要一个额外的步骤,并且可能会使用令人困惑的语法。关于每个JSON_*函数的完整解释,请参见在线 MySQL 参考手册。

如果您经常使用旧的 MySQL 客户端来查询具有宽行的数据,那么您可能已经使用了\G选项来以垂直格式显示结果,这使得读取数据更加容易。有了 shell,我们可以通过使用--json选项来显示数据。虽然这个选项更容易阅读,但是它有点冗长。清单 4-6 展示了使用--json选项的 shell 查询。

$ mysqlsh root:root@localhost --json --sql
mysqlsh: [Warning] Using a password on the command line interface can be insecure.
{
    "info": "Creating a session to 'root@localhost'"
}
{
    "info": "Fetching schema names for autocompletion... Press ^C to stop."
}
{
    "info": "Your MySQL connection id is 17 (X protocol) Server version: 8.0.11 MySQL Community Server - GPL No default schema selected; type \\use <schema> to set one."
}
{
    "info": "MySQL Shell 8.0.11 Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners

."
}

{
    "info": "Type '\\help' or '\\?' for help; '\\quit' to exit."
}

MySQL  localhost:33060+ ssl  SQL > SELECT * FROM world_x.city WHERE Name in ('Charlotte', 'Daytona');
{
    "executionTime": "0.0060 sec",
    "warningCount": 0,
    "warnings": [],
    "rows": [
        {
            "ID": 3818,
            "Name": "Charlotte",
            "CountryCode": "USA",
            "District": "North Carolina",
            "Info": "{\"Population\": 540828}"
        },
        {
            "ID": 4080,
            "Name": "Charlotte",
            "CountryCode": "USA",
            "District": "North Carolina",
            "Info": "{\"Population\": 792862, \"Places_of_interest\": [{\"name\": \"NASCAR Hall of Fame\"}, {\"name\": \"Charlotte Motor Speedway\"}]}"
        },
        {
            "ID": 4081,
            "Name": "Daytona",
            "CountryCode": "USA",
            "District": "Florida",
            "Info": "{\"Population\": 590280, \"Places_of_interest\": [{\"name\": \"Daytona Beach\"}, {\"name\": \"Motorsports Hall of Fame of America\"}, {\"name\": \"Daytona Motor Speedway\"}]}"
        }
    ],
    "hasData": true,
    "affectedRowCount": 0,
    "autoIncrementValue": 0
}

 MySQL  localhost:33060+ ssl  SQL > \q
Bye!

Listing 4-6Using the JSON Option (MySQL Shell)

请注意,输出更加详细,甚至来自 shell 的消息也是 JSON 格式的,但是它确实有助于更好地读取 JSON 数据。

最后,我们可以用DELETE SQL 命令删除这些行:

MySQL  localhost:33060+ ssl  SQL > DELETE FROM city WHERE Name in ('Charlotte', 'Daytona');
Query OK, 3 rows affected (0.00 sec)

摘要

MySQL Shell 对于 MySQL 客户端来说是一个巨大的技术飞跃。它不仅设计为以更智能的方式使用 MySQL 中的 SQL,还支持 JavaScript 和 Python 的原型。您可以使用任何您想要的语言,并在它们之间轻松切换,而不必重新启动应用或断开连接。多酷啊。

如果这还不够的话,X DevAPI 和内置对象的额外好处是允许使用 shell 作为文档存储的前端,因此您不必编写单独的应用来管理您的数据。您只需选择适合您需求的模式(语言),切换到该语言,然后执行任务。正如您在第 1 章中所了解到的,shell 还构成了最新特性的前端,包括 InnoDB Cluster,为您提供了满足所有 MySQL 管理、编程和高可用性需求的一站式客户端。

在本章中,您学习了如何使用 MySQL Shell。您回顾了启动选项、shell 命令、连接、会话,甚至学习了如何使用 interactive SQL。虽然这一章没有提供 MySQL Shell 所有特性的详尽介绍,但它提供了一个广泛的教程,说明如何使用它来完成最常见的任务。

在下一章中,您将探索通过使用 MySQL Shell 和 AdminAPI 建立一个实验性 InnoDB 集群的概述。

五、沙箱中的高可用性

现在,您已经了解了什么是 InnoDB 集群以及构成该特性的组件,包括组复制和 MySQL Shell,您已经有足够的知识来设置一个小型的实验性 InnoDB 集群。您将通过 MySQL Shell 使用新的 AdminAPI 来实现这一点。

接下来是部署三台服务器的演示,并在新的 MySQL Shell 中使用 X AdminAPI 通过组复制将它们配置为一个集群。虽然这听起来很费力,但实际上并不是。其实真的很简单。

让我们从 AdminAPI 的概述开始。

注意

您还必须在我们的目标机器上至少安装 MySQL Server 和 MySQL Shell。如果您还没有安装服务器和 shell,请分别参考第 2 和第 4 章。

AdminAPI 入门

允许我们建立实验性 InnoDB 集群的关键组件叫做沙箱。AdminAPI 有几种方法可以在本地机器上的沙箱中使用 MySQL 服务器。然而,AdminAPI 也有一些类,这些类的方法用于在远程机器上使用 MySQL 服务器的 InnoDB 集群。本章概述了 AdminAPI 中可用的类和方法。在下一节的 InnoDB 集群演示中,我们将使用本节中讨论的一些方法。

什么是沙箱?

沙箱是一个术语,描述数据和元数据(配置文件等)的组织方式,它不会影响(替换)任何现有的数据或产品安装。在 MySQL AdminAPI 的例子中,它实现的沙箱确保 InnoDB 集群中服务器的任何配置都不会影响机器上 MySQL 的任何现有安装。

回想一下第 2 章中的,AdminAPI 有两个主要的类:dbacluster。让我们来看看每一个的细节。

注意

以下是在线文档的压缩版本,提供了概述,而不是具体的使用示例。

dba 类

dba类使您能够通过使用 AdminAPI 来管理 InnoDB 集群。dba类使您能够管理集群——例如,创建一个新的集群,使用沙箱配置(一种在同一台机器上使用几个 MySQL 实例来试验 InnoDB 集群的方法),检查实例和集群的状态。

因为这个类是 API 的设置和配置部分,所以它有使用沙箱的方法和使用远程服务器的方法。表 5-1 显示了在沙箱中处理实例的可用方法(名称中带有sandbox的方法)。

表 5-1

沙箱方法(dba 类)

|

返回

|

功能

|

描述

|
| --- | --- | --- |
| 没有人 | delete_sandbox_instance(int port, dict options) | 删除本地主机上现有的 MySQL 服务器实例 |
| 情况 | deploy_sandbox_instance(int port, dict options) | 在本地主机上创建新的 MySQL 服务器实例 |
| 没有人 | kill_sandbox_instance(int port, dict options) | 终止本地主机上正在运行的 MySQL 服务器实例 |
| 没有人 | start_sandbox_instance(int port, dict options) | 在本地主机上启动现有的 MySQL 服务器实例 |
| 没有人 | stop_sandbox_instance(int port, dict options) | 停止本地主机上正在运行的 MySQL 服务器实例 |

请注意,有一些方法可以在沙箱中部署实例,删除或终止实例(delete删除实例,kill停止实例,但保留数据和元数据),以及启动和停止实例(kill发出不受控制的关闭)。在后面的小节中,我们将在沙箱中演示 InnoDB 集群时使用这些方法中的大部分。

还要注意,这些方法需要一个端口号和一个选项字典。AdminAPI 中可用于这些方法和其他方法的选项取决于方法本身,因为每个方法都允许一个或多个选项。表 5-2 显示了表 5-1 中方法可用的选项。

表 5-2

沙箱方法的选项(dba 类)

|

功能

|

[计]选项

|

描述

|
| --- | --- | --- |
| delete_sandbox_instance | sandboxDir | 将部署新实例的路径 |
| deploy_sandbox_instance | portx | 新实例将侦听 X 协议连接的端口 |
| sandboxDir | 将部署新实例的路径 |
| password | 新实例上 MySQL root 用户的密码 |
| allowRootFrom | 创建远程 root 帐户,仅限于给定的地址模式(例如,%) |
| ignoreSslError | 默认情况下,为新实例添加 SSL 支持时忽略错误:true |
| kill_sandbox_instance | sandboxDir | 将部署新实例的路径 |
| start_sandbox_instance | sandboxDir | 将部署新实例的路径 |
| stop_sandbox_instance | password | 新实例上 MySQL root 用户的密码 |
| sandboxDir | 将部署新实例的路径 |

这些选项以简单 JSON 文档的形式在字典中指定。例如,如果您想要停止端口 13004 上的实例,并指定沙箱目录和密码,您可以按如下方式调用方法:

stop_sandbox_instance(13004, {'sandboxDir':'/home/cbell/data1', 'password':'secret'})

5-3 显示了该类中用于设置和配置 MySQL 实例和集群的其余方法。

表 5-3

实例和集群方法(dba 类)

|

返回

|

功能

|

描述

|
| --- | --- | --- |
| 数据 | check_instance_configuration(InstanceDef instance, dict options) | 验证 MySQL InnoDB 集群使用情况的实例 |
| 没有人 | configure_local_instance(InstanceDef instance, dict options) | 验证和配置 MySQL InnoDB 集群使用的本地实例 |
| 没有人 | configure_instance(InstanceDef instance, dict options) | 验证和配置 MySQL InnoDB 集群使用的实例 |
| 串 | create_cluster(str name, dict options) | 创建一个 MySQL InnoDB 集群 |
| 没有人 | drop_metadata_schema(dict options) | 删除元数据架构 |
| 串 | get_cluster(str name, dict options) | 从元数据存储中检索到一个群集 |
| 没有人 | reboot_cluster_from_complete_outage(str clusterName, dict options ) | 当所有成员脱机时,使群集重新联机 |

这些方法有更多的选择。事实上,有些方法允许一长串选项。下面的列表没有列出每种方法的每个选项,而是将选项总结为三个类别。在演示过程中,您将会看到其中的一些操作。某些方法需要更具体的选项。

  • 通用:一些方法的通用选项。

    • verifyMyCnf:实例的 MySQL 配置文件的可选路径。如果给定了这个选项,那么除了全局 MySQL 系统变量之外,还将验证配置文件的预期选项值。

    • outputMycnfPath:写入实例的 MySQL 配置文件的备选输出路径。

    • password:连接时使用的密码。

    • clusterAdmin:要创建的 InnoDB 集群管理员用户的名称。支持的格式是标准的 MySQL 帐户名格式。

    • clusterAdminPassword:InnoDB 集群管理员账户的密码。

    • clearReadOnly:用于确认super_read_only必须禁用的布尔值。

    • interactive:用于在命令执行中禁用向导的布尔值(不向用户提供提示,不显示确认提示)。

  • URI 或字典:安全连接选项。

    • ssl-mode:连接中使用的 SSL 模式。

    • ssl-ca:PEM 格式的 X509 认证机构路径。

    • ssl-capath:包含 PEM 格式的 X509 认证机构的目录路径。

    • ssl-cert:PEM 格式的 X509 证书路径。

    • ssl-key:PEM 格式的 X509 键的路径。

    • ssl-crl:包含证书撤销列表的文件的路径。

    • ssl-crlpath:包含证书撤销列表文件的目录路径。

    • ssl-cipher:要使用的 SSL 密码。

    • tls-version:安全连接允许的协议列表。

    • auth-method:认证方式。

    • get-server-public-key:根据 RSA 密钥对向服务器请求密码交换所需的公钥。在禁用 SSL 模式的情况下,使用经典 MySQL 会话连接 MySQL 8.0 服务器时使用。

    • server-public-key-path:包含服务器基于 RSA 密钥对进行密码交换所需的公钥的客户端副本的文件的路径名。

  • 连接字典:连接参数。

    • scheme:连接使用的协议。

    • user:连接时使用的 MySQL 用户名。

    • dbUser:为user的别名。

    • password:连接时使用的密码。

    • dbPassword:同password

    • host:要在 TCP 连接上使用的主机名或 IP 地址。

    • port:TCP 连接中使用的端口。

    • socket:通过 UNIX 套接字连接时使用的套接字文件名。

    • schema:连接完成后要选择的模式。

该类还有一个名为verbose的属性,它允许开发者在提供服务器(在沙箱中部署服务器)的过程中设置调试和相关语句输出的详细级别。不建议将此属性设置为生产使用。verbose设置如下:

  • 0:禁用mysqlprovision详细度

  • 1:启用mysqlprovision详细度

  • >1:大于或等于 2 的值启用mysqlprovision调试详细度。

集群类

cluster类是 InnoDB 集群的句柄(想想对象实例)。该类使您能够使用集群来添加实例、删除实例、获取集群的状态(健康状况)等。

因为这个类用于直接处理实例和集群,所以大多数方法都设计为处理通过dba类检索的集群的特定实例。表 5-4 列出了cluster类中的方法。

表 5-4

Cluster 类的方法

|

返回

|

功能

|

描述

|
| --- | --- | --- |
| None | add_instance(InstanceDef instance, dict options) | 向群集添加实例 |
| dict | check_instance_state(InstanceDef instance, str password) | 验证与集群相关的实例 GTID 状态 |
| str | describe() | 描述群集的结构 |
| None | disconnect() | 断开群集对象使用的所有内部会话 |
| None | dissolve(Dictionary options) | 溶解群集 |
| None | force_quorum_using_partition_of(InstanceDef instance, str password) | 从仲裁丢失中恢复群集 |
| str | get_name() | 检索群集的名称 |
| None | rejoin_instance(InstanceDef instance, dict options) | 将实例重新加入集群 |
| None | remove_instance(InstanceDef instance, dict options) | 从集群中删除实例 |
| None | rescan() | 重新扫描集群 |
| str | status() | 描述群集的状态 |

请注意,我们有添加、移除和重新加入实例的方法。在管理集群中的实例时,我们会经常用到它们。还有几种方法可以获取信息和状态,并强制更新元数据,例如get_name()status()rescan()

还要注意,像dba类一样,一些方法接受选项字典。这些选项也是该方法所特有的,但是通常使用上一节中描述的相同选项来连接到实例。如前所述,有些允许特定于方法的选项。

该类有一个属性;集群的名称。该属性简单地命名为name,可以通过编程方式设置,但通常是在使用dba类创建集群时设置的。

小费

参见 https://dev.mysql.com/doc/dev/mysqlsh-api-python/8.0/group___admin_a_p_i.html 了解更多关于 AdminAPI 的信息。

现在您已经对 AdminAPI 中的类和方法有了一个简要的概述,让我们来看看它的实际应用吧!

示范

本节简要演示了如何通过 MySQL Shell 和 AdminAPI 使用沙箱部署方法创建 InnoDB 集群。我们将创建一个 InnoDB 集群,在本地机器上运行四个实例。我们不仅将看到如何设置要使用的集群,还将看到集群如何处理故障转移,最后,我们将看到如何设置 DevOps 脚本来快速设置和关闭实验集群。

设置和配置

要准备使用沙箱,您只需要决定一些参数,并在系统上准备一个区域来处理集群的数据。需要一个参数:我们必须决定实验集群要使用什么端口号。在这种情况下,我们将使用端口 3311–3314 作为服务器监听端口。

我们还可以指定一个目录来包含沙箱数据。虽然这不是必需的,但如果您希望以后重新初始化群集,建议这样做。否则不需要指定目录,因为管理 API 使用沙箱的预定路径。例如,在 Windows 上,它位于名为MySQL\mysql-sandboxes的用户目录中。该文件夹形成了存储沙箱的所有数据和元数据的根。例如,当您使用端口 3312 将实例部署到沙箱时,您将看到一个具有该名称的文件夹,如下所示:

C:\Users\olias\MySQL\mysql-sandboxes\3312

如果您计划以后重用该集群,您可能希望通过使用sandboxDir选项来指定一个特定的文件夹。例如,您可以将字典指定为{'sandboxDir':'c://idc_sandbox'}和 AdminAPI。但是,该文件夹必须存在,否则当您调用deploy_sandbox_instance()方法时会得到一个错误。清单 5-1 显示了 Windows 上的一个定制沙箱目录,在端口 3311 上部署了一个实例。

C:\idc_sandbox>dir
 Volume in drive C is Local Disk
 Volume Serial Number is AAFC-6767

 Directory of C:\idc_sandbox

04/23/2018  04:18 PM    <DIR>          .
04/23/2018  04:18 PM    <DIR>          ..
04/23/2018  04:18 PM    <DIR>          3311
               0 File(s)              0 bytes
               3 Dir(s)  172,731,768,832 bytes free

C:\idc_sandbox>dir 3311
 Volume in drive C is Local Disk
 Volume Serial Number is AAFC-6767

 Directory of C:\idc_sandbox\3311

04/23/2018  04:19 PM    <DIR>          .
04/23/2018  04:19 PM    <DIR>          ..
04/23/2018  04:19 PM                 6 3311.pid
04/23/2018  04:18 PM               726 my.cnf
04/23/2018  04:18 PM    <DIR>          mysql-files
04/23/2018  04:18 PM    <DIR>          sandboxdata
04/23/2018  04:18 PM               147 start.bat
04/23/2018  04:18 PM               207 stop.bat
               4 File(s)          1,086 bytes

               4 Dir(s)  172,557,893,632 bytes free

Listing 5-1Creating a Directory for the Sandbox

注意

要重用实例数据,必须启动实例。尝试使用相同的端口重新部署它将会产生错误,因为目录不为空。

下面的演示使用 Python 模式下的 MySQL Shell。所示的命令也可以在 JavaScript 模式下工作,但是一些方法名的拼写可能略有不同(比如使用下划线和首字母大写)。这样做是为了让 AdminAPI 符合特定于语言的命名约定和惯例。 1

创建 InnoDB 集群的沙箱部署需要几个步骤:

  1. 在沙箱中创建和部署实例:设置和配置我们的 MySQL 服务器。

  2. 创建集群:创建集群类的对象实例。

  3. 向集群添加实例:向集群添加沙箱实例。

  4. 检查集群状态:检查集群健康状况。

您还将看到通过终止其中一个实例来演示集群中的故障转移是如何工作的。我们开始吧!

在沙箱中创建和部署实例

让我们从使用 AdminAPI 启动 shell 并部署四台服务器开始。在这种情况下,我们将使用端口 3311–3314 和dba对象中的deploy_sandbox_instance()方法为每个服务器创建新的实例。所有这些都将在我们的本地主机上运行,在这个例子中是 Windows 相应地调整路径,以便在其他系统上使用。清单 5-2 展示了如何部署四台服务器。使用的命令以粗体突出显示,有助于从消息中识别命令。请注意,我以 Python 模式启动了 shell。

小费

没有必要导入dba类。每当您使用\py命令(或--py命令行选项)切换到 Python 模式,或使用\js命令(或--js命令行选项)切换到 JavaScript 模式时,MySQL Shell 都可以使用它。

C:\idc_sandbox>mysqlsh --py

MySQL Shell 8.0.11

Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners

.

Type '\help' or '\?' for help; '\quit' to exit.

 MySQL  Py > dba.deploy_sandbox_instance(3311, {'sandboxDir':'c://idc_sandbox'})
A new MySQL sandbox instance will be created on this host in
c://idc_sandbox\3311

Warning: Sandbox instances are only suitable for deploying and
running on your local machine for testing purposes and are not
accessible from external networks.

Please enter a MySQL root password for the new instance: ****
Deploying new MySQL instance...

Instance localhost:3311 successfully deployed and started.
Use shell.connect('root@localhost:3311'); to connect to the instance.

 MySQL  Py > dba.deploy_sandbox_instance(3312, {'sandboxDir':'c://idc_sandbox'})
A new MySQL sandbox instance will be created on this host in
c://idc_sandbox\3312

Warning: Sandbox instances are only suitable for deploying and
running on your local machine for testing purposes and are not
accessible from external networks.

Please enter a MySQL root password for the new instance: ****
Deploying new MySQL instance...

Instance localhost:3312 successfully deployed and started.
Use shell.connect('root@localhost:3312'); to connect to the instance.

 MySQL  Py > dba.deploy_sandbox_instance(3313, {'sandboxDir':'c://idc_sandbox'})
A new MySQL sandbox instance will be created on this host in
c://idc_sandbox\3313

Warning: Sandbox instances are only suitable for deploying and
running on your local machine for testing purposes and are not
accessible from external networks

.

Please enter a MySQL root password for the new instance: ****
Deploying new MySQL instance...

Instance localhost:3313 successfully deployed and started.
Use shell.connect('root@localhost:3313'); to connect to the instance

.

 MySQL  Py > dba.deploy_sandbox_instance(3314, {'sandboxDir':'c://idc_sandbox'})
A new MySQL sandbox instance will be created on this host in
c://idc_sandbox\3314

Warning: Sandbox instances are only suitable for deploying and
running on your local machine for testing purposes and are not
accessible from external networks.

Please enter a MySQL root password for the new instance: ****
Deploying new MySQL instance...

Instance localhost:3314 successfully deployed and started.
Use shell.connect('root@localhost:3314'); to connect to the instance.

 MySQL  Py >

Listing 5-2Creating Local Server Instances

注意,deploy_sandbox_instance()方法显示沙箱数据和元数据的位置(例如,c://idc_sandbox\3314),并提示输入实例的密码。如果您打算重新启动或重新使用群集,请确保使用一个容易记住的密码。可以对所有实例使用相同的密码。运行完所有命令后,本地机器上将运行四个实例。

小费

JavaScript 是区分大小写的,所以要确保对变量、对象和方法使用正确的拼写。名为abc的变量与名为Abc的变量不同。

创建集群

我们需要做的下一件事是建立一个新的集群。我们用dba对象中的create_cluster()方法来实现这一点,它为cluster类创建了一个对象实例。但是首先,我们必须连接到我们希望作为主服务器的服务器。请注意,这是我们的 shell 会话的延续,演示了如何创建新的集群。

我们还将在单主模式下设置集群。将有一个主要角色为(读/写)的实例,所有其他实例都是只读的。为此,我们必须将multiMaster选项设置为False。注意清单 5-3 中是如何做到的。

MySQL  Py > \connect root@localhost:3311

Creating a session to 'root@localhost:3311'
Enter password: ****
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 12
Server version: 8.0.11 MySQL Community Server - GPL

No default schema selected; type \use <schema> to set one.

 MySQL  localhost:3311 ssl  Py > my_cluster = dba.create_cluster('MyCluster', {'multiMaster':False})

A new InnoDB cluster will be created on instance 'root@localhost:3311'.ster', {'multiMaster':False})

Validating instance at localhost:3311...
Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters for use within the same host.

This instance reports its own address as DESKTOP-JBL081L

Instance configuration is suitable.
Creating InnoDB cluster 'MyCluster' on 'root@localhost:3311'...
Adding Seed Instance...

Cluster successfully created. Use Cluster.add_instance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.

 MySQL  localhost:3311 ssl  Py >

Listing 5-3Creating a Cluster in InnoDB Cluster

注意,我们将集群命名为MyCluster,并使用一个名为my_cluster的变量来存储从create_cluster()方法返回的对象。还要注意,我们首先连接的服务器已经成为主服务器,AdminAPI 检测到我们正在沙箱中运行。

注意

如果退出 shell,可以用get_cluster()方法检索正在运行的集群。

将实例添加到集群

接下来,我们添加另外两个服务器实例来完成集群。我们现在使用保存在变量my_cluster中的cluster类实例,并使用add_instance()。我们将剩余的三个实例添加到集群中。这些服务器自动成为组中的辅助服务器。清单 5-4 展示了如何向集群添加实例。

 MySQL  localhost:3311 ssl  Py > my_cluster.add_instance('root@localhost:3312')
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@localhost:3312': ****
Adding instance to the cluster ...

Validating instance at localhost:3312...
Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters for use within the same host.

This instance reports its own address as DESKTOP-JBL081L

Instance configuration is suitable.
The instance 'root@localhost:3312' was successfully added to the cluster.

 MySQL  localhost:3311 ssl  Py > my_cluster.add_instance('root@localhost:3313')
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@localhost:3313': ****
Adding instance to the cluster ...

Validating instance at localhost:3313...
Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters for use within the same host.

This instance reports its own address as DESKTOP-JBL081L

Instance configuration is suitable.
The instance 'root@localhost:3313' was successfully added to the cluster.

 MySQL  localhost:3311 ssl  Py > my_cluster.add_instance('root@localhost:3314')
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@localhost:3314': ****
Adding instance to the cluster ...

Validating instance at localhost:3314...
Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters for use within the same host.

This instance reports its own address as DESKTOP-JBL081L

Instance configuration is suitable.
The instance 'root@localhost:3314' was successfully added to the cluster.

 MySQL  localhost:3311 ssl  Py >

Listing 5-4Adding Instances to the Cluster

注意,add_instance()方法接受一个带有 URI 连接信息的字符串。在这种情况下,它只是用户名、at 符号(@)、主机名和端口,格式为<user>@<host>:<port>。另请注意,该方法提示输入实例的密码。

至此,您已经看到了 InnoDB Cluster 如何设置服务器并将它们添加到组中。花点时间思考第 3 章和组复制教程。您在幕后看不到的是所有的组复制机制—您可以免费获得它们!这有多酷?

显然,使用 shell 来设置和管理集群比设置和管理标准的组复制要容易得多。具体来说,您不必手动配置复制!更好的是,如果服务器出现故障,您不必担心重新配置应用或拓扑来确保解决方案仍然可行。InnoDB Cluster 会自动为您完成这项工作。

检查集群的状态

创建集群并添加实例后,我们可以通过使用我们的my_cluster对象的status()方法来获取集群的状态,如清单 5-5 所示。在这个例子中,您还将看到如何通过连接\connect命令并使用来自dba类的get_cluster()方法,从一个正在运行的服务器实例中检索集群。还可以使用命令行(mysqlsh root@localhost:3313)连接到服务器实例。请注意,您不必连接到第一个(或主)服务器实例来检索集群。您可以连接到任何服务器来检索集群。

C:\idc_sandbox>mysqlsh --py
MySQL Shell 8.0.11

Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type '\help' or '\?' for help; '\quit' to exit.

 MySQL  Py > \connect root@localhost:3311
Creating a session to 'root@localhost:3311'
Enter password: ****
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 55
Server version: 8.0.11 MySQL Community Server - GPL

No default schema selected; type \use <schema> to set one.

 MySQL  localhost:3311 ssl  Py > my_cluster = dba.get_cluster('MyCluster')
 MySQL  localhost:3311 ssl  Py > my_cluster.status()
{
    "clusterName": "MyCluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "localhost:3311",
        "ssl": "REQUIRED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "localhost:3311": {
                "address": "localhost:3311",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "localhost:3312": {
                "address": "localhost:3312",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "localhost:3313": {
                "address": "localhost:3313",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "localhost:3314": {
                "address": "localhost:3314",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"

            }
        }
    },
    "groupInformationSourceMember": "mysql://root@localhost:3311"
}

 MySQL  localhost:3311 ssl  Py >

Listing 5-5Getting the Status of the Cluster

注意,输出是 JSON 文档的形式,包含关于集群的元数据,包括所有实例、它们的角色和状态。您希望确保所有实例都在线。

故障切换

现在,让我们重新创建第 3 章中的故障转移场景。在这种情况下,我们将有目的地删除其中一个实例。让我们杀了在 3311 端口上运行的那个。我们可以通过多种方式来实现这一点,包括使用操作系统终止mysqld进程,从 shell 或 MySQL 客户端使用shutdown SQL 命令,或者使用dba类。清单 5-6 展示了如何终止实例以及实例停止后的状态结果。

MySQL  localhost:3311 ssl  Py > dba.kill_sandbox_instance(3311, {'sandboxDir':'c://idc_sandbox'})

The MySQL sandbox instance on this host in
c://idc_sandbox\3311 will be killed

Killing MySQL instance...

Instance localhost:3311 successfully killed.

 MySQL  localhost:3311 ssl  Py > my_cluster.status()
Traceback (most recent call last):
  File "<string>", line 1, in <module>
SystemError: RuntimeError: Cluster.status: Unable to detect target instance state. Please check account privileges.

MySQL  localhost:3311 ssl  Py > \connect root@localhost:3312

Creating a session to 'root@localhost:3312'
Enter password: ****
Fetching schema names for autocompletion... Press ^C to stop.
Closing old connection...
Your MySQL connection id is 44
Server version: 8.0.11 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.

MySQL  localhost:3312 ssl  Py > my_cluster = dba.get_cluster('MyCluster')

 MySQL  localhost:3312 ssl  Py > my_cluster.status()
{
    "clusterName": "MyCluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "localhost:3312",
        "ssl": "REQUIRED",
        "status": "OK_PARTIAL",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure. 1 member is not active",
        "topology": {
            "localhost:3311": {
                "address": "localhost:3311",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "(MISSING)"
            },
            "localhost:3312": {

                "address": "localhost:3312",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "localhost:3313": {
                "address": "localhost:3313",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "localhost:3314": {
                "address": "localhost:3314",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }
        }
    },
    "groupInformationSourceMember": "mysql://root@localhost:3312"
}

 MySQL  localhost:3312 ssl  Py >

Listing 5-6Failover Demonstration

请注意,我们终止了运行在端口 3311 上的服务器。然而,当我们再次检查状态时,我们得到了一个错误。这是因为我们已经连接到该服务器。我们需要连接到另一台服务器并再次检索集群来刷新数据。然后我们可以获得状态,当我们这样做时,我们看到端口 3311 上的服务器实例被列为缺失,端口 3312 上的服务器已经接管了读/写功能。

此时,我们可以尝试恢复端口 3311 上的服务器实例,或者将其从集群中删除。清单 5-7 演示了如何将其从集群中移除。注意,我们使用设置为Trueforce选项来删除实例,因为我们无法连接到它(它已关闭)。

MySQL  localhost:3312 ssl  Py > my_cluster.remove_instance('root@localhost:3311', {'force':True})
The instance will be removed from the InnoDB cluster. Depending on the
instance being the Seed or not, the Metadata session might become invalid.
If so, please start a new session to the Metadata Storage R/W instance

.

The instance 'root@localhost:3311' was successfully removed from the cluster.

 MySQL  localhost:3312 ssl  Py > my_cluster.status()
{
    "clusterName": "MyCluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "localhost:3312",
        "ssl": "REQUIRED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "localhost:3312": {
                "address": "localhost:3312",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "localhost:3313": {
                "address": "localhost:3313",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "localhost:3314": {
                "address": "localhost:3314",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }
        }
    },
    "groupInformationSourceMember": "mysql://root@localhost:3312"
}

 MySQL  localhost:3312 ssl  Py >

Listing 5-7Removing the Downed Instance from the Cluster

DevOps 脚本

现在,让我们创建 DevOps 脚本,用于设置和关闭 InnoDB 集群。幸运的是,我们在这里不需要发明太多,因为我们将使用在演示中使用的相同命令。唯一的区别是我们将添加print语句以获得更好的反馈,并用来自shel l 类的方法替换 shell 命令,该类是 MySQL API 的一部分。

第一个脚本在沙箱中设置了一个 InnoDB 集群。我们遵循的步骤与之前使用的步骤相同。清单 5-8 展示了用 Python 编写的完整脚本。请随意研究该脚本,以确保您看到 Python 命令如何与前面的演示相匹配。如果您喜欢这个脚本,您可以创建一个名为setup_idc_sandbox.py的文件并保存在您的系统上。

# Introducing InnoDB Cluster
#
# This Python script is designed to set up an InnoDB Cluster in a sandbox.
#
# Note: Change the cluster directory to match your preferred directory setup.
#
# The steps include:
# 1) create the sandbox directory
# 2) deploy instances
# 3) create the cluster
# 4) add instances to the cluster
# 5) show the cluster status
#
# Dr. Charles Bell, 2018
#
import os
import time

# Method to deploy sandbox instance
def deploy_instance(port):
    try:
        dba.deploy_sandbox_instance(port, {'sandboxDir':'c://idc_sandbox', 'password':'root'})
    except:
        print("ERROR: cannot setup the instance in the sandbox.")
    time.sleep(1)

# Add instance to cluster
def add_instance(cluster, port):
    try:
        cluster.add_instance('root:root@localhost:{0}'.format(port))
    except:
        print("ERROR: cannot add instance to cluster.")
    time.sleep(1)

print("##### STEP 1 of 5 : CREATE SANDBOX DIRECTORY #####")
os.mkdir('c://idc_sandbox')

print("##### STEP 2 of 5 : DEPLOY INSTANCES #####")
deploy_instance(3311)
deploy_instance(3312)
deploy_instance(3313)
deploy_instance(3314)

print("##### STEP 3 of 5 : CREATE CLUSTER #####")

shell.connect('root:root@localhost:3311')
my_cluster = dba.create_cluster('MyCluster', {'multiMaster':False})
time.sleep(1)

print("##### STEP 4 of 5 : ADD INSTANCES TO CLUSTER #####")
add_instance(my_cluster, 3312)
add_instance(my_cluster, 3313)
add_instance(my_cluster, 3314)

print("##### STEP 5 of 5 : SHOW CLUSTER STATUS #####")
shell.connect('root:root@localhost:3311')
time.sleep(1)
my_cluster = dba.get_cluster('MyCluster')
time.sleep(1)
status = my_cluster.status()
print(status)

Listing 5-8DevOps Script to Set Up the InnoDB Cluster (Sandbox)

为了执行脚本,我们对 shell 使用了-f选项,提供了脚本的名称。这告诉 shell 加载并执行脚本:

mysqlsh -f setup_idc_sandbox.py

清单 5-9 显示了运行脚本的输出摘录。为了简洁起见,省略了重复的部分。

##### STEP 1 of 5 : CREATE SANDBOX DIRECTORY #####
##### STEP 2 of 5 : DEPLOY INSTANCES #####
Deploying new MySQL instance...

Instance localhost:3311 successfully deployed and started.
Use shell.connect('root@localhost:3311'); to connect to the instance.
...
##### STEP 3 of 5 : CREATE CLUSTER #####
A new InnoDB cluster will be created on instance 'root@localhost:3311'.

Validating instance at localhost:3311...
Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters for use within the same host.

This instance reports its own address as DESKTOP-JBL081L

Instance configuration is suitable.
Creating InnoDB cluster 'MyCluster' on 'root@localhost:3311'...
Adding Seed Instance...

Cluster successfully created. Use Cluster.add_instance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.

##### STEP 4 of 5 : ADD INSTANCES TO CLUSTER #####
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Adding instance to the cluster ...

Validating instance at localhost:3312...
Instance detected as a sandbox.
Please note that sandbox instances are only suitable for deploying test clusters for use within the same host.

This instance reports its own address as DESKTOP-JBL081L

Instance configuration is suitable.
The instance 'root@localhost:3312' was successfully added to the cluster.
...
##### STEP 5 of 5 : SHOW CLUSTER STATUS #####
{"clusterName": "MyCluster", "defaultReplicaSet": {"name": "default", "primary": "localhost:3311", "ssl": "REQUIRED", "status": "OK", "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", "topology": {"localhost:3311": {"address": "localhost:3311", "mode": "R/W", "readReplicas": {}, "role": "HA", "status": "ONLINE"}, "localhost:3312": {"address": "localhost:3312", "mode": "R/O", "readReplicas": {}, "role": "HA", "status": "ONLINE"}, "localhost:3313": {"address": "localhost:3313", "mode": "R/O", "readReplicas": {}, "role": "HA", "status": "ONLINE"}, "localhost:3314": {"address": "localhost:3314", "mode": "R/O", "readReplicas": {}, "role": "HA", "status": "ONLINE"}}}, "groupInformationSourceMember": "mysql://root@localhost:3311"}

Listing 5-9Setup Script Output

现在,让我们创建一个脚本来关闭集群。在这种情况下,我们将编写一个脚本来关闭所有实例并删除沙箱目录。为此,我们首先尝试从集群中删除实例;然后我们删除实例,最后删除目录。清单 5-10 显示了完整的关机脚本。如果您喜欢这个脚本,您可以创建一个名为shutdown_idc_sandbox.py的文件并保存在您的系统上。

# Introducing InnoDB Cluster
#
# This Python script is designed to perform an orderly shutdown of an InnoDB Cluster.
#
# Note: Change the cluster directory to match your preferred directory setup.
#
# The steps include:
# 1) connect to one of the instances
# 2) retrieve the cluster
# 3) remove the instances from the cluster
# 4) kill all instances
# 5) delete the sandbox directory
#
# Dr. Charles Bell, 2018
#
import shutil
import time

# Method to remove instance
def remove_instance(cluster, port):
    try:
        cluster.remove_instance('root@localhost:{0}'.format(port), {'password':'root', 'force':True})
    except:
        pass
    time.sleep(1)

# Method to kill the instance
def kill_instance(port):
    try:
        dba.kill_sandbox_instance(port, {'sandboxDir':'c://idc_sandbox'})
    except:
        pass
    time.sleep(1)

my_cluster = None
try:
    print("##### STEP 1 of 5 : CONNECT TO ONE INSTANCE (3314) #####")
    shell.connect('root:root@localhost:3314')
    print("##### STEP 2 of 5 : GET THE CLUSTER #####")
    my_cluster = dba.get_cluster('MyCluster')
    time.sleep(1)
except:
    print("ERROR: cannot connect to or remove instances from cluster.")
if my_cluster:
    print("##### STEP 3 of 5 : REMOVE INSTANCES FROM THE CLUSTER #####")
    remove_instance(my_cluster, 3311)
    remove_instance(my_cluster, 3312)
    remove_instance(my_cluster, 3313)
    remove_instance(my_cluster, 3314)

print("##### STEP 4 of 5 : KILL THE INSTANCES #####")
kill_instance(3311)
kill_instance(3312)
kill_instance(3313)

kill_instance(3314)

print("##### STEP 5 of 5 : REMOVE THE SANDBOX DIRECTORY #####")
try:
    shutil.rmtree('c://idc_sandbox')
except:
    print("Cannot remove directory!")

Listing 5-10DevOps Script to Shut Down InnoDB Cluster (Sandbox)

我们使用以下命令以与安装脚本相同的方式运行该脚本:

mysqlsh -f shutdown_idc_sandbox.py

清单 5-11 显示了运行脚本的输出摘录。为了简洁起见,省略了重复的部分。

##### STEP 1 of 5 : CONNECT TO ONE INSTANCE (3314) #####
##### STEP 2 of 5 : GET THE CLUSTER #####
##### STEP 3 of 5 : REMOVE INSTANCES FROM THE CLUSTER #####
The instance will be removed from the InnoDB cluster. Depending on the
instance being the Seed or not, the Metadata session might become invalid.
If so, please start a new session to the Metadata Storage R/W instance.

The instance 'root@localhost:3311' was successfully removed from the cluster.

##### STEP 4 of 5 : KILL THE INSTANCES #####
The MySQL sandbox instance on this host in
c://idc_sandbox\3311 will be killed

Killing MySQL instance...

Instance localhost:3311 successfully killed.
...
##### STEP 5 of 5 : REMOVE THE SANDBOX DIRECTORY #####

Listing 5-11Shutdown Script Output

在沙箱中使用 InnoDB 集群的演示到此结束。

摘要

MySQL Shell 是 MySQL 的游戏规则改变者,这听起来像是炒作,但现在应该开始看起来更像事实了。在前一章中我们谈到了 MySQL Shell 的一小部分,在这一章中你会看到更多。在本例中,我们看了一下用于在沙箱中创建 InnoDB 集群的 AdminAPI。

如果您阅读本章或其他介绍如何设置 InnoDB 集群的文章,您可能不会看到或欣赏 MySQL Shell 和 AdminAPI 在用户友好性方面向前迈出的巨大一步。另一方面,如果您曾经使用过 MySQL 复制,尤其是组复制,并且学会了忍受完成所有工作所需的特定命令和操作,您将会看到 MySQL Shell 和 AdminAPI 带来的强大功能。

简而言之,InnoDB Cluster 使得使用组复制成为学习一些 API 类和方法的简单事情。最好的一点是,它为 InnoDB 集群的开发操作和自动化打开了大门——到目前为止,这需要昂贵的定制工具。是的,InnoDB 集群更易于管理和自动化。

但是,您还没有完成对 InnoDB 集群的学习。在下一章中,您将看到 InnoDB 集群的最后一部分:MySQL 路由。您将看到什么是 MySQL 路由以及如何使用它。第 7 章展示了 MySQL 路由的使用。

六、MySQL 路由

现在,您已经了解了 InnoDB Cluster 的工作原理,并看到了如何通过 MySQL Shell 使用 AdminAPI 快速配置测试集群的简短演示,为了使您的应用真正具有高可用性,您还需要 InnoDB Cluster 的一个部分。

考虑一下,如果我们连接到集群,而我们所连接的机器离线,应用会做什么。我们知道集群可以从这样的损失中恢复(假设组中的服务器数量足够(参见第 3 章)),但是我们对我们的应用做些什么呢?如果服务器离线,我们无法知道哪一个(或者是否)另一个服务器已经接管。

一些开发者可能会尝试在他们的应用中构建重试连接的能力,或者在与当前服务器的连接失败时重试预先确定的不同连接的能力。虽然这可以得到您所需要的,但是它是脆弱的,需要将集群中服务器的所有主机名、端口等嵌入或提供给应用。显然,一定有更好的方法。Oracle 用 MySQL 路由回应了这一呼吁。

本章讨论 MySQL 路由,包括如何设置和配置路由。本章最后给出了修改应用以充分利用路由的建议。

概观

MySQL 路由是 MySQL InnoDB 集群的透明连接路由服务,提供负载平衡、应用连接故障转移和客户端路由。路由的连接路由特性意味着我们可以编写应用来连接到路由,并让 MySQL Router 通过使用一种支持的路由策略来处理到正确服务器的连接路由。路由成为应用和集群之间的中介服务。

其他连接路由应用和服务器可能会检查路由到正确服务器的数据包(或除此之外),但 MySQL 路由(因此,路由)不会进行这种检查。您的通信数据包未被打开、检查或更改;它们只是根据几个选项之一被转发到正确的服务器。

虽然这种简单性意味着路由是轻量级的,需要很少的资源,但是路由的配置有一些挑战。更具体地说,单个写集群的简单设置和配置是容易的。事实上,MySQL 安装程序有一个自动配置步骤,使在 Windows 上安装路由成为一件简单的事情。然而,对于更复杂的配置,您可能希望花一些时间用可能的策略和其他选项来配置和测试您的应用。您将在后面的章节中看到更多关于配置路由的内容。

对于那些希望增强其诊断或调试工作的人,路由还支持记录消息,包括关于连接如何路由、状态、错误和警告等的声明。这只是完全可定制的几个关键功能之一。

该路由可用于多种用例,包括为 MySQL 服务器提供高可用性甚至可伸缩性。在这种情况下,高可用性是通过自动故障转移功能实现的,如果群集中指定(选定)的服务器出现故障,该功能会自动路由客户端连接。

当与 MySQL InnoDB Cluster(通过底层组复制)一起使用以跨多个服务器复制数据库,同时在服务器出现故障时执行自动故障转移时,路由充当代理来隐藏网络上的多个 MySQL 实例,并将数据请求映射到其中一个集群实例。如果有足够的在线副本并且可靠的网络通信是可能的,那么使用该路由的应用将能够联系剩余的服务器之一。路由通过让应用连接到 MySQL 路由而不是直接连接到特定的 MySQL 服务器来实现这一点。图 6-1 显示了路由相对于您的应用和 InnoDB 集群所处位置的逻辑视图。

img/460910_1_En_6_Fig1_HTML.jpg

图 6-1

使用 MySQL 路由的应用架构(Oracle 公司出品)

注意,我们描述了两个应用,每个都连接到 MySQL 路由的一个实例。路由通过位于应用和 MySQL 服务器之间来工作。当应用连接到路由时,路由从候选服务器池中选择合适的 MySQL 服务器并进行连接,将所有网络流量从应用转发到该服务器(并将响应从服务器返回到应用)。

小费

您可以在一台或多台机器上运行多个 MySQL Router 实例,在路由点提供一定程度的容错。您不需要将路由隔离到一台机器上。这是可能的,因为路由与其主机没有亲缘关系。

在后台,路由存储来自 InnoDB 集群的服务器列表及其状态。这个服务器列表(或缓存)最初是从配置文件中读取的,路由和集群之间的后续通信确保它在拓扑变化时得到更新。当服务器丢失时,它们被路由标记为离线,路由会跳过它们。同样,如果集群中添加了新的服务器,路由的缓存也会更新以包含这些服务器。

为了保持高速缓存更新,路由保持与集群中一个服务器的开放连接,从性能模式数据库中查询集群元数据。每当检测到集群状态发生变化时,组复制就会实时更新这些表(或视图),例如,如果某个 MySQL 服务器意外关闭。

最后,该路由使开发者能够通过使用定制用例的插件来扩展 MySQL 路由。如果您的应用需要不同的路由策略,或者您想要在解决方案中构建数据包检测,您可以使用自定义插件来扩展路由。尽管为路由构建定制插件超出了本章的范围,也没有可供研究的示例,但请务必查看 MySQL 开发者网站( https://dev.mysql.com )和 MySQL 工程博客( https://mysqlserverteam.com /)以获取最新的信息和示例。

现在,您已经了解了什么是路由以及它应该放在基础设施中的什么位置,下面让我们来看看如何安装和配置路由。

装置

安装 MySQL 路由很容易。Oracle 为 MySQL 服务器支持的所有平台提供路由安装包。路由也包含在 Windows 的 MySQL 安装程序中。

在本节中,您将学习如何在 Windows 上安装 MySQL Router,并观看 MySQL 安装程序中提供的示例配置步骤的演示。您还将简要了解如何为其他平台安装路由。您将在后面的章节中学习如何手动配置路由。让我们先来看看 MySQL 安装程序。

MySQL Installer (Windows)

如果您已经按照前面的章节安装了 MySQL Server、MySQL Shell 和其他组件,那么本节中的信息将会很熟悉。但是,如果这是您第一次在 Windows 上使用路由,让我们一步一步来安装。

小费

如果您的环境不是同构的,并且您的应用服务器是一台 Windows 机器,您将希望使用本节作为在您的 Windows 应用服务器上设置路由的指南。

使用 MySQL 安装程序安装 MySQL 路由遵循与安装 MySQL Shell 相同的模式。但是,因为安装程序已经在系统中,我们只需再次启动它,并添加我们想要的 MySQL 产品(如路由)。当您启动安装程序时,会出现一个欢迎面板,其中包含已安装产品的列表。图 6-2 显示了 MySQL Shell 安装后 MySQL 安装程序的欢迎面板。

img/460910_1_En_6_Fig2_HTML.jpg

图 6-2

安装程序欢迎面板-安装后(MySQL 安装程序)

请注意,我们可以选择添加(添加… )新产品,修改(修改… )已安装产品的安装或配置,从目录(安装程序)中升级(升级… )具有较新版本的已安装产品,或者移除已安装产品(移除… )。请注意,还有一个名为 Catalog… 的按钮,它允许您更新安装程序中的产品目录。这允许您用新版本更新产品。

对于本教程,我们希望安装 MySQL 路由和连接器/Python 数据库连接器。我们将通过编写一个基本的连接示例来使用数据库连接器测试路由安装,这意味着我们还需要安装 Python。如果您的 Windows 机器上没有安装 Python,请参见 https://www.python.org/downloads/windows/ 下载并安装最新版本(版本 2.7 或 3.6 或更高版本)。

回想一下,我们单击“添加”继续。然后您将看到产品选择面板,如图 6-3 所示。只需导航左侧的树找到 MySQL 路由,选择它,然后单击绿色箭头将其添加到右侧的列表中。重复上述步骤,选择与您的 Python 安装相匹配的连接器/Python 版本。添加完两者后,单击“下一步”按钮继续。

小费

MySQL 安装程序将灰显任何与您的 Python 安装不匹配的版本。如果您不确定您安装了哪个版本的 Python,您可以从命令提示符下运行python --version

img/460910_1_En_6_Fig3_HTML.jpg

图 6-3

选择产品和功能(MySQL 安装程序)

6-4 中显示的下一个面板是一个安装摘要,允许您确认是否有正确的产品列出进行安装。该面板还会指示状态,以便您可以观察安装进度。当您准备好开始安装所选产品时,请单击执行。

img/460910_1_En_6_Fig4_HTML.jpg

图 6-4

安装对话框–暂存(MySQL 安装程序)

安装开始后,你会看到每个产品的进度,如图 6-5 所示。

img/460910_1_En_6_Fig5_HTML.jpg

图 6-5

安装对话框–安装进度(MySQL 安装程序)

所有产品安装完成后,安装面板将显示所有安装的状态为完成,并将底部的按钮改为显示下一步,如图 6-6 所示。准备好后,单击下一步。

img/460910_1_En_6_Fig6_HTML.jpg

图 6-6

安装对话框–安装完成(MySQL 安装程序)

安装的下一步是配置任何具有安装后选项的产品。MySQL Server 就是这样一种产品,它允许您设置几个配置项来完成安装。图 6-7 显示了产品配置面板。准备好开始配置时,单击“下一步”。

在这一步中,我们将配置路由用于沙箱安装。如果您尚未将 InnoDB 集群配置为在沙箱中运行,请参见第 5 章了解详细信息。在使用 MySQL 安装程序配置路由之前,必须运行 InnoDB 集群。

img/460910_1_En_6_Fig7_HTML.jpg

图 6-7

产品配置(MySQL 安装程序)

这一步开始 MySQL 路由配置。请注意,在图 6-8 中,安装程序正在询问集群中某个服务器的连接信息。回想一下第 5 章,我们使用端口 3311 作为主端口。因此,我们在端口文本框中输入 3311,并在密码文本框中提供根密码。我们将使用传统协议端口的默认值。准备好后,单击下一步。

img/460910_1_En_6_Fig8_HTML.jpg

图 6-8

MySQL 路由配置(MySQL 安装程序)

此时,安装程序将向您显示要执行的步骤摘要,并允许您开始该过程或返回并进行更改,如图 6-9 所示。准备好继续时,单击执行按钮。

img/460910_1_En_6_Fig9_HTML.jpg

图 6-9

应用配置-暂存(MySQL 安装程序)

当配置过程运行时,面板将变灰执行和返回按钮,并在每个步骤旁边的点上显示绿色复选标记,如图 6-10 所示。如果出现错误,面板将显示一个红色 X,如果错误禁止继续,可能会显示一条错误消息。

img/460910_1_En_6_Fig10_HTML.jpg

图 6-10

应用配置-完成(MySQL 安装程序)

当产品配置完成后,面板会发生变化,显示已配置产品的摘要,如图 6-11 所示。准备好后,请单击“下一步”按钮继续。

img/460910_1_En_6_Fig11_HTML.jpg

图 6-11

产品配置–摘要(MySQL 安装程序)

确认无误后,点击下一步按钮进入最后一个面板,如图 6-12 所示。您可以单击“完成”按钮来完成安装。

img/460910_1_En_6_Fig12_HTML.jpg

图 6-12

安装完成(MySQL 安装程序)

当所有操作完成后,安装程序将返回到欢迎面板,显示所有已安装产品的列表,如图 6-13 所示。完成后,您可以关闭安装程序,或者添加、修改、升级或移除其他产品。您还可以通过单击“重新配置”链接来重新配置任何已安装的产品。

img/460910_1_En_6_Fig13_HTML.jpg

图 6-13

安装程序欢迎面板-安装后(MySQL 安装程序)

配置路由时出现问题

如果您已经安装了 MySQL 路由,但是配置步骤失败了,这很可能是因为 root 用户的密码错误。这也可能是由于对 InnoDB 集群中的服务器使用了错误的端口(通常,这是单主服务器设置中的主服务器)造成的。如果您更正了这些信息,但仍然无法完成配置,请尝试连接到您的 InnoDB 集群,如第 5 章所述。当您可以成功连接到集群时,路由的配置应该会成功。如果您想再次运行配置,只需重新启动 MySQL 安装程序并单击 Reconfigure 链接即可重新运行配置。

现在我们已经在 Windows 上安装了路由,并为 InnoDB 集群进行了配置,让我们使用一个简单的 Python 脚本来测试路由。

清单 6-1 展示了一个通过路由连接到 InnoDB 集群的简单 Python 脚本。回想一下,我们在 Windows 机器上安装了路由,因此这个脚本(为了一致性,如果不是练习的话)应该在同一台机器上执行。花点时间检查一下代码。即使您从未使用过 Connector/Python 或编写过 Python,也应该很清楚发生了什么。

#
# Introducing MySQL InnoDB Cluster
#
# This example shows how to use the MySQL Router to connect to
# the cluster. Notice how connecting via the router port 6446
# results in a seamless transport to one of the cluster servers,
# in this case, the server with the primary role.
#
# Dr. Charles Bell, 2018
#
import mysql.connector

# Simple function to display results from a cursor
def show_results(cur_obj):
  for row in cur:
    print(row)

my_cfg = {
  'user':'root',
  'passwd':'secret',
  'host':'127.0.0.1',
  'port':6446   # <<<< Router port (R/W)
}

# Connecting to the server
conn = mysql.connector.connect(**my_cfg)

print("Listing the databases on the server.")
query = "SHOW DATABASES"
cur = conn.cursor()
cur.execute(query)
show_results(cur)

print("\nRetrieve the port for the server to which we're connecting.")
query = "SELECT @@port"
cur = conn.cursor()
cur.execute(query)
show_results(cur)

# Close the cursor and connection
cur.close()
conn.close()

Listing 6-1Router Connection Test

代码的第一部分导入连接器并定义一个连接术语字典——在本例中,是路由的用户、密码、主机和端口。我们使用的是端口号 6446,如 MySQL 安装程序中路由配置期间所示(参见图 6-8 )。

接下来,代码打开一个连接,然后运行两个查询;一个用于获取数据库列表并显示它们(使用定义为show_results()的函数),另一个用于选择服务器的当前端口。第二个查询结果可能会让您大吃一惊。

要执行代码,保存这个名为router_connect_test.py的文件(扩展名将其标识为 Python 脚本)。然后,使用以下命令运行代码:

python ./router_connect_test.py

运行后,您将看到如下输出:

Listing the databases on the server.
('information_schema',)
('mysql',)
('mysql_innodb_cluster_metadata',)
('performance_schema',)
('sys',)

Retrieve the port for the server to which we're connecting.
(3311,)

等等!为什么输出显示端口 3311?不应该显示端口 6446 吗?毕竟,这是我们在代码中使用的端口。回想一下,路由只是将通信路由到适当的服务器;它本身不是服务器连接。因此,路由成功地将我们的连接路由到端口 3311 上的机器。这台机器是主机器(在集群中列为读/写)。

那么,我们如何连接到集群中的只读服务器呢?我们需要做的就是修改程序来连接只读服务器(在端口 6447 上)。当我们重新运行该脚本时,我们将看到以下输出:

Listing the databases on the server.
('information_schema',)
('mysql',)
('mysql_innodb_cluster_metadata',)
('performance_schema',)
('sys',)

Retrieve the port for the server to which we're connecting.
(3312,)

现在我们看到我们连接到的不是端口 3311 上的服务器。回想一下沙箱设置,端口 3312、3313 和 3314 上的机器都是只读的。

虽然这个例子很简单,但它说明了路由如何将连接重定向到其他 MySQL 服务器。这个例子还有助于强化这样一个概念,即我们必须将我们的应用连接到路由本身,而不是连接到集群中的机器,并允许路由为我们完成所有繁重的连接路由。正如您所看到的,MySQL 路由非常复杂,它从初始(和后来缓存的)配置中知道基于使用的端口请求哪些服务器。在这种情况下,我们使用 6446 进行读/写连接,使用 6447 进行只读连接。是的,就是这么简单。不再有复杂的硬编码端口!

其他平台

如前所述,如果不是使用 Windows,仍然可以使用 MySQL 路由;路由不需要使用 Windows。然而,在其他平台上设置路由实际上需要更多的手动操作。

在其他平台上安装 MySQL 路由最好使用第 2 章中描述的特定于平台的存储库。本节演示如何在 Ubuntu 上使用 MySQL APT 库安装 shell。前提条件是您已经安装了存储库。

在安装了存储库并用sudo apt-get update更新了系统之后,您可以简单地发出sudo apt-get install mysql-router命令,如清单 6-2 所示。

$ sudo apt-get install mysql-router
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages were automatically installed and are no longer required:
  libevent-core-2.0-5 libllvm4.0 libqmi-glib1 snap-confine
Use 'sudo apt autoremove' to remove them.
The following NEW packages will be installed:
  mysql-router
0 upgraded, 1 newly installed, 0 to remove and 38 not upgraded.
Need to get 2,388 kB of archives.
After this operation, 15.3 MB of additional disk space will be used.
Get:1 http://repo.mysql.com/apt/ubuntu xenial/mysql-tools amd64 mysql-router amd64 8.0.11-1ubuntu16.04 [2,388 kB]
Fetched 2,388 kB in 20s (115 kB/s)
Selecting previously unselected package mysql-router.
(Reading database ... 309484 files and directories currently installed.)
Preparing to unpack .../mysql-router_8.0.11-1ubuntu16.04_amd64.deb ...
Unpacking mysql-router (8.0.11-1ubuntu16.04) ...
Processing triggers for libc-bin (2.23-0ubuntu10) ...
/sbin/ldconfig.real: /usr/lib/libmysqlcppconn.so.7 is not a symbolic link

Processing triggers for systemd (229-4ubuntu21.2) ...
Processing triggers for ureadahead (0.100.0-19) ...
Setting up mysql-router (8.0.11-1ubuntu16.04) ...
Processing triggers for libc-bin (2.23-0ubuntu10) ...
/sbin/ldconfig.real: /usr/lib/libmysqlcppconn.so.7 is not a symbolic link

Processing triggers for systemd (229-4ubuntu21.2) ...
Processing triggers for ureadahead (0.100.0-19) ...

Listing 6-2Installing the MySQL Router (Ubuntu)

现在路由已经安装好了,回想一下,我们还希望安装 Connector/Python,以便像在上一节中一样测试路由。我们使用清单 6-3 中所示的sudo apt-get install mysql-connector-python命令来安装连接器/Python。

$ sudo apt-get install mysql-connector-python
[sudo] password for cbell:
Reading package lists... Done
Building dependency tree
Reading state information... Done

The following packages were automatically installed and are no longer required:
  libevent-core-2.0-5 libllvm4.0 libqmi-glib1 snap-confine
Use 'sudo apt autoremove' to remove them.
The following NEW packages will be installed:
  mysql-connector-python
0 upgraded, 1 newly installed, 0 to remove and 38 not upgraded.
Need to get 174 kB of archives.
After this operation, 1,339 kB of additional disk space will be used.
Get:1 http://repo.mysql.com/apt/ubuntu xenial/mysql-tools amd64 mysql-connector-python all 8.0.11-1ubuntu16.04 [174 kB]
Fetched 174 kB in 6s (25.7 kB/s)
Selecting previously unselected package mysql-connector-python.
(Reading database ... 309396 files and directories currently installed.)
Preparing to unpack .../mysql-connector-python_8.0.11-1ubuntu16.04_all.deb ...
Unpacking mysql-connector-python (8.0.11-1ubuntu16.04) ...
Setting up mysql-connector-python (8.0.11-1ubuntu16.04) ...

Listing 6-3Installing the MySQL Connector/Python (Ubuntu)

如上所述,以这种方式安装 MySQL 路由不会像在 Windows 上演示的那样运行自定义配置步骤。要在其他平台上使用路由或进行自定义安装,我们必须手动配置路由以匹配我们的集群。

配置

配置 MySQL 路由并不难,但是配置项的数量可能会让它看起来很难。本节给出了一个简单的例子,说明如何配置 MySQL 路由,以便与运行在沙箱中的 InnoDB 集群一起使用;本节演示了使路由工作所需的手动步骤。我们还将使用上一节中显示的相同的简单 Python 脚本来测试路由设置。

在沙箱中设置 InnoDB 集群

让我们花点时间来回顾一下我们是如何在沙箱中设置 InnoDB 集群的。如果您没有尝试过上一章中的示例,或者您没有在 Windows 上使用路由,或者之前没有设置过路由,这在这一点上可能会有所帮助。回想一下第 5 章,我们有两个脚本通过 MySQL Shell 使用 Python 命令来设置和关闭一个示例集群。

在这种情况下,我们将使用第 5 章中所示的相同脚本来设置一个集群,运行一个写(主)和三个只读(辅助)服务器。但是,因为该脚本是为 Windows 编写的,所以我们必须对其稍加修改,以更改集群数据的默认目录。清单 6-4 显示了setup_idc_sandbox.py文件的修改版本,其中的变化以粗体显示。

# Introducing InnoDB Cluster

#
# This Python script is designed to set up an InnoDB Cluster in a sandbox.
#
# Note: Change the cluster directory to match your preferred directory setup.
#
# The steps include:
# 1) create the sandbox directory
# 2) deploy instances
# 3) create the cluster
# 4) add instances to the cluster
# 5) show the cluster status
#
# Dr. Charles Bell, 2018
#
import os
import time

# Method to deploy sandbox instance
def deploy_instance(port):
    try:
        dba.deploy_sandbox_instance(port, {'sandboxDir':'/home/cbell/idc_sandbox', 'password':'secret'})
    except:
        print("ERROR: cannot setup the instance in the sandbox.")
    time.sleep(1)

# Add instance to cluster

def add_instance(cluster, port):
    try:
        cluster.add_instance('root:secret@localhost:{0}'.format(port))
    except:
        print("ERROR: cannot add instance to cluster.")
    time.sleep(1)

print("##### STEP 1 of 5 : CREATE SANDBOX DIRECTORY #####")
os.mkdir('/home/cbell/idc_sandbox')

print("##### STEP 2 of 5 : DEPLOY INSTANCES #####")
deploy_instance(3311)
deploy_instance(3312)
deploy_instance(3313)
deploy_instance(3314)

print("##### STEP 3 of 5 : CREATE CLUSTER #####")
shell.connect('root:secret@localhost:3311')
my_cluster = dba.create_cluster('MyCluster', {'multiMaster':False})
time.sleep(1)

print("##### STEP 4 of 5 : ADD INSTANCES TO CLUSTER #####")
add_instance(my_cluster, 3312)
add_instance(my_cluster, 3313)
add_instance(my_cluster, 3314)

print("##### STEP 5 of 5 : SHOW CLUSTER STATUS #####")
shell.connect('root:secret@localhost:3311')
time.sleep(1)
my_cluster = dba.get_cluster('MyCluster')
time.sleep(1)
status = my_cluster.status()

print(status)

Listing 6-4Setup InnoDB Cluster Script (Ubuntu)

注意

您还必须更改脚本中的密码和路径,以匹配您的配置。

清单 6-5 显示了运行在 Ubuntu 上的这个脚本的摘录。回想一下,因为脚本使用的是 AdminAPI,所以我们必须使用 MySQL Shell 来执行它。

$ mysqlsh -f setup_idc_sandbox.py
##### STEP 1 of 5 : CREATE SANDBOX DIRECTORY #####
##### STEP 2 of 5 : DEPLOY INSTANCES #####
Deploying new MySQL instance...

Instance localhost:3311 successfully deployed and started.
Use shell.connect('root@localhost:3311'); to connect to the instance.
...
##### STEP 3 of 5 : CREATE CLUSTER #####
A new InnoDB cluster will be created on instance 'root@localhost:3311'.
...
##### STEP 4 of 5 : ADD INSTANCES TO CLUSTER #####
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Adding instance to the cluster ...

Validating instance at localhost:3312...
Instance detected as a sandbox.
...
##### STEP 5 of 5 : SHOW CLUSTER STATUS #####
{"clusterName": "MyCluster", "defaultReplicaSet": {"name": "default", "primary": "localhost:3311", "ssl": "REQUIRED", "status": "OK", "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", "topology": {"localhost:3311": {"address": "localhost:3311", "mode": "R/W", "readReplicas": {}, "role": "HA", "status": "ONLINE"}, "localhost:3312": {"address": "localhost:3312", "mode": "R/O", "readReplicas": {}, "role": "HA", "status": "ONLINE"}, "localhost:3313": {"address": "localhost:3313", "mode": "R/O", "readReplicas": {}, "role": "HA", "status": "ONLINE"}, "localhost:3314": {"address": "localhost:3314", "mode": "R/O", "readReplicas": {}, "role": "HA", "status": "ONLINE"}}}, "groupInformationSourceMember": "mysql://root@localhost:3311"}

Listing 6-5Setting Up the InnoDB Cluster in a Sandbox

修改配置文件

MySQL 路由将其初始配置存储在一个文件中。该文件设置路由与集群配合工作的参数,包括故障转移协议、路由信息、为应用通告(监听)的端口以及集群本身的连接元数据等。

小费

参见在线 MySQL 路由文档( https://dev.mysql.com/doc/mysql-router/8.0/en/ )了解 MySQL 路由所有选项和配置项的完整说明。

尽管路由没有任何平台关联性(它在所有平台上都运行良好),但配置文件的位置可能会有所不同。对于 Windows,配置文件位于名为mysqlrouter.confC:\ProgramData\MySQL\MySQL Router文件夹中。在 Linux 上,配置文件位于名为mysqlrouter.conf/etc/mysqlrouter/文件夹中。

您可以通过运行命令mysqlrouter --help立即找到文件的位置以及大量其他信息,该命令不仅记录了所有选项,还显示了文件的位置。清单 6-6 显示了输出的摘录。

$ mysqlrouter --help
MySQL Router v8.0.11 on Linux (64-bit) (GPL community edition)
Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
...
Configuration read from the following files in the given order (enclosed
in parentheses means not available for reading):
  /etc/mysqlrouter/mysqlrouter.conf
  (/home/cbell/.mysqlrouter.conf)
Plugins Path:
  /usr/lib/x86_64-linux-gnu/mysqlrouter
Default Log Directory:
  /var/log/mysqlrouter
Default Persistent Data Directory:
  /var/lib/mysqlrouter
Default Runtime State Directory:
  /run/mysqlrouter

...

Listing 6-6MySQL Router Help (Ubuntu)

现在我们知道集群正在运行,并且安装了 MySQL 路由,让我们看看路由的默认配置文件。清单 6-7 显示了典型 Linux 安装的配置文件。

 [DEFAULT]
logging_folder = /var/log/mysqlrouter/
plugin_folder = /usr/lib/x86_64-linux-gnu/mysqlrouter
runtime_folder = /var/run/mysqlrouter
config_folder = /etc/mysqlrouter

[logger]
level = INFO

# If no plugin is configured that starts a service, keepalive
# will make sure MySQL Router will not immediately exit. It is
# safe to remove after Router is configured.
[keepalive]
interval = 60

Listing 6-7Default Router Configuration File

请注意文件中的最后几行。当我们完成配置时,我们将看到这些被删除。还要注意,文件中没有关于集群配置的信息。完成路由配置需要两个基本组件。首先,我们必须设置正确的路径,以确保路由能够访问正确的密码加密机制。其次,我们必须在初始配置中为每台服务器配置所有路由参数和策略。虽然您可以自己手动操作,但使用--bootstrap选项可以快速启动。

使用引导选项进行配置

特殊的引导选项将连接到您的集群,读取元数据,然后自动更新配置文件。如果您正在使用沙箱安装,这是设置路由的最快和最可靠的方法。即使您没有使用沙箱安装,也可以使用这种方法快速设置基本配置,以后可以根据需要进行更改。

让我们看看如何使用引导选项。要使用该选项,我们需要其他参数。简而言之,我们必须提供用于保护配置文件的连接信息和用户。我们还将添加一个可选参数来为配置提供一个名称,这在您使用不同的集群或配置时会很有帮助。以下是我们需要的参数:

  • --bootstrap <server_url>:引导并配置路由,以便与 MySQL InnoDB 集群一起运行。也可以使用快捷键-B

  • --name (optional):给路由实例一个符号名。

  • --user <username>:以指定名称的用户身份运行mysqlrouter(在 Windows 上不可用)。你也可以使用快捷键-u

在本例中,我们以 URI(如<username>:<password>@<hostname>:<port>)的形式提供带有引导选项的连接信息。我们将使用本地用户,以便通过沙箱安装更容易地运行路由,沙箱安装也在当前用户下运行。我们使用提升的权限,因为路由文件的默认位置是受保护的。以下是命令:

sudo mysqlrouter --bootstrap root:secret@localhost:3311 --name sandbox --user cbell

当我们运行这个命令时,路由将联系我们指定的服务器,并检索集群的所有元数据,为我们自动创建路由。清单 6-8 显示了该命令的输出示例。

$ sudo mysqlrouter --bootstrap root:secret@localhost:3311 --name sandbox --user cbell

Bootstrapping system MySQL Router instance...
Module “ not registered with logger - logging the following message as 'main' instead
MySQL Router 'sandbox' has now been configured for the InnoDB cluster 'MyCluster'.

The following connection information can be used to connect to the cluster.

Classic MySQL protocol connections to cluster 'MyCluster':
- Read/Write Connections: localhost:6446
- Read/Only Connections: localhost:6447
X protocol connections to cluster 'MyCluster':
- Read/Write Connections: localhost:64460
- Read/Only Connections: localhost:64470

Existing configurations backed up to '/etc/mysqlrouter/mysqlrouter.conf.bak'

Listing 6-8Configuration with the Bootstrap Option

请注意,路由已经分别使用默认端口 6446 和 6447 识别了读/写和只读连接。我们还看到,引导步骤分别在端口 64460 和 64470 上创建了使用 X 协议的路由。在我们测试路由之前,让我们了解一下自举方法为我们做了什么。具体来说,我们将查看修改后的配置文件。

使用引导配置文件

bootstrap 选项创建的配置文件包含在应用和集群中使用路由所需的所有信息。它使用所有设置的默认值和集群中的值来设置队列和端口。经典协议的默认端口为 6446、6447,X 协议的默认端口为 64460、64470。

清单 6-9 显示了清单 6-8 中所示的执行所产生的配置文件。

# File automatically generated during MySQL Router bootstrap
[DEFAULT]
name=sandbox
user=cbell
keyring_path=/var/lib/mysqlrouter/keyring
master_key_path=/etc/mysqlrouter/mysqlrouter.key
connect_timeout=30
read_timeout=30

[logger]
level = INFO

[metadata_cache:MyCluster]
router_id=10
bootstrap_server_addresses=mysql://localhost:3311,mysql://localhost:3312,mysql://localhost:3313,mysql://localhost:3314
user=mysql_router10_r7dqvu1t52bj
metadata_cluster=MyCluster
ttl=5

[routing:MyCluster_default_rw]
bind_address=0.0.0.0
bind_port=6446
destinations=metadata-cache://MyCluster/default?role=PRIMARY
routing_strategy=round-robin
protocol=classic

[routing:MyCluster_default_ro]
bind_address=0.0.0.0
bind_port=6447
destinations=metadata-cache://MyCluster/default?role=SECONDARY
routing_strategy=round-robin
protocol=classic

[routing:MyCluster_default_x_rw]
bind_address=0.0.0.0
bind_port=64460
destinations=metadata-cache://MyCluster/default?role=PRIMARY
routing_strategy=round-robin
protocol=x

[routing:MyCluster_default_x_ro]
bind_address=0.0.0.0
bind_port=64470
destinations=metadata-cache://MyCluster/default?role=SECONDARY
routing_strategy=round-robin
protocol=x

Listing 6-9Bootstrap Configuration File

哇,这与我们之前看到的简短默认配置文件非常不同。这看起来可能很复杂,但实际上并不复杂。让我们看看配置文件中的每个部分。更具体地说,该文件的格式类似于许多其他配置文件:方括号用于定义节,每个节中有一个或多个带有赋值的关键字(变量)(有时称为键/值对)。

[默认]

默认部分是放置路由实例的所有常规设置的地方,即路由正确执行所需的所有参数。下面总结了示例中出现的变量及其用法:

  • name:引导选项中为集群提供的名称

  • user:用于访问路由实例的配置文件和元数据的用户名

  • keyring_path:密匙环安全文件的路径

  • master_key_path:key ring 主密钥文件的路径

  • connect_timeout:尝试连接时超时的秒数

  • read_timeout:等待读取请求时超时的秒数

如您所见,bootstrap 步骤为所有这些变量提供了默认值,除了 name 和 user,它们是在命令行上指定的。

[记录者]

logger 部分没有修改默认文件。在这种情况下,它只记录信息性消息。要了解有关记录选项的更多信息,包括如何自定义记录的内容,请参见在线参考手册中的“使用记录功能”一节( https://dev.mysql.com/doc/mysql-router/8.0/en/mysql-router-server-logging.html )。

[元数据 _ 缓存:我的群集]

本节定义了集群的初始元数据。此示例使用以下变量:

  • router_id:和server_id一样,这是路由实例的唯一值,每个路由实例都应该有自己的 ID 值。

  • bootstrap_server_addresses:初始队列中的服务器列表。

  • user:使用密匙环服务连接到服务器的自动生成用户。

  • metadata_cluster:集群的名称。

  • ttl:元数据高速缓存中信息的生存时间(秒)。

在本节中,您应该了解更多的两个关键变量是bootstrap_server_addressesuserbootstrap_server_addresses变量需要集群中每个服务器的host:port值的逗号分隔列表。因为引导操作连接到群集,所以它为我们检索信息。这里,列表包括集群中的所有服务器。这里的user变量是一个自动生成的用户,用于密匙环协议。你不需要改变这个。

如果手动构建配置文件,bootstrap_server_addresses是获得正确结果的最关键的值。确保包括群集中所有已知的服务器。请记住,路由会随着集群拓扑的变化自动更新其缓存,但它需要一个稳定的起点,这个变量提供了该数据。

[路由: ]

此部分是连接路由部分,用于定义路由为应用提供的连接路由。路由按类型组织,如读/写和只读。对于我们想要定义的每种类型的路线,该部分重复一次。在这种情况下,我们有四个路由:一个用于使用传统(经典)协议的读/写,另一个用于只读,一个用于使用 X 协议的读/写,还有一个用于使用 X 协议的只读。对于每个部分,我们指定以下变量。每个部分名称必须以routing:开头,后跟唯一的名称标签。在这种情况下,引导操作选择描述每个连接的读/写状态和所用协议的名称。

  • bind_address:路由绑定的地址,如果没有定义端口,也使用bind_port

  • bind_port:默认端口bind_address使用

  • destinations:以逗号分隔的 MySQL 服务器列表的形式路由目的地

  • routing_strategy:路由策略(可选),路由如何选择目的 MySQL 服务器

  • protocol:用于值为classic的连接的协议(也称为传统协议),或者简称为 X 协议的x

bind_addressbind_port变量特定于用于定义连接的服务器。类似地,protocol让我们可以选择用于连接的协议。因此,我们看到读/写和只读路由的两个连接路由:一个用于 classic,另一个用于 X 协议。此外,指定的端口是路由侦听连接路由的端口,而不是路由将路由定向到的服务器的端口。这可能是配置连接路由时需要掌握的最重要的概念。

这里最有趣的是destinations变量。此变量为建立连接提供主机信息。它接受逗号分隔的目标地址列表或指向群集的元数据缓存链接。引导操作生成元数据缓存选项。该字符串定义如下:

destinations=metadata-cache://mycluster/default?role=PRIMARY

角色可以是PRIMARYSECONDARYPRIMARY_AND_SECONDARY。这决定了连接可用的实例类型。

最后,routing_strategy可以用来改变路由选择队列中下一个服务器的方式。默认值为round-robin,但也可以是以下值之一:

  • first-available:新连接被路由到目的地列表中第一个可用的服务器。如果失败,将使用下一个可用的服务器。这个循环一直持续到所有服务器都不可用。

  • next-available:与first-available类似,新连接被路由到目的地列表中第一个可用的服务器。与first-available不同,如果一台服务器被标记为不可达,它就会被丢弃,并且永远不会再被用作目的地。

  • 为了负载平衡,每个新连接都以循环方式连接到下一个可用的服务器。

  • round-robin-with-fallback:为了负载平衡,每个新连接都以循环方式连接到下一个可用的辅助服务器。如果辅助服务器不可用,则以循环方式使用主列表中的服务器。

花点时间再次浏览文件,看看每个连接路由是如何配置的。一旦你习惯了这些术语,就不会觉得奇怪了。

等等,其他服务器的连接在哪里?

如果您想知道为什么集群中的其他服务器没有连接路由,您必须记住,路由被设计为查询集群中的元数据并自动选择服务器。不需要为每个服务器指定特定的连接路由。相反,我们为每种类型的服务器定义路由:读/写、只读等等。

现在,您对如何配置路由有了更多的了解,让我们开始测试吧。

启动路由

最后,我们可以使用以下命令启动路由:

$ mysqlrouter &

这将启动路由,它将读取配置文件。请注意,我们没有使用提升的特权。这是因为我们在引导步骤中提供了一个用户选项,允许用户读取文件。这对于保护您的安装非常重要,您将在后面的章节中探讨这一点。

现在我们已经配置了路由,让我们用示例 Python 连接脚本来测试它。

测试新配置

为了测试路由,我们将使用清单 6-1 (名为router_connect_test.py)中的 Python 脚本。回想一下,这是一个包含 Python 中 AdminAPI 调用的脚本。我们必须从 MySQL Shell 运行这个脚本。幸运的是,它不需要任何改变就可以在 Ubuntu 上运行。但是,如果您遵循前面的示例,您应该更改连接字典以连接到读/写端口 6446,如下所示:

my_cfg = {
  'user':'root',
  'passwd':'secret',
  'host':'127.0.0.1',
  'port':6446   # <<<< Router port (R/W)
}

回想一下,我们使用python ./router_connect_test.py命令告诉 shell 加载 Python 脚本并执行它。当您这样做时,您应该会看到如下所示的输出:

$ python ./router_connect_test.py
Listing the databases on the server.
(u'information_schema',)
(u'mysql',)
(u'mysql_innodb_cluster_metadata',)
(u'performance_schema',)
(u'sys',)

Retrieve the port for the server to which we're connecting.

(3311,)

在这里,我们看到我们确实通过路由连接到了 InnoDB 集群,报告的端口是 3311,这是主(读/写)服务器的端口。酷!

演示如何手动安装和配置路由到此结束。对于大多数安装,这是使路由为您的应用工作所需要的。但是,如果您需要更高级的路由或特定选项,您可以查看所有可用选项的在线参考手册,如果您计划为多个单主模式配置您的集群,或者您想要在多个应用服务器上部署路由的多个实例,强烈建议您查看该手册。例如,如果您想要使用 SSL 连接运行路由,请参见以--ssl*开头的选项。

小费

要了解有关配置路由的更多信息,包括设置不同的路由策略,请参见在线参考手册 https://dev.mysql.com/doc/mysql-router/8.0/en/mysql-router-configuration.html

下一节将介绍在应用中使用路由的一些方面,包括修改应用以充分利用路由的技巧和建议。

在应用中使用路由

至此,我们有了一个基本的工作路由实例,它将连接路由到我们的 InnoDB 集群。您已经看到了我们如何使用应用中路由配置中列出的端口连接到集群。具体来说,我们有以下可用端口,它们映射到路由配置文件中定义的连接路由:

  • 6446:用于读/写操作的经典协议连接(例如,PRIMARY)

  • 6447:只读操作的经典协议连接(例如,SECONDARY)

  • 64460:用于读/写操作的 X 协议连接(例如,PRIMARY

  • 64470:只读操作的 X 协议连接(如SECONDARY

太棒了,对吧?但是这对我们的应用意味着什么呢?让我们后退一步,考虑一下在我们的应用中使用路由的条件和后果。

首先,理解 MySQL 路由不需要特定的库或接口是很重要的。它是自包含的,没有任何先决条件或依赖性,例如需要服务器实例才能工作。这对于规划您的应用部署来说是个好消息。您可以在您的应用服务器上部署路由,而不用担心依赖性冲突或者甚至性能影响。

其次,在应用中使用路由时,您应该始终连接到连接路由中定义的端口,而不要直接连接到集群服务器本身。此外,您应该为您的连接建立一个重试机制。这是必要的,因为当路由检测到服务器脱机时,它可能会断开连接,并与下一台服务器重新建立连接。更具体地说,由于 MySQL 路由在尝试连接时重定向连接,并且不读取数据包或执行分析,因此如果 MySQL 服务器出现故障,路由会将连接错误返回给应用。因此,应该编写应用来测试连接错误,如果遇到错误,就重试连接。幸运的是,开发者通常将这种机制构建到他们的应用中,因此您可能只需将连接更改为路由的连接即可。

让我们考虑一些在您的应用中实现路由的用例。当然,路由的使用方式有很多,下面仅列出了一些较为常见的使用案例:

  • 我希望我的应用连接到一个服务,这样默认情况下,它可以连接到组复制集群的当前主服务器。

  • 我想设置多个服务,所以 MySQL 路由监听每个高可用性副本集(集群)的不同端口。

  • 我希望能够在端口 3306 上运行连接路由服务,这样对用户或应用来说就更加透明了。

  • 我想为每个连接路由服务配置一个模式,这样我就可以指定返回主服务还是辅助服务。

最后,让我们讨论一下路由的工作流程,这样您就可以了解它如何适合规划您的应用。以下绝不是唯一的工作流,但它是大多数使用案例的典型:

  1. 应用连接到 MySQL 路由,例如连接到端口 6446。

  2. 路由检查可用的 MySQL 服务器。

  3. 路由打开到合适的 MySQL 服务器的连接。

  4. 路由在应用和 MySQL 服务器之间来回转发数据包。

  5. 如果连接的 MySQL 服务器出现故障,路由会断开应用。

  6. 然后,应用可以重试连接到路由,路由选择一个不同的可用 MySQL 服务器。

在下一章的典型部署场景中,我们将探索如何使用 InnoDB 集群,同时我们将考虑在我们的应用中使用 MySQL 路由的更多含义。

摘要

我们现在有了一个完整的高可用性故事。尽管 MySQL InnoDB Cluster 为我们的数据提供了高可用性,但它并不能(直接)帮助我们在应用级别实现高可用性。是的,您可以编写您的应用来查询集群,并获取信息,以便在服务器离线时帮助您的应用“恢复”,但是实践表明这是一个脆弱的解决方案,过于依赖于已知的参数。如果集群配置发生任何变化,应用可能会失败,或者需要重新启动。

这对大多数组织来说并不理想。我们需要的是能够快速轻松地使我们的应用适应集群中的变化。更具体地说,如果集群中的服务器脱机或被脱机或其角色改变,应用不应该停止。这就是 MySQL 路由大放异彩的地方。

MySQL Router 将连接路由的负担从应用中分离出来,并将其放在自己的轻量级、易于配置的实例中。现在,可以构建应用来依赖路由进行所有连接路由,包括故障转移事件或正常的高可用性恢复事件。

由于路由体积小、重量轻,您甚至可以通过让多个实例运行单独的连接路由,在路由中构建冗余。MySQL 路由有什么不喜欢的地方?不多。这是许多高可用性解决方案似乎缺少的一部分。

下一章将介绍如何在一组机器上部署 MySQL InnoDB 集群,并使用一个简单的应用配置路由,以展示如何为自己的应用实现高可用性目标。

七、MySQL 高可用性部署示例

到目前为止,进入 InnoDB 集群的旅程一直是探索和实验的过程。为了使 InnoDB Cluster 对您的基础设施真正有用和有意义,我们必须使用真实的数据将它部署到真实的机器上。尽管对一个正在运行的生产系统进行演练可能是有益的,但对于那些第一次探索 InnoDB Cluster 和 MySQL 高可用性的人来说,这可能太超前了。我们无法从沙箱等模拟环境进入产品部署。没有管理员会这么做。我们必须从可以用来试验和测试我们的应用的开发部署开始。只有经过仔细的测试,我们才能将其推广到我们的生产环境中。

在本章中,我们将探索一个使用独立服务器的开发环境,以展示 MySQL 的通用高可用性部署。 1 我们将在几台机器上安装 MySQL,在其中建立一个 InnoDB 集群,部署一台应用服务器,通过 MySQL 路由托管一个简单的应用。我们将使用 MySQL Shell 来完成这一切。我们开始吧!

建立服务器

本节概述了如何建立模拟小型 InnoDB 集群部署所需的服务器。我们必须做的第一件事是设置和配置几台服务器。只要 MySQL 支持,你可以使用任何你想要的操作系统和硬件。Oracle 花费了很长时间努力让 MySQL 运行在几乎所有的现代平台和最新版本上。假设您使用其中的一个,安装 MySQL 应该没有任何问题。但是,如果您想完成这个演练,我们将建立五台服务器,在每台服务器上安装 MySQL 产品。所有服务器还需要安装 MySQL Shell。我们将使用以下内容:

  • MySQL 服务器:我们需要四台运行 MySQL 8.0.11 或更高版本的服务器。

  • 应用服务器:我们需要一台运行我们的示例应用和 MySQL Router 8.0.11 或更高版本的服务器。

另一件要考虑的事情是您要在这次演示中使用的硬件。您可以使用典型的商用硬件,或者选择一种不太传统但更经济的替代方案,使用 Raspberry Pi 计算机。我们将在这一部分简要讨论每个选项,但是演练使用的是 Raspberry Pi。

Raspberry Pi 计算机体积小,价格便宜,但考虑到处理器速度较慢和内存有限,它们运行得相对较好。您不太可能将整个生产基础架构建立在 Raspberry Pi 计算机上。但是使用 Raspberry Pi 计算机有助于试验新技术、开发安装和类似的非关键任务实现。

为什么是树莓派?

如果你想知道为什么有人会选择在开发环境中使用 Raspberry Pi 计算机,现在考虑一下成本。一杯典型的 3B(或 3B+)树莓酒大约要 50 美元。即使添加所需的大容量存储设备和机箱,您也可以轻松组装一台运行 MySQL 8.0.11 的相当快速和稳定的 Raspberry Pi 计算机,价格不到 100 美元。

另外,你不需要为每一个树莓派配备键盘、鼠标和显示器。配置好之后,您可以通过远程访问(sshscpmysqlsh)来无头重启它(没有键盘、鼠标和显示器)。事实上,在所有的服务器都配置好之后,您就不需要键盘、鼠标和显示器了。您可以简单地从另一个系统或您的备件中借用它们。 2

也许更有趣的是,树莓派非常小,超便携。事实上,您可以在比典型的微型塔式服务器更少的空间内建立一个完整的 Raspberry Pi 机器集群。最后,Raspberry Pi 易于使用,因为它运行 Linux 的一个变体,这使得它为许多开发者和管理员所熟悉。

在商用硬件上安装 MySQL

如果您选择使用更传统的 PC 或服务器硬件,您可以这样做,但建议(但不是必须)您对所有服务器使用相同的硬件。这种常见的做法使维修更容易,允许您切换机器,而不必适应或修改硬件差异。

同样,建议您对每台服务器使用相同的操作系统。同样,这不是一个要求,但它使事情变得更容易。也就是说,对 MySQL 服务器使用一个平台,对应用服务器使用另一个平台是常见的做法。例如,您可以将 Ubuntu 用于 MySQL 服务器,将 Windows 用于应用服务器。

无论如何,如果您想使用任何可用的机器自己运行这个演练,这是没问题的。用一组平台和操作系统不同的机器进行实验是很常见的。例如,如果你有几台运行 Ubuntu 和 Windows 的台式电脑,以及一台或两台运行 macOS 的笔记本电脑,你仍然可以使用所有这些,因为 MySQL 运行在所有这些平台上。

混合使用旧硬件的唯一问题可能是您的一个平台使用了不同的字节顺序。 3 字节序是指字节在内存或通信中的顺序。虽然这不是大多数人关心的问题,但是如果您使用任何旧的 Oracle Sun 服务器或类似的平台,您可能希望忽略它们。这并不意味着它们不起作用——恰恰相反——但它们可能会增加你可能不想(或不需要)经历的复杂程度。

下面总结了在商用硬件上安装 MySQL 以便与 InnoDB Cluster 一起使用的必要步骤。因为您已经看到了如何安装 MySQL、MySQL Shell 和其他产品,所以我们将安装的细节推迟到前面的章节。以下是基本步骤:

  1. 安装操作系统。

  2. 将操作系统配置为用作服务器(网络、用户帐户等)。

  3. 安装 MySQL 服务器。

如果你有设置桌面或服务器硬件的经验,这个列表应该不会令人惊讶。您也可能有一些通常用于安装新计算机的替代过程。在设置服务器和安装 MySQL 时,请随意使用您自己的判断。

在每台服务器上安装 MySQL Shell 也是一个好主意。如果您以前没有下载和安装过 MySQL,请参考前面章节中的安装和一般配置示例。

小费

请参阅在线参考手册( https://dev.mysql.com/doc/refman/8.0/en/installing.html )中的“安装和升级 MySQL”一节,了解有关在特定平台上安装 MySQL 的更多信息。

在 Raspberry Pi 上安装 MySQL

在本章中,我们将使用 Raspberry Pi 计算机,而不是更昂贵的主流服务器硬件。如果您想继续学习并使用更传统的服务器硬件,请随意浏览详细介绍 Raspberry Pi 设置和编译 MySQL 的部分。然而,Raspberry Pi 上使用的命令与您在典型的基于 Linux 的平台上使用的命令相同或相似。

唯一可能让那些不知道如何使用 Raspberry Pi 的人犹豫的是 MySQL 8.0.11 缺少 Raspberry Pi 安装包。更具体地说,没有 32 位 ARM 二进制文件。尽管对基于 ARM 的企业操作系统的 64 位二进制文件有一些需求,但对 32 位 Raspberry Pi 操作系统的商业需求很少。因此,尽管我们可能会看到对基于 ARM 的平台的一些支持(目前通过第三方来源),但找到 32 位二进制文件更具挑战性。幸运的是,因为 MySQL 是开源的,我们可以自己下载源代码、编译和安装。事实上,我们将在本演练中做到这一点。以下是准备用于 InnoDB 集群的 Raspberry Pi 计算机的必要步骤:

  1. 下载拉斯扁图像(或 NOOBS)。

  2. 准备好 USB 驱动器(或 SD 卡)。

  3. 配置 Raspbian(网络、用户帐户等)。

  4. 构建 MySQL。

  5. 手动安装 MySQL。

  6. 配置 MySQL。

  7. 构建 MySQL Shell。

  8. 安装 MySQL Shell。

注意

我们将在所有机器上构建和安装 MySQL Shell,并在应用服务器上安装 MySQL 路由。

ARM64 怎么样?

如果您正在使用 64 位平台,并且希望在 Raspberry Pi 上使用 64 位操作系统,您可以这样做。Oracle 最近宣布支持用于 Oracle Enterprise Linux 7.5 的 Raspberry Pi 3(https://blogs.oracle.com/linux/announcing-the-general-availability-of-oracle-linux-7-for-arm)。更重要的是,Oracle 还包括一个 yum 存储库,其中包含 MySQL Server 8.0.11 及其组件。因此,如果您想使用 64 位操作系统,或者您更熟悉 Oracle Linux、Red Hat 或 Fedora,那么您会想要查看用于 Raspberry Pi 3 的 Oracle Enterprise Linux 的定制版本。

我写了一篇完整的教程,介绍如何使用 Oracle Enterprise Linux 在 Raspberry Pi 3 上设置 InnoDB 集群。参见我的博客“MySQL 8.0 InnoDB Cluster on arm 64 with Oracle Linux and the Raspberry Pi 3B”(http://drcharlesbell.blogspot.com/2018/06/mysql-80-innodb-cluster-on-arm64-with.html)了解详细的演练。与本章一样,本教程将向您展示如何为集群实例构建映像,以及如何设置应用服务器。

那些更熟悉 Raspbian 或不熟悉 Raspberry Pi 的人可能想先学习本章的教程,然后再学习 ARM64 的教程。正如您将看到的,步骤是相似的,但命令和设置过程有所不同。

这个列表类似于您在商用硬件上设置 MySQL 的过程,但是构建和配置步骤是使 MySQL 在 Raspbian 上工作所必需的。值得注意的是,这些额外的步骤并不是 Raspbian 独有的。事实上,从源代码构建、安装和配置 MySQL 是使用安装包的可行替代方案。在 Raspbian(和其他平台)上之所以有必要,是因为没有 Raspbian 的安装包。您可以在在线参考手册( https://dev.mysql.com/doc/refman/8.0/en/source-installation.html )的“从源代码安装 MySQL”一节中找到为各种平台构建 MySQL 的说明。

如果您选择使用 Raspberry Pi,以下部分将演示如何设置每台计算机运行最新版本的 Raspberry Pi 默认操作系统。我们从一个关于设置 Raspberry Pi 的简短教程开始,它包含了这个过程的前三个步骤。

Raspberry Pi 设置教程

Raspberry Pi 是一台拥有惊人的功能和通用性的个人电脑。您可能会认为它是一个玩具或一个严重受限的平台,但这与事实相差甚远。通过添加 USB、以太网和 HDMI 视频等板载外围设备,Raspberry Pi 具备了轻型台式计算机所需的一切。如果您考虑添加通用输入输出(GPIO)头,Raspberry Pi 就不仅仅是一台简单的桌面计算机,它还是一个旨在促进硬件实验的计算系统。

以下部分提供了一个简短的教程,介绍如何开始使用新的 Raspberry Pi,从裸板到完全可操作的平台。

注意

一些优秀的作品更详细地讨论了这个主题。如果你发现自己被困住了,或者想知道更多关于开始使用 Raspberry Pi 和 Raspbian 操作系统的信息,请参阅 Peter Membrey 和 David Hows 的Learn Raspberry Pi with Linux(a press,2012)。如果您想了解更多关于在硬件项目中使用 Raspberry Pi 的信息,一个很好的资源是 Brendan Horan 的实用 Raspberry Pi(a press,2013)。

入门指南

树莓电脑没有任何配件。对于本演练,您至少需要一个至少 16GB 的 USB 驱动器(或 SD 卡)、一个额定电流为 700mA 或更高的 USB 电源(带有一个公微型 USB 连接器)、一个键盘、一个鼠标和一个 HDMI 显示器或一个带有 HDMI 适配器的 DVI 显示器。然而,在您将这些东西插入您的 Raspberry Pi 并享受它的光辉之前,您需要为您的 USB 驱动器创建一个引导映像。幸运的是,大多数人都有备用的键盘、鼠标和显示器,除了输入设备的 USB 接口,显示器需要正常大小的 HDMI 电缆。

此外,建议您使用 USB 驱动器,如小型 SanDisk Cruzer Fit,这是一种相对快速且便宜的小型驱动器(参见 www.sandisk.com/home/usb-flash/cruzer-fit )。SD 卡更脆弱,更容易损坏,因此不太适合密集使用,就像我们在 InnoDB Cluster 中看到的那样。但是,你仍然可以使用 SD 卡。请务必正确关闭您的设备,并小心处理 SD 卡。

小费

为了获得最佳效果,请使用 USB 拇指驱动器来存储操作系统和文件。

安装启动映像包

安装启动映像的过程包括选择映像、下载映像,然后将其复制到 USB 驱动器(或 SD 卡)中。对于这个演练,我们将使用来自 raspberrypi.org 的 Raspbian 图像。我们将下载这个文件,提取它(如果压缩),并将图像复制到 USB 驱动器。有多种方法可以做到这一点,但是我们将使用 Etcher 应用,它适用于大多数平台。我们开始吧!

我们将构建一台服务器,安装 MySQL 并对其进行配置,而不是为所有服务器安装相同的基础映像。然后,我们将克隆该驱动器,并在其他服务器上重新创建它。正如您将看到的,这将为您节省大量时间。唯一的问题是,我们必须对每个克隆的服务器做一些小的修改来完成设置,但是大部分繁重的工作已经完成了。

通过访问 www.raspberrypi.org/downloads/raspbian/ 开始图像创建过程,并下载带有桌面图像的 Raspbian Stretch。尽管我们通常不会在生产服务器上为操作系统使用图形用户界面,但它使使用 Raspberry Pi 变得更加容易(并且为一些人所熟悉)。下载.zip文件(例如2018-04-18-raspbian-stretch.zip,下载完成后解压。这将产生一个镜像文件(例如,2018-04-18-raspbian-stretch.img),我们将使用 Etcher 将其写入 USB 驱动器。

小费

如果您想了解树莓派可用的其他操作系统,请参见 www.raspberrypi.org/downloads/

当我们下载完图像后,我们需要将它复制到 USB 驱动器(或 SD 卡)中。为此,我们将使用 Etcher,它是在 Linux、macOS 和 Windows 上工作的格式化程序。如果你访问 Etcher 网站, https://etcher.io /,你可以选择平台并下载其安装程序。然后,您可以在您的系统上安装 Etcher,遵循您的平台的正常安装过程。

当你准备好了,把你的 u 盘插入你的电脑,启动 Etcher。用户界面很直观。我们需要做三件事:选择我们想要写入的映像,选择要写入的驱动器,并开始映像传输。因此,对于这一步,我们选择之前下载的 Raspbian 的映像,然后选择我们想要使用的 USB 驱动器。图 7-1 显示了完成这两个步骤的示例。

img/460910_1_En_7_Fig1_HTML.jpg

图 7-1

使用蚀刻机安装 Raspbian (USB 驱动器)

当您检查以确保您选择了正确的映像和驱动器时,您可以单击闪存!开始图像复制。复制完成后,Etcher 将验证图像。如果一切正常,Etcher 会给你一个信息,告诉你过程成功了。完成后,您可以关闭 Etcher 并移除 USB 驱动器/SD 卡。现在,它已经可以用在你的树莓派中了。

注意

如果您使用 Windows,您的系统可能会告诉您,它已检测到一个需要在复制后格式化的驱动器。忽略那些消息!不要格式化驱动器,否则会覆盖图像。

初始服务器配置

下一步是在 Raspberry Pi 中插入 USB 驱动器,打开它,并配置它与 MySQL 一起使用。这是需要键盘、鼠标和显示器的地方。将它们连接到 Raspberry Pi,插入 USB 驱动器,并连接电源。电源通常是 USB 5V 电源,如平板电脑或手机的充电器。只要保证至少 700 毫安就行了。树莓皮没有电源开关,所以当你插上电源,树莓皮就会启动。

大约一分钟后,您应该会在屏幕上看到一系列树莓标志,后面是一长串启动消息。这些都很正常。最终,系统将自动登录并启动到图形用户界面,这是很好的,简单易用。第一次启动可能会慢一点,但后续启动会快一点。

我的树莓派无法从 USB 启动!

如果您的 Raspberry Pi 无法从 USB 驱动器启动,您必须在固件中设置一个特殊的一次性写入位。大多数较新的 Raspberry Pi 3 电脑将从 USB 驱动器自动启动。 www.raspberrypi.org/documentation/hardware/raspberrypi/bootmodes/msd.md。如果你的 Raspberry Pi 不能从 USB 启动。

简而言之,我们必须准备一个 SD 卡映像,并修改它以允许从 USB 启动,然后用这个命令启用 USB 启动模式

echo program_usb_boot_mode=1 | sudo tee -a /boot/config.txt.

这会将program_usb_boot_mode=1添加到/boot/config.txt的末尾。完成后,用 SD 卡重启 Raspberry Pi,然后关机。取出 SD 卡并插入 USB 驱动器。打开 Raspberry Pi,它应该从 USB 驱动器启动。

要配置机器用于本演练,必须执行以下任务。请记住,我们对一个映像执行此操作,稍后会将其复制到其他服务器:

  1. 更改 root 密码。

  2. 启用 SSH。

  3. 设置键盘区域/语言。

  4. 配置网络。

以下部分描述了如何执行这些任务。

更改 Root 密码

root 用户帐户被命名为pi,密码为raspberry。改变这一点,这样窥探的眼睛就不会进入你的电脑,无意中改变事情。对于从未用过树莓的人来说,尤其是使用 Linux 的人来说,玩树莓是多么诱人,你会感到惊讶。选择一个您容易记住的密码。因为我们正在克隆该驱动器以供其他服务器使用,所以在此处更改密码可以确保所有系统使用相同的密码。酷。

Raspbian 提供了一个很好的工具来修改系统,包括修改 root 用户密码,称为 Raspberry Pi 配置工具。您可以通过单击左上角的 Raspberry Pi 徽标,选择首选项,然后单击 Raspberry Pi 配置来访问它。图 7-2 显示了该工具的示例。

img/460910_1_En_7_Fig2_HTML.jpg

图 7-2

Raspberry Pi 配置工具

要更改密码,请点按“更改密码”并输入您的新密码。还强烈建议您通过取消选中作为当前用户复选框来关闭自动登录。但是先不要关闭工具。我们需要用这个工具做两件事。

启用 SSH

当使用 Raspberry Pi 作为服务器(与大多数服务器一样)时,管理员发现从他们的工作站远程登录到服务器更容易,而不是沿着长长的走廊走到满是人的安全房间,或者闪烁的灯光在无尽的服务器机架中搜索,只找到一个哑终端,连接它,然后登录。你明白了——远程登录要容易得多。

幸运的是,我们可以用拉斯扁做到这一点。要将 Raspbian 配置为允许通过安全套接字连接和安全 Shell (SSH)进行远程登录,我们需要在 Raspberry Pi 配置工具中单击 Interfaces 选项卡,并在 SSH 行上选中 Enabled 复选框。图 7-3 显示了打开 SSH 连接后的界面。

img/460910_1_En_7_Fig3_HTML.jpg

图 7-3

启用 SSH 连接

设置键盘布局/区域

我们需要用配置工具再做一件事:设置默认的键盘布局。Raspbian 自带的键盘布局设置为英国英语(Raspberry Pi 内置于此)。如果你住在世界上的其他地方,不使用这种键盘布局,你需要改变它。

要更改键盘布局,单击实用程序中的本地化选项卡,然后单击设置键盘按钮,如图 7-4 所示。

img/460910_1_En_7_Fig4_HTML.jpg

图 7-4

tab 位置

按照对话框选择您的国家和键盘布局。完成后,您可以关闭该实用程序并重启您的 Raspberry Pi。重新启动后,确保您的所有设置都是正确的——尤其是键盘!

您也可以更改键盘布局:单击首选项➤鼠标和键盘设置,然后单击键盘布局选项卡,最后单击键盘布局按钮。

配置网络

设置 Raspberry Pi 操作系统的最后一项任务是启用网络。你有两个选择。树莓 Pi 3 配有 Wi-Fi 和以太网连接。建议您使用以太网连接,并使用集线器连接群集中的所有计算机。虽然如果您决定使用 Wi-Fi,则不需要集线器,但使用具有以太网连接的集线器象征着大多数集群安装的做法——将它们安装在隔离的子网中,以确保不会因竞争或网络负载而丢失流量。对于这个演练来说,这并不重要,但是一个集线器会使连接变得更容易一些。

注意

虽然本节详细描述了如何为一个 MySQL 服务器设置网络,但是在我们克隆了基本服务器之后,我们将不得不对其他服务器做一些小的更改。例如,我们需要为每台服务器编辑特定的 IP 和主机名,这比在每台机器上执行同样长的步骤更容易。

无论您选择哪个接口,我们都必须设置我们的服务器来使用静态 IP 地址。检查家庭(或工作)网络上的动态主机配置协议(DHCP) 4 服务器,并从可用范围中选择五个 IP 地址。大多数家用路由都运行 DHCP 服务。或者,最好保留一个 IP 地址范围以供使用。如果你不确定这是什么意思,没关系。您可以从该范围的顶部(较大的数字)选择五个 IP 地址,您应该是安全的。

例如,如果您的 DHCP 服务器使用的范围是 192 . 168 . 1 . 2–192 . 168 . 1 . 254,那么您可以选择 192 . 168 . 1 . 240–192 . 168 . 1 . 244,应该没问题。但是,如果您遇到其他机器抱怨 IP 地址重复的问题,您可能需要更改您使用的范围。

注意

如果您不是 DHCP 服务器的所有者(例如,您正在使用别人的网络),您可能需要联系系统管理员并请求 IP 地址。

在本次演示中,我们将 Raspberry Pi 计算机的 IP 地址更改为使用 192 . 168 . 42 . 240–192 . 168 . 42 . 244,如下所示:

  • 192.168.42.240:应用服务器

  • 192 . 168 . 42 . 241:InnoDB 集群中的第一台服务器

  • 192 . 168 . 42 . 242:InnoDB 集群中的第二台服务器

  • 192 . 168 . 42 . 243:InnoDB 集群中的第三台服务器

  • 192 . 168 . 42 . 244:InnoDB 集群中的第四台服务器

您还需要知道路由 IP 地址和至少一个名称服务器 IP 地址。您可以在您的路由(通常是运行 DHCP 服务的同一台机器)上找到此信息。例如,路由 IP 地址可能是子网地址和范围内的第一个数字。例如,192.168.42.XXX 网络使用 192.168.42.1 作为路由 IP 地址。

同样,同一网络上的域名服务器(DNS)也是 192.168.42.1 地址。如果您在同一网络上有其他机器,您也可以通过检查网络设置来找到此信息。比如,你可以在 Linux 和 macOS 机器上运行ifconfig来查看所有的网络接口。在 Windows 上,您可以使用ipconfig /all来查看网络接口及其设置。

要设置静态 IP 地址,我们必须编辑dhcpcd.conf文件。您可以在终端窗口中使用以下命令。该命令通过超级用户(sudo)命令和 Nano 编辑器使用提升的权限来编辑文件:

$ sudo nano /etc/dhcpcd.conf

点击工具栏左上方的小终端图标,可以打开一个终端,如图 7-5 (用方框标注表示)。

img/460910_1_En_7_Fig5_HTML.jpg

图 7-5

定位终端应用

打开文件后,向下滚动到文件的底部(end ),添加以下行,替换您的网络的基本 IP 范围。例如,如果您的网络使用 10.0.1.XXX,请使用 10.0.1 而不是 192.168.42.XXX。在这种情况下,我们将第一台 MySQL 服务器的静态 IP 地址设置为 192.168.42.241。使用检查路由时遇到的路由 IP 和名称服务器 IP 地址。

# Example static IP configuration:
interface eth0
static ip_address=192.168.42.241/24
static routers=192.168.42.1
static domain_name_servers=192.168.42.1

添加完这些行之后,通过按 Ctrl+X 和 Y 来保存文件。保存后,您可以重新启动机器。当机器重新启动时,您可以在终端窗口中使用以下命令来检查设置:

$ ip a

检查输出,确保 eth0 接口显示正确的 IP 地址,如下所示。请注意,报告显示的 IP 地址是 192.168.42.241:

...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether b8:27:eb:5e:7f:71 brd ff:ff:ff:ff:ff:ff
    inet 192.168.42.241/24 brd 192.168.42.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 2001:5b0:4eca:9f28:cc4e:31cb:ecb0:226a/64 scope global mngtmpaddr noprefixroute dynamic
       valid_lft 1175sec preferred_lft 1175sec
    inet6 fe80::a38:d8b:653b:81c/64 scope link
       valid_lft forever preferred_lft forever
...

接下来,测试网络连接以确保其正常工作。如果您可以连接到互联网或网络上的其他计算机,您就完成了。如果不能,请返回并检查您的设置,如果有必要,通过在您添加到文件的每一行之前放置一个#来恢复更改,然后重新启动。

小费

关于如何在 Raspberry Pi 上为以太网或 Wi-Fi 连接设置静态 IP 地址的完整讨论,请参见 www.raspberrypi.org/learning/networking-lessons/rpi-static-ip-address/

好了,我们还没有完成网络配置。我们还需要为每台服务器设置一个唯一的主机名。我们可以使用 Raspberry Pi 配置工具来设置主机名。在本演示中,我们将使用主机名cluster-rpi[1-4]。例如,第三个服务器设置的主机名是cluster-rpi3。我们将使用主机名cluster-rpi-app作为应用服务器。要更改主机名,请打开 Raspberry Pi 配置工具,找到第一个选项卡上的主机名行,并将文本框中的文本更改到右侧。图 7-6 显示了为应用服务器设置主机名的示例。

img/460910_1_En_7_Fig6_HTML.jpg

图 7-6

设置主机名

完成更改后,您需要重新启动服务器,但是我们还需要做一项更改。我们还必须编辑/etc/hosts文件,为您的集群添加新的 IP 地址和主机名。这允许我们使用集群,而不必向我们的 DNS 服务器注册主机名。如果您的家庭网络有自己的 DNS,这是可能的,但这不太可能。您需要添加的行是文件底部的行(以粗体显示):

127.0.0.1       localhost
::1             localhost ip6-localhost ip6-loopback

ff02::1         ip6-allnodes
ff02::2         ip6-allrouters

192.168.42.240  cluster-rpi-app

192.168.42.241  cluster-rpi1

192.168.42.242  cluster-rpi2

192.168.42.243  cluster-rpi3

192.168.42.244  cluster-rpi4

您现在可以重新启动服务器。我们现在准备在 Raspbian 上构建、安装和配置 MySQL。强烈建议您使用另一台机器来执行此任务(例如应用服务器)。

小费

如果你想听从建议在一台单独的 Raspberry Pi 计算机上构建 MySQL,你应该为另一个 USB 驱动器和机器执行前面的步骤。例如,您可以构建应用服务器,并使用该机器构建 MySQL。

构建 MySQL 服务器、Shell 和路由

请记住,没有适用于 MySQL 或其产品的 Raspbian 二进制发行版。我们必须构建 MySQL 服务器、Shell 和路由,然后在我们的 Raspberry Pi 计算机上手动安装它们。幸运的是,这个过程很简单,包括一些准备我们系统的次要系统配置项目和两个命令:cmakemake。这一节将通过大量的例子和每一步的文档来引导你完成所有这些步骤。

从源代码构建 MySQL 的任务对于那些从来没有编程过的人或者已经有一段时间没有编写过程序的人来说可能会令人望而生畏,但是不要绝望。在 Raspberry Pi 上编译 MySQL 最困难的部分是等待过程完成。汇编所有内容可能需要几个小时。但是,对于能够使用 Raspberry Pi 计算机来试验 InnoDB 集群来说,这是一个很小的代价。

然而,在我们开始修改系统以构建代码之前,让我们讨论一个部署计划。

部署计划

制定部署高可用性解决方案的计划总是一个好主意。我们已经讨论了一般的计划:在集群中有一个应用服务器和四个服务器。现在让我们讨论如何最好地构建和部署 MySQL 产品。

我们将使用一台机器来构建 MySQL 服务器、Shell 和路由。但是,我们不需要在集群中的每台机器上安装所有三个组件。相反,我们需要在将成为集群成员的四台机器上安装 MySQL Server 和 MySQL Shell。应用服务器只需要安装 MySQL Shell 和路由。

方法是使用应用服务器作为构建机器来构建所有三个产品。然后,您可以将安装文件(以 TAR 文件的形式,它代表磁带归档)复制到您在前面几节中准备的基础机器上。

我们将在应用服务器上构建所有三个产品,但是只在那台机器上安装 MySQL Shell 和 Router。MySQL 服务器和 Shell 安装在我们之前讨论过的基本 MySQL 服务器映像上。我们将克隆该映像以形成集群中的机器。

让我们开始在 Raspbian 上编译 MySQL,从先决条件开始。

先决条件

要准备您的 Raspberry Pi 来编译 MySQL 产品,您需要采取几个步骤。首先要考虑的是,是否要在以后要克隆的同一台服务器上构建 MySQL。如果您在我们刚刚花时间配置为与 InnoDB Cluster 一起使用的同一台服务器上构建 MySQL,您将需要将所有开发代码和应用复制到每台机器上,这是不必要的。

因此,第一个先决条件是设置一台新机器作为我们的构建机器。如上所述,这可以是您的应用服务器。此外,如果您有一个额外的至少 8GB 的 USB 驱动器(不要为构建机器使用 SD 卡——它会太慢),通过复制您先前下载的 Raspbian 映像来准备它(参见前面的“安装引导映像”一节中关于使用 Etcher 的内容)。如果您没有额外的 USB 驱动器,您可以使用为应用服务器指定的驱动器,因为我们不会克隆该系统。如果您只有一个键盘、鼠标和显示器,那么您需要在启动第一个 MySQL 服务器之前关闭应用服务器。

在您至少按照前面“Raspberry Pi 设置教程”一节中的步骤准备好应用服务器之后(确保使用正确的 IP 地址和主机名—192.168.42.240,cluster-rpi-app),我们将安装以下软件:

  • 诅咒 5 ( libncurses5-dev):终端输出库

  • Bison:一个语言处理器

  • OpenSSL ( libssl-dev ): SSL 库

  • CMake :制作配置工具

要同时安装所有这些库,请在终端窗口中使用以下命令:

$ sudo apt-get install libncurses5-dev bison libssl-dev cmake

这将下载并安装必要的文件。请注意,我们必须使用提升的权限来安装库。

Sudo 是什么?

在大多数 Linux 平台上,您必须使用提升的权限来安装软件和修改系统文件。sudo命令(对于超级用户 do ,参见 https://linuxacademy.com/blog/linux/linux-commands-for-beginners-sudo/ )允许你暂时获得普通用户账户的那些权限。在配置 Raspbian 和类似的 Linux 系统时,您会经常看到这种情况。

我们必须再做一件事。我们必须增加交换文件的大小。这对于让系统有足够的内存来编译和链接一些较大的源代码文件是必要的。建议将交换文件大小设置为 2048。为此,请打开终端窗口并输入以下命令来编辑交换文件配置文件:

$ sudo nano /etc/dphys-swapfile

在文件中找到以下行,并将值更改为 2048。文件中没有其他行需要更改:

CONF_SWAPSIZE=2048

要启用新的交换空间大小,请重新启动计算机或输入以下命令:

$ sudo /etc/init.d/dphys-swapfile stop
$ sudo /etc/init.d/dphys-swapfile start

这些方法通过停止系统进程并重新启动它,将交换文件重置为新的大小。现在我们准备构建 MySQL 服务器、Shell 和路由。

构建 MySQL 服务器

好了,这是在 MySQL 8.0.11 上使用树莓 Pi 最棘手的地方。因为没有我们可以下载使用的 Oracle 二进制文件(有些第三方可能最终会构建一些),所以我们必须自己编译 MySQL。这是可能的,因为 MySQL 是开源的,正如您在上一节中看到的,我们可以下载源代码。酷。

在 Raspberry Pi 上构建 MySQL 只需要三个步骤。我们首先运行名为 CMake 的预处理器,然后用make构建代码,最后用make package命令构建安装包。让我们看看每个步骤的细节,从 CMake 开始。

CMake ( cmake.org )是另一个用于构建、测试和打包软件的开源产品。回想一下,我们在上一节安装了 CMake。您可以使用许多不同的选项来构建软件,其中许多也适用于 MySQL。事实上,您可以花费大量时间定制 CMake 命令选项,以便为几乎任何平台进行构建。因为我们下载了带有 Boost 库的普通 Linux 的 MySQL 源代码,所以我们拥有了我们需要的一切。

我们需要与 CMake 一起使用的命令选项很少,包括以下内容:

  • 您应该设置-DWITH_UNIT_TESTS=OFF来节省编译时间(不需要单元测试)。

  • 你应该把PREFIX设置到安装路径,这样便于安装。

  • 我们必须用发布代码来构建(debug 对于 Raspberry Pi 来说需要太多内存)。

  • 我们必须添加额外的编译和构建标志,以确保代码在 ARM32 上正确构建。

每一个都将被更详细地解释,但是让我们从下载源代码开始吧。

准备源代码

现在我们已经设置好了构建 MySQL 的机器,唯一的先决条件是我们必须下载 MySQL 服务器源代码。前往 https://dev.mysql.com/downloads/mysql/ 。从选择操作系统下拉框中,选择源代码。然后点击通用 Linux(架构独立),压缩后的 TAR 存档列表底部包含 Boost Headers 下载链接,如图 7-7 所示。这个文件包含我们需要的另一个库(Boost)以及服务器源代码。这是最容易开始构建的下载。

img/460910_1_En_7_Fig7_HTML.jpg

图 7-7

下载 MySQL 服务器源代码

运行 CMake(准备编译)

我们要做的第一件事是提取我们下载的 TAR 文件。您可以使用以下命令来完成此操作:

$ cd /home/pi
$ mkdir source
$ cd source
$ cp ~/Downloads/mysql- boost-8.0.11.tar.gz .
$ tar -xvf mysql-boost-8.0.11.tar.gz

这将创建一个名为mysql-8.0.11的文件夹。建议您将此文件解压缩到 root 用户主文件夹中的一个文件夹中,例如/home/pi/source。解包过程需要几分钟,因为它包含大量代码。

小费

如果您想节省启动驱动器上的空间,您可以格式化并使用第二个 USB 驱动器来下载和编译代码。只要确保用 ext4 文件系统格式化驱动器。

接下来,我们将使用以下命令创建一个目录来存储所有编译后的代码。这有助于防止编译时发生意外,并保留源代码:

$ cd mysql-8.0.11
$ mkdir build
$ cd build

现在我们可以运行 CMake 命令了。清单 7-1 显示了您需要在构建文件夹中使用的完整命令。请注意,该命令指定了许多选项,包括(按外观顺序)、使用 UNIX makefiles、将构建设置为发布代码(而不是调试)、忽略一体化(AIO)检查、设置 Boost 文件夹(包含在我们下载的 TAR 文件中)、关闭单元测试,以及设置一些用于在 ARM32 上编译的神秘选项。

$ cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=release -DBUILD_CONFIG=mysql_release -DDEBUG_EXTNAME=OFF -DIGNORE_AIO_CHECK=1 -DWITH_BOOST=/home/pi/source/mysql-8.0.11/boost -DWITH_UNIT_TESTS=OFF -DCMAKE_C_LINK_FLAGS="-Wl,--no-keep-memory,--icf=safe" -DCMAKE_CXX_LINK_FLAGS="-Wl,--no-keep-memory,--icf=safe" -DCMAKE_C_FLAGS_RELEASE="-fPIC" -DCMAKE_CXX_FLAGS_RELEASE="-fPIC" -DCMAKE_INSTALL_PREFIX="/usr/local/mysql" ..

-- Running cmake version 3.7.2
-- Found Git: /usr/bin/git (found version "2.11.0")
-- Configuring with MAX_INDEXES = 64U
-- The C compiler identification is GNU 6.3.0
-- The CXX compiler identification is GNU 6.3.0
-- Check for working C compiler: /usr/bin/cc
...
-- COMPILE_DEFINITIONS: _GNU_SOURCE;_FILE_OFFSET_BITS=64;BOOST_GEOMETRY_SQRT_CHECK_FINITENESS;HAVE_CONFIG_H;RAPIDJSON_NO_SIZETYPEDEFINE;__STDC_LIMIT_MACROS;__STDC_FORMAT_MACROS;_USE_MATH_DEFINES;HAVE_LIBEVENT1;UNISTR_FROM_STRING_EXPLICIT=explicit;UNISTR_FROM_CHAR_EXPLICIT=explicit
-- CMAKE_C_FLAGS:  -Wall -Wextra -Wformat-security -Wvla -Wundef -Wwrite-strings
-- CMAKE_CXX_FLAGS:  -Wall -Wextra -Wformat-security -Wvla -Wundef -Woverloaded-virtual -Wno-missing-field-initializers -Wlogical-op

-- CMAKE_C_LINK_FLAGS: -Wl,--no-keep-memory,--icf=safe -fuse-ld=gold -Wl,--gc-sections

-- CMAKE_CXX_LINK_FLAGS: -Wl,--no-keep-memory,--icf=safe -fuse-ld=gold -Wl,--gc-sections

-- CMAKE_C_FLAGS_RELEASE: -fPIC  -DDBUG_OFF
-- CMAKE_CXX_FLAGS_RELEASE: -fPIC -std=c++11  -DDBUG_OFF
-- Configuring done
-- Generating done
-- Build files have been written to: /home/pi/source/mysql-8.0.11/build

Listing 7-1Running the CMake Command (ARM32)

如果这个命令看起来很奇怪,也不用担心。您不需要理解我们在编译和链接阶段使用的所有特殊设置。但是,如果您确实想了解更多关于这些选项的信息,您可以查看关于 GNU 编译器( http://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html )和链接器( https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html )选项的文档。

运行该命令可能需要几分钟时间。确保没有错误,并且最后几行表明构建文件已经被写入到build文件夹中。请特别注意结尾的LINK_FLAGS消息。CMake 命令中的选项不包括空格。如果您不小心添加了空格,逗号分隔的列表将在 CMake 输出中显示它们。确保没有空格。如果有空格,您可能会得到一个错误,指出--icf=safe(或其他)选项无效。如果发生这种情况,请再次运行不带空格的命令。

如果你已经走了这么远而没有错误,你几乎可以放松了。下一步,编译代码,很容易,但可能需要一段时间运行在树莓 Pi 3B+(至少两到三个小时)。

运行 make(编译)

下一步是编译代码。这可以简单地用make命令来完成。这个命令允许我们指定想要使用的并行线程的数量。对于树莓 Pi 3,总共四个 CPU 核心,使用三个核心进行编译是安全的。如果您有一个正在运行的 CPU 使用监控器,您将会看到这三个,有时可能是所有四个内核都在 100%运行。如果你的树莓派 3B 安装在一个箱子里,确保你有足够的通风或风扇吹过电路板。这对于树莓派 3B+(最新的主板)来说不是绝对必要的,但也不会有什么坏处。

清单 7-2 展示了使用命令make -j3编译 MySQL 服务器代码的步骤。该清单是您可能会看到的消息的摘录(将有数千行),但需要注意的是最后几行。这些确保代码编译无误。

小费

在代码编译时,您可能会看到轻微的警告,您可以忽略这些警告。但是,您应该看不到任何编译错误。如果是这样,请返回检查 CMake 命令,并在必要时重新运行它。如果所有这些都失败了,删除build目录并重新开始。

$ make -j3
[  0%] Built target INFO_SRC
[  0%] Built target INFO_BIN
[  0%] Building C object extra/zlib/CMakeFiles/zlib.dir/adler32.o
[  0%] Generating common.h
[  0%] Generating help.c
[  0%] Generating help.h
[  0%] Generating vi.h
[  0%] Generating emacs.h
[  0%] Building C object extra/zlib/CMakeFiles/zlib.dir/compress.o
[  0%] Generating fcns.c
[  0%] Generating fcns.h
...
...
[100%] Linking CXX static library ../archive_output_directory/libsql_main.a
[100%] Built target sql_main
Scanning dependencies of target mysqld
[100%] Building CXX object sql/CMakeFiles/mysqld.dir/main.cc.o
[100%] Linking CXX executable ../runtime_output_directory/mysqld
[100%] Built target mysqld

Listing 7-2Compiling MySQL Server

编译完成后,下一步是构建一个包(TAR 文件),我们可以用它在我们的服务器上安装 MySQL。

制作包装

我们需要做的最后一件事是构建安装包。在这种情况下,我们将构建一个压缩的 TAR 文件,我们可以将它复制到我们的初始服务器并进行安装。我们用make package命令来做这件事,如清单 7-3 所示。

$ make package
[  0%] Built target abi_check
[  0%] Built target INFO_SRC
[  0%] Built target INFO_BIN
[  0%] Built target zlib
[  1%] Built target edit
[  8%] Built target icuuc
[ 16%] Built target icui18n
[ 16%] Built target icustubdata
...
Run CPack packaging tool...
CPack: Create package using TGZ
CPack: Install projects
CPack: - Run preinstall target for: MySQL
CPack: - Install project: MySQL
CPack: Create package
CPack: - package: /home/pi/source/mysql-8.0.11/build/mysql-8.0.11-linux-armv7l.tar.gz generated.

Listing 7-3Building the TAR Package

就这样!我们在 Raspberry Pi 上构建了 MySQL!还不算太糟,是吧?现在让我们看看如何在我们的服务器上安装和测试 MySQL。

安装 MySQL 服务器

回想一下,我们在应用服务器上构建了 MySQL。因此,我们需要将 TAR 文件复制到一个可移动驱动器上,并启动我们之前构建的第一个 MySQL 服务器。如果您只有一个键盘、鼠标和显示器,那么您需要在启动第一个 MySQL 服务器之前关闭应用服务器。复制完文件后,现在就开始做吧。

服务器启动后,登录并转到/usr/local目录,创建一个名为mysql的新文件夹。然后,切换到新文件夹,并将 TAR 文件复制到该文件夹。最后,使用以下命令解压文件。有很多文件,所以解压缩可能需要几分钟时间。

$ cd /usr/local/
$ mkdir mysql
$ cd mysql
$ sudo cp ~/source/mysql-8.0.11/build/mysql-8.0.11-linux-armv7l.tar.gz .
$ sudo tar -xvf mysql-8.0.11-linux-armv7l.tar.gz --strip-components=1

请注意,最后一个命令使用一个选项从提取的文件目录中删除一个组件(第一个文件夹— mysql-8.0.11-linux-armv7l)。这确保了 MySQL 文件被复制到/usr/local/mysql

但是,我们需要再运行一个命令。因为我们很节省空间,所以我们不需要 MySQL 测试文件,所以我们可以用下面的命令删除它们。当我们处理完 TAR 文件后,我们也可以删除它,如下所示:

$ sudo rm -rf mysql-test

$ sudo rm mysql-8.0.11-linux-armv7l.tar.gz

从 TAR 文件安装比从典型的特定于平台的包安装需要更多的步骤。这是因为安装包通常会处理几个必需的配置步骤——所有这些都在在线参考手册的“使用通用二进制文件在 Unix/Linux 上安装 MySQL”(https://dev.mysql.com/doc/refman/8.0/en/binary-installation.html)一节中有详细介绍。

配置 MySQL 服务器

现在我们已经复制了文件,我们可以完成设置。这个过程并不繁琐,但确实涉及到从终端运行几个命令,所以需要耐心确保所有命令都输入正确。

我们首先创建一个名为mysql的新组,添加一个名为mysql的用户,创建一个供 MySQL 使用的文件夹,并授予mysql用户对该文件夹的访问权限。下面显示了所需的命令;从终端运行这些命令(这些命令没有输出):

$ sudo groupadd mysql
$ sudo useradd -r -g mysql -s /bin/false mysql
$ cd /usr/local/mysql
$ sudo mkdir mysql-files
$ sudo chown mysql:mysql mysql-files
$ sudo chmod 750 mysql-files

第一次启动服务器之前的下一步是使用mysql_ssl_rsa_setup工具建立安全连接,并确保mysql用户可以访问所有数据文件。最后,我们还需要准备数据目录。下面显示了所需的命令;在终端窗口的/usr/local/mysql文件夹中运行这些:

$ sudo ./bin/mysql_ssl_rsa_setup --datadir=data
$ sudo chown mysql:mysql data
$ sudo chmod 750 data

我们可以使用--initialize选项轻松初始化数据目录,如下一行代码所示。注意,我们使用提升的权限运行命令,并指定要使用的用户(mysql)。下面显示了一个输出示例,成功的消息以粗体突出显示。如果您看到错误,请参考在线参考手册来解决它们。请注意,输出包含初始 root 用户密码。下一步您将需要它。

$ sudo bin/mysqld --initialize --user=mysql
...
2018-05-23T18:37:53.120312Z 0 [System] [MY-013169] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.11) initializing of server in progress as process 15340

2018-05-23T18:38:26.383044Z 5 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: pPX4Ll8/grR2
2018-05-23T18:38:52.713071Z 0 [System] [MY-013170] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.11) initializing of server has completed

好了,我们现在准备好第一次启动 MySQL 了。使用mysqld_safe命令从命令行启动 MySQL。我们使用这个命令代替/etc/init.d/mysql start 命令,这样我们可以检查输出中的错误。如果没有错误,您应该会看到如下输出:

$ sudo ./bin/mysqld_safe --user=mysql &
$ Logging to '/usr/local/mysql/data/raspberrypi.err'.
2018-05-23T18:42:34.968096Z mysqld_safe Starting mysqld daemon with databases from /usr/local/mysql/data

现在我们可以用旧客户端测试我们的 MySQL 服务器。确保使用初始化数据目录时显示的密码。清单 7-4 展示了第一次使用mysql客户端连接到服务器的例子。我们将首先显示版本,然后更改 root 用户密码。请注意,我们还使用shutdown SQL 命令关闭了服务器。

$ ./bin/mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.11

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> SET PASSWORD = 'root';
Query OK, 0 rows affected (0.06 sec)

mysql> SELECT @@version;
+-----------+
| @@version |
+-----------+
| 8.0.11    |
+-----------+
1 row in set (0.00 sec)

mysql> shutdown;
Query OK, 0 rows affected (0.00 sec)

mysql> \q

Listing 7-4Connecting to MySQL for the First Time

接下来,我们必须添加 MySQL 二进制文件的路径。我们可以通过用命令nano ~/.bashrc编辑我们的 Bash 资源文件很容易地做到这一点。当文件打开时,将下面一行添加到文件底部:

export PATH=${PATH}:/usr/local/mysql/bin

下次打开终端时,无需指定路径就可以执行 MySQL 应用和工具。

还需要最后一步:我们必须复制启动和关闭脚本(服务)以允许我们在引导时自动启动 MySQL。为此,从构建的support-files文件夹中复制mysql.server文件到/etc/init.d/mysql文件,如下所示。我们还将再次测试服务器连接,然后用/etc/init.d/mysql脚本关闭它。请注意,使用此脚本时,可能会提示您输入密码。

$ /etc/init.d/mysql start
[ ok ] Starting mysql (via systemctl): mysql.service.
$ mysql -uroot -p -e "select @@version"
Enter password:
+-----------+
| @@version |
+-----------+
| 8.0.11    |
+-----------+
$ /etc/init.d/mysql stop
[ ok ] Stopping mysql (via systemctl): mysql.service.

就这样!我们已经安装了 MySQL 服务器,并测试了它的工作情况。在每台服务器上安装 MySQL Shell 也是一个好主意。

构建 MySQL Shell

在 Raspberry Pi 上构建 MySQL Shell 就像构建 MySQL 服务器一样,只是前提条件有点不同,需要下载并编译 Protobuf 库。安装稍微容易一些,因为不需要安装后配置。在这一节中,您将探索如何在 Raspberry Pi 上构建和安装 MySQL Shell。

准备源代码

我们需要做的第一件事是下载 MySQL Shell 源代码。前往 https://dev.mysql.com/downloads/shell/ 。在选择操作系统下拉框中,选择源代码。然后点击列表底部的通用 Linux(架构独立),压缩 TAR 存档下载链接,如图 7-8 所示。您可以暂时保留该文件,因为我们将在下一部分用到它。

img/460910_1_En_7_Fig8_HTML.jpg

图 7-8

下载 MySQL Shell 源代码

接下来,我们还需要下载 Protobuf 2.6.1 源代码。我们将需要下载源代码,编译它,并安装它来使用 MySQL Shell。进入 https://github.com/google/protobuf/releases/tag/v2.6.1 ,点击下载protobuf-2.6.1.tar.gz文件。将它复制或放置在您的/home/pi/source文件夹中,并使用以下命令将其解压缩:

$ cd /home/source
$ cp ~/Downloads/protobuf-2.6.1.tar.gz .
$ tar -xvf protobuf-2.6.1.tar.gz

接下来,切换到我们解压缩的文件夹并运行configure命令:。

$ cd protobuf-2.6.1
$ ./configure
checking whether to enable maintainer-specific portions of Makefiles... yes
checking build system type... armv7l-unknown-linux-gnueabihf
...
config.status: creating build-aux/config.h
config.status: executing depfiles commands
config.status: executing libtool commands

这确保了代码是为 Raspberry Pi 平台配置的(就像 CMake 为服务器代码所做的那样)。

接下来,我们使用提升的权限运行make命令,如下所示。下面是典型输出的摘录。可能会生成数百行,但最后不会有“完成”或“完成”消息。如果看不到任何错误,则代码编译正确。

$ make
make  all-recursive
make[1]: Entering directory '/home/pi/source/protobuf-2.6.1'
Making all in .
make[2]: Entering directory '/home/pi/source/protobuf-2.6.1'
make[2]: Leaving directory '/home/pi/source/protobuf-2.6.1'
Making all in src
...

最后,我们用特权提升的make install命令安装这个库,如下所示。下面是典型输出的摘录。可能会生成数百行,但最后不会有“完成”或“完成”消息。如果您没有看到任何错误,则代码安装正确。

$ sudo make install

Making install in .
make[1]: Entering directory '/home/pi/source/protobuf-2.6.1'
make[2]: Entering directory '/home/pi/source/protobuf-2.6.1'
...

还有最后一步:我们需要告诉库加载器我们安装了一个新的库。如果您现在尝试编译 MySQL Shell,您可能会遇到一个错误,指出 Protobuf 库不存在。确实如此;只是还没有加载到缓存中。以下命令确保库已加载:

$ sudo ldconfig

现在我们准备构建 MySQL Shell。

注意

您必须在具有已编译的 MySQL 服务器源代码树的系统上构建 MySQL Shell。

运行 CMake(准备编译)

我们要做的第一件事是提取我们下载的 TAR 文件。您可以使用以下命令来完成此操作:

$ cd /home/pi/source
$ cp ~/Downloads/mysql-shell-8.0.11-src.tar.gz .
$ tar -xvf mysql-shell-8.0.11-src.tar.gz

这将创建一个名为mysql-shell-8.0.11-src的文件夹。建议您将该文件解压到 root 用户主文件夹中的一个文件夹中,例如/home/pi/source。解压缩过程需要几分钟,因为该文件包含大量代码。

注意

与服务器(和路由)源代码不同,我们将从源代码树的根构建 MySQL Shell。

接下来,我们可以运行 CMake 命令。与 MySQL 服务器代码一样,我们需要指定一些选项。我们需要引用服务器源代码目录、服务器源代码构建目录和 Protobuf 源代码的位置。最后,我们将指定我们希望使用 Python 进行构建,并设置安装前缀。清单 7-5 显示了 CMake 命令,后跟一个输出示例。请注意用粗体标记的行。

$ cmake -DMYSQL_SOURCE_DIR=/home/pi/source/mysql-8.0.11 -DMYSQL_BUILD_DIR=/home/pi/source/mysql-8.0.11/build -DWITH_PROTOBUF=/home/pi/source/ -DHAVE_PYTHON=1 -DCMAKE_INSTALL_PREFIX=/usr/local/mysql .
DHAVE_PYTHON=1 .
-- MySQL Shell 8.0.11

CMake Warning at CMakeLists.txt:110 (message):

  V8 is unavailable: building without JavaScript support.

-- Python 2.7.13

-- PYTHON_INCLUDE_DIR: /usr/include/python2.7
-- PYTHON_LIRARIES: /usr/lib/arm-linux-gnueabihf/libpython2.7.so
PROTOBUF_INCLUDE_DIRS: /usr/local/include
PROTOBUF_LIBRARIES: /usr/local/lib/libprotobuf.so;-lpthread
-- /home/pi/source/mysql-8.0.11/build/scripts/mysql_config --libs: -L/usr/local/mysql/lib -lmysqlclient -lpthread -lm -lrt -lssl -lcrypto -ldl

-- /home/pi/source/mysql-8.0.11/build/archive_output_directory/libmysqlclient.a
-- Found MySQL client Libraries
...
-- Performing Test HAVE_IMPLICIT_DEPENDENT_NAME_TYPING
-- Performing Test HAVE_IMPLICIT_DEPENDENT_NAME_TYPING - Failed
-- Library mysqlshdk-static depends on OSLIBS -lpthread
-- Configuring done
-- Generating done
-- Build files have been written to: /home/pi/source/mysql-shell-8.0.11-src

Listing 7-5Running CMake for MySQL Shell

请注意我们在没有 JavaScript 支持的情况下构建的警告。这是因为 V8 库丢失了。编译和安装这些库需要在 Raspbian 上做一些工作,并且对于本演示来说不是绝对必要的,所以我们将构建仅支持 SQL 和 Python 的 MySQL Shell。

运行 make(编译)

下一步是编译代码。与服务器代码一样,我们将使用make -j3命令来编译。您可能会看到一些警告,但是对于服务器代码,除非有错误,否则您可以忽略这些警告。下面显示了编译 shell 代码的简短摘录:

$ make -j3
Scanning dependencies of target utils
Scanning dependencies of target db
Scanning dependencies of target shellcore
...
 [ 98%] Linking CXX executable ../bin/mysqlsh
[ 99%] Linking CXX executable ../bin/mysqlshrec
[100%] Built target mysqlsh

制作包装

我们需要做的最后一件事是构建安装包。在这种情况下,我们将构建一个压缩的 TAR 文件,我们可以将它复制到我们的初始服务器并进行安装。我们将使用与服务器代码相同的make package命令。下面显示了为 shell 构建包的简短摘录:

$ make package
[  0%] Creating mysqlprovision.zip
[  0%] Built target mysqlprovision
[ 10%] Built target shellcore
...
CPack: - package: /home/pi/source/mysql-shell-8.0.11-src/mysql-shell-8.0.11-.tar.gz generated.
CPack: Create package using DEB
CPack: Install projects
CPack: - Run preinstall target for: mysqlsh
CPack: - Install project: mysqlsh
CPack: Create package
CPack: - package: /home/pi/source/mysql-shell-8.0.11-src/mysql-shell-8.0.11-.deb generated.

该命令构建一个 Debian 安装程序(DEB)文件和一个 TAR 文件。为了保持一致,我们将使用 TAR 文件。此外,安装 Shell 是微不足道的。

安装 MySQL Shell

安装带有 TAR 文件的 MySQL Shell 很简单,只需将 TAR 文件复制到/usr/local/mysql文件夹中,然后解压缩即可。您应该在集群中的所有机器上安装 MySQL Shell 这意味着在应用服务器和我们之前构建的基础 MySQL 服务器上安装它。下面显示了安装 shell 所需的命令。最后一个命令删除了 TAR 文件,因为在安装之后我们不再需要它了:

$ cd /usr/local/mysql
$ sudo cp /home/pi/source/mysql-shell-8.0.11-src/mysql-shell-8.0.11-.tar.gz .
$ sudo tar -xvf mysql-shell-8.0.11-.tar.gz --strip-components=3
$ sudo rm mysql-shell-8.0.11-.tar.gz

好了,现在我们可以测试 MySQL Shell 的安装了。如果我们还没有这样做,我们要做的第一件事就是使用以下命令启动服务器:

$ /etc/init.d/mysql start

如果已经设置了路径,就可以用mysqlsh命令启动 shell。清单 7-6 展示了一个使用 shell 连接到我们的服务器并运行一个简单命令然后退出的例子。我们还可以在会话结束时关闭服务器。

$ mysqlsh
MySQL Shell 8.0.11
...
 MySQL  Py > \connect root@localhost:3306
Creating a session to 'root@localhost:3306'
Enter password: ****
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 9
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.

 MySQL  localhost:3306 ssl  Py > \sql
Switching to SQL mode... Commands end with ;

 MySQL  localhost:3306 ssl  SQL > SELECT @@version;
+-----------+
| @@version |
+-----------+
| 8.0.11    |
+-----------+
1 row in set (0.0012 sec)

 MySQL  localhost:3306 ssl  SQL > SHUTDOWN;

 MySQL  localhost:3306 ssl  SQL > \q
Bye!

Listing 7-6Testing the MySQL Shell on Raspberry Pi

如果您在启动 shell 时遇到一个错误,显示为/usr/local/mysql/bin/mysqlsh: error while loading shared libraries: libprotobuf.so.9: cannot open shared object file: No such file or directory,请确保首先运行以下命令:

$ sudo ldconfig

This will fix the error by ensuring that the Protobuf library is loaded in the cache.

构建 MySQL 路由

在 Raspberry Pi 上构建 MySQL 路由也像构建 MySQL 服务器一样,但前提条件只是简单地下载源代码。安装稍微容易一些,因为我们将使用自动化的基本安装脚本。在本节中,您将探索如何在 Raspberry Pi 上构建和安装 MySQL 路由。

注意

您只需要在应用服务器上安装 MySQL 路由。如果您一直使用不同的服务器来构建服务器和 shell,请确保只在应用服务器上安装路由。

准备源代码

我们需要做的第一件事是下载 MySQL 路由源代码。前往 https://dev.mysql.com/downloads/router/ 。从选择操作系统下拉框中,选择源代码。然后点击列表底部的通用 Linux(架构独立),压缩 TAR 存档下载链接,如图 7-9 所示。您可以暂时保留该文件,因为我们将在下一部分用到它。

img/460910_1_En_7_Fig9_HTML.jpg

图 7-9

下载 MySQL 路由源代码

运行 CMake(准备编译)

我们要做的第一件事是提取我们下载的 TAR 文件。您可以使用以下命令来完成此操作:

$ cd /home/pi/source
$ cp ~/Downloads/mysql-router-8.0.11.tar.gz .
$ tar -xvf mysql-router-8.0.11.tar.gz

这将创建一个名为mysql-router-8.0.11的文件夹。建议您将此文件解压缩到 root 用户主文件夹中的一个文件夹中,例如/home/pi/source。解压缩过程需要几分钟,因为该文件包含大量代码。

接下来,我们可以运行 CMake 命令。与 MySQL 服务器代码一样,我们将从构建目录运行该命令。与服务器不同,我们不需要指定 CMake 选项。清单 7-7 显示了为路由运行 CMake 的命令和示例输出的摘录。如你所见,它遵循一个熟悉的模式。

$ cd /home/pi/source/mysql-router-8.0.11
$ mkdir build
$ cd build

$ cmake ..

-- The C compiler identification is GNU 6.3.0
-- The CXX compiler identification is GNU 6.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
...
-- Loading module 'x_protocol'
-- Performing Test CXX_HAVE_SIGN_COMPARE
-- Performing Test CXX_HAVE_SIGN_COMPARE - Success
-- Performing Test CXX_HAVE_PEDANTIC
-- Performing Test CXX_HAVE_PEDANTIC - Success
-- Performing Test CXX_HAVE_CONVERSION
-- Performing Test CXX_HAVE_CONVERSION - Success
-- Configuring done
-- Generating done
-- Build files have been written to: /home/pi/source/mysql-router-8.0.11/build

Listing 7-7Running CMake for MySQL Router

运行 make(编译)

下一步是编译代码。与服务器代码一样,我们将使用make -j3命令来编译代码。您可能会看到一些警告,但是与服务器代码一样,除非有错误,否则您可以忽略这些警告。下面是编译路由代码的简短摘录。编译路由可能需要一段时间,因为它使用的 Protobuf 版本较新,已经是源代码树的一部分。

$ make -j3
Scanning dependencies of target router_taocrypt
Scanning dependencies of target libprotobuf-lite
Scanning dependencies of target libprotobuf_3_0
[  0%] Building CXX object ext/yassl/CMakeFiles/router_yassl.dir/src/handshake.cpp.o
...
[100%] Linking CXX shared library ../../stage/lib/mysqlrouter/routing.so
[100%] Built target routing

制作包装

我们需要做的最后一件事是构建安装包。在这种情况下,我们将构建一个压缩的 TAR 文件,我们可以将它复制到我们的初始服务器并进行安装。我们将使用与服务器代码相同的make package命令。清单 7-8 显示了为路由构建包的简要摘录。

$ make package
...
Run CPack packaging tool...
CPack: Create package using STGZ
CPack: Install projects
CPack: - Run preinstall target for: MySQLRouter
CPack: - Install project: MySQLRouter
CPack: Create package

CPack: - package: /home/pi/source/mysql-router-8.0.11/build/mysql-router-8.0.11-Linux.sh generated.

CPack: Create package using TGZ
CPack: Install projects
CPack: - Run preinstall target for: MySQLRouter
CPack: - Install project: MySQLRouter
CPack: Create package

CPack: - package: /home/pi/source/mysql-router-8.0.11/build/mysql-router-8.0.11-Linux.tar.gz generated.

CPack: Create package using TZ
CPack: Install projects
CPack: - Run preinstall target for: MySQLRouter
CPack: - Install project: MySQLRouter
CPack: Create package
CPack: - package: /home/pi/source/mysql-router-8.0.11/build/mysql-router-8.0.11-Linux.tar.Z generated.

Listing 7-8Building the Package for MySQL Router

这个包构建了一个名为mysql-router-8.0.11-Linux.sh的特殊文件,我们将使用它在应用服务器上安装 MySQL 路由。

安装 MySQL 路由

路由在三个 MySQL 产品中是独一无二的,因为它有一个特殊的设置脚本,让机器准备好以默认设置运行路由。更具体地说,路由配置将使用端口 6446、6447 使用传统客户端协议进行读/写和只读,使用端口 64460、64470 使用 X 协议进行读/写和只读。

您可以使用我们对服务器和 shell 使用的熟悉的复制和解包方法。但是,安装会为我们完成所有这些工作,所以我们不必手动完成。如果您更愿意使用复制和解包方法,以下是步骤:

$ cd /usr/local/mysql
$ sudo cp /home/pi/source/mysql-router-8.0.11/build/mysql-router-8.0.11-Linux.tar.gz .
$ sudo tar -xvf mysql-router-8.0.11-Linux.tar.gz --strip-component=1
$ sudo rm mysql-router-8.0.11-Linux.tar.gz

让我们看看如何使用安装脚本。回想一下,我们只在应用服务器上安装路由。因此,如果您使用了该服务器和构建机器,那么您可以在构建完包之后安装它。您可以直接从源构建文件夹运行脚本,但是您必须使用提升的权限。以下是您可以用来启动脚本的命令:

$ sudo ./mysql-router-8.0.11-Linux.sh --prefix=/usr/local/mysql

当脚本启动时,它将生成敷衍的问候和许可数据,这是您应该阅读的。您可以通过重复按空格键跳过这些数据,或者通过按 Q 键来简化该过程。 5 脚本会询问你是否接受许可(按 Y)以及你想把路由安装在哪里(回复按 N 把目标改为/usr/local/mysql)。清单 7-9 显示了运行安装程序的脚本。

Licensing Information User Manual
MySQL Router 8.0
...

Do you accept the license? [yN]:

y

By default the mysql-router will be installed in:
  "/usr/local/mysql/mysql-router-8.0.11-Linux"

Do you want to include the subdirectory mysql-router-8.0.11-Linux?

Saying no will install in: "/usr/local/mysql" [Yn]:

n

Using target directory: /usr/local/mysql
Extracting, please wait...

Unpacking finished successfully

Listing 7-9Installing MySQL Router via the Install Script

路由已经安装,但是在设置和配置 InnoDB 集群之前,我们无法对其进行配置和测试。

注意

如果您在没有 MySQL 的情况下手动安装路由,或者在应用服务器之外的机器上构建 MySQL,您可能需要libmysqlclient.so库。从构建机器上复制该文件,并将其放在应用服务器上的/usr/local/mysql/lib文件夹中。

在我们开始之前,让我们克隆基本的 MySQL 服务器驱动器并制作副本,这样我们就有了四个实例。

克隆 MySQL 服务器映像

在这一步中,我们将使用我们用来安装 MySQL 服务器和 Shell 的 USB 驱动器,并将其复制(克隆)到其他三个 USB 驱动器。有多种方法可以做到这一点,包括少量适用于 Windows 的工具(其中很少是免费/开源的)。然而,因为我们有一个运行 Linux 的完美的应用服务器,我们将使用它。本节演示如何在 Raspberry Pi 上克隆 USB 驱动器。您可以在其他形式的 Linux 和 macOS 上使用相同的技术!

第一步是确保我们已经用正确的映像启动了应用服务器。如果你有一堆 USB 驱动器,尤其是你没有标记它们,这很快就会变得很混乱!在应用服务器的 USB 驱动器上做一个标记可能是一个好主意,这样您就可以知道它是哪一个。

接下来,我们需要确定设备的系统路径。如果您使用 Linux 或 macOS 的桌面发行版,检测驱动器很容易:只需右键单击驱动器并查看属性。或者,正如我们必须对 Raspberry Pi 所做的那样,我们使用 fdisk 实用程序来列出驱动器。

小费

在插入要克隆的 USB 驱动器之前,运行sudo fdisk -l。然后插入 USB 驱动器并再次运行该命令。第二遍显示的是您的目标驱动器。

要列出设备,使用命令sudo fdisk -l。您将看到所有驱动器及其统计信息的列表。如果在插入要克隆的驱动器之前运行此命令,然后在插入驱动器之后再次运行此命令,则可以确定要克隆哪个驱动器,因为第二遍将包含一个新驱动器。下面显示了在 Raspberry Pi 上运行该命令的摘录。在这里,我发现新设备在/dev/sdc上:

$ sudo fdisk -l
[sudo] password for cbell:
...
Device     Boot Start       End   Sectors  Size Id Type
/dev/sdc1        8192     93802     85611 41.8M  c W95 FAT32 (LBA)
/dev/sdc2       98304 123174911 123076608 58.7G 83 Linux

找到设备后,使用以下命令创建映像。请注意,这可能需要一段时间,具体取决于驱动器的大小。我们使用压缩来保持文件最小。

$ sudo dd if=/dev/sdc | bzip2 > idc_clone_rpi.bz2

遗憾的是,在操作过程中,您将得不到任何反馈。但是,您可以通过如下列出文件的大小来粗略地检查进度。尽管您无法获得一个准确的完成百分比,但是后续对该文件大小的检查将确保该命令仍在运行。抱歉——这是我们在 Linux 上使用内置工具所能做到的最好的了。其他工具也适用于 Linux,但是如果您打算在 Raspberry Pi 上克隆 USB,这些命令将会起作用。

$ ls -lsa idc_clone_rpi.bz2
1993436 -rw-rw-r-- 1 cbell cbell 2041274368 May  9 10:00 idc_clone_rpi.bz2

以下是图像创建和压缩后的输出:

$ sudo dd if=/dev/sdc | bzip2 > idc_clone_rpi.bz2
123174912+0 records in
123174912+0 records out
63065554944 bytes (63 GB, 59 GiB) copied, 3478.5 s, 18.1 MB/s

创建映像后,您可以移除它并插入下一个驱动器,然后使用以下命令将映像复制到新的 USB 驱动器。一定要先检查设备列表!

$ bzcat idc_clone_rpi.bz2 | sudo dd of=/dev/sdc

同样,在操作过程中您不会得到任何反馈,但是当您完成操作时,您可以移除驱动器并将其放入您的 Raspberry Pi 中,然后启动它来验证映像。

您还可以使用我们之前使用的 Etcher 应用在创建映像后克隆它。只需将idb_clone_rpi.bz2镜像复制到你的桌面,然后使用 Etcher 将其复制到其他驱动器。

创建或克隆所有 USB 驱动器后,您需要对每个驱动器(映像)做一些小的更改。让我们现在做那件事。

MySQL 服务器实例的最终配置

现在我们有了四个相同的 MySQL 服务器副本,我们需要使每个副本都是独一无二的。因此,我们需要依次连接键盘、鼠标和显示器来启动每台机器,并进行更改。我们需要在每台机器上更改一些东西,包括以下内容。幸运的是,我们之前安装和配置基本 MySQL 服务器的过程为我们节省了大量时间,所以这些变化很小。但是请记住,您必须在每个 MySQL 服务器驱动器/映像上运行这四个步骤:

  • 报告了唯一的server_id和主机名

  • 独特的 UUID

  • 静态 IP 地址

  • 唯一主机名

为了确保服务器有一个惟一的server_id,您现在应该编辑每台机器上的配置文件,并选择一个惟一的服务器 ID ( server_id)。例如,本演示使用范围 1–4。用sudo nano /etc/my.cnf命令编辑配置文件,并设置如下所示的值..文件可能不存在,也没关系;只需输入以下几行:

[mysqld]
server-id=1
report-host=cluster-rpi1

这里有两个项目。首先是我们讨论过的服务器 ID。第二个由组复制用来向 InnoDB 集群报告主机名。这可以确保服务器在集群中拥有唯一的主机名。您也应该包括这一行,以便在我们配置集群时更容易通过主机名来引用服务器。

接下来,我们必须使用唯一的 UUIDs。UUID 存储在名为auto.conf的文件中,由服务器用来在服务器中唯一标识它(作为组复制的一部分)。但是,因为我们克隆了映像,所以所有服务器都有相同的 UUID,这是克隆映像的一个缺陷。如果您单独构建每台机器,这是可以的,因为在每次安装时都会生成auto.cnf。我们可以通过删除auto.conf文件来强制服务器重新生成 UUID。下面显示了显示文件内容、删除文件、启动服务器,然后显示新内容的脚本。如您所见,这将很好地完成这个任务:

$ sudo more /usr/local/mysql/data/auto.cnf
[auto]
server-uuid=4127c169-520f-11e8-a834-b827ebcb9200
$ sudo rm /usr/local/mysql/data/auto.cnf
$ sudo /etc/init.d/mysql restart

[ ok ] Restarting mysql.server (via systemctl): mysql.server.service.
$ sudo more /usr/local/mysql/data/auto.cnf
[auto]
server-uuid=bb1e15c5-53d1-11e8-982c-b827eb6ea7ce

接下来,我们必须确保每台机器都有自己唯一的静态 IP 地址。回想一下,在示例中,我们为 MySQL 服务器使用了 192 . 168 . 42 . 241–192 . 168 . 42 . 244 范围内的 IP 地址。要设置正确的 IP 地址,我们必须再次编辑dhcpcd.conf文件并设置 IP 地址。下面显示了为第一个 MySQL 服务器添加的代码行示例:

# Example static IP configuration:
interface eth0
static ip_address=192.168.42.241/24
static routers=192.168.42.1
static domain_name_servers=192.168.42.1

最后,我们必须设置唯一的主机名。我们通过使用 Raspberry Pi 配置工具来实现这一点,正如我们在前面的部分中所做的那样。为了方便起见,这里重复了这些说明。

回想一下,我们将使用主机名cluster-rpi[1-4]。例如,第三个服务器设置的主机名是cluster-rpi3。我们将使用主机名cluster-rpi-app作为应用服务器。要更改主机名,请打开 Raspberry Pi 配置工具,找到第一个选项卡上的主机名行,然后将文本框中的文本更改到右边。图 7-10 显示了为应用服务器设置主机名的示例。

img/460910_1_En_7_Fig10_HTML.jpg

图 7-10

设置主机名

请记住,您必须在所有四台机器上执行这些步骤。如果不自定义这三项,您可能会在启动群集时遇到问题。

您可能需要执行一个可选步骤。您可能想让服务器在没有桌面(命令行界面)的情况下启动,并且不自动登录。您可以通过使用 Raspberry Pi 配置工具来实现这一点。只需打开工具,找到引导和自动登录线,如图 7-11 所示。要关闭桌面,请选择 To CLI 单选按钮。要关闭自动登录,请取消选中作为当前用户复选框。

img/460910_1_En_7_Fig11_HTML.jpg

图 7-11

关闭桌面和自动登录

现在我们已经有了配置了操作系统的基本服务器,应用和路由也准备好了,让我们看看如何配置我们的集群。

设置集群

现在,我们可以开始在我们的小型 Raspberry Pi 计算机上设置 InnoDB 集群了。您可能要考虑的第一件事是您的测试系统的物理布局。回想一下,每台计算机都不需要键盘、鼠标和显示器。在我们打开它们的电源并等待它们启动后,我们可以远程访问它们。但我们仍然会在自己的箱子里放五台小树莓电脑, 6 每台都连接到自己的电源和以太网集线器,这可能会造成电线的混乱。

建议您将 Raspberry Pi 电脑、电源和以太网集线器放置在便于使用的位置。例如,你可以把电线捆起来,这样它们就不会像意大利面条一样到处乱放,然后用带子把它们绑起来。或者你可以找到一种方法,将 Raspberry Pi Shell、电源和以太网集线器安装到电路板或其他东西上,使其更容易携带。例如,您可以投资构建一个 Pi 堆栈。

有 Pi 栈吗?

如果您想让您的 Raspberry Pi 服务器集具有可移植性,您可以为自己构建一个 Pi 堆栈。有几种方法可以做到这一点,包括使用可堆叠的丙烯酸安装板。我没有使用这些,而是选择使用现有的 3D 打印的 Raspberry Pi Shell,并添加我自己的堆叠硬件。

我的 Pi 堆栈将所有五台计算机一台接一台地放在一个堆栈中,并带有用于安装以太网集线器和电源模块的支架。下面是我用半透明紫色打印的 Pi 堆栈。

img/460910_1_En_7_Figa_HTML.jpg

如果你有自己的 3D 打印机或者可以使用,你可以在 Thingiverse 的 www.thingiverse.com/thing:2893284 找到我的组件。

确保所有服务器都已启动并准备就绪。对于 Raspberry Pi 电脑,如果您没有使用监控器来观察启动顺序,您应该给它们大约 3-5 分钟的启动时间。

放置好您的 Raspberry Pi 计算机和外围设备后,您应该执行一个关键的准备步骤来设置集群。您可能还记得第 5 章中的内容,我们可以使用一个命令来准备在集群中使用的机器。该命令对于沙箱安装是唯一的;然而,我们在dba类中有另一个方法,可以用来准备远程机器。这个方法被命名为dba.configure_local_instance(),它将本地机器的连接作为一个参数。是的,这意味着我们必须在本地机器上运行 MySQL Shell。这就是我们在所有机器上安装 Shell 的原因。

首先打开所有 MySQL 服务器的电源(您可以暂时关闭应用服务器的电源)。然后,从一台桌面计算机(或应用服务器,如果您使用键盘、鼠标和监控器打开它)打开一个终端并输入以下命令:

$ ssh pi@192.168.42.241

这将建立到第一个 MySQL 服务器的远程连接。如果您在连接到机器时遇到问题,请确保您的台式计算机连接到相同的网络,并且 MySQL 服务器上的静态 IP 地址和主机文件已设置,并且机器可以访问网络。

连接机器后,使用 MySQL Shell 运行配置本地实例。我们将使用 localhost 作为主机名和端口 3306 来引用服务器。在我们发出配置本地实例的方法后,系统会提示我们提供 root 用户的密码。接下来,我们将被要求选择如何创建用于管理集群的用户。为了便于使用集群,建议选择选项(1),该选项使用相同的 root 用户,对其进行修改以正确访问集群。有关其他选项的更多信息,请参见在线参考手册(https://dev . MySQL . com/doc/dev/mysqlsh-API-python/8.0/classmysqlsh _ 1 _ 1 DBA _ 1 _ 1 _ cluster . html)。

接下来,系统会提示我们提供用户帐户(root)登录时使用的主机。在本演示中,我们将再次使用通配符(%),以便于使用集群。但是,对于生产部署,您可能希望指定一个用户/主机组合来进一步保护访问。

然后,该方法将检查服务器以查看是否需要修复任何东西,如果需要,将询问您是否希望自动修复它们(我们希望,因为这是该方法的优点之一)。

最后,当操作完成时,我们可以选择重新启动实例,以使更改生效。请务必对提示回答“是”。

哇,这种方法有很多优点。但是,这也是通过 AdminAPI 将 shell 与 InnoDB Cluster 结合使用的好处——它节省了我们处理小细节的大量时间,而我们想要的只是让它“正常工作”清单 7-10 显示了在一个服务器上运行该方法的脚本。

$ mysqlsh
MySQL Shell 8.0.11

Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type '\help' or '\?' for help; '\quit' to exit.

MySQL  Py > dba.configure_local_instance('localhost:3306')
Please provide the password for 'root@localhost:3306': ****
Configuring local MySQL instance listening at port 3306 for use in an InnoDB cluster...

This instance reports its own address as cluster-rpi3

WARNING: User 'root' can only connect from localhost.
If you need to manage this instance while connected from other hosts, new account(s) with the proper source address specification must be created.

1) Create remotely usable account for 'root' with same grants and password
2) Create a new admin account for InnoDB cluster with minimal required grants
3) Ignore and continue
4) Cancel

Please select an option [1]: 1
Please provide a source address filter for the account (e.g: 192.168.% or % etc) or leave empty and press Enter to cancel.
Account Host: %

Some configuration options need to be fixed:
+--------------------------+---------------+----------+---------------------+
| Variable                 | Current Value | Required | Note                |                                              Value
+--------------------------+---------------+----------+---------------------+
| binlog_checksum          | CRC32         | NONE     | Update the                                                         server variable     |
| enforce_gtid_consistency | OFF           | ON       | Update read-only                                                         variable and restart the server  |
| gtid_mode                | OFF           | ON       | Update read-only                                                         variable and restart the server  |
+--------------------------+---------------+----------+---------------------+

Do you want to perform the required configuration changes? [y/n]: y
Do you want to restart the instance after configuring it? [y/n]: y

Cluster admin user 'root'@'%' created.
Configuring instance...
The instance 'localhost:3306' was configured for cluster usage.
Restarting MySQL...
MySQL server at localhost:3306 was restarted.

Listing 7-10Configuring the Local Instance for Use with InnoDB Cluster

现在,该服务器的所有设置都已正确。确保在其他三台服务器上执行相同的步骤。如果您不确定操作是否成功或者您忘记了您已经完成了哪个服务器,您可以使用dba.check_instance_configuration()方法来测试服务器的正确设置。如果您看到状态ok,您就完成了,服务器准备好了。清单 7-11 展示了一个使用这种方法的例子。

 MySQL  Py > dba.check_instance_configuration('root@localhost:3306')
Please provide the password for 'root@localhost:3306': ****
Validating local MySQL instance listening at port 3306 for use in an InnoDB cluster...

This instance reports its own address as cluster-rpi1

Checking whether existing tables comply with Group Replication requirements...
No incompatible tables detected

Checking instance configuration...
Instance configuration is compatible with InnoDB cluster

The instance 'localhost:3306' is valid for InnoDB cluster usage.
{
    "status": "ok"
}

Listing 7-11Checking the Instance Configuration

一旦我们在所有服务器上运行了该命令,我们就可以配置集群了。

配置集群

现在我们已经准备好配置我们的服务器了。如果您还没有启动应用服务器,现在是这样做的好时机。确保连接键盘、鼠标和显示器。这将允许您使用应用服务器远程连接到集群中的其他机器。

使用远程机器配置集群的过程与第 5 章中的沙箱示例相同。也就是说,我们将在一台机器上创建服务器,然后将其他机器添加到集群中。我们完成了在应用服务器上配置路由。

注意

确保所有服务器都已启动并准备就绪。对于 Raspberry Pi 电脑,如果您没有使用监控器来观察启动顺序,您应该给它们大约 3-5 分钟的启动时间。

让我们首先打开到第一个服务器的远程连接(cluster-rpi1)。回想一下,我们使用 SSH 连接到远程机器。只需打开一个终端,输入以下命令之一。第一个命令可以在应用服务器上使用。如果您没有在应用服务器上运行该命令,您可能需要使用第二个命令。唯一的区别是第一个使用主机名,第二个使用 IP 地址。出现提示时,输入pi用户的密码。

$ ssh pi@cluster-rpi1
$ ssh pi@192.168.42.241

首先,我们使用 shell 连接并创建集群。使用名称RPI_Cluster作为集群的名称。因为我们使用的是自己分配并放在 hosts 文件中的主机名,所以我们不必使用 IP 地址来确保我们可以连接。回想一下,当我们启动 shell 并使用dba.create_cluster(<name>)方法创建集群时,我们可以使用--uri选项连接到服务器。清单 7-12 展示了一个使用 MySQL Shell 创建集群的例子。

$ mysqlsh --uri root@cluster-rpi1:3306
Creating a session to 'root@ cluster-rpi1:3306'
Enter password: ****
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 19
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.
MySQL Shell 8.0.11

Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type '\help' or '\?' for help; '\quit' to exit.

 MySQL  cluster-rpi1:3306 ssl  Py > cluster = dba.create_cluster('RPI_Cluster')
A new InnoDB cluster will be created on instance 'root@cluster-rpi1:3306'.

Validating instance at cluster-rpi1:3306...

This instance reports its own address as cluster-rpi1

Instance configuration is suitable.
Creating InnoDB cluster 'RPI_Cluster' on 'root@cluster-rpi1:3306'...
Adding Seed Instance...

Cluster successfully created. Use Cluster.add_instance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.

Listing 7-12Creating the Cluster

接下来,我们添加从cluster-rpi2 (192.168.42.242)开始的实例。但是首先,让我们检查集群状态。清单 7-13 展示了我们运行cluster.status()方法时的预期输出示例。回想一下,如果您已经退出了 shell,那么您必须使用命令cluster = dba.get_cluster()来检索集群。

MySQL  cluster-rpi1:33060+ ssl  Py > cluster.status()
{
    "clusterName": "RPI_Cluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "cluster-rpi1:3306",
        "ssl": "REQUIRED",
        "status": "OK_NO_TOLERANCE",
        "statusText": "Cluster is NOT tolerant to any failures.",
        "topology": {
            "cluster-rpi1:3306": {
                "address": "cluster-rpi1:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }
        }
    },
    "groupInformationSourceMember": "mysql://root@cluster-rpi1:3306"
}

Listing 7-13Checking the Status of the Cluster

接下来,让我们从cluster-rpi2开始添加每个服务器实例。清单 7-14 显示了将实例添加到集群的命令。请注意,我们可以在登录到cluster-rpi1机器时远程执行此操作。

 MySQL  cluster-rpi1:33060+ ssl  Py > cluster.add_instance('root@cluster-rpi2:3306')
A new instance will be added to the InnoDB cluster. Depending on the amount
of data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@cluster-rpi2:3306': ****
Adding instance to the cluster ...

Validating instance at cluster-rpi2:3306...

This instance reports its own address as cluster-rpi2

Instance configuration is suitable.
The instance 'root@cluster-rpi2:3306' was successfully added to the cluster.

Listing 7-14Adding an Instance to the Cluster

如果我们再次检查状态,我们将看到新实例被添加,但是我们可能会在一段时间内看到状态为RECOVERING。这是因为组复制正在与新的主节点协商。清单 7-15 显示了添加第一个实例后的状态。

 MySQL  cluster-rpi1:33060+ ssl  Py > cluster.status()
{
    "clusterName": "RPI_Cluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "cluster-rpi1:3306",
        "ssl": "REQUIRED",
        "status": "OK_NO_TOLERANCE",
        "statusText": "Cluster is NOT tolerant to any failures. 1 member is not active",
        "topology": {

            "cluster-rpi1:3306": {
                "address": "cluster-rpi1:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"

            },
            "cluster-rpi2:3306": {
                "address": "cluster-rpi2:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "RECOVERING"
            }
        }
    },
    "groupInformationSourceMember": "mysql://root@cluster-rpi1:3306"
}

Listing 7-15Cluster Status After the First Instance Is Added

注意,新服务器的状态是RECOVERING。这很正常。在 Raspberry Pi 上,这个过程可能需要一段时间,但最终您会看到状态更改为ONLINE,如清单 7-16 所示。

 MySQL  cluster-rpi1:33060+ ssl  Py > cluster.status()
{
    "clusterName": "RPI_Cluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "cluster-rpi1:3306",
        "ssl": "REQUIRED",
        "status": "OK_NO_TOLERANCE",
        "statusText": "Cluster is NOT tolerant to any failures.",
        "topology": {
            "cluster-rpi1:3306": {
                "address": "cluster-rpi1:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"

            },
            "cluster-rpi2:3306": {
                "address": "cluster-rpi2:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }
        }
    }, 

    "groupInformationSourceMember": "mysql://root@cluster-rpi1:3306"
}

Listing 7-16Cluster Status After the First Instance Is Reconciled

现在,让我们添加另外两个实例。清单 7-17 显示了添加cluster-rpi3cluster-rpi4的命令。

 MySQL  cluster-rpi1:33060+ ssl  Py > cluster.add_instance('root@cluster-rpi3:3306')
A new instance will be added to the InnoDB cluster. Depending on the amount
of data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@cluster-rpi3:3306': ****
Adding instance to the cluster ...

Validating instance at cluster-rpi3:3306...

This instance reports its own address as cluster-rpi3

Instance configuration is suitable.
The instance 'root@cluster-rpi3:3306' was successfully added to the cluster.

 MySQL  cluster-rpi1:33060+ ssl  Py > cluster.add_instance('root@cluster-rpi4:3306')
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@cluster-rpi4:3306': ****
Adding instance to the cluster ...

Validating instance at cluster-rpi4:3306...

This instance reports its own address as cluster-rpi4

Instance configuration is suitable.
The instance 'root@cluster-rpi4:3306' was successfully added to the cluster.

Listing 7-17Adding the Remaining Instances to the Cluster

过一会儿,我们应该会看到状态全部清除,集群的状态为ONLINE和就绪,如清单 7-18 所示。

 MySQL  cluster-rpi1:33060+ ssl  Py > cluster.status()
{
    "clusterName": "RPI_Cluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "cluster-rpi1:3306",
        "ssl": "REQUIRED",
        "status": "OK_PARTIAL",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure. 1 member is not active",
        "topology": {
            "cluster-rpi1:3306": {
                "address": "cluster-rpi1:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"

            },
            "cluster-rpi2:3306": {
                "address": "cluster-rpi2:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi3:3306": {
                "address": "cluster-rpi3:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "RECOVERING"
            },
            "cluster-rpi4:3306": {
                "address": "cluster-rpi4:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }
        }
    },
    "groupInformationSourceMember": "mysql://root@cluster-rpi1:3306"
}

Listing 7-18Cluster Status After All Instances Are Added and Reconciled

现在,我们准备启动路由。

引导路由

虽然严格来说这不是配置集群的一部分,但是该过程的下一步是引导(配置)MySQL 路由,以便用于访问集群的应用。如果您还没有登录到应用服务器,您应该现在就登录。

回想一下,引导路由需要提供--boostrap选项、使用- -user选项的用户帐户以及连接到集群中一个服务器的 URL。清单 7-19 显示了在应用服务器上引导路由的命令以及一个执行脚本的例子。

$ sudo /usr/local/mysql/bin/mysqlrouter --bootstrap root@cluster-rpi4:3306 --user=pi
Please enter MySQL password for root:

Bootstrapping system MySQL Router instance...

Module “ not registered with logger - logging the following message as 'main' instead
MySQL Router  has now been configured for the InnoDB cluster 'RPI_Cluster'.

The following connection information can be used to connect to the cluster.

Classic MySQL protocol connections to cluster 'RPI_Cluster':
- Read/Write Connections: localhost:6446
- Read/Only Connections: localhost:6447
X protocol connections to cluster 'RPI_Cluster':
- Read/Write Connections: localhost:64460
- Read/Only Connections: localhost:64470

Listing 7-19Bootstrapping MySQL Router on the Application Server

下面是引导操作为创建配置文件所做的工作。这几乎是我们所需要的;如果您手动安装了路由,我们可能需要添加plugin_folder路径,如清单 7-20 所示。

$ sudo more /usr/local/mysql/lib/mysql-router/mysqlrouter.conf
# File automatically generated during MySQL Router bootstrap
[DEFAULT]
user=pi

plugin_folder=/usr/local/mysql/lib/mysqlrouter

logging_folder=/usr/local/mysql/lib/mysql-router/log
runtime_folder=/usr/local/mysql/lib/mysql-router/run
data_folder=/usr/local/mysql/lib/mysql-router/data
keyring_path=/usr/local/mysql/lib/mysql-router/data/keyring
master_key_path=/usr/local/mysql/lib/mysql-router/mysqlrouter.key
connect_timeout=30
read_timeout=30

[logger]
level = INFO

[metadata_cache:RPI_Cluster]
router_id=2
bootstrap_server_addresses=mysql://cluster-rpi1:3306,mysql://cluster-rpi2:3306,mysql://cluster-rpi4:3306,mysql://cluster-rpi3:3306
user=mysql_router2_ty03b9q0tzzg
metadata_cluster=RPI_Cluster
ttl=5

[routing:RPI_Cluster_default_rw]
bind_address=0.0.0.0
bind_port=6446
destinations=metadata-cache://RPI_Cluster/default?role=PRIMARY
routing_strategy=round-robin
protocol=classic

[routing:RPI_Cluster_default_ro]
bind_address=0.0.0.0
bind_port=6447
destinations=metadata-cache://RPI_Cluster/default?role=SECONDARY
routing_strategy=round-robin
protocol=classic

[routing:RPI_Cluster_default_x_rw]
bind_address=0.0.0.0
bind_port=64460
destinations=metadata-cache://RPI_Cluster/default?role=PRIMARY
routing_strategy=round-robin
protocol=x

[routing:RPI_Cluster_default_x_ro]
bind_address=0.0.0.0
bind_port=64470
destinations=metadata-cache://RPI_Cluster/default?role=SECONDARY
routing_strategy=round-robin
protocol=x

Listing 7-20Example MySQL Router Configuration File

现在,我们使用以下命令运行路由:

$ sudo /usr/local/mysql/bin/mysqlrouter -c /usr/local/mysql/lib/mysql-router/mysqlrouter.conf --user=pi &

如果你想看日志,你可以在这里找到它:

$ sudo more /usr/local/mysql/lib/mysql-router/log/mysqlrouter.log

就这样!我们的集群已经配置好,可以使用了。虽然我们没有测试集群的应用(但是您将在下一章中看到),但是我们可以使用一个简单的 Python 应用通过路由来测试集群。

测试集群

要测试集群,您可以使用以下命令通过路由手动连接到其中一个服务器。在这种情况下,我们可以通过端口 6446 登录,然后执行查询以返回变量report_host,这将显示我们连接到的主机。下面显示了在批处理模式下通过 MySQL Shell 运行时的情况:

$ mysqlsh root@cluster-rpi-app:6446 --sql -e "SHOW VARIABLES LIKE 'report_host'"
Enter password: ****
+---------------+-------------+
| Variable_name | Value       |
+---------------+-------------+
| report_host   | cluster-rp1 |
+---------------+-------------+

这向我们展示了路由正在将我们的请求转发到集群中的一个服务器(读/写服务器,因为我们使用了端口 6446),但是这有什么意思呢?

让我们使用 Python 来测试集群。在下一章中,您将看到一个用 Python 编写的高可用性应用示例。为了运行该应用——更好的是,为了测试路由——我们需要在应用服务器上下载并安装连接器/Python 数据库连接器。

要下载 Connector/Python,请访问 https://dev.mysql.com/downloads/connector/python/From 选择操作系统下拉框,选择平台无关,然后选择名为平台无关(架构无关)的压缩 TAR 存档文件,如图 7-12 所示。

img/460910_1_En_7_Fig12_HTML.jpg

图 7-12

下载 MySQL 连接器/Python

文件下载完成后,可以解压包,然后运行这里显示的 Python setup 命令;请注意,该命令需要提升权限:

$ tar -xvf mysql-connector-python-8.0.11.tar.gz
...
$ sudo python3 ./setup.py install
running install
Not Installing MySQL C Extension

running build
running build_py
creating build/lib
creating build/lib/mysql
copying lib/mysql/__init__.py -> build/lib/mysql
creating build/lib/mysql/connector
...
Copying lib/mysql_connector_python.egg-info to /usr/local/lib/python3.5/dist-packages/mysql_connector_python-8.0.11.egg-info

现在我们已经安装了 Connector/Python,我们可以运行测试了。我们将从应用服务器运行它,因为我们将在那里运行我们的测试应用。清单 7-21 显示了一个简单的 Python 脚本,它通过路由连接到应用服务器并运行一个简单的 SQL 语句。

import mysql.connector
conn_dict = {
'user':'root',
'passwd':'root',
'port':6446,
'host':'cluster-rpi-app',
}
conn = mysql.connector.connect(**conn_dict)
cur = conn.cursor()
res = cur.execute("SHOW DATABASES")
for row in cur:
  print(row)
cur.close()
conn.close()

Listing 7-21Simple Python Script for Testing the Cluster

您可以创建一个文件,将脚本命令放在文件中,并通过 MySQL Shell 执行它,也可以使用 Python 控制台。清单 7-22 展示了一个使用 Python 控制台的例子。我们只需从终端使用命令python来启动 Python 控制台。您可以在任何想要的机器上运行这个脚本(例如,应用服务器),但是最好从您的桌面计算机上运行它。在这种情况下,一定要使用清单中所示的应用服务器的 IP 地址。

C:\Users\cbell> python
Python 3.4.4 (v3.4.4:737efcadf5a6, Dec 20 2015, 20:20:57) [MSC v.1600 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import mysql.connector
>>> conn_dict = {
... 'user':'root',
... 'passwd':'root',
... 'port':3306,
... 'host':'localhost',
... }
>>> conn = mysql.connector.connect(**conn_dict)
>>> cur = conn.cursor()
>>> res = cur.execute("SHOW DATABASES")
>>> for row in cur:
...   print(row)
...
('information_schema',)
('mysql',)
('performance_schema',)
('sys',)
('world_x',)
>>> cur.close()
True
>>> conn.close()
>>> quit()

Listing 7-22Testing the Cluster via the Python Console

好了,现在我们知道集群工作正常,路由配置正确。我们可以继续看如何开发一个小型的、高可用性的应用来使用集群。

关闭和重新启动集群

通常,InnoDB 集群(或任何高可用性系统)永远不会完全关闭。事实上,保持系统运行是我们的目标。然而,对于运行在一组 Raspberry Pi 计算机上的开发集群,我们可能不想让机器长时间运行。我们不仅希望关闭集群,还可能希望稍后再打开它。本节介绍一种安全关闭集群电源然后再打开电源的方法。

正在关闭集群

简而言之,InnoDB 集群的设计不是为了随意打开和关闭电源。相反,关闭所有服务器将导致集群完全失去集群连续性。尽管这对于生产系统、我们的开发集群(或者任何类似的集群)来说是非常糟糕的,但是我们可能确实希望在不使用它的时候关闭它。

你是做什么的?AdminAPI 在dba模块中包含从完全丢失中恢复集群的方法。但是,只有在对集群中的服务器执行受控关闭时,这种方法才有效。下面概述了可以用来关闭集群电源的过程:

  1. 获取集群状态并记下读/写服务器。

  2. 连接到每个只读服务器并关闭它们。

  3. 关闭读/写服务器。

回想一下,我们可以连接到集群中的任何机器,获取集群,并使用status()方法找到读/写服务器。应该通过 MySQL Shell 或 MySQL 客户端发出 shutdown SQL 命令来连接到只读服务器并关闭它们:

$ mysqlsh --uri root@192.168.42.242:3306 --sql -e "SHUTDOWN"

对其他只读服务器重复此命令,然后对读/写服务器重复此命令。记下哪个服务器是读/写服务器。此时,您可以使用以下命令关闭每台 Raspberry Pi 计算机:

$ sudo shutdown -h now

重新启动集群

您可能希望集群通过重新启动集群中的所有服务器来重新建立自身,但事实并非如此。当从头开始重新启动集群时,我们必须在 AdminAPI 中使用一种特殊的方法。这种方法适用于没有出现任何错误的集群,即那些已经成功关闭的集群。这被称为从完全中断中恢复集群。然而,这只有在所有服务器都已重启、MySQL 已在所有服务器上启动并且它们可以访问网络(以及相互访问)的情况下才有效。

警告

在尝试重新启动集群之前,请确保所有服务器都已开机,并且 MySQL 正在运行并且可以通过网络访问。

下面演示了如何从完全中断中恢复集群。具体来说,所有服务器都已重新启动(关闭电源并重新启动),您需要从最后一个已知正常的位置重新启动集群。我们将使用dba.reboot_cluster_from_complete_outage()方法重启集群。首先,按照关闭服务器时的提示登录到读/写服务器,并运行如下所示的命令:

 MySQL  localhost:33060+ ssl  Py > cluster = dba.reboot_cluster_from_complete_outage('RPI_Cluster')
Reconfiguring the cluster 'RPI_Cluster' from complete outage...

The instance '192.168.42.244:3306' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The instance '192.168.42.243:3306' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The instance '192.168.42.242:3306' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The cluster was successfully rebooted.

该命令读取集群元数据,并尝试重新连接(重新加入)所有服务器。如果成功,您将会看到指示集群已重新启动的消息。如果遇到错误,请确保所有服务器都在运行并且可以访问网络,更正任何问题,然后重试该命令。如果仍然不起作用,请参见第 9 章中的“最后手段”一节,了解如何重置集群。

摘要

随着 MySQL 8.0.11 版本的发布,构建高可用性 MySQL 解决方案变得更加容易。许多可用的工具和资源使得安装和配置 InnoDB Cluster 及其组件变得非常简单。回想一下前面的章节,这一切都是在抑制组复制的复杂性的同时发生的。显然,MySQL 已经从一个简单的互联网后端数据库存储走了很长的路。

本章介绍了使用几台独立的 MySQL 机器的开发部署。我们使用 Raspberry Pi 计算机来降低成本,并帮助几乎无成本地犯任何错误或意外配置。毕竟,谁想告诉 IT 经理他们必须重新构建开发者数据库服务器,因为我们正在测试一些东西呢?也许有些人会侥幸逃脱,但我们中很少有人有足够的勇气去冒险(并接受后果)。

正如您所看到的,将 InnoDB 集群部署到一组服务器并不比使用沙箱方法困难多少。这仅仅意味着我们需要进行一些远程连接和一些次要的设置,以确保我们的服务器能够正常通信。

此外,现在我们已经有了一个开发环境,可以用来测试我们希望构建到高可用性 MySQL 解决方案中的应用,我们可以看看帮助我们计划将 InnoDB Cluster 部署到生产环境中所需的其余知识。

在下一章中,您将看到如何构建一个使用路由并将数据存储在集群中的基本高可用性应用。

八、示例应用

现在,您已经学习了如何在一小组测试服务器上设置 InnoDB 集群,包括应用服务器和路由,您已经准备好使用一个示例应用来完成我们的演练。与其用第 7 章的简单 Python 脚本来结束本教程,不如开发一个简单但功能齐全的高可用性应用来完成您的启蒙。编写该应用时,写请求将通过路由定向到当前的读/写服务器,读请求也将通过路由定向到只读服务器队列。

虽然这看起来与普通应用可能做的事情没有太大区别,但是当您考虑到应用必须能够容忍集群中发生的故障时,这种区别就很明显了。请记住,路由将重新路由我们的连接,但我们必须重试任何打开的连接,以获得当前的活动连接(路由)。

在这一章中,你将看到如何开发一个高可用性的基于 web 的应用。我们将在应用中使用 Python,因为它简单易学,代码读起来比其他语言更清晰。但是如果你喜欢另一种语言,也不用担心。你可以很容易地用任何有 MySQL 数据库连接器的语言重写本章的代码。

让我们从这个示例应用将做什么以及它将如何工作的概述开始。幸运的是,所有的源代码都可以从 Apress book 网站下载,所以您不必手工输入代码。

小费

参见 www.apress.com/us/book/9781484238844 下载本章的完整源代码。

首先,您必须了解示例应用是如何设计的以及它是如何工作的。毕竟,最好的例子应该是你可以在自己的环境中使用的东西;这个例子必须足够复杂和完整才有意义。

概观

我们将在本章中编写的示例应用是一个简单的杂货店或待办事项列表。它故意很小并且功能有限,因为我们希望专注于为 InnoDB Cluster 编写应用的基本机制,而不是花时间在编写用户界面或应用代码的巧妙或酷的方法上。例如,它是单用户应用,而高可用性应用是多用户的。移除多用户方面使应用更简单,更容易被更多的人使用。最后,错误处理代码是基本的。 1

这一点很重要,因为我们不需要一个非常复杂、功能全面的企业级应用来测试 MySQL InnoDB 集群,尤其是对于那些技术(或 MySQL)新手来说。有意保持应用简单(用户界面和功能),意味着我们可以专注于高可用性应用的基本功能,因为它与数据库中的数据交互。

我们只需要考虑四个基本的数据函数;创建、读取、更新和删除(CRUD)。我们将使用 MySQL 连接器/Python 编写数据库代码来实现这些功能。事实上,我们将开发一个单独的模块来处理数据库代码,为用户界面提供一个代码模块(由几个相关文件组成),为数据库代码提供一个代码模块。

应用会将一个项目列表(待办事项、购物项目等)作为默认视图(一个读取动作)呈现出来。然后,用户可以创建新项目以添加到列表中,或者在单独的详细视图中单击项目以更新或删除它。用户可以将项目标记为“完成”或“已购买”,并隐藏这些项目(或显示它们)。总而言之,这是一个管理列表的简单应用。

另一方面,用户界面使事情变得有点复杂。我们可以通过使用熟悉的用户界面设计来缓解这一问题。为此,我们将使用一个 web 应用。不幸的是,用纯 Python 编写 web 应用是乏味的,并且需要比我们在这种规模的工作中所能期望的更多的关于 web 应用工作方式的知识。

为了克服这个挑战,我们将使用一个流行的 Python web 应用框架。在这种情况下,我们将使用 Flask,包括入门、教程和用户界面代码的演练。正如您将看到的,Flask 也很容易学习,只有中等数量的细微差别和概念。Flask 最初是由阿明·罗纳彻开发的,已经被证明是 Python 最简单、最稳定的 web 平台之一。

因为这是一个简化的应用,所以在设计和实现中用户界面代码很少。精明的开发者可能会发现代码中需要改进的地方,以提供企业级服务,但同样,这不是像这样的实验所关心的。但是,如果你认为合适的话,可以随意修饰和改进应用。事实上,如果你选择扩展应用,本章末尾会有一些建议。

让我们从学习 Flask 的短暂旅程开始吧。

开始使用 Flask

如果您想继续并实现这个示例项目,您需要在您的计算机上安装一些东西。本节将帮助您为计算机准备所需的工具—您需要安装什么以及如何配置您的环境。您还将看到关于用户界面工具的简短介绍。

因为我们将开发在 Raspberry Pi 应用服务器(cluster-rpi-app)上运行的应用,所以我们将在该系统上安装我们需要的工具。如果您还没有这样做,请继续使用键盘、鼠标和显示器设置您的应用服务器,然后启动并登录。除非另有说明,否则所有命令都将从终端窗口运行。

设置您的环境

对你的环境的改变并不困难,也不漫长。我们将安装 Flask 和一些应用用户界面所需的扩展。Flask 是可以与 Python 一起使用的几个 web 库之一。这些 web 库使得用 Python 开发 web 应用比使用原始的 HTML 代码并为请求编写自己的处理程序和代码要容易得多。另外,Flask 并不难学。

8-1 列出了我们需要安装的库。该表列出了库/扩展的名称、简短描述以及产品文档的 URL。

表 8-1

必需的库

|

|

描述

|

文件

|
| --- | --- | --- |
| 瓶 | Python Web API | http://flask.pocoo.org/docs/0.12/installation/ |
| 烧瓶脚本 | Flask 的脚本支持 | https://flask-script.readthedocs.io/en/latest/ |
| 烧瓶自举 | 用户界面的改进和增强 | https://pythonhosted.org/Flask-Bootstrap/ |
| 烧瓶-WTF | WTForms 集成 | https://flask-wtf.readthedocs.io/en/latest/ |
| https://wtforms.readthedocs.io/en/latest/ |

当然,你应该已经在你的系统上安装了 Python,因为它附带了 Raspberry Pi 操作系统(回想一下, PiPython 的简称)。

要安装这些库,我们可以使用 Python 包管理器pip3,从命令行安装这些库。Raspberry Pi 中也包含了pip3实用程序,它被设计用来安装与 Python 版本 3 一起使用的包。

注意

如果您的系统上安装了多个版本的 Python,那么pip命令将安装到默认的 Python 版本环境中。要使用pip安装到特定版本,请使用pipN,其中N是版本。例如,pip3在 Python 3 环境中安装包。

pip命令非常方便,因为它使得安装注册的 Python 包(那些在 Python 包索引中注册的包,缩写为 PyPI2(https://pypi.python.org/pypi))变得非常容易。pip命令将使用一个命令下载、解压和安装。让我们来看看如何安装我们需要的每个包。

清单 8-1 展示了如何使用pip3命令为每个库(Flask、Flask-Script、Flask-Bootstrap 和 Flask-WTF)安装 Flask 和支持库。

$ sudo pip3 install Flask

Collecting Flask

  Downloading https://files.pythonhosted.org/packages/7f/e7/08578774ed4536d3242b14dacb4696386634607af824ea997202cd0edb4b/Flask-1.0.2-py2.py3-none-any.whl (91kB)
    100% |█████████████████| 92kB 1.6MB/s
Requirement already satisfied: click>=5.1 in /usr/lib/python3/dist-packages (from Flask)
Collecting Jinja2>=2.10 (from Flask)
  Downloading https://files.pythonhosted.org/packages/7f/ff/ae64bacdfc95f27a016a7bed8e8686763ba4d277a78ca76f32659220a731/Jinja2-2.10-py2.py3-none-any.whl (126kB)
    100% |█████████████████| 133kB 1.3MB/s
Requirement already satisfied: itsdangerous>=0.24 in /usr/lib/python3/dist-packages (from Flask)
Collecting Werkzeug>=0.14 (from Flask)
  Downloading https://files.pythonhosted.org/packages/20/c4/12e3e56473e52375aa29c4764e70d1b8f3efa6682bef8d0aae04fe335243/Werkzeug-0.14.1-py2.py3-none-any.whl (322kB)
    100% |█████████████████| 327kB 637kB/s
Requirement already satisfied: MarkupSafe>=0.23 in /usr/lib/python3/dist-packages (from Jinja2>=2.10->Flask)
Installing collected packages: Jinja2, Werkzeug, Flask
  Found existing installation: Jinja2 2.8
    Not uninstalling jinja2 at /usr/lib/python3/dist-packages, outside environment /usr
  Found existing installation: Werkzeug 0.11.15
    Not uninstalling werkzeug at /usr/lib/python3/dist-packages, outside environment /usr
Successfully installed Flask-1.0.2 Jinja2-2.10 Werkzeug-0.14.1

$ sudo pip3 install Flask-Script

Collecting Flask-Script
  Downloading https://www.piwheels.org/simple/flask-script/Flask_Script-2.0.6-py3-none-any.whl
Collecting Flask (from Flask-Script)
  Downloading https://files.pythonhosted.org/packages/7f/e7/08578774ed4536d3242b14dacb4696386634607af824ea997202cd0edb4b/Flask-1.0.2-py2.py3-none-any.whl (91kB)
    100% |█████████████████| 92kB 2.1MB/s
Collecting click>=5.1 (from Flask->Flask-Script)
  Downloading https://files.pythonhosted.org/packages/34/c1/8806f99713ddb993c5366c362b2f908f18269f8d792aff1abfd700775a77/click-6.7-py2.py3-none-any.whl (71kB)
    100% |█████████████████| 71kB 1.0MB/s

Installing collected packages: Flask-Script
Successfully installed Flask-Script-2.0.6

$ sudo pip3 install

Flask-Bootstrap

Collecting Flask-Bootstrap
  Downloading https://www.piwheels.org/simple/flask-bootstrap/Flask_Bootstrap-3.3.7.1-py3-none-any.whl (461kB)
    100% |█████████████████| 471kB 25kB/s
Collecting visitor (from Flask-Bootstrap)
  Downloading https://www.piwheels.org/simple/visitor/visitor-0.1.3-py3-none-any.whl
Collecting dominate (from Flask-Bootstrap)
  Downloading https://www.piwheels.org/simple/dominate/dominate-2.3.1-py3-none-any.whl
Requirement already satisfied: Flask>=0.8 in /usr/lib/python3/dist-packages (from Flask-Bootstrap)
Installing collected packages: visitor, dominate, Flask-Bootstrap
Successfully installed Flask-Bootstrap-3.3.7.1 dominate-2.3.1 visitor-0.1.3

$ sudo pip3 install

Flask-WTF

Collecting Flask-WTF
  Downloading https://files.pythonhosted.org/packages/60/3a/58c629472d10539ae5167dc7c1fecfa95dd7d0b7864623931e3776438a24/Flask_WTF-0.14.2-py2.py3-none-any.whl
Requirement already satisfied: Flask in /usr/lib/python3/dist-packages (from Flask-WTF)
Collecting WTForms (from Flask-WTF)
  Downloading https://www.piwheels.org/simple/wtforms/WTForms-2.1-py2.py3-none-any.whl (140kB)
    100% |█████████████████| 143kB 41kB/s
Installing collected packages: WTForms, Flask-WTF
Successfully installed Flask-WTF-0.14.2 WTForms-2.1

Listing 8-1
Installing Flask

请注意,该命令下载必要的组件,提取它们,然后运行每个组件的安装程序。在这种情况下,Flask 由几个组件组成,包括 Werkzeug、MarkupSafe 和 Jinja2。在接下来的“烧瓶初级读本”一节中,你将会学到更多。

您还应该安装 MySQL 连接器/Python 8.0.11 或更高版本的数据库连接器。如果您没有在第 7 章的演练中安装它,请参见该章中的“测试集群”一节,并安装数据库连接器。

现在我们的计算机已经安装好了,让我们上一堂关于 Flask 及其相关扩展的速成课。

弗拉斯克第一

Flask 是与 Python 一起使用的几个 web 应用库(有时称为框架应用编程接口,或 API)之一。Flask 在众多选择中是独一无二的,因为它很小,而且在你熟悉它的工作原理之后,它很容易使用。编写初始化代码后,使用 Flask 的大部分工作将局限于创建网页、重定向响应和编写功能代码。

Flask 被认为是一个微框架,因为它小巧轻便,它不会强迫你进入一个盒子,专门编写代码来与框架交互。它提供了您需要的一切,没有什么是您不需要的,让您自己选择在代码中使用什么。

Flask 由提供其基本功能的两个主要组件组成:一个 Web 服务器网关接口(WSGI ),用于处理托管网页的所有工作;一个模板库,用于简化网页开发,减少学习 HTML 的需要,删除重复的结构,并为 HTML 代码提供脚本功能。WSGI 组件被命名为 Werkzeug ,大致翻译自德语,意思是工作素材 ( http://werkzeug.pocoo.org /)。模板组件被命名为 Jinja2 ,并模仿 Django ( http://jinja.pocoo.org/docs/2.10/ )。两者都是由 Flask 的创始人开发和维护的。最后,当您安装 Flask 时,这两个组件都会被安装。

Flask 也是一个可扩展的库,允许其他开发者创建基本库的附件(扩展)来添加功能。在上一节中,您看到了如何安装 Flask 可用的一些扩展。我们将在本章中使用脚本、引导和 WTForms 扩展。能够挑选您想要的扩展意味着您可以保持您的应用尽可能小,只添加您需要的。

您可能认为 Flask“缺少”的组件之一是与其他服务(如数据库系统)交互的能力。这是一个有目的的设计,像这样的功能可以通过扩展来实现。事实上,Flask 有几个数据库扩展可用,包括那些允许您使用 MySQL 的扩展。但是,因为我们希望将应用与 InnoDB 集群一起使用,所以我们必须使用 Oracle 提供的连接器 MySQL Connector/Python。

Flask 和前面描述的扩展一起,提供了用 Python 制作 web 应用所需的所有连接和管道。它消除了编写 web 应用所需的几乎所有负担,例如解释客户机响应包、路由、HTML 表单处理等等。如果您曾经用 Python 编写过 web 应用,您将会体会到创建健壮的 web 页面的能力,而无需编写 HTML 和样式表的复杂性。一旦你熟悉如何使用 Flask,它将允许你专注于你的应用的代码,而不是花大量的时间编写用户界面。

现在,让我们开始学习 Flask!如果您不着急,尝试一下示例应用,您的第一个 Flask 应用将在第一次尝试时就能工作。学习 Flask 最难的部分已经过去了——安装 Flask 及其扩展。剩下的就是学习在 Flask 中编写应用的概念。在此之前,让我们了解一下 Flask 中的术语,以及如何设置我们将用来初始化本章中使用的应用实例的基本代码。

小费

如果你想进一步了解 Flask,可以考虑阅读在线文档、用户指南和位于 http://flask.pocoo.org/docs/0.12/ 的示例。

术语

Flask 旨在减少编写 web 应用的繁琐。按照 Flask 的说法,使用代码的两个部分来呈现一个网页:一个在 HTML 文件中定义的视图,以及一个处理客户端请求的路由。回想一下,我们可以看到两个请求中的一个:一个是请求加载网页的GET请求(从客户端的角度读取),另一个是从客户端通过网页向服务器发送数据的POST请求(从客户端的角度写入)。这两个请求都在 Flask 中通过使用您定义的函数来处理。

然后,这些函数呈现网页,并将其发送回客户端以满足请求。Flask 调用函数视图函数(或简称视图)。Flask 知道调用哪个方法的方式是使用识别 URL 路径的装饰器(在 Flask 中称为 route )。你可以用一条或多条路线来装饰一个功能,这样就可以提供多种到达视图的方式。用的装饰师是@app.route(<path>)。以下显示了查看功能的多条路线的示例:

@app.route('/list_item', methods=['GET', 'POST'])
@app.route('/list_item/<string:item_selected>', methods=['GET', 'POST'])
def list_view(isbn_selected=None):
    notes = None
    form = ListForm()
...
    if request.method == 'POST':
        pass
    return render_template("list.html", form=form)

注意这里有多个装饰者。第一个是list,它允许我们使用类似于localhost:5000/list_item的 URL,这使得 Flask 将执行路由到list_view()函数。第二个是list_item/<item_selected>,演示了如何使用变量向视图传递信息。在这种情况下,如果用户(应用)使用 URL localhost:5000/list_item/9,Flask 将值9放在item_selected变量中。这样,我们可以动态地将信息传递给我们的视图。

还要注意,路由指定了每条路由允许的方法。在这个应用中,我们可以为任何一个路由设置一个GETPOST。如果你不使用装饰器,默认设置是GET,使网页只读。

最后,请注意,在函数的末尾,我们通过调用render_template()函数(从 Flask 模块导入)返回,该函数告诉 Flask 返回(刷新)带有我们获取或分配的数据的网页。这个网页,book.html在 Flask 中被称为一个窗体。我们将使用这个概念从数据库中检索信息并将其发送给用户。我们可以返回一个简单的 HTML 字符串(或整个文件)或所谓的表单。因为我们使用 Flask-WTF 和 WTForms 扩展,所以我们可以返回一个呈现为表单类的模板。我们将在后面的章节中讨论表单、表单类以及章节项目的其他路径和视图。正如您将看到的,模板是另一个强大的功能,它使创建网页变得很容易。

什么是室内设计师?

在 Python 中,我们可以通过使用 decorators 来指定特殊的处理参数。装饰器只是改变函数行为的一种方式。例如,您可以使用 decorators 来添加更强的类型检查、定义宏以及在执行前后调用函数。在 Flask 中使用 decorator 进行路由是正确使用 decorator 的最好例子之一。要了解更多关于装修工的信息,请参见 www.python.org/dev/peps/pep-0318

Flask 构建了一个应用中所有路径的列表,使得应用在被请求时可以很容易地将执行路由到正确的函数。但是,如果请求了一条路线,但该路线在应用中不存在,会发生什么情况呢?默认情况下,您会得到类似于Not Found. The requested URL was not found on the server的一般错误消息。在后面的小节中,您将看到如何添加我们自己的自定义错误处理路由。

既然您已经了解了 Flask 中使用的术语以及它是如何与 web 页面一起工作的,那么让我们来看看一个典型的 Flask 应用的构造以及我们需要的扩展。

初始化和应用实例

Flask 及其扩展为您的 web 应用提供了入口点。Flask 会为您完成这些工作,而不是自己编写所有繁重的代码!我们将在本章使用的 Flask 扩展包括 Flask-Script、Flask-Bootstrap、Flask-WTF 和 WTForms。以下各节简要介绍了每一种方法。

烧瓶脚本

Flask-Script 通过添加一个命令行解析器(显示为manager)来启用 Flask 应用中的脚本,您可以使用该解析器链接到您编写的函数。这可以通过用@manager.command修饰函数来实现。理解这为我们做了什么的最好方法是通过一个例子。

下面是一个基本的原始 Flask 应用,它什么也不做。它甚至不是一个“hello,world”示例,因为没有显示任何内容,也没有托管任何网页—它只是一个原始的 Flask 应用:

from flask import Flask      # import the Flask framework
app = Flask(__name__)        # initialize the application
if __name__ == "__main__":   # guard for running the code
    app.run()                # launch the application

注意这个app.run()调用。这被称为服务器启动,当我们使用 Python 解释器加载脚本时执行。当我们运行这段代码时,我们看到的只是来自 Flask 的默认消息:

$ python3 ./flask-ex.py --help
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

请注意,我们无法查看帮助,因为没有这样的选项。代码通过使用 web 服务器的默认值启动(如果需要,我们可以在代码中更改)。例如,我们可以改变服务器监听的端口。

使用 Flask-Script,我们不仅添加了帮助选项,还添加了控制服务器的选项。下面的代码显示了添加语句来启用 Flask-Script 是多么容易。新语句以粗体突出显示:

from flask import Flask          # import the Flask framework

from flask_script import Manager # import the flask script manager class

app = Flask(__name__)            # initialize the application

manager = Manager(app)           # initialize the script manager class

# Sample method linked as a command-line option

@manager.command

def hello_world():

    """Print 'Hello, world!'"""
    print("Hello, world!")

if __name__ == "__main__":       # guard for running the code
    manager.run()                # launch the application via manager class

当这段代码运行时,我们可以看到额外的选项可用。请注意,文档字符串(紧跟在方法定义之后)显示为所添加命令的帮助文本:

$ python ./flask-script-ex.py --help
usage: flask-script-ex.py [-?] {hello_world,shell,runserver} ...

positional arguments:
  {hello_world,shell,runserver}
    hello_world         Print 'Hello, world!'
    shell               Runs a Python shell inside Flask application context.
    runserver           Runs the Flask development server i.e. app.run()

optional arguments:
  -?, --help            show this help message and exit

我们可以看到我们添加的命令行参数(命令),hello_world,但是我们也可以看到 Flask-Script 提供的两个新参数:shellrunserver。启动服务器时,您必须选择其中一个命令。shell命令允许您在 Python 解释器或类似工具中使用代码,而runserver执行代码启动 web 服务器。

我们不仅可以获得关于命令和选项的帮助,而且 Flask-Script 还提供了从命令行对服务器的更多控制。事实上,通过添加--help选项,我们可以看到每个命令的所有选项:

$ python ./flask-script-ex.py runserver --help
usage: flask-script-ex.py runserver [-?] [-h HOST] [-p PORT] [--threaded]
                                    [--processes PROCESSES]
                                    [--passthrough-errors] [-d] [-D] [-r] [-R]
                                    [--ssl-crt SSL_CRT] [--ssl-key SSL_KEY]

Runs the Flask development server i.e. app.run()

optional arguments:
  -?, --help            show this help message and exit
  -h HOST, --host HOST
  -p PORT, --port PORT
  --threaded
  --processes PROCESSES
  --passthrough-errors
  -d, --debug           enable the Werkzeug debugger (DO NOT use in production
                        code)
  -D, --no-debug        disable the Werkzeug debugger
  -r, --reload          monitor Python files for changes (not 100% safe for
                        production use)
  -R, --no-reload       do not monitor Python files for changes
  --ssl-crt SSL_CRT     Path to ssl certificate
  --ssl-key SSL_KEY     Path to ssl key

在这里,我们可以控制服务器的所有方面,包括端口、主机,甚至它是如何执行的。

最后,我们可以执行我们装饰为命令行选项的方法:

$ python ./flask-script-ex.py hello_world
Hello, world!

Flask-Script 仅用几行代码就提供了一些强大的功能。你一定会喜欢的!

烧瓶自举

Flask-Bootstrap 最初由 Twitter 开发,用于制作统一、好看的网络客户端。幸运的是,Twitter 将其扩展为 Flask,这样每个人都可以利用它的特性。Flask-Bootstrap 是一个独立的框架,它提供了更多的命令行控制和用户界面组件,以获得干净、漂亮的网页。它也兼容最新的网络浏览器。

该框架在幕后发挥其神奇的作用,作为级联样式表(CSS)和脚本的客户端库,这些样式表和脚本是从 Flask 中的 HTML 模板(通常称为 HTML 文件模板文件)调用的。您将在后面的章节中了解更多关于模板的内容。因为应用是客户端的,所以在主应用中初始化它,我们不会看到太多。不管怎样,下面显示了如何将 Flask-Bootstrap 添加到我们的应用代码中。这里,我们有一个框架,其中初始化并配置了 Flask-Script 和 Flask-Bootstrap:

from flask import Flask          # import the Flask framework
from flask_script import Manager # import the flask script manager class
from flask_bootstrap import Bootstrap  # import the flask bootstrap extension

app = Flask(__name__)            # initialize the application
manager = Manager(app)           # initialize the script manager class
bootstrap = Bootstrap(app)       # initialize the bootstrap extension

if __name__ == "__main__":       # guard for running the code
    manager.run()                # launch the application via manager class

WTForms

为了支持 Flask-WTF 扩展,WTForms 是我们需要的一个组件。它提供了 Flask-WTF 组件所提供的大部分功能(因为 Flask-WTF 组件是 WTForms 的 Flask 特定包装器)。我们只需要安装它作为 Flask-WTF 的先决条件,我们将在 Flask-WTF 的上下文中讨论它。

注意

Flask-WTF 的一些包装装置可以包括 WTForms。

烧瓶-WTF

Flask-WTF 扩展是一个有趣的组件,它提供了几个有用的附加功能——对于我们的目的来说,最显著的是与 WTForms(一个框架无关的组件)的集成,允许创建表单类,以及以跨站点请求伪造(CSRF)保护的形式提供额外的 web 安全性。这两个特性允许您将 web 应用提升到更高的复杂程度。

表单类

表单类提供了一个类的层次结构,使得定义网页更加合理。使用 Flask-WTF,您可以用两段代码定义表单:一个从Form类(从 Flask 框架导入)派生的特殊类,用于使用一个或多个提供数据编程访问的附加类来定义字段,以及一个用于呈现网页的 HTML 文件(或模板)。这样,我们在 HTML 文件上看到了一个抽象层(表单类)。在下一节中,您将了解更多关于 HTML 文件的内容。

使用表单类,您可以定义一个或多个字段,比如文本的TextField,字符串的StringField,等等。更好的是,您可以定义允许您以编程方式描述数据的验证器。例如,您可以为文本字段定义最小和最大字符数。如果提交的字符数超出范围,将生成一条错误消息。是的,您可以定义错误消息!查看 http://wtforms.readthedocs.io/en/latest/validators.html 获得验证器的完整列表。以下是一些可用的验证器:

  • DataRequired:判断输入栏是否为空

  • Email:确保该字段遵循电子邮件 ID 约定

  • IPAddress:验证 IP 地址

  • Length:确保文本长度在给定范围内

  • NumberRange:确保文本是数字,并且在给定的范围内

  • URL:验证 URL

为了形成类,我们必须导入类和任何我们想在应用的序言中使用的字段类。下面显示了一个导入表单类和表单域类的示例。在本例中,我们还导入了用于自动验证数据的验证器:

from flask_wtf import FlaskForm
from wtforms import (HiddenField, TextField, TextAreaField, SelectField,
                     SelectMultipleField, IntegerField, SubmitField)
from wtforms.validators import Required, Length

要定义一个表单类,我们必须从FlaskForm派生一个新类。从那里,我们可以构造我们想要的类,但是它允许您定义字段。FlaskForm父类包括 Flask 需要实例化和使用 form 类的所有必要代码。

让我们看一个简单的,假设的例子。下面显示了一个网页的 form 类,该网页显示作者的名和姓。在这个例子中,有一个名为authors的表,我们可以通过 view 函数将它链接到这个代码。authors表包含三个字段:自动增量字段(authorid)、作者的名字(firstname)和作者的姓氏(lastname)。因为用户不需要看到作者 ID 字段,所以我们隐藏了该字段,其他字段是TextField()类的派生。请注意这些是如何在清单中定义的,名称(标签)作为第一个参数:

class AuthorForm(FlaskForm):
    authorid = HiddenField('AuthorId')
    firstname = TextField('First name', validators=[
            Required(message=REQUIRED.format("Firstname")),
            Length(min=1, max=64, message=RANGE.format("Firstname", 1, 64))
        ])
    lastname = TextField( 'Last name', validators=[
            Required(message=REQUIRED.format("Lastname")),
            Length(min=1, max=64, message=RANGE.format("Lastname", 1, 64))
        ])
    create_button = SubmitField('Add')
    del_button = SubmitField('Delete')

还要注意,我们已经为字段定义了一组验证器,其形式为从 WTForms 组件导入的函数调用。在每一种情况下,我们都为消息使用字符串,以使代码更容易阅读,更统一。这些字符串包括以下内容:

REQUIRED = "{0} field is required."
RANGE = "{0} range is {1} to {2} characters."

我们使用Required()验证器来指示字段必须有一个值。我们用字段的名称增加了默认的错误消息,使用户更容易理解。我们还使用了一个Length()验证函数来定义字段数据的最小和最大长度。我们再次增加了默认的错误消息。验证器只应用于POST操作(当提交事件发生时)。

接下来,我们有两个SubmitField()实例:一个用于创建(添加)按钮,另一个用于删除按钮。正如您可能猜测的那样,按照 HTML 的说法,这些字段被呈现为类型为submit<input>字段。

最后,为了使用一个表单类,我们在一个视图函数中实例化这个类。下面显示了作者视图函数的存根。我们实例化名为AuthorForm()的表单类,并将其赋给名为form的变量,该变量被传递给render_template()函数:

@app.route('/author', methods=['GET', 'POST'])
@app.route('/author/<int:author_id>', methods=['GET', 'POST'])
def author(author_id=None):
    form = AuthorForm()
    if request.method == 'POST':
        pass
    return render_template("author.html", form=form)

有几个字段类可供使用。表 8-2 显示了一个最常用的字段类的例子(也称为 HTML 字段)。您还可以从这些字段派生来创建自定义字段类,并为可以显示在字段旁边的标签提供文本(例如,作为按钮文本)。在后面的部分中,您将看到一个这样的例子。

表 8-2

WTForms 字段类

|

字段类

|

描述

|
| --- | --- |
| BooleanField | 具有真值和假值的复选框 |
| DateField | 接受日期值 |
| DateTimeField | 接受日期时间值 |
| DecimalField | 接受十进制值 |
| FileField | 文件上传字段 |
| FloatField | 接受浮点值 |
| HiddenField | 隐藏文本字段 |
| IntegerField | 接受整数值 |
| PasswordField | 密码(屏蔽)文本字段 |
| RadioField | 单选按钮列表 |
| SelectField | 下拉列表(选择一个) |
| SelectMultipleField | 下拉选项列表(选择一项或多项) |
| StringField | 接受简单文本 |
| SubmitField | 表单提交按钮 |
| TextAreaField | 多行文本字段 |

跨站点请求伪造(CSRF)保护

跨站点请求伪造(CSRF)保护是一种允许开发者用加密密钥签署网页的技术,这使得黑客欺骗GETPOST请求更加困难。这是通过首先在应用代码中放置一个特殊的键,然后在每个 HTML 文件中引用这个键来实现的。下面显示了一个应用序言的示例。我们所需要做的就是用一个短语给app.config数组的SECRET_KEY索引赋值。这应该是一个不容易猜到的短语。

from flask import Flask          # import the Flask framework
from flask_script import Manager # import the flask script manager class
from flask_bootstrap import Bootstrap  # import the flask bootstrap extension

app = Flask(__name__)            # initialize the application

app.config['SECRET_KEY'] = "He says, he's already got one!"

manager = Manager(app)           # initialize the script manager class
bootstrap = Bootstrap(app)       # initialize the bootstrap extension

if __name__ == "__main__":       # guard for running the code
    manager.run()                # launch the application via manager class

要激活网页中的 CSRF,我们只需将form.csrf_token添加到 HTML 文件中。这是一个特殊的隐藏字段,Flask 使用它来验证请求。在后面的部分中,您将了解到更多关于在哪里放置它的信息。但首先,让我们看看 Flask 的一个很酷的功能,叫做 flash

信息闪烁

Flask 有很多很酷的功能。Flask 的创建者和 Flask 扩展的创建者似乎考虑到了一切——甚至错误消息。考虑一个典型的 web 应用。你如何向用户传达错误?你是否重定向到一个新页面,【3】弹出一个, 4 或者在页面上显示错误?Flask 有一个解决方案叫做消息闪烁

消息闪烁是使用 Flask 框架中的flash()方法完成的。我们在代码的序言中导入它,当我们想要显示一条消息时,我们调用flash()函数,传入我们想要看到的错误消息。Flask 将在表单顶部的一个格式良好的框中显示错误。它不会取代表单,也不是弹出窗口,但是它允许用户关闭消息。您可以使用 flash messaging 向用户传达错误、警告甚至状态更改。

8-1 显示了一个闪光信息的例子。在本例中,两条 flash 消息演示了您可以同时显示多条消息。请注意用于消除图像的消息右侧的小 X。

img/460910_1_En_8_Fig1_HTML.jpg

图 8-1

示例简讯

在下一节中,您将看到一种将 flash 消息构建到我们所有网页中的机制。

HTML 文件和模板

让我们回顾一下到目前为止的旅程。您已经了解了如何用各种组件初始化应用,并学习了 Flask 如何通过 decorators 使用路由来为应用创建一组 URLs 这些路由指向一个视图函数,该函数实例化了 form 类。难题的下一部分是将 HTML 网页链接到 form 类。

回想一下,这是通过render_template()函数完成的,在这里我们传入一个 HTML 文件的名称进行处理。名称中有模板的原因是我们可以使用 Jinja2 模板组件使编写网页更容易。更具体地说,HTML 文件包含 HTML 标记和 Jinja2 模板构造。

注意

所有 HTML 文件(模板)必须存储在与主应用代码相同的位置的templates文件夹中。例如,如果你的代码在一个名为my-flask-app.py的文件中,那么在与my-flask-app.py相同的文件夹中应该有一个templates文件夹。如果你把模板放在其他地方,Flask 将找不到 HTML 文件。

模板和表单类是设计用户界面的地方。简而言之,模板用于包含表示逻辑,HTML 文件用于包含表示数据。一些读者可能需要花一些时间来尝试如何使用模板。下面几节将简要介绍 Jinja2 模板,并演示如何在我们的 HTML 文件中使用它们。有关更多详细信息,请参见在线文档。

Jinja2 模板概述

Jinja2 模板,也就是模板,用于包含任何表示逻辑,例如循环遍历数据数组,决定显示什么,甚至选择格式和表示设置。如果您熟悉其他 web 开发环境,您可能已经看到过这种封装在脚本中或通过嵌入式脚本(如 JavaScript)实现的功能。

回想一下,我们在主代码中呈现了网页。这个函数告诉 Flask 读取指定的文件,并将模板结构转换(渲染)成 HTML。Flask 会将模板结构扩展并编译成 HTML,web 服务器可以将它呈现给客户机。

您可以使用几个模板构造来控制执行、循环甚至注释的流程。每当你想使用一个模板构造(想想脚本语言,你用前缀和后缀{% %}把它括起来。这样做是为了让 Flask 框架将该构造识别为模板操作,而不是 HTML。

然而,看到模板结构与 HTML 标记混杂在一起并不罕见(也很正常)。事实上,这正是你应该做的。毕竟,您将创建的文件被命名为.html。它们只是碰巧包含模板构造。这是否意味着在使用 Flask 时只能使用模板?不,当然不是。如果你愿意,你可以渲染一个纯 HTML 文件!

起初,查看模板可能会令人望而生畏。但也没那么难。只需查看所有将{%%}作为“代码”部分的行。 5 你也可以看到以{# #}前缀和后缀形式出现的评论。

警告

所有模板构造都要求在{%之后和%}之前有一个空格。

如果您查看模板,您会看到使用两个空格的缩进来格式化的构造和标记。缩进和一般的空白,在标签和构造之外无关紧要。然而,大多数开发者会使用某种形式的缩进来使文件更容易阅读。事实上,大多数编码指南都要求缩进。

模板除了构造之外的一个很酷的特性(想想代码)是创建模板层次结构的能力。这允许您创建一个其他模板可以使用的“基础”模板。例如,您可以创建一个模板构造和 HTML 标记的样板文件,这样您的所有网页看起来都一样。

回想一下 Flask-Bootstrap,Bootstrap 提供了几个很好的格式化特性。这些特性中的一个创建了一个看起来很不错的导航条。很自然,我们希望它出现在我们所有的网页上。我们可以通过在基本模板中定义它并在我们的其他模板(HTML)文件中扩展它来做到这一点。让我们看一下库应用的基本模板。清单 8-2 显示了本章中示例应用的基础模板。为了便于讨论,添加了行号。

01 {% extends "bootstrap/base.html" %}
02 {% block title %}ShoppingList{% endblock %}
03 {% block navbar %}
04 <div class="navbar navbar-inverse" role="navigation">
05     <div class="container">
06         <div class="navbar-header">
07             <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
08                 <span class="sr-only">Toggle navigation</span>
09                 <span class="icon-bar"></span>
10                 <span class="icon-bar"></span>
11                 <span class="icon-bar"></span>
12             </button>
13             <a class="navbar-brand" href="/">Shopping List</a>
14         </div>
15         <div class="navbar-collapse collapse">
16             <ul class="nav navbar-nav">
17                 <li><a href="/">List</a></li>
18             </ul>
19         </div>
20     </div>
21 </div>
22 {% endblock %}
23
24 {% block content %}
25 <div class="container">
26     {% for message in get_flashed_messages() %}
27     <div class="alert alert-warning">
28         <button type="button" class="close" data-dismiss="alert">&times;</button>
29         {{ message }}
30     </div>
31     {% endfor %}
32
33     {% block page_content %}{% endblock %}
34 </div>
35 {% endblock %}

Listing 8-2Sample Base Template

哇,这里发生了很多事情!注意第一行。这告诉我们,我们正在继承(扩展)另一个名为bootstrap/base.html的模板。这是在你安装 Flask-Bootstrap 时免费提供给你的,正是这个模板包含了对 Bootstrap 导航栏特性的支持。这是为 Flask 应用构建一组 HTML 文件的常用方法,您将在本节的后面看到。

让我们从鸟瞰图开始我们的旅行。请注意,有两个“模块”分别标有{% block <> %}{% endblock %}(第 2、3、22、24、33 和 35 行)。这些是用于逻辑部分的,在这里我们可以将格式应用于块内的标记和构造。用编码术语来说,这就像一个代码块。第一个块定义了页面的标题——在本例中是Shopping List,这是库应用的可执行名称。

第二块定义了应用的导航栏(想想菜单)。在该部分中,第 5–21 行定义了简单的 HTML <div>标签,这些标签构成了导航栏上的项目。值得注意的是第 13 行,它指定了用作应用名称的文本,该文本出现在导航栏的左侧,类似于“home”链接。第 15–19 行定义了导航栏项目(提交按钮),但是在这个应用中只有一个表单。还要注意关键字collapse。这表明可以折叠导航栏。

第 24–35 行的最后一个块定义了模板结构和 flash 消息的 HTML 标签。让我们更深入地看看这段代码(为了方便起见,这里重复了一遍):

24 {% block content %}
25 <div class="container">
26     {% for message in get_flashed_messages() %}
27     <div class="alert alert-warning">
28         <button type="button" class="close" data-dismiss="alert">&times;</button>
29         {{ message }}
30     </div>
31     {% endfor %}
32
33     {% block page_content %}{% endblock %}
34 </div>
35 {% endblock %}

这里,我们看到另一个包含按钮的<div>标签。这是我们用来关闭简讯的按钮。注意,这个标签被放在一个用{% for ... %}指定的for循环中,并以{% endfor %}结束。在本例中,我们循环从get_flashed_messages()函数返回的消息,这些消息是由应用代码中的flash()函数收集的。这告诉我们几件事:我们可以在模板中使用循环,模板允许显示多个图像(您在前面已经看到了),模板可以调用函数!这是模板威力的一个例子。

注意

模板不需要以任何方式格式化。空白在 HTML 标签或模板构造之外没有任何作用。

最后,注意我们在第 26 行的 for 循环中定义的变量。这个变量message被定义在它出现的块的本地(在本例中是for循环),并且可以通过将它放在{{ }}中在任何时候被引用。例如,在第 29 行中,我们在<div>标签中使用了{{ message }},这意味着该文本将出现在客户机上,由 Flask 就地呈现。当我们讨论如何用模板构建用户界面时,变量的使用将变得更加重要。

模板语言结构

Jinja2 模板有很多特性,对所有特性的完整讨论超出了本书的范围。然而,快速参考 Jinja2 的主要结构是很方便的。下面是一些常用的构造,包括上一节中的一些(为了完整性)。每一个都有一个简短的例子来说明这个结构在模板中的出现方式。在本章后面探索库应用或编写自己的 Flask 应用时,请随意参考本节。

评论

您可以在模板中嵌入自己的注释。您可能希望这样做,以确保您充分解释了您正在做的事情,并在以后重用代码时作为一个提醒。 6 下面是一个在模板中使用注释的例子。回想一下,注释以{#开始,以#}结束,可以跨多行。

{# This is a line comment written by Dr. Charles Bell on 25 May 2018\. #}

{#
  Introducing MySQL InnoDB Cluster

  This template defines the base template used for all of the HTML forms and responses in the MyLibrary application. It also defines the menu for the basic operations.

  Dr. Charles Bell, 2018
#}

包括

如果您的模板文件增长了,并且您发现有些部分是可重用的,比如一个<div>标签,您可以将标签和模板构造保存在一个单独的文件中,并通过使用{% include %}构造将它包含在其他模板中。{% include %}构造将您想要包含的文件的名称作为参数。像模板一样,这些必须驻留在templates文件夹中。这样,我们避免了重复和维护重复代码的麻烦和容易出错的任务。

{# Include the utilities common tags for a list. #}
{% include 'utilities.html' %}

宏指令

减少重复代码的另一种形式是创建一个宏在你的模板中使用(想想函数)。在这种情况下,我们使用{% macro ... %}{% endmacro %}构造来定义一个宏,稍后我们可以在代码中调用(使用)它。下面显示了一个定义简单宏并在循环中使用它的示例。注意我们是如何将变量传递给宏来操作数据的。

{# Macro definition #}
{% macro bold_me(data) %}
    <b>{{ data }}</b>
{% endmacro %}

{# Invoke the macro #}
{% for value in data %}
    {{ bold_me(value) }}
{% endfor %}

导入

使用宏的最好方法之一是将它们放在一个单独的代码文件中,从而进一步增强可重用性。为了使用一个单独文件中的宏,我们使用了{% import ... %}构造,提供了要导入的文件的名称。下面显示了一个在单独的文件中导入先前定义的宏的示例。和 include 一样,这个文件必须在templates文件夹中。请注意,我们可以使用别名,并通过使用点符号来引用宏。

{% import 'utilities.html' as utils %}
...
{{ utils.bold_me(value) }}

扩展(继承)

我们可以通过继承(扩展)模板来使用模板的层次结构。当我们检查一个基本模板时,您已经看到了这一点。在这种情况下,我们使用{% extend ... %}构造,提供我们想要扩展的模板的名称。下面显示了上一个基本模板中的一个示例。

{% extends "base.html" %}

阻碍

块用于隔离执行和范围(对于变量)。每当我们想要隔离一组模板构造时,我们就使用块(想想代码块)。{% block ... %}构造与{% endblock %}构造一起用于定义块。这些构件允许您命名块。下面是一个例子。

{% block if_true %}
...
{% endblock if_true %}

循环是多次执行同一个块的一种方式。我们用{% for <variable> in <data_array> %}构造来做这件事。在这种情况下,循环将迭代数组,用数组的每个索引中的值替换<variable>中的值。这种结构非常适合遍历数组来创建表格、显示数据列表以及类似的演示活动。下面显示了一个用于构建表格的for循环。注意,我们使用了两个for循环:一个循环遍历名为columns的数组中的列,另一个循环遍历名为rows的数组中的行。

<table border="1" cellpadding="1" cellspacing="1">
  <tr>
    <td style="width:80px"><b>Action</b></td>
    {% for col in columns %}
      {{ col|safe }}
    {% endfor %}
  </tr>
  {% for row in rows %}
    <tr>
      <td><a href="{{ '/%s/%s'%(kind,row[0]) }}">Modify</a></td>
      {% for col in row[1:] %}
        <td> {{ col }} </td>
      {% endfor %}
    </tr>
  {% endfor %}
</table>

此时,您可能想知道列和行中的数据是如何到达模板的。调用render_template(功能。如果想要将数据传递给模板,只需在呈现模板时将数据列在参数中。在这种情况下,我们将按如下方式传递列和行。在这种情况下,row_datacol_data是在视图函数中定义的变量,并通过赋值传递给模板中的rowscolumns变量。酷吧。

render_template("list.html", form=form, rows=row_data, columns=col_data)

条件式

条件语句或if语句(在 Jinja2 文档中称为测试)允许您在模板中做出决定。我们使用{% if <condition> %}构造,它以{% endif %}构造结束。如果您想要一个“else”,您可以使用{% else %}构造。此外,你可以用{% elif <condition> %}来连锁条件。您通常在条件中使用变量或表单元素,并且可以使用通用比较器(有关测试列表,请参见 http://jinja.pocoo.org/docs/2.10/templates/#builtin-tests )。

例如,您可能希望根据特定事件更改提交字段的标签。您可能希望定义一个提交按钮来添加或更新数据;当网页用于添加新的数据项时,文本应该显示为“add”,但是当您使用相同的网页更新数据时,我们希望文本显示为“update”这是为GETPOST请求(读和写)重用模板的关键之一。下面显示了以这种方式使用的条件的一个示例。

{% if form.create_button.label.text == "Update" %}
  {{ form.new_note.label }}
  {{ form.new_note(rows='2',cols='100') }}
{% endif %}

{% if form.del_button %}
  {{ form.del_button }}
{% endif %}

这个例子有两个条件。第一个示例演示如何检查窗体上标签的文本。注意,这里我们用form.create_button引用表单上的元素,这是我们在表单类中定义的字段类的名称,它在呈现模板之前被实例化(您将在后面的小节中看到如何做到这一点)。表单变量在render_template("book.html", form=form)调用中被传递给模板。在这种情况下,只有当按钮文本被设置为Update时,我们才显示new_note字段及其标签。

第二个例子显示了一个简单的测试,如果表单上的delete_button是活动的(没有隐藏或删除),我们就显示它。这是一个如何显示可选提交字段的例子。

变量和变量过滤器

变量是保存数据值供以后处理的一种方式。变量最常见的用途是引用从视图函数传递到模板的数据(通过render_template()函数)。我们还可以在模板中使用变量来保存数据,比如计数器、for循环数据值等等。回想一下,我们用花括号{{ variable }}来引用变量,或者在for循环的情况下,它是在for循环结构中定义的。请注意,当在 HTML 标记中引用时,构造中的空格将被忽略。

您还可以在模板中使用过滤器来更改变量中的值。变量筛选器是一种以编程方式更改值的方法,以便在表示逻辑中使用。您可以更改大小写,删除空白,甚至去掉 HTML 标签或直接使用原始文本。在最后一种情况下,我们使用safe过滤器,它告诉模板使用文本,即使它有 HTML 标签。这有点棘手,因为它可能会为攻击打开方便之门,但是如果您使用 WTForms 的特殊安全特性(在下一节中显示),这样做通常是可以的,但是要谨慎。表 8-3 显示了常用的可变过滤器。

表 8-3

可变过滤器

|

过滤器

|

描述

|
| --- | --- |
| capitalize | 将文本的第一个字符转换为大写 |
| lower | 将文本转换为小写字符 |
| safe | 呈现文本,不转义特殊字符 |
| striptags | 从文本中删除 HTML 标签 |
| title | 将字符串中的每个单词大写 |
| trim | 删除前导和尾随空白 |
| upper | 将文本转换为大写 |

小费

要更深入地了解 Jinja2 模板构造,请参见 http://jinja.pocoo.org /

现在,您已经对模板的工作原理有了一个大致的了解,并且已经为库应用定义了一个基本模板,让我们看看如何使用这个基本模板来为我们的 web 页面形成 HTML 文件。正如你将看到的,它涉及到我们一直在讨论的三个概念;这些概念将结束我们关于 Flask 在构建网页并将其发送给客户端时的工作方式的讨论。在后面的小节中,您将看到如何从客户机获取数据。

使用模板的 HTML 文件

现在,您已经准备好了解如何显示我们在表单类中定义的字段类。让我们从演示如何在示例应用中为 item detail 视图(data)显示数据开始。我们从定义给视图函数的表单类和字段类开始,它呈现模板,最后是模板本身。

回想一下,表单类是我们定义一个或多个表单字段的地方。我们将使用这些字段类实例来访问视图函数和模板中的数据。清单 8-3 显示了项目细节视图的表单类。

class ItemForm(FlaskForm):
    row_id = HiddenField('Id')
    description = TextField('Description', validators=[
            Required(message=REQUIRED.format("Description")),
            Length(min=1, max=64, message=RANGE.format("Description", 1, 64))
        ])
    note = TextField( 'Note')
    create_button = SubmitField('Add')

Listing 8-3Item Form Class

form 类创建三个字段:一个用于隐藏字段(id),一个用于项目描述(description),另一个允许用户对项目进行注释(notes)。我们还有一个提交字段(create_button)用于提交(张贴)表单。

在视图函数中实例化表单数据之后,当呈现表单数据时,我们将表单数据传递给模板。清单 8-4 显示了列表项数据的查看功能。这里,我们首先实例化列表视图表单类,然后将其传递给模板。为了简洁起见,省略了与数据库代码模块接口的代码。

@app.route('/item', methods=['GET', 'POST'])
def item_view(row_id=None):
    operation = request.args.get("operation", "Add")
    row_id = request.args.get("row_id")
    # Get data from the form if present
    form_row_id = form.row_id.data
    form_description = form.description.data
    form_note = form.note.data
    form.create_button.label.text = operation
    # If the route with the variable is called, retrieve the data item
    # and populate the form.
    if row_id:
        # Read data here
    if request.method == 'POST':
        # First, determine if we must create, update, or delete when form posts.
        if form.create_button.data:
            if form.create_button.label.text == "Update":
                operation = "Update"
            elif form.create_button.label.text == "Delete":
                operation = "Delete"
        if form.validate_on_submit():
            # Get the data from the form here
            if operation == "Add":
                # Add a new item here
            elif operation == "Update":
                # Update the data here
            else:
                # Delete the item here
            return redirect('/')
        else:
            flash_errors(form)
    return render_template("item.html", form=form)

Listing 8-4Item View Function

这里我们看到了我们为视图定义的路线。我们还为请求设置了包含GETPOST的方法。注意,我们可以检查请求是否是一个POST(数据提交)。在这种情况下,我们可以从 form 类实例中检索数据,并将其保存到数据库中。当我们添加数据库功能时,您会看到更多。

最后,请注意,我们实例化了 publisher form 类(form)的一个实例,然后将它作为参数传递给render_template("item.html", form=form)调用。在这种情况下,我们现在渲染存储在templates文件夹中的item.html模板。

好了,现在我们有了表单类和视图函数。现在的焦点是当我们呈现 HTML 模板文件时会发生什么。清单 8-5 显示了列表项数据的 HTML 文件(模板)。

{% extends "base.html" %}
{% block title %}Shopping List{% endblock %}
{% block page_content %}
  <form method=post> {{ form.csrf_token }}
    <fieldset>
      <legend>Item View</legend>
      {{ form.hidden_tag() }}
      <div style=font-size:20pz; font-weight:bold; margin-left:150px;s>
        {{ form.description.label }} <br>
        {{ form.description(size=75) }} <br>
        {{ form.note.label }} <br>
        {{ form.note(size=75) }} <br><br>
        {{ form.create_button }}
      </div>
    </fieldset>
  </form>
{% endblock %}

Listing 8-5Item HTML File

模板从扩展(继承)我们之前讨论过的base.html模板文件开始。我们看到一个块定义了标题,另一个块定义了页面内容。在这个块中,我们看到如何定义页面上的字段,从表单类实例(form)中引用字段类实例。事实上,我们引用字段的标签以及数据。标签是在声明字段类时定义的,数据是存储值的地方。当我们想要填充一个表单(GET)时,我们将数据元素设置为值;当我们想要读取数据时(POST,我们引用数据元素。

还要注意,为了安全起见,我们添加了 CSRF 令牌,用form.hidden_tag()函数呈现隐藏字段,并包含提交字段(create_button)。

咻!这就是 Flask 呈现网页的方式。一旦您习惯了,这是一种很好的方式来分离几层功能,并使从用户那里获取数据或呈现给用户变得容易。如果您认为您只看到了示例应用的一小部分,那么您是对的。展示示例应用的这些部分是为了让您对未来有所了解。在本章的后面你会看到完整的应用代码。

现在,让我们看看如何在我们的应用中构建定制的错误处理程序,以及稍后如何将应用中的控制重定向到正确的视图函数。

错误处理程序

我提到过,可以为应用中的错误创建自己的错误处理机制。您应该考虑建立两个这样的错误机制:一个用于 404(未找到)错误,另一个用于 500(应用错误)。为了定义每一个,我们首先创建一个用@app.errorhandler(num)修饰的视图函数、一个视图函数和一个 HTML 文件。让我们看看每个例子。

未找到(404)错误

为了处理 404(未找到)错误,我们创建了一个带有特殊错误处理程序路由函数的视图函数,该函数呈现 HTML 文件。Flask 会自动将所有未找到的错误条件定向到此视图。下面显示了 404 未找到错误处理程序的视图函数。如您所见,这很简单:

@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404

相关的错误处理程序 HTML 代码在名为404.html的文件中,如下所示:

{% extends "base.html" %}
{% block title %}MyLibrary ERROR: Page Not Found{% endblock %}
{% block page_content %}
<div class="page-header">
    <h1>Page not found.</h1>
</div>
{% endblock %}

我们从base.html文件继承而来,因此生成的网页看起来与应用中的任何其他网页一样,并包含来自引导组件的菜单。我们还可以定义错误消息的文本和标题。随意修饰你自己的错误处理程序,让你的用户更感兴趣。7

应用(500)错误

为了处理 500 个(应用)错误,我们遵循与前面相同的模式。以下是应用错误的错误处理程序:

@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

相关的错误处理程序 HTML 代码在名为500.html的文件中,如下所示:

{% extends "base.html" %}
{% block title %}MyLibrary ERROR{% endblock %}
{% block page_content %}
<div class="page-header">
    <h1>OOPS! Application error.</h1>
</div>
{% endblock %}

我们从base.html文件继承而来,所以生成的 web 页面看起来和应用中的任何其他页面一样,包括来自 bootstrap 组件的菜单。

强烈建议所有 Flask 应用创建这些基本的错误处理程序。在开发应用时,您可能会发现应用错误处理程序非常有用。您甚至可以扩充代码,以提供要在网页中显示的调试信息。

重新寄送

此时,您可能想知道 Flask 应用如何以编程方式将执行从一个视图定向到另一个视图。答案是 Flask 中的另一个简单构造:重定向。我们使用带有 URL 的redirect()函数(从 Flask 模块导入)将控制重定向到另一个视图。例如,假设您有一个应用,根据用户点击的按钮(通过POST提交表单),您希望显示不同的 web 页面。下面演示了如何使用redirect()功能来实现这一点:

if kind == 'book' or not kind:
    if request.method == 'POST':
        return redirect('book')
    return render_template("list.html", form=form, rows=rows,
                           columns=columns, kind=kind)
elif kind == 'author':
    if request.method == 'POST':
        return redirect('author')
    return render_template("list.html", form=form, rows=rows,
                           columns=columns, kind=kind)
elif kind == 'publisher':
    if request.method == 'POST':
        return redirect('publisher')
    return render_template("list.html", form=form, rows=rows,
                           columns=columns, kind=kind)

这里,我们在一个POST请求后有三个重定向。在每种情况下,我们都使用应用中定义的一个路由来告诉 Flask 调用相关的视图函数。这样,我们可以创建一个菜单或一系列提交字段,允许用户从一个页面移动到另一个页面。

redirect()函数需要一个有效的路径,在大多数情况下,它只是您在装饰器中提供的文本。但是,如果需要形成一个复杂的 URL 路径,可以在重定向之前使用url_for()函数来验证路由。如果您重组或更改路线,该功能还有助于避免断开链接。例如,您可以使用redirect(url_for("author"))来验证路线并为其形成一个 URL。

附加功能

Flask 的内容远不止你在这个速成班中看到的内容。以下是一些未讨论的主题,您可能有兴趣了解更多信息(这只是其中的一部分)。如果您对这些感兴趣,可以考虑在在线文档中查找它们。

烧瓶审查:样品应用

现在,您已经对 Flask 有了一个简单的了解,让我们在深入到示例应用之前看看所有这些是如何工作的。本节以典型 Flask web 应用的基本布局形式回顾了您所学到的内容。在本章的后面,您将使用它作为编写示例应用的指南。不要太担心这段代码的执行,因为它并没有做多少事情,只是作为章节项目的一个开始。但是,它确实演示了如何将您所学的所有部分组合在一起,使 Flask web 应用在没有定义表单的情况下运行。

清单 8-6 显示了一个 Flask 应用的样例应用布局。花点时间通读一下。您应该可以找到我们到目前为止讨论过的所有主题,包括字段类、表单类和视图函数的占位符。

#
# Introducing MySQL InnoDB Cluster - Template
#
# This file contains a template for building Flask applications. No form
# classes, routes, or view functions are defined, but placeholders for each
# are defined in the comments.
#
# Dr. Charles Bell, 2018
#
from flask import Flask, render_template, request, redirect, flash
from flask_script import Manager
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import (HiddenField, TextField, TextAreaField, SelectField,
                     SelectMultipleField, IntegerField, SubmitField)
from wtforms.validators import Required, Length

#
# Set up Flask, Bootstrap, and security.
#
app = Flask(__name__)
app.config['SECRET_KEY'] = "He says, he's already got one!"
manager = Manager(app)
bootstrap = Bootstrap(app)

#
# Utility functions
#
def flash_errors(form):
    for error in form.errors:
        flash("{0} : {1}".format(error, ",".join(form.errors[error])))

#
# Customized fields for skipping prevalidation
#
<custom field classes go here>

#
# Form classes - the forms for the application
#
<form classes go here>

#
# Routing functions - the following defines the routing functions for the
# menu including the index or "home", book, author, and publisher.
#
<routing functions (view functions) go here>

#
# Error handling routes
#
@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

#
# Main entry
#
if __name__ == '__main__':
    manager.run()

Listing 8-6Sample Flask Application Template

请注意,在这个模板中有一样东西我们还没有谈到:效用函数。这些是你自己的函数来支持你的应用。您可以考虑在所有 Flask 应用中包含这样一个函数,它可以遍历表单上的错误,并在 flash 消息中显示它们。回想一下,flash 消息在网页上显示为弹出框。为了清楚起见,下面给出了效用函数。注意,我们使用了一个for循环来遍历表单实例的 errors 数组,闪烁每条消息。这允许您在网页上显示多条消息。

def flash_errors(form):
    for error in form.errors:
        flash("{0} : {1}".format(error, ",".join(form.errors[error])))

创建自己的 Flask 应用时,可以随意使用这个模板。我们还将在后面的部分中使用它来定义示例应用的用户界面。

小费

有关 Flask 以及如何使用它和它的相关包的更多信息,关于该主题的优秀参考是 Miguel Grinberg 的Flask Web Development:Developing Web Applications with Python,Second Edition(O ' Reilly Media 2018)。

既然您已经设置了 Flask 环境并发现了 Flask 及其扩展,那么让我们更详细地看看示例应用。

示例应用

因为我们想要测试我们部署的 InnoDB 集群,所以我们需要一个至少符合典型应用服务特征的应用。在这种情况下,我们将使用一个简单的 Python web 应用,该应用使用 Flask 来提供简单的数据。出于几个原因,我们尽可能保持简单。首先也是最重要的一点,这本书并没有探讨编写高可用性应用的每一个细节。相反,应用代码以简洁的快速入门指南的形式呈现。如果您需要帮助来使应用工作,并且上一节中提供的信息没有涵盖您的具体情况,请参考上一节中提供的 URL。

在本节中,我们将逐步介绍如何创建示例应用。我们将首先在集群上创建数据库,并创建一个库(代码模块)来对数据进行 CRUD 操作。然后,我们将继续讨论用户界面代码和使其工作的 HTML 文件。最后,我们用一个简短的执行示例来演示应用是如何工作的。

注意

您不需要理解这个应用的所有细微差别。在您建立了自己的开发环境之后,您可以用您的示例应用来代替这个环境。在任何情况下,应用连接参数都是相同或相似的。

但是,在开始之前,您需要启动并运行您的集群。如果将其关闭,请参见第 7 章的结尾,了解如何安全重启集群。如果您的集群处于离线状态,并且您不想启动它,您可以跳过下面的“数据库”部分,但是当集群处于在线状态并且准备就绪时,您需要返回到该部分。您还必须在应用服务器上安装、配置并运行路由,才能继续。

注意

要开发和使用示例应用,您必须运行集群,并在应用服务器上运行路由。

数据库ˌ资料库

我们将在应用中使用的数据库是一个简单的单个表,包含有限数量的列。再说一次,这不是一本关于数据库设计的书,所以我们宁可简单一些。精明的数据库开发者可能会找到改进设计的方法。除非这些欲望给你带来了一些困扰,否则我建议你在开始“修复”东西之前,坚持下去,看看应用是如何工作的。

让我们从在集群上创建数据库开始。

创建数据库

对于这一步,我们将使用 MySQL Shell 并通过路由连接到我们的读/写服务器。回想一下,路由运行在应用服务器上(cluster-rpi-app)。以下命令是一个示例,您可以使用它通过路由连接到读/写服务器:

 $ mysqlsh --sql root@cluster-rpi-app

您可以使用应用服务器进行连接,也可以使用台式计算机,只要它与您的集群连接在同一个网络上。如果您使用 PC,您可能需要通过 IP 地址(例如,192.168.42.240)访问应用服务器。同样,回想一下路由使用端口 6446 进行读/写路由(连接)。在我们连接到读/写服务器之前,让我们看一下数据库设计。

我们将创建的数据是一个简单的购物清单。它有一个用于存储唯一 ID(自动递增)的列,一个用于描述商品的列,另一个用于添加注释的列,以及一个我们可以用来指示商品是否被购买的列(比如在列表上勾掉商品)。我们将数据库命名为shopping,表命名为list。下面显示了创建数据库和表所需的 SQL 命令:

CREATE DATABASE shopping;
CREATE TABLE shopping.list (
  id int AUTO_INCREMENT PRIMARY KEY,
  description char(200),
  notes char(100),
  ticked bool default FALSE
);

好了,现在我们准备好连接到读/写服务器并创建数据库。清单 8-7 显示了运行前面的 SQL 命令来创建数据库的脚本。SQL 命令以粗体显示。

$ mysqlsh --sql root@cluster-rpi-app:6446
Creating a session to 'root@ cluster-rpi-app:6446'
Enter password: ****
Fetching schema names for autocompletion... Press ^C to stop.
Error during auto-completion cache update: The user specified as a definer ('mysql.infoschema'@'localhost') does not exist
Your MySQL connection id is 10 (X protocol)
Server version: 8.0.11 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.
MySQL Shell 8.0.11

Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners

.

Type '\help' or '\?' for help; '\quit' to exit.

 MySQL  localhost:33060+ ssl  SQL > CREATE DATABASE shopping;
Query OK, 1 row affected (0.0391 sec)

 MySQL  localhost:33060+ ssl  SQL > CREATE TABLE shopping.list (
                                   ...   id int AUTO_INCREMENT PRIMARY KEY,
                                   ...   description char(200),
                                   ...   notes char(100),
                                   ...   ticked bool default FALSE
                                   ... );
Query OK, 0 rows affected (0.0437 sec)

Listing 8-7
Creating the Shopping List Database

好了,现在我们已经创建了数据库,让我们添加一些数据。在这里,我们将添加一些简单的测试数据。下面显示了可用于创建一些数据来测试应用的示例 SQL 语句:

INSERT INTO shopping.list VALUES (NULL, "Bagels", "Plain, of course.", NULL);
INSERT INTO shopping.list VALUES (NULL, "Jelly", "Grape, please!", NULL);
INSERT INTO shopping.list VALUES (NULL, "Milk", "2%", True);
INSERT INTO shopping.list VALUES (NULL, "Lunch meat", "Suprise me.", NULL);
INSERT INTO shopping.list VALUES (NULL, "Paper towels", "", NULL);

当我们仍然连接到读/写服务器时,我们可以运行这些 SQL 命令。下面显示了运行前面的 SQL 命令向数据库添加示例数据的脚本。SQL 命令以粗体显示:

MySQL  localhost:33060+ ssl  SQL > INSERT INTO shopping.list VALUES (NULL, "Bagels", "Plain, of course.", NULL);
Query OK, 1 row affected (0.0570 sec)

 MySQL  localhost:33060+ ssl  SQL > INSERT INTO shopping.list VALUES (NULL, "Jelly", "Grape, please!", NULL);
Query OK, 1 row affected (0.0056 sec)

 MySQL  localhost:33060+ ssl  SQL > INSERT INTO shopping.list VALUES (NULL, "Milk", "2%", True);
Query OK, 1 row affected (0.0073 sec)

 MySQL  localhost:33060+ ssl  SQL > INSERT INTO shopping.list VALUES (NULL, "Lunch meat", "Suprise me.", NULL);
Query OK, 1 row affected (0.0056 sec)

 MySQL  localhost:33060+ ssl  SQL > INSERT INTO shopping.list VALUES (NULL, "Paper towels", "", NULL);
Query OK, 1 row affected (0.0055 sec)

当我们仍然连接时,让我们运行一个查询来检索数据,只是为了检查它。下面显示了运行一个SELECT SQL 命令来检索表中所有行的结果:

 MySQL  localhost:33060+ ssl  SQL > SELECT * FROM shopping.list;
+----+--------------+-------------------+--------+
| id | description  | notes             | ticked |
+----+--------------+-------------------+--------+
|  1 | Bagels       | Plain, of course. |   NULL |
|  2 | Jelly        | Grape, please!    |   NULL |
|  3 | Milk         | 2%                |      1 |
|  4 | Lunch meat   | Suprise me.       |   NULL |
|  5 | Paper towels |                   |   NULL |
+----+--------------+-------------------+--------+
5 rows in set (0.0009 sec)

现在我们有了一些样本数据,让我们编写一个代码模块来使用 Python 中的数据库。

准备目录结构

在我们开始实现示例应用之前,我们需要创建几个文件夹(目录)。回想一下“Flask Primer”一节,我们需要文件夹来包含.html文件(表单模板)。我们还将把与 MySQL 接口的代码放在一个名为database的文件夹中。下面显示了建议的目录结构。您可以随意命名该根。例如,你可以把它命名为shopping

root folder (e.g. shopping)
  |
  +-- database
  |
  +-- templates

创建数据库库

常见的做法是创建一个单独的代码模块,其中包含与数据库(或任何此类数据存储)交互的代码。我们将为示例应用这样做。在这种情况下,我们将创建一个名为shopping_lib.py的文件,并将其放在名为database的文件夹中。

注意

您还需要创建一个名为__init__.py的空文件。这是 Python 在database目录中定位模块所必需的。

在这一节中,我们总结了类和方法,而不是详细解释库中的每一行代码,将代码本身的理解留在后面的练习中。然而,我们将检查处理高可用性方面的代码的一些关键部分。

让我们从课程开始。有两个类:一个名为Library的通用类和一个名为ShoppingList的通用类。Library类用于通过路由创建和管理读写连接。ShoppingList类实现了对数据库中数据的 CRUD 操作。我们使用Library类创建一个连接,并将其传递给ShoppingList类来访问数据。让我们看看每个类的方法。

8-4 显示了Library类的方法。这里不应该有任何意外,但是我们将在本节的后面讨论connect()方法和连接标准。

表 8-4

库类的方法

|

方法

|

返回

|

描述

|
| --- | --- | --- |
| __init__(self) |   | 构造函数。 |
| connect(self, read_write=False) |   | 通过路由建立连接。如果read_write设置为True,则默认使用读/写端口或只读端口。 |
| get_connection(self) | 连接类别 | 检索当前连接。 |
| is_connected(self) | 布尔代数学体系的 | 如果连接处于活动状态,则返回True。 |
| disconnect(self) |   | 断开与服务器的连接。 |
| sql(self, query_str, fetch=True, buffered=False) | 结果集或游标 | 执行查询并返回任何结果。 |
| get_list(self, all=True) | 结果集 | 检索表格中的所有行,或者如果all设置为False,则只检索那些未被选择的行。 |

8-5 显示了ShoppingList类的方法。同样,这里不应该有任何意外,因为这些方法对于实现 CRUD 操作的类来说是典型的。此外,每种方法都设计为连接到数据库,执行操作,然后断开连接。这有助于确保在 CRUD 操作请求之间发生故障转移事件时,我们能够获得有效的连接。

表 8-5

ShoppingList 类的方法

|

方法

|

必需的参数

|

描述

|
| --- | --- | --- |
| def __init__(self, library) | library | 构造函数。需要库类的实例。 |
| create(self, description, note) | description | 在表中创建一行。 |
| read(self, rowid) | rowid | 返回表中 id = rowid 的行。如果找不到匹配项,则返回 None。 |
| update(self, rowid, description, note) | rowid, Description | 更新用 id = rowid 指定的行的数据。 |
| update_purchased(self, rowid, purchased=1) | rowid | 为 id = rowid 的表中的行设置 Purchased 列。True是默认值,但是如果您为 purchase d传递 False,它将清除该行的 Purchased 列。 |
| delete(self, rowid) | rowid | 删除表中 id = rowid 的行。 |

您可能想知道我们如何提供连接信息,如用户 ID、密码和主机名。虽然这不是生产应用的推荐做法,但是我们将数据放在库代码中。在代码的顶部,我们为读/写端口和只读端口定义了常量。我们还定义了一个值,用于通过路由重新建立连接的最大重试次数。这是必要的,因为如果连接失败(集群中有故障转移事件),我们必须在路由重新连接后重试连接以重新建立连接:

RW_PORT = 6446
RO_PORT = 6447
MAX_RETRY = 6

我们使用字典来存储用户 ID、密码和主机。我们将默认端口设置为只读端口。我们可以用connect()方法覆盖它。

self.config = {
    'user': 'root',
    'password': '<secret>',
    'host': 'cluster-rpi-app',
    'port': RO_PORT,
    'database': None,
}

我们来看一下connect()方法。下面显示了该方法的完整代码。请注意,我们有一个参数,如果没有提供,它将确保我们始终使用只读连接。如果我们通过True,该方法将连接到读/写连接。这是一个如何使用单一方法来有选择地选择连接路径的示例——写入数据或读取数据。

def connect(self, read_write=False):
    attempts = 0
    while attempts < MAX_RETRY:
        self.config['port'] = (RO_PORT, RW_PORT)[read_write]
        try:
            self.db_conn = mysql.connector.connect(**self.config)
            break
        except mysql.connector.Error as err:
            print("Connection failed. Error = {0} Retrying.".format(err))
        attempts += 1
        time.sleep(1)
    if attempts >= MAX_RETRY:
       self.db_conn = None
       raise mysql.connector.Error("Connection timeout reached.")

请注意,该方法还实现了一个用于重试连接的循环。我们通过在操作前后连接和断开ShoppingList类中的 CRUD 方法,减少了大量重试的需要。但是,在集群变得不可访问的情况下,这里仍然需要重试循环。

现在,让我们看看数据库库的完整代码。回想一下,我们将这段代码放在一个名为shopping_lib.py的文件中,该文件放在database文件夹中。数据库库是用模块顶部声明的 SQL 字符串编写的,后面是两个类。清单 8-8 显示了购物清单数据库的完整代码。为了简洁起见,省略了注释。

import mysql.connector
import time

RW_PORT = 6446
RO_PORT = 6447
MAX_RETRY = 6  # Maximum times to attempt a reconnect should connection fail

ALL_ITEMS = """
    SELECT * FROM shopping.list
    ORDER BY description
"""
READ_ITEM = """
    SELECT * FROM shopping.list
    WHERE rowid = '{0}'
"""
UNCHECKED_ITEMS = """
    SELECT * FROM shopping.list
    WHERE purchased = 0
    ORDER BY description
"""
INSERT_ITEM = """
    INSERT INTO shopping.list (description, note) VALUES ('{0}','{1}')
"""
GET_LASTID = "SELECT @@last_insert_id"
UPDATE_ITEM = """
    UPDATE shopping.list
    SET description = '{1}', note = '{2}'
    WHERE rowid = '{0}'
"""
UPDATE_PURCHASED = """
    UPDATE shopping.list
    SET purchased = {1}
    WHERE rowid = '{0}'
"""
DELETE_ITEM = """
    DELETE FROM shopping.list WHERE rowid = '{0}'
"""

class ShoppingList(object):
    def __init__(self, library):
        self.library = library

    def create(self, description, note):
        assert description, "You must supply a description for a new item."
        self.library.connect(True)
        query_str = INSERT_ITEM
        last_id = None

        try:
            self.library.sql(query_str.format(description, note))
            last_id = self.library.sql(GET_LASTID)
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot add item: {0}".format(err))
        self.library.disconnect()
        return last_id

    def read(self, rowid):
        assert rowid, "You must supply a rowid."
        self.library.connect()
        query_str = READ_ITEM.format(rowid)
        results = self.library.sql(query_str)
        self.library.disconnect()
        return results

    def update(self, rowid, description, note):
        assert rowid, "You must supply a rowid."
        assert description, "You must supply a description for a new item."
        self.library.connect(True)
        query_str = UPDATE_ITEM
        try:
            self.library.sql(query_str.format(rowid, description, note))
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot update list item: {0}".format(err))
        self.library.disconnect()

    def update_purchased(self, rowid, Purchased=1):
        assert rowid, "You must supply a rowid."
        self.library.connect(True)
        query_str = UPDATE_PURCHASED
        try:
            self.library.sql(query_str.format(rowid, Purchased))
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot update list item: {0}".format(err))
        self.library.disconnect()

    def delete(self, rowid):
        assert rowid, "You must supply a rowid."
        self.library.connect(True)
        query_str = DELETE_ITEM.format(rowid)
        try:
            self.library.sql(query_str)
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot delete item: {0}".format(err))
        self.library.disconnect()

class Library(object):
    def __init__(self):
        self.config = {
            'user': 'root',
            'password': '<secret>',
            'host': 'cluster-rpi-app',
            'port': RO_PORT,
            'database': None,
        }

        self.db_conn = None

    def connect(self, read_write=False):
        attempts = 0
        while attempts < MAX_RETRY:
            self.config['port'] = (RO_PORT, RW_PORT)[read_write]
            try:
                self.db_conn = mysql.connector.connect(**self.config)
                break
            except mysql.connector.Error as err:
                print("Connection failed. Error = {0} Retrying.".format(err))
            attempts += 1
            time.sleep(1)
        if attempts >= MAX_RETRY:
           self.db_conn = None
           raise mysql.connector.Error("Connection timeout reached.")

    def get_connection(self):
        return self.db_conn

    def is_connected(self):
        return (self.db_conn and (self.db_conn.is_connected()))

    def disconnect(self):
        try:
            self.db_conn.disconnect()
        except:
            pass

    def sql(self, query_str, fetch=True, buffered=False):
        # If we are fetching all, we need to use a buffered
        if fetch:
            cur = self.db_conn.cursor(buffered=True)
        else:
            cur = self.db_conn.cursor(raw=True)

        try:
            cur.execute(query_str)
        except Exception as err:
            cur.close()
            print("Query error. Command: {0}:{1}".format(query_str, err))
            raise

        # Fetch rows (only if available or fetch = True).
        if cur.with_rows:
            if fetch:
                try:
                    results = cur.fetchall()
                except mysql.connector.Error as err:
                    print("Error fetching all query data: {0}".format(err))
                    raise
                finally:
                    cur.close()
                return results
            else:
                # Return cursor to fetch rows elsewhere (fetch = false).
                return cur
        else:
            return cur

    def get_list(self, all=True):
        self.connect()

        try:
            if all:
                results = self.sql(ALL_ITEMS)
            else:
                results = self.sql(UNCHECKED_ITEMS)
        except Exception as err:
            print("ERROR: {0}".format(err))
            raise
        self.disconnect()
        return results

Listing 8-8Shopping List Database Library

好了,现在让我们看看应用的用户界面。

用户界面

在深入研究代码之前,让我们看看 web 表单的呈现。默认视图显示表中的所有行,而不管商品是否被标记为已购买。图 8-2 显示了应用的主视图。该列表表示数据的读取操作。我们还将看到更新和删除操作的读取操作(我们在更新或删除之前读取数据并显示它)。

img/460910_1_En_8_Fig2_HTML.jpg

图 8-2

主表单-列表(默认)

该视图有三个按钮:“新建”向列表中添加一个新项目,“全部显示”显示表中的所有行,“刷新”隐藏表中选择了“已购买”列的行。在这个视图中,如果用户为几个项目选择了 Purchased 列,然后单击 Refresh,列表将更新为只显示未选中的行。单击“全部显示”按钮将显示所有行。还要注意,我们将更新和删除操作作为链接放在列表中每一行的旁边。这允许用户选择要更新或删除的行。

项目视图表单可重复用于创建、更新和删除操作。我们重用了同一个 HTML 文件,只改变了提交按钮的标签(以及操作)。具体来说,我们使用 New 创建行,Update 更新行,Delete 删除行。

当用户点击 New 按钮时,我们显示项目视图的新版本,如图 8-3 所示。该表单允许用户添加描述(必填)和注释(可选),然后单击 add 将项目添加到列表中。添加(创建)项目后,视图返回到列表视图。

img/460910_1_En_8_Fig3_HTML.jpg

图 8-3

项目视图(新)

当用户点击列表中项目旁边的更新链接时,我们显示项目视图的更新版本,如图 8-4 所示。该表单允许用户更改描述(必填)和注释(可选),然后单击更新来更改数据。更新(创建)项目后,视图返回到列表视图。

img/460910_1_En_8_Fig4_HTML.jpg

图 8-4

项目视图(更新)

当用户点击列表中项目旁边的删除链接时,我们显示项目视图的删除版本,如图 8-5 所示。该表单允许用户查看数据,然后单击删除来删除该行。移除项目后,视图返回到列表视图。

img/460910_1_En_8_Fig5_HTML.jpg

图 8-5

项目视图(更新)

在下面的部分中,您将发现用户界面元素的顺序与前面讨论的模板相同。唯一的例外是我们没有任何自定义字段类,所以我们将跳过这一部分。

如果您想跟随代码,打开一个名为shopping_list.py的文件,从文件的顶部开始,包含以下代码部分。将此文件保存在应用文件夹的根目录下(例如,shopping)。

注意

接下来的部分用一般的术语描述代码,但是不要绝望。如果您是 Python 编程的新手,请花点时间通读代码。除了导入部分,代码易于阅读和理解。

进口科

文件的顶部是我们放置所有导入语句的地方。这允许 Python 找到并包含应用所需的库。在这种情况下,有一长串与烧瓶相关的进口产品。下面显示了示例应用所需的导入:

from flask import Flask, render_template, request, redirect, flash
from flask_script import Manager
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import (HiddenField, TextField, SelectField,
                     SelectMultipleField, IntegerField, SubmitField)
from wtforms.validators import Required, Length

接下来,我们导入之前创建的名为Library()的数据库库,并定义几个字符串来简化验证器的定义:

from database.shopping_lib import Library, ShoppingList

REQUIRED = "{0} field is required."
RANGE = "{0} range is {1} to {2} characters."

接下来是初始化部分。这里,我们初始化我们在教程中学到的 Flask 组件,包括Manager()Bootstrap()库:

app = Flask(__name__)
app.config['SECRET_KEY'] = "He says, he's already got one!"
manager = Manager(app)
bootstrap = Bootstrap(app)

最后,我们可以添加一行代码来初始化数据库库:

library = Library()

实用功能

我们需要的实用程序函数是前面描述的收集 Flask flash 错误的函数。下图显示了flash_errors()功能:

def flash_errors(form):
    for error in form.errors:
        flash("{0} : {1}".format(error, ",".join(form.errors[error])))

表单类

在 form classes 部分,我们定义了用于将 HTML 文件中的控件与填充数据的视图函数链接起来的方法。回想一下用户界面的介绍,我们有两个表单:默认的列表视图和条目细节视图。让我们看看名为ListForm()的列表视图的表单类。这里,我们看到表单中添加了三个字段:NewShow AllRefresh

class ListForm(FlaskForm):
    submit = SubmitField('New')
    show_all = SubmitField('Show All')
    hide_checked = SubmitField('Refresh')

接下来,我们看到名为ItemForm()的条目视图的表单类。这里,我们有一个隐藏字段Id,一个用于描述和注释的文本字段。最后,我们有一个名为Add的提交字段。回想一下,我们将在创建、更新和删除操作中重用这个表单。

class ItemForm(FlaskForm):
    row_id = HiddenField('Id')
    description = TextField('Description', validators=[
            Required(message=REQUIRED.format("Description")),
            Length(min=1, max=64, message=RANGE.format("Description", 1, 64))
        ])
    note = TextField( 'Note')
    create_button = SubmitField('Add')

路由功能

路由功能是实现用户界面的流控制或执行路径的地方。回想一下,路由函数如此命名是因为我们在代码中定义了 Flask 将根据提交的 URL 重定向(路由)执行的位置。

路由功能通常会很快变得复杂。这是因为它们通常被编写为支持两个或更多操作以及GETPOST操作。因此,我们经常看到代码看起来杂乱无章。但是,如果仔细观察,您会看到一个守卫(if语句),它重定向POST操作的执行。读取路由功能代码的方法是假设所有代码都用于GET操作(因为GET通常在POST之前或之后),其中POST操作码与保护符分开。不用担心;这在你通读代码后会更清楚。

有两个主要的路由功能。一个是默认的,在这里我们显示总列表。另一个是 item view route,它为创建、更新和删除操作显示项目的详细信息。我们先来看列表路由函数。

列表路由函数被命名为shopping_list()。这段代码有点长,因为我们混合了几个操作。事实上,我们有所有四个 CRUD 操作,你会看到每个操作在代码中的位置。创建、更新和删除操作非常简单,因为每当用户单击这些按钮和链接时,我们都会将代码重定向(路由)到 item routing 函数。但是,如果单击了 Show All 或 Refresh 按钮,我们必须做更多的工作—要么显示(读取)所有行,要么只显示没有勾选 purchased 列的行。清单 8-9 显示了列表视图路由功能的完整代码。

#
# Shopping List
#
# This is the default page for "home" and listing rows in the database.
#
@app.route('/', methods=['GET', 'POST'])
def shopping_list(row_id=None):
    rows = []
    columns = []
    form = ListForm()
    if request.method == 'POST':
        if form.submit.data:
            return redirect('item')
        elif form.hide_checked.data:
            # returns list of all checkboxes
            checkboxes = request.form.getlist('checkboxes')
            # returns a list of values that were checked
            list_row_ids = request.form.getlist('row_ids')
            for list_row_id in list_row_ids:
                shopping_list_db = ShoppingList(library)
                purchased_value = (0, 1)[list_row_id in checkboxes]
                shopping_list_db.update_purchased(list_row_id, purchased_value)
                print(">>> Setting purchased = {1} for rowid = {0}.".format(list_row_id, purchased_value))
            rows = library.get_list(False)
        else:
            rows = library.get_list()
    else:
        # Default is to get all items
        rows = library.get_list()
    columns = (
        '<td style="width:200px">Description</td>',
        '<td style="width:200px">Note</td>',
        '<td style="width:80px">Purchased?</td>',
    )
    return render_template("list.html", form=form, rows=rows,
                           columns=columns)

Listing 8-9List View Routing Function

需要特别注意的一点是列表中列的定义方式。注意,在代码中,我们使用 HTML 代码作为列列表中的字符串。这是我们在 Flask 应用中编写列表时可以使用的一个好技巧。花一些时间通读代码。再次强调,没有必要理解所有的细微差别;该示例仅用于测试集群,但是如果您想了解更多关于编写 Flask 的内容,该示例可以帮助您实现这一点。

另一个名为item_view()的路由函数用于条目视图,我们允许用户通过首先显示数据来创建一个新条目或者更新或删除一个现有条目。因为我们有三个操作要实现,所以这个函数的代码有点长。为清楚起见,添加了行号。

我们先讨论读操作。在函数的顶部,我们看到了建立数据库库类实例的代码(ShoppingList)。之后,我们捕获表单中已经存在的任何数据。如果 URL 包含表中某一行的 ID,我们就读取数据并在表单中显示出来。我们在第 14–20 行看到了这一点。

在下一段代码中,我们检查POST操作。这里,我们显示数据并等待提交操作。POST部分从第 21 行开始。我们做的第一件事是尝试确定我们将要允许哪些数据库操作。我们通过检查提交按钮的标签来了解这一点。回想一下,我们在视图函数中将标签设置为添加、更新或删除。默认值是 add,我们在第 03 行检查它是否保存到名为operation的变量中。我们检查提交按钮的标签,并将结果保存在第 23–27 行代码的operation变量中。

接下来,我们看到添加、更新和删除操作的代码,我们将在后面讨论。清单 8-10 显示了项目视图功能的完整代码。

01 @app.route('/item', methods=['GET', 'POST'])
02 def item_view(row_id=None):
03     operation = request.args.get("operation", "Add")
04     row_id = request.args.get("row_id")
05     shopping_list_db = ShoppingList(library)
06     form = ItemForm()
07     # Get data from the form if present
08     form_row_id = form.row_id.data
09     form_description = form.description.data
10     form_note = form.note.data
11     form.create_button.label.text = operation
12     # If the route with the variable is called, retrieve the data item
13     # and populate the form.
14     if row_id:
15         data = shopping_list_db.read(row_id)
16         if data == []:
17             flash("Item not found!")
18         form.row_id.data = row_id
19         form.description.data = data[0][1]
20         form.note.data = data[0][2]
21     if request.method == 'POST':
22         # First, determine if we must create, update, or delete when form posts.
23         if form.create_button.data:
24             if form.create_button.label.text == "Update":
25                 operation = "Update"
26             elif form.create_button.label.text == "Delete":
27                 operation = "Delete"
28         if form.validate_on_submit():
29             # Get the data from the form here
30             if operation == "Add":
31                 try:
32                     shopping_list_db.create(form_description, form_note)
33                     flash("Added.")
34                 except Exception as err:
35                     flash(err)
36             elif operation == "Update":
37                 try:
38                     print(">>> {0}".format(form.row_id.data))
39                     shopping_list_db.update(form_row_id, form_description, form_note)
40                     flash("Updated.")
41                 except Exception as err:
42                     flash(err)
43             else:
44                 try:
45                     shopping_list_db.delete(form_row_id)
46                     flash("Deleted.")
47                 except Exception as err:
48                     flash(err)
49             return redirect('/')
50         else:
51             flash_errors(form)
52     return render_template("item.html", form=form)

Listing 8-10Item View Routing Function

第 30–35 行显示了添加操作。这里,我们从表单中检索数据,并使用shopping_list_db.create()方法将其保存到数据库中。注意,我们将代码放在了一个try块中,这允许我们捕捉任何错误。这是一种常见的做法,有助于捕捉错误并通过 Flask flash 机制显示出来。

第 36–42 行显示了更新操作。这里,我们从表单中获取数据,并使用shopping_list_db.update()方法更新数据库中的行。

第 44–48 行显示了删除操作。我们使用shopping_list.delete()方法从数据库中删除该行。

哇,对于这样一个简单的应用来说,代码太多了,不是吗?现在您明白了为什么我们让示例应用尽可能小了。即便如此,一个典型的基于 web 的应用(Flask 应用)需要的代码比您想象的要多一些。我们快完成了。让我们看看代码的其余部分。

错误处理功能

下一节有两个额外的路由函数用于处理错误。我们有一个用于页面未找到错误,另一个用于应用的一般错误。下面显示了两条路由的代码:

@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

主要著录

最后,我们实现主入口代码,它只是调用 Flask manager 类的run()方法。我们在它前面放置了一个保护(if语句)来确保它只执行一次,当代码作为主入口运行时(不是导入的)。下面显示了主入口部分的代码:

if __name__ == '__main__':
    manager.run()

这就是应用代码。现在,让我们通过查看所需的 HTML 文件来完成代码。

HTML 文件

示例应用的 HTML 文件中没有秘密。下面列出了应用所需的 HTML 文件。我们已经看到了错误处理和基本 HTML 文件的内容。回想一下,我们将 HTML 文件放在名为templates的文件夹中:

  • 404.html:未发现错误

  • 500.html:一般应用错误

  • base.html:基础模板

  • item.html:物品详细信息

  • list.html:购物清单明细

因为这些文件是基本的 Flask 模板代码(Jinja2 ),所以我们不会详细研究它们。相反,我们展示它们是为了完成对代码的遍历,而将理解留给以后的练习。

我们先来看一下list.html文件。这个文件被list_view()函数用来呈现应用的默认视图。清单 8-11 显示了列表 HTML 文件(模板)的代码。

{% extends "base.html" %}
{% block title %}Shopping List Query Results{% endblock %}
{% block page_content %}
  <legend>My Shopping List</legend>
  <form method=post> {{ form.csrf_token }}
    <fieldset>
      {{ form.submit }} {{ form.show_all }} {{ form.hide_checked }} <br><br>
    </fieldset>
    <table border="1" cellpadding="1" cellspacing="1">
      <tr>
        <td style="width:120px"><b>Actions</b></td>
        {% for col in columns %}
          {{ col|safe }}
        {% endfor %}
      </tr>
      {% for row in rows %}
        <input type="hidden" name="row_ids" value="{{row[0]}}">
        <tr>
          <td>
            <a href="{{ '/item?operation=Update&row_id=%s'%(row[0]) }}">Update</a>
            <label>  </label>
            <a href="{{ '/item?operation=Delete&row_id=%s'%(row[0]) }}">Delete</a>
          </td>
          <td> {{ row[1] }} </td>
          <td> {{ row[2] }} </td>
          <td>
            <div align="center">
              {% if row[3] == 1 %}
                <input type="checkbox" name="checkboxes" value="{{row[0]}}" checked >
              {% else %}
                <input type="checkbox" name="checkboxes" value="{{row[0]}}" >
              {% endif %}
            </div>
          </td>
        </tr>
      {% endfor %}
    </table>
  </form>
  <br>
{% endblock %}

Listing 8-11List HTML Template

模板中需要注意的一点是我们为更新和删除操作定义链接的方式。这是一个在列表视图中添加链接的便利技巧。很好。此外,请注意,代码的后面是一个如何在列表中添加和填充复选框的示例——知道这一点也很好。

另一个 HTML 文件是item.html文件。您在本章前面的“使用模板的 HTML 文件”一节中看到了这个文件。如果您想再次查看文件,请参考该部分。

就这样!这是示例应用的所有代码和支持文件。现在,让我们看看如何运行它并显示一些数据。

运行示例应用

要运行示例应用,必须将所有代码放在磁盘上的正确位置。请参考前面的“准备目录结构”一节,以确保您已经创建了正确的目录。如果您在启动应用时遇到问题,并且所有的 Flask 库都已正确安装,那么您可能在目录中放错了一个文件。

您还需要确保集群已经启动并正在运行,并且已经创建了数据库和样本数据。如果您一直在学习本章,您应该已经完成了这些任务。

当我们运行示例应用时,我们将从终端运行它,就像我们对 Flask primer 中的示例所做的那样。在这种情况下,我们传入选项runserver,并用选项-h将主机设置为应用服务器的 IP 地址。下面显示了如何启动应用的示例。注意,您需要在应用文件夹的根目录下运行这个命令(这个文件夹就是shopping_list.py所在的文件夹)。要停止 web 服务器,请在启动它的终端中按 Ctrl+C。

$ python3 ./shopping_list.py runserver -h 192.168.42.240

要连接到应用,请使用您的浏览器并在 URL 输入框中输入http://<IP>:5000(其中<IP>是您选择的 IP 地址)。注意,:5000是 Flask web 服务器的默认端口。这将引导您的浏览器到应用服务器上的 Flask web 服务器,并显示初始表单(列表视图),如图 8-6 所示。在本例中,我们为应用服务器使用了 IP 地址 192.168.42.123。

img/460910_1_En_8_Fig6_HTML.jpg

图 8-6

通过应用服务器连接到应用

当您从终端启动应用时,您将看到一系列输出消息。每当浏览器连接到 web 服务器时,您都会看到一条消息以及诊断消息。下面显示了您应该在终端(控制台)中看到的内容的示例。请注意,Flask 中有一些消息解释了 web 服务器的情况,后面还有一些附加消息。

$python3 ./shopping_list.py runserver -h 192.168.42.240
 * Serving Flask app "shopping_list" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://192.168.42.240:5000/ (Press CTRL+C to quit)
192.168.42.240 - - [27/May/2018 16:46:34] "GET / HTTP/1.1" 200 -
>>> Setting purchased = 1 for rowid = 2.
>>> Setting purchased = 0 for rowid = 3.
>>> Setting purchased = 1 for rowid = 1.
192.168.42.240 - - [27/May/2018 16:47:47] "POST / HTTP/1.1" 200 -
192.168.42.240 - - [27/May/2018 16:47:49] "GET /item?operation=Update&row_id=3 HTTP/1.1" 200 -
>>> 3
192.168.42.240 - - [27/May/2018 16:48:04] "POST /item?operation=Update&row_id=3 HTTP/1.1" 302 -
192.168.42.240 - - [27/May/2018 16:48:04] "GET / HTTP/1.1" 200 -
192.168.42.240 - - [27/May/2018 16:48:12] "POST / HTTP/1.1" 302 -
192.168.42.240 - - [27/May/2018 16:48:12] "GET /item HTTP/1.1" 200 -
192.168.42.240 - - [27/May/2018 16:48:48] "GET /item HTTP/1.1" 200 -
192.168.42.240 - - [27/May/2018 16:48:57] "POST /item HTTP/1.1" 302 -
192.168.42.240 - - [27/May/2018 16:48:57] "GET / HTTP/1.1" 200 -

还要注意,我们有几种类型的消息,包括来自代码本身的消息。例如,Setting purchasedshopping_list()路由函数中被写入日志。如果您想了解这是如何实现,请查看代码。

好了,现在我们已经让应用工作了,让我们做一些实验,看看当坏事情发生在我们集群中的服务器上时会发生什么。

使用应用测试集群

因此,让我们测试一下新的开发集群。让我们在模拟故障期间测试我们的应用。我们将模拟两种类型的故障:一种是读/写服务器(主服务器)离线;在另一种情况下,当前使用的只读服务器脱机。幸运的是,集群中任何服务器的丢失都是罕见的事件。尽管如此,看看 InnoDB 集群和应用如何处理事件还是很有趣的。我们将在本节中探讨这两种场景。

场景:读/写(主)离线

在这个场景中,我们通过在应用运行时断开以太网电缆来禁用读/写服务器;我们破坏了服务器的网络功能。如果您选择使用 Wi-Fi 而不是以太网,并且您想要运行这个场景,您可能需要关闭机器或者终止 MySQL 服务器进程。

那么,我们如何知道哪个服务器是读/写服务器呢?回想一下,我们可以通过 MySQL Shell 简单地使用这些命令来查找读/写服务器,这是首选方法:

 MySQL  cluster-rpi2:3306 ssl  Py > cluster = dba.get_cluster('RPI_Cluster')
 MySQL  cluster-rpi2:3306 ssl  Py > cluster.status()
...
            "cluster-rpi4:3306": {
                "address": "cluster-rpi4:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }
...

这里,我们在这个例子中看到,cluster-rpi4是读/写服务器。准备就绪后,启动应用,然后拔下以太网电缆。

接下来,尝试在应用中添加一个新项目。当应用通过路由重试失败的连接时,您可能会看到轻微的延迟。当路由重新连接到新的读/写服务器时,应用操作将完成。那么,除了轻微的延迟,我们如何知道集群自愈了呢?

我们可以通过 shell 查看集群状态,但是当我们检查路由的日志时,可以清楚地看到事件已经发生。下面显示了一个示例摘录。请注意显示cluster-rpi4丢失并替换为作为读/写服务器的cluster-rpi3的事件。酷。

...
2018-05-14 21:28:28 metadata_cache INFO [72eff430] Metadata for cluster 'RPI_Cluster' has 1 replicasets:
2018-05-14 21:28:28 metadata_cache INFO [72eff430] 'default' (4 members, single-master)
2018-05-14 21:28:28 metadata_cache INFO [72eff430]     cluster-rpi1:3306 / 33060 - role=HA mode=n/a
2018-05-14 21:28:28 metadata_cache INFO [72eff430]     cluster-rpi2:3306 / 33060 - role=HA mode=RO
2018-05-14 21:28:28 metadata_cache INFO [72eff430]     cluster-rpi4:3306 / 33060 - role=HA mode=RW
2018-05-14 21:28:28 metadata_cache INFO [72eff430] Replicaset 'default' has a new Primary cluster-rpi4:3306
2018-05-14 21:28:28 metadata_cache INFO [72eff430]     cluster-rpi3:3306 / 33060 - role=HA mode=RO
2018-05-14 21:28:29 routing INFO [724ff430] Retrying connection for 'default' after possible failover
...

现在,插回以太网电缆,然后用 MySQL Shell 打开到活动服务器的连接,并发出cluster.rejoin_instance()方法。稍等片刻,然后检查状态以查看集群自我修复。下面显示了cluster-rpi4服务器的更新状态。请注意,它已作为只读服务器返回到群集。不错。

 MySQL  cluster-rpi2:3306 ssl  Py > cluster.rejoin_instance('root@cluster-rpi1:3306')
 MySQL  cluster-rpi2:3306 ssl  Py > cluster.status()
...
            "cluster-rpi4:3306": {
                "address": "cluster-rpi4:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
...

现在,让我们看看当我们失去一个只读服务器时会发生什么。

场景:只读(辅助)脱机

这个例子需要更多的工作。这里,我们需要找到路由要读取哪个服务器。这可能很棘手,取决于列表中的下一台机器是哪一台。为了简单起见,我们已经使用默认路由策略配置了路由,因此我们应该能够预测哪个服务器是读取队列中的“下一个”。我们将通过在数据库代码中添加一个print()语句来报告连接的主机名,从而稍微欺骗一下

这个场景的设置要求集群启动时所有四台服务器都在运行并加入集群。我们将使用应用服务器,因此您现在可以登录到该机器。如果路由正在运行,您可以停止它,因为我们将截断日志,以便更容易找到示例消息。

此外,在该场景的演示中,读/写服务器是cluster-rpi1。但是,您的集群可能为读/写服务器选择了不同的机器,您可能会在演示中看到略有不同的输出。

好了,让我们首先将下面一行代码(以粗体显示)添加到database/shopping_lib.pyLibrary类的connect()方法的末尾。这将打印出我们所连接的服务器的主机名。确保缩进与if语句匹配,而不是该方法的最后一行,如下所示:

...
if attempts >= MAX_RETRY:
   self.db_conn = None
   raise mysql.connector.Error("Connection timeout reached.")

print(">>> Hostname =", self.sql("SHOW VARIABLES LIKE 'report_host'")[0][1])

接下来,让我们截断路由日志。如果路由正在运行,您可以停止它。如果您从命令行启动它,您可以使用ps a命令找到进程 ID,然后用sudo kill -9 <id>终止它。这不会损坏路由。停止后,使用以下命令删除日志文件:

$ sudo rm /usr/local/mysql/var/log/mysqlrouter/mysqlrouter.log

接下来,我们可以用下面的命令启动我们的应用。让您用来运行此命令的终端保持打开状态,以便您可以看到显示的消息。

$ python ./shopping_list.py runserver -h 192.168.42.240

应用启动后,打开浏览器并通过 URL http://192.168.42.240:5000连接。你会看到应用默认页面,如图 8-7 所示。

img/460910_1_En_8_Fig7_HTML.jpg

图 8-7

购物清单应用

请注意“全部显示”按钮。我们将使用这个按钮来测试场景。该按钮将通过只读端口(6447)连接到路由,并从数据库中检索所有行。因为没有缓存任何东西,所以每次单击按钮时都会连接。

如果您现在使用以下命令查看路由日志,您将在顶部附近看到消息,路由在此连接到集群并建立了其连接队列。例如,下面显示了本演示中的集群将读/写服务器作为cluster-rp1,将只读服务器作为cluster-rpi2-4。您可能需要按几次空格键来查找数据。按 Q 退出more实用程序。

$ sudo more /usr/local/mysql/var/log/mysqlrouter/mysqlrouter.log
...
2018-05-28 18:46:07 metadata_cache INFO [75c40430]     cluster-rpi1:3306 / 33060 - role=HA mode=RW
2018-05-28 18:46:07 metadata_cache INFO [75c40430]     cluster-rpi2:3306 / 33060 - role=HA mode=RO
2018-05-28 18:46:07 metadata_cache INFO [75c40430]     cluster-rpi3:3306 / 33060 - role=HA mode=RO
2018-05-28 18:46:07 metadata_cache INFO [75c40430]     cluster-rpi4:3306 / 33060 - role=HA mode=RO

现在,请点击“全部显示”按钮三次。然后检查启动应用的终端窗口。您应该注意到,应用会报告每次单击按钮时所连接的主机。例如,下面显示了启动应用,然后单击按钮四次的结果:

* Running on http://192.168.42.240:5000/ (Press CTRL+C to quit)
('>>> Hostname =', u'cluster-rpi2')
192.168.42.240 - - [28/May/2018 18:46:21] "POST / HTTP/1.1" 200 -
('>>> Hostname =', u'cluster-rpi3')
192.168.42.240 - - [28/May/2018 18:46:22] "POST / HTTP/1.1" 200 -
('>>> Hostname =', u'cluster-rpi4')
192.168.42.240 - - [28/May/2018 18:46:23] "POST / HTTP/1.1" 200 -
('>>> Hostname =', u'cluster-rpi2')
192.168.42.240 - - [28/May/2018 18:46:24] "POST / HTTP/1.1" 200 -
('>>> Hostname =', u'cluster-rpi3')
192.168.42.240 - - [28/May/2018 18:46:25] "POST / HTTP/1.1" 200 –

我们在这里看到的是一些有趣的东西。路由将连接指向cluster-rpi2,然后是cluster-rpi3,然后是cluster-rpi4。然后它重复这个序列。为什么呢?因为默认情况下,路由使用循环策略在只读服务器之间分配读数。

现在,让我们来看看我们演示中的疯狂科学家部分。转到您的集群,断开cluster-rpi2的以太网电缆。等待大约 30 秒,然后再次检查路由日志。您应该会发现一条消息,说明路由检测到一个变化,并且它将cluster-rpi2的状态标记为n/a,因为它不再可用。如果您查看集群状态,您会看到机器被标记为MISSING。以下显示了路由日志中应包含的内容示例:

2018-05-28 18:47:03 metadata_cache INFO [72eff430] Changes detected in cluster 'RPI_Cluster' after metadata refresh
2018-05-28 18:47:03 metadata_cache INFO [72eff430] Metadata for cluster 'RPI_Cluster' has 1 replicasets:
2018-05-28 18:47:03 metadata_cache INFO [72eff430] 'default' (4 members, single-master)
2018-05-28 18:47:03 metadata_cache INFO [72eff430]     cluster-rpi1:3306 / 33060 - role=HA mode=RW
2018-05-28 18:47:03 metadata_cache INFO [72eff430]     cluster-rpi2:3306 / 33060 - role=HA mode=n/a
2018-05-28 18:47:03 metadata_cache INFO [72eff430]     cluster-rpi3:3306 / 33060 - role=HA mode=RO
2018-05-28 18:47:03 metadata_cache INFO [72eff430]     cluster-rpi4:3306 / 33060 - role=HA mode=RO

现在试试这个应用,点击几次 Show All 按钮。您应该在运行应用的终端上看到几个连接,但是您应该注意到cluster-rpi2不在其中。应用将继续运行,但是cluster-rpi2不会是您用来读取数据的机器之一。下面显示了一个输出示例:

('>>> Hostname =', u'cluster-rpi3')
192.168.42.240 - - [28/May/2018 18:47:10] "POST / HTTP/1.1" 200 -
('>>> Hostname =', u'cluster-rpi4')
192.168.42.240 - - [28/May/2018 18:47:17] "POST / HTTP/1.1" 200 -

现在,让我们更进一步。让我们断开cluster-rpi4的以太网电缆。如果查看路由日志,您会发现另一个元数据刷新事件,如下所示:

2018-05-28 18:47:36 metadata_cache INFO [72eff430] Changes detected in cluster 'RPI_Cluster' after metadata refresh
2018-05-28 18:47:36 metadata_cache INFO [72eff430] Metadata for cluster 'RPI_Cluster' has 1 replicasets:
2018-05-28 18:47:36 metadata_cache INFO [72eff430] 'default' (4 members, single-master)
2018-05-28 18:47:36 metadata_cache INFO [72eff430]     cluster-rpi1:3306 / 33060 - role=HA mode=RW
2018-05-28 18:47:36 metadata_cache INFO [72eff430] Replicaset 'default' has a new Primary cluster-rpi1:3306 [4127c169-520f-11e8-a834-b827ebcb9200].
2018-05-28 18:47:36 metadata_cache INFO [72eff430]     cluster-rpi2:3306 / 33060 - role=HA mode=n/a
2018-05-28 18:47:36 metadata_cache INFO [72eff430]     cluster-rpi3:3306 / 33060 - role=HA mode=RO
2018-05-28 18:47:36 metadata_cache INFO [72eff430]     cluster-rpi4:3306 / 33060 - role=HA mode=n/a

此时,如果您单击几次 Show All 按钮,您应该仍然会看到应用刷新,但是现在它只使用cluster-rpi3进行读取。如果您断开cluster-rpi4的以太网电缆并立即尝试应用,您可能会遇到应用捕获路由连接错误的情况。下面显示了您将在应用中看到的消息示例。回想一下,应用将重试,所以它将继续运行,因为从技术上讲,我们仍然有一个可行的只读服务器。下面显示了发生此事件时您应该看到的错误消息和恢复消息的示例(为简洁起见,删除了一些消息):

('>>> Hostname =', u'cluster-rpi4')
192.168.42.240 - - [28/May/2018 18:46:59] "POST / HTTP/1.1" 200 -
Connection failed. Error = 2003: Can't connect to remote MySQL server for client connected to '192.168.42.240:6447' Retrying.
('>>> Hostname =', u'cluster-rpi4')
192.168.42.240 - - [28/May/2018 18:47:20] "POST / HTTP/1.1" 200 -
----------------------------------------
Exception happened during processing of request from ('192.168.42.240', 37546)
..
error: [Errno 32] Broken pipe
----------------------------------------
('>>> Hostname =', u'cluster-rpi3')
192.168.42.240 - - [28/May/2018 18:47:20] "POST / HTTP/1.1" 200 -

如果这看起来像天书 Python 喷涌,那就 ok 了。它来自 Python,这是 Python 交流网络错误的正常方式。重要的消息是最后两行,它显示我们能够重新连接和检索数据。这是一个你可以在应用中改进的地方,如果你想扩展它的话。

所以现在我们的集群只剩下两台机器,这不是一个理想的状态。我们需要重新连接丢失的服务器。继续连接cluster-rpi2cluster-rpi4的以太网电缆。给他们一些时间重新联系;然后启动 MySQL Shell,连接到cluster-rpi1。我们应该首先获取集群,然后显示状态,以验证两个服务器是否缺失。在状态中需要特别注意的是,集群报告OK_NO_TOLERANCE,这意味着它不能容忍另一个服务器出现故障。我们用cluster.rejoin_instance()方法重新加入服务器。清单 8-12 显示了一段摘录,其中丢失的服务器被重新连接。

 MySQL  cluster-rpi1:3306 ssl  Py > cluster = dba.get_cluster()
 MySQL  cluster-rpi1:3306 ssl  Py > cluster.status()
{
    "clusterName": "RPI_Cluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "cluster-rpi1:3306",
        "ssl": "REQUIRED",
        "status": "OK_NO_TOLERANCE",
        "statusText": "Cluster is NOT tolerant to any failures. 2 members are not active",
        "topology": {
            "cluster-rpi1:3306": {
                "address": "cluster-rpi1:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi2:3306": {
                "address": "cluster-rpi2:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "(MISSING)"
            },
            "cluster-rpi3:3306": {
                "address": "cluster-rpi3:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi4:3306": {
                "address": "cluster-rpi4:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "(MISSING)"
            }
        }

    },
    "groupInformationSourceMember": "mysql://root@cluster-rpi1:3306"
}

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.rejoin_instance('root@cluster-rpi2:3306')
Rejoining the instance to the InnoDB cluster. Depending on the original
...
The instance 'cluster-rpi2:3306' was successfully rejoined on the cluster.
 MySQL  cluster-rpi1:3306 ssl  Py > cluster.rejoin_instance('root@cluster-rpi4:3306')
Rejoining the instance to the InnoDB cluster. Depending on the original
...
The instance 'cluster-rpi4:3306' was successfully rejoined on the cluster.
 MySQL  cluster-rpi1:3306 ssl  Py > cluster.status()
{
    "clusterName": "RPI_Cluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "cluster-rpi1:3306",
        "ssl": "REQUIRED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "cluster-rpi1:3306": {
                "address": "cluster-rpi1:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }, 

            "cluster-rpi2:3306": {
                "address": "cluster-rpi2:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi3:3306": {
                "address": "cluster-rpi3:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi4:3306": {
                "address": "cluster-rpi4:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }
        }
    },
    "groupInformationSourceMember": "mysql://root@cluster-rpi1:3306"
}

Listing 8-12Rejoining Instances to the Cluster

好了,集群恢复正常了。如果我们现在再单击几次按钮,我们会看到路由已经恢复了服务器,并且正在将连接重定向到这些服务器。以下是您在运行应用的终端中应该看到的内容的示例:

('>>> Hostname =', u'cluster-rpi2')
192.168.42.240 - - [28/May/2018 18:49:56] "POST / HTTP/1.1" 200 -
('>>> Hostname =', u'cluster-rpi3')
192.168.42.240 - - [28/May/2018 18:49:56] "POST / HTTP/1.1" 200 -
('>>> Hostname =', u'cluster-rpi4')
192.168.42.240 - - [28/May/2018 18:49:57] "POST / HTTP/1.1" 200 -

如果您现在查看路由日志,您将看到另一个元数据刷新事件,显示已修复的集群:

2018-05-28 18:49:44 metadata_cache INFO [72eff430]     cluster-rpi1:3306 / 33060 - role=HA mode=RW
2018-05-28 18:49:44 metadata_cache INFO [72eff430]     cluster-rpi2:3306 / 33060 - role=HA mode=RO
2018-05-28 18:49:44 metadata_cache INFO [72eff430]     cluster-rpi3:3306 / 33060 - role=HA mode=RO
2018-05-28 18:49:44 metadata_cache INFO [72eff430]     cluster-rpi4:3306 / 33060 - role=HA mode=RO

如果您曾经对路由以及只读服务器如何处理故障转移有任何疑问,这个演示应该可以打消这些疑问。

注意

如果我们禁用剩余的只读服务器,应用将会停止。尽管路由允许在读/写端口(6446)上进行读取,但应用被设计为仅连接到只读端口(6447)。因此,有可能建立一个最后的连接尝试,将读取重定向到读/写端口。但是,这不是最佳做法,因为我们通常将读取和写入分开。

扩展示例应用

就像一个努力在不使事情过于复杂的情况下交流关键方面的例子一样,本章中的示例高可用性应用在实际功能上是有限的。但是,如果您喜欢这个应用,或者看到可以用它做进一步实验的方法,您可能想探索一些东西。下面是您可能要考虑的示例应用改进建议的简短列表,按复杂性递增的顺序列出:

  • 改进应用的错误处理。

  • 尽管不是最佳实践,但是如果只读连接反复失败,可以将只读连接重定向到读/写服务器(通过路由的端口)。

  • 添加通过电子邮件将列表发送给他人的功能。

  • 添加处理多个列表的功能。

  • 将列表转换为文档存储,允许您存储每个项目的附加信息。

  • 修改应用以允许多个用户存储他们自己的列表。

  • 在开发多用户功能时,允许用户对其他人进行只读访问。

  • 结合扫描功能,读取产品上的 UPC 标签,以将项目添加到列表中或在列表上勾掉项目。

摘要

构建高可用性应用需要解决与典型的多用户应用相同的所有问题。让高可用性应用变得更加困难的是,当服务器离线时,必须处理故障。幸运的是,InnoDB 集群与 MySQL 路由的结合使这变得更加容易。事实上,最大的挑战是构建到数据库(集群)的连接,以便在出现错误时重试。精明的企业应用开发者会告诉你这是他们一直在做的事情。

在这一章中,我们使用 Flask 和 Python 开发了一个简单的基于 web 的高可用性示例应用。尽管该应用缺乏真正的多用户应用的复杂性,但该示例有助于测试 InnoDB Cluster 和试验 InnoDB Cluster(和路由)从故障转移事件中恢复的方式。

您看到了如何通过路由连接到集群,以及如何隔离我们的数据库连接,以便它们在出现错误时重试。您还看到了一种最小化连接保持打开时间的技术:我们只在需要执行操作时打开连接,然后在操作完成时关闭它(断开连接)。最后,您看到了一个来自路由的日志示例,此时读/写服务器出现故障,集群恢复。您已经看到了使用该应用进行的演示。

下一章将介绍保持 InnoDB 集群健康运行和高可用性所需的各种管理任务。正如你将看到的,不需要做太多,但是管理任务的简化并不意味着我们在维护时可以不小心!

九、InnoDB 集群管理

正如您已经发现的,设置 InnoDB Cluster 并不困难,除了学习如何使用 MySQL Shell 和 AdminAPI 中的一些类和方法之外,配置 InnoDB Cluster 的步骤也同样简单。然而,我们从经验中知道,设置和管理在复杂性上并不总是一致的。

事实上,在 InnoDB Cluster 之前,管理和维护 MySQL 复制需要学习一系列特定的任务和技术,以保持一切正常运行。幸运的是,对于 InnoDB 集群来说,情况并非如此。在 InnoDB 集群的发展过程中,管理任务是一个关键的目标领域,其目标是以更低的复杂性更容易使用。正如您将看到的,除了一些罕见的例外情况,AdminAPI 已经达到了这个目标。

在本章中,您将了解管理 InnoDB 集群的常见任务。当您在自己的环境中探索 InnoDB 集群时,可以将本章作为参考。正如您将看到的,我们已经通过第 578 章中的沙箱和实时服务器部署示例浏览了一些任务。为了完整起见,本章使用简短的示例总结了这些任务。任务我们还没有更详细地讨论功能示例。

您还将通过研究保护集群的管理任务,了解如何使您的 InnoDB 集群更加安全。

概观

管理 InnoDB 集群需要通过 MySQL Shell 使用 AdminAPI。尽管可以直接使用 InnoDB 集群和组复制,但不建议这样做。InnoDB 集群的管理应该始终通过 AdminAPI 进行。

警告

始终通过 MySQL Shell 使用 AdminAPI 来管理 InnoDB 集群。不建议直接使用组复制或 InnoDB 集群元数据。

你在第 5 章中看到了 AdminAPI 的概述。AdminAPI 中有两个类:dba类,用于建立集群并使用实例为集群做准备,以及cluster类,用于管理集群,从检查状态到解决实例问题。

让我们先简要回顾一下这些工具,然后看看常见的任务。然后我们将讨论故障排除任务,这些任务并不经常使用,只是在出现问题时才使用。

工具,命令

使用 InnoDB Cluster 的首选工具是 MySQL Shell,这是本书的重点。这是唯一推荐使用集群的工具。但是,应该注意的是,您可以用 Java 或 Python 编写自己的特殊脚本来处理集群。这两种方法都使用 AdminAPI 来处理集群。同样,您不应该使用任何不使用 AdminAPI 的工具或实用程序。如果您是使用组复制的专家,您可能会尝试通过操作组复制参数和选项来使用群集,但不建议这样做。您应该迁移到使用 MySQL Shell。

在本节中,我们将研究使用 InnoDB 集群的常见管理任务。我们将任务分为以下几类:

  • 常规:状态和相关检查的基本管理任务

  • 实例:使用实例的任务

因为本章旨在用作参考,所以示例有些简短,仅显示了可用于完成任务的基本命令。但是,对于那些更频繁使用的任务,会显示详细的示例和输出。此外,示例省略了 MySQL Shell 的连接步骤。有关使用 MySQL Shell 连接到服务器的更多信息,请参见第 4 章。

一般

此类别中的任务是那些更频繁执行的任务,包括获取集群、检查状态和描述集群配置。

获取集群

当使用 MySQL Shell 管理 InnoDB 集群时,我们必须首先请求一个cluster类的实例。回想一下,在本书中你已经多次看到这种情况。为了检索cluster实例,我们使用了dba.get_cluster()方法。下面显示了一个检索群集的示例。请记住,我们必须先连接到集群中的一个服务器,然后才能检索它。

\connect root@cluster-rpi1:3306
cluster = dba.get_cluster()

您也可以将集群的名称作为参数传递,但这是用于额外的错误检查(如果服务器不属于指定的集群,则会引发错误)。当您的基础架构中有多个集群时,这很有帮助。

检查集群状态

与上一个任务一样,您之前已经看到了如何检索集群状态报告。回想一下,我们使用的是cluster.status()方法。我们必须连接到集群中的一个服务器,检索它,然后使用状态方法:

\connect root@cluster-rpi1:3306
cluster = dba.get_cluster()
cluster.status()

您还可以将检索集群和显示状态的方法链接起来:

dba.get_cluster().status()

如果您想在批处理模式下使用 MySQL Shell 来快速检索状态,这可能会很方便。

描述集群

您还可以获得关于集群的信息,比如集群中机器的主机名以及每个 MySQL 实例使用的端口。我们使用清单 9-1 中展示的cluster.describe()方法。

MySQL  cluster-rpi2:3306 ssl  Py > cluster = dba.get_cluster('RPI_Cluster')
MySQL  cluster-rpi2:3306 ssl  Py > cluster.describe()
{
    "clusterName": "RPI_Cluster",
    "defaultReplicaSet": {
        "name": "default",
        "topology": [
            {
                "address": "cluster-rpi1:3306",
                "label": "cluster-rpi1:3306",
                "role": "HA"
            },
            {
                "address": "cluster-rpi2:3306",
                "label": "cluster-rpi2:3306",
                "role": "HA"
            },
            {
                "address": "cluster-rpi3:3306",
                "label": "cluster-rpi3:3306",
                "role": "HA"
            },
            {
                "address": "cluster-rpi4:3306",
                "label": "cluster-rpi4:3306",
                "role": "HA"
            },
        ]

    }
}

Listing 9-1Describing the Cluster

现在,让我们看看使用实例的任务。

例子

此类别中的任务旨在用于单个实例,以将实例加入集群,将实例重新加入集群,检查实例是否适合与 InnoDB 集群一起使用,以及类似的任务。

检查实例是否适合与 InnoDB 集群一起使用

您已经看到了可以用来准备与 InnoDB Cluster 一起使用的实例的两种方法。第一个是dba.configure_local_instance(),用于准备在集群中使用的本地机器。第二个是dba.check_instance_configuration(),可以用来测试服务器的设置是否正确。与第一种方法不同,检查实例配置方法可以远程运行。以下是每个命令的示例:

\connect root@localhost:3306
dba.configure_local_instance()
dba.check_instance_configuration('root@cluster-rpi2:43306')

检查实例的集群状态

您还可以通过使用dba.check_instance_state()方法来检查实例的当前或最后已知状态。此方法将服务器连接信息作为参数,并返回四种状态之一:

  • OK new:实例没有执行过任何 GTID 事务,所以不能与集群执行的 GTID 冲突。

  • OK recoverable:该实例已经执行了与集群种子实例的已执行 gtid 不冲突的 gtid。

  • ERROR diverged:该实例已执行的 gtid 与集群种子实例已执行的 gtid 不同。

  • ERROR lost_transactions:实例的已执行 gtid 多于集群种子实例的已执行 gtid。

清单 9-2 展示了一个运行这个命令的例子。

MySQL  Py > \connect root@localhost:3306
MySQL  Py > dba.configure_local_instance('localhost:3306')
Please provide the password for 'root@localhost:3306': ****
Configuring local MySQL instance listening at port 3306 for use in an InnoDB cluster...

This instance reports its own address as cluster-rpi3

WARNING: User 'root' can only connect from localhost.
If you need to manage this instance while connected from other hosts, new account(s) with the proper source address specification must be created.

1) Create remotely usable account for 'root' with same grants and password
2) Create a new admin account for InnoDB cluster with minimal required grants
3) Ignore and continue
4) Cancel

Please select an option [1]: 1
Please provide a source address filter for the account (e.g: 192.168.% or % etc) or leave empty and press Enter to cancel.
Account Host: %

Some configuration options need to be fixed:
+--------------------------+---------------+----------+-------------------+
| Variable                 | Current Value | Required | Note              |                                              Value
+--------------------------+---------------+----------------+-------------+
| binlog_checksum          | CRC32         | NONE     | Update the server variable          |
| enforce_gtid_consistency | OFF           | ON       | Update read-only variable and restart the server |
| gtid_mode                | OFF           | ON       | Update read-only variable and restart the server |
+--------------------------+---------------+----------+-------------------+

Do you want to perform the required configuration changes? [y/n]: y
Do you want to restart the instance after configuring it? [y/n]: y

Cluster admin user 'root'@'%' created.
Configuring instance...
The instance 'localhost:3306' was configured for cluster usage.
Restarting MySQL...

MySQL server at localhost:3306 was restarted.

Listing 9-2Configuring the Local Instance

将实例加入集群

在本书中,您已经多次看到了如何将实例加入集群。回想一下,我们使用cluster.add_instance()方法,传入连接信息以连接到一个实例,从而加入当前集群。此处显示了将实例加入集群的命令示例;我们必须连接到群集中的服务器才能使用此方法:

\connect root@cluster-rpi1:3306
cluster = dba.get_cluster()
cluster.add_instance('root@cluster-rpi3:3306')

将实例重新加入集群

如果服务器与集群断开连接,我们可以使用cluster.rejoin_instance()方法将其重新加入集群。这将连接信息作为参数,并尝试将实例重新连接到群集。你在第 8 章中已经看到了这种方法。下面显示了一个将实例重新加入集群的示例。在操作之后检查集群的状态总是一个好主意。

\connect root@cluster-rpi1:3306
\connect root@cluster-rpi1:3306
cluster = dba.get_cluster()
cluster.rejoin_instance('root@cluster-rpi4:3306')
cluster.status()

从集群中删除一个实例

如果您需要在物理机器或服务器上运行的 MySQL 实例上执行维护,您应该首先将它从集群中删除。我们可以用cluster.remove_instance(方法做到这一点:

\connect root@cluster-rpi1:3306
cluster = dba.get_cluster()
cluster.remove_instance('root@cluster-rpi2:3306')
cluster.status()

在操作之后检查集群的状态总是一个好主意。如果 InnoDB 集群元数据完好无损,您可以使用cluster.rejoin_instance()方法将服务器添加回集群。如果您必须删除数据或重建服务器,您可能需要使用cluster.add_instance()方法。

创建实例的白名单

假设我们想要指定一个允许与集群一起使用的服务器列表(一个称为白名单 1 的许可列表),该列表限制只有列表中的机器才能连接到集群。我们可以通过向create_cluster()add_instance()rejoin_instance()方法传递一个特殊的参数来显式地创建白名单。例如,下面显示了如何创建一个群集来限制与特定子网(192.168.42.X)上的服务器的连接:

\connect root@cluster-rpi1:3306
cluster = dba.create_cluster('RPI_Cluster', {ipWhitelist: '192.168.42.0/24"})

自定义集群元数据

您可能还记得本书和本章中的各种例子,除了命名集群之外,我们通常不会向集群元数据添加任何定制。这是因为为组名、本地地址和种子实例等选项设置了默认值。然而,我们可以通过在设置和配置方法中覆盖它们来指定这些属性的值,包括dba.create_cluster()cluster.add_instance()

设置组名

要设置复制组的名称,请使用dba.create_cluster()方法的groupName选项。但是,您必须使用有效的 UUID。下面是一个例子:

dba.create_cluster('RPI_Cluster', {'groupName':'0cc3ebad-6759-11e8-966f-c49ded13bebf'})

您可以通过发出以下 SQL 语句来查看系统变量group_replication_group_name中的值:

\sql;
SHOW VARIABLES LIKE 'group_replication_group_name';

设置本地地址

本地地址是一个实例为来自其他实例的连接提供的地址。当调用dba.create_cluster()cluster.add_instance()方法时,可以通过使用localAddress选项来设置本地地址。这将在实例上设置系统变量group_replication_local_address

您必须使用字符串格式host:port提供一个值。请注意,该地址必须是群集中所有实例都可以访问的地址(由 DNS 或 hosts 文件条目支持)。此外,Oracle 在《在线参考手册》中指出,该值只能用于内部的实例对实例的通信,而不能用于外部访问集群。以下是如何使用该选项的示例:

dba.create_cluster('RPI_Cluster', {'localAddress':'192.168.42.241:3306'})

您可以通过发出以下 SQL 语句来查看系统变量group_replication_local_address中的值:

\sql;
SHOW VARIABLES LIKE 'group_replication_local_address';

设置组种子

组种子是当一个实例加入集群时用来连接的那些实例的列表。这允许实例获取正确的元数据以用于集群。当调用dba.create_cluster()cluster.add_instance()方法时,可以通过使用groupSeeds选项来设置组种子。

使用格式host1:port1,host2:port2, <etc>将地址指定为逗号分隔的列表(字符串)。以下是如何使用该选项的示例:

dba.create_cluster('RPI_Cluster', {'groupSeeds':'192.168.42.241:3306,192.168.42.242:3306'})

您可以通过发出以下 SQL 语句来查看系统变量group_replication_group_seeds中的值:

\sql;
SHOW VARIABLES LIKE ' group_replication_group_seeds ';

现在,我们来看看故障排除任务。

InnoDB 集群故障排除

如果您正在努力组建集群并添加实例,那么您的机器或这些机器上的 MySQL 可能会发生一些奇怪的事情。例如,假设您无法添加一个实例,并且已经通过dba.check_instance_configuration()方法检查了该实例是否有效,但是集群仍然不接受该实例。在这种情况下,群集可能已经消失(元数据错误、损坏或丢失)。

故障排除类别中的任务是在出现问题或失败时您可能会遇到或需要的任务。这些不是你通常会执行的任务——相反,我们希望永远不需要这些任务!事实上,有些可能被认为是危险的,所以要谨慎使用。

从仲裁丢失中恢复

在不寻常的情况下,一个实例可能会失去其法定人数(在失败的情况下,参与投票方案以确定主要成员的能力)。我们可以使用cluster.force_quorum_using_partition_of()方法来重新建立法定人数。当实例的状态被设置为NO_QUORUM时,您就会知道这已经发生了。下面显示了 AdminAPI 的命令和输出示例:

cluster.force_quorum_using_partition_of ("localhost:3310")

  Restoring replicaset 'default' from loss of quorum, by using the partition composed of [localhost:3310]

  Please provide the password for 'root@localhost:3310': ******
  Restoring the InnoDB cluster ...

  The InnoDB cluster was successfully restored using the partition from the instance 'root@localhost:3310'.

警告

使用这种方法要非常小心。可以遇到裂脑的情况。为了避免这种情况,请确保副本集的所有其他成员都被删除或重新加入到被还原的组中。

从完全丢失中重新启动

如果您需要在完全中断或丢失后重新启动集群,比如集群中的所有服务器都已关闭(或类似事件),您可以使用dba.reboot_cluster_from_complete_outage()方法。只需确保所有集群服务器都在线,并且 MySQL 在所有节点上都已启动和运行。该任务可用于有意或出于必要而关闭的试验性或开发性集群。清单 9-3 展示了一个从完全丢失状态重启集群的例子。

MySQL  Py > \connect root@cluster-rpi1:3306
Creating a session to 'root@cluster-rpi1:3306'
Enter password: ****
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 15
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.

 MySQL  cluster-rpi1:3306 ssl  Py > cluster = dba.reboot_cluster_from_complete_outage('RPI_Cluster')
Reconfiguring the cluster 'RPI_Cluster' from complete outage...

The instance 'cluster-rpi2:3306' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The instance 'cluster-rpi4:3306' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The instance 'cluster-rpi3:3306' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The cluster was successfully rebooted.

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.status()
{
    "clusterName": "RPI_Cluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "cluster-rpi1:3306",
        "ssl": "REQUIRED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "cluster-rpi1:3306": {
                "address": "cluster-rpi1:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi2:3306": {
                "address": "cluster-rpi2:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }, 

            "cluster-rpi3:3306": {
                "address": "cluster-rpi3:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi4:3306": {
                "address": "cluster-rpi4:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }
        }

    },
    "groupInformationSourceMember": "mysql://root@cluster-rpi1:3306"
}

Listing 9-3Rebooting the Cluster from Complete Loss

注意

您必须指定群集名称才能使用此方法。否则,您将得到一个错误。

重新扫描集群元数据

如果您(或某人)在不使用 AdminAPI 的情况下对集群进行了更改(比如手动从组复制中添加或删除一台机器),您可以使用cluster.rescan()方法修复 InnoDB 集群元数据。

此方法将尝试修复元数据,并尝试将实例添加到群集。系统将提示您批准找到的每台服务器。如果不允许服务器加入集群或遇到错误,将会报告该服务器,以便您可以在以后尝试添加实例。以下是运行此命令的示例:

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.rescan()
Rescanning the cluster...

Result of the rescanning operation:
{
    "defaultReplicaSet": {
        "name": "default",
        "newlyDiscoveredInstances": [],
        "unavailableInstances": []
    }
}

在这里,我们看不到任何新的或不可用的实例。如果有的话,您会看到它们如下:

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.rescan()
Rescanning the cluster...

Result of the rescanning operation:
{
    "defaultReplicaSet": {
        "name": "default",
        "newlyDiscoveredInstances": [],
        "unavailableInstances": [
            {
                "host": "cluster-rpi4:3306",
                "label": "cluster-rpi4:3306",
                "member_id": "1fec2731-53d2-11e8-914c-b827ebcb9200"
            }
        ]
    }
}

请注意,实例cluster-rpi4:3306不再是高可用性设置的一部分。它或者脱机,或者已经离开高可用性组。您可以尝试使用cluster.rejoin_instance('cluster-rpi4:3306')命令再次将它添加到集群中,或者从集群配置中删除它。

Would you like to remove it from the cluster metadata? [Y/n]: n

对于这个问题,我们通常不希望在正常情况下删除实例,例如,当您需要将实例脱机进行修复时。所以要回复N。另一方面,如果应该删除实例,可以通过回答问题Y来实现。

以下是使用cluster.rejoin_instance()方法将实例重新加入集群的示例:

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.rejoin_instance('root@cluster-rpi4:3306')
Rejoining the instance to the InnoDB cluster. Depending on the original
problem that made the instance unavailable, the rejoin operation might not be successful and further manual steps will be needed to fix the underlying problem.

Please monitor the output of the rejoin operation and take necessary action if the instance cannot rejoin.

Please provide the password for 'root@cluster-rpi4:3306': ****
Rejoining instance to the cluster ...

The instance 'cluster-rpi4:3306' was successfully rejoined on the cluster.

现在,如果我们重新扫描群集,我们应该会看到不再有任何实例不可用或对群集来说是新的,如下所示:

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.rescan()
Rescanning the cluster...

Result of the rescanning operation:
{
    "defaultReplicaSet": {
        "name": "default",
        "newlyDiscoveredInstances": [],
        "unavailableInstances": []
    }

}

集群拆卸

在极少数情况下,您需要从集群中完全删除所有实例并删除集群,您可以使用cluster.dissolve()方法。如果您将force选项设置为True,该方法将删除所有实例并解散集群。与类似的方法一样,这是一种作为最后手段或用于实验或开发的方法。下面是使用方法的一个示例:

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.dissolve({'force':True})
The cluster was successfully dissolved.
Replication was disabled but user data was left intact.

You can now rebuild your cluster.

如果您执行此任务是为了尝试修复错误或重建群集,则可以这样做,因为所有元数据和设置都已从实例中删除。清单 9-4 显示了如何重建第 7 章中的示例集群。

 MySQL  cluster-rpi1:3306 ssl  Py > dba.create_cluster('RPI_Cluster')
A new InnoDB cluster will be created on instance 'root@cluster-rpi1:3306'.

The MySQL instance at 'cluster-rpi1:3306' currently has the super_read_only
system variable set to protect it from inadvertent updates from applications. You must first unset it to be able to perform any changes to this instance.
For more information see: https://dev.mysql.com/doc/refman/en/server-system-variables.html#sysvar_super_read_only.

Note: there are open sessions to 'cluster-rpi1:3306'.
You may want to kill these sessions to prevent them from performing unexpected updates:

1 open session(s) of 'root@192.168.42.240'.

Do you want to disable super_read_only and continue? [y/N]: y

Validating instance at cluster-rpi1:3306...

This instance reports its own address as cluster-rpi1

Instance configuration is suitable.
Creating InnoDB cluster 'RPI_Cluster' on 'root@cluster-rpi1:3306'...
Adding Seed Instance...

Cluster successfully created. Use Cluster.add_instance() to add MySQL instances. At least 3 instances are needed for the cluster to be able to withstand up to one server failure.

<Cluster:RPI_Cluster>

 MySQL  cluster-rpi1:3306 ssl  Py > cluster = dba.get_cluster()
 MySQL  cluster-rpi1:3306 ssl  Py > cluster.add_instance('root@cluster-rpi2:3306')
A new instance will be added to the InnoDB cluster. Depending on the amount of data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@cluster-rpi2:3306': ****
Adding instance to the cluster ...

Validating instance at cluster-rpi2:3306...

This instance reports its own address as cluster-rpi2

Instance configuration is suitable.

The instance 'root@cluster-rpi2:3306' was successfully added to the cluster.

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.add_instance('root@cluster-rpi3:3306')
A new instance will be added to the InnoDB cluster. Depending on the amount of data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@cluster-rpi3:3306': ****
Adding instance to the cluster ...

Validating instance at cluster-rpi3:3306...

This instance reports its own address as cluster-rpi3

Instance configuration is suitable.
The instance 'root@cluster-rpi3:3306' was successfully added to the cluster.

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.add_instance('root@cluster-rpi4:3306')
A new instance will be added to the InnoDB cluster. Depending on the amount of data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@cluster-rpi4:3306': ****
Adding instance to the cluster ...

Validating instance at cluster-rpi4:3306...

This instance reports its own address as cluster-rpi4

Instance configuration is suitable.

The instance 'root@cluster-rpi4:3306' was successfully added to the cluster.

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.status()
{
    "clusterName": "RPI_Cluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "cluster-rpi1:3306",
        "ssl": "REQUIRED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "cluster-rpi1:3306": {
                "address": "cluster-rpi1:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }, 

            "cluster-rpi2:3306": {
                "address": "cluster-rpi2:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi3:3306": {
                "address": "cluster-rpi3:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi4:3306": {
                "address": "cluster-rpi4:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }
        }

    },
    "groupInformationSourceMember": "mysql://root@cluster-rpi1:3306"
}

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.describe()
{
    "clusterName": "RPI_Cluster",
    "defaultReplicaSet": {
        "name": "default",
        "topology": [
            {
                "address": "cluster-rpi1:3306",
                "label": "cluster-rpi1:3306",
                "role": "HA"
            },
            {
                "address": "cluster-rpi2:3306",
                "label": "cluster-rpi2:3306",
                "role": "HA"
            },
            {
                "address": "cluster-rpi3:3306",
                "label": "cluster-rpi3:3306",
                "role": "HA"
            },

            {
                "address": "cluster-rpi4:3306",
                "label": "cluster-rpi4:3306",
                "role": "HA"
            }
        ]
    }
}

Listing 9-4Reestablishing the Cluster after Dissolving

从元数据错误中恢复

如果您的服务器关机,比如说,您在一天结束时将它们全部关闭,那么当第二天重新开始工作时,您可能会遇到以下错误(或类似的错误)。这是因为 InnoDB 集群(以及任何高可用性系统)不是为关闭和重启而设计的。您可以关闭一些服务器进行维护,但是您应该始终保持至少三台服务器运行。尽管 InnoDB 集群将继续运行,但在至少三台服务器添加到集群之前,日志中会出现警告和潜在错误。因此,如果您只有三个实例,并且必须让其中一个脱机进行维护,那么您可以。但是,您应该考虑在维护任务期间向集群添加另一个实例,以确保您拥有最少数量的实例。

 MySQL  localhost:33060+ ssl  Py > cluster = dba.get_cluster('RPI_Cluster')
Traceback (most recent call last):
  File "<string>", line 1, in <module>
SystemError: RuntimeError: Dba.get_cluster: This function is not available through a session to a standalone instance (metadata exists, but GR is not active)

下面演示了如何从完全中断中恢复集群。具体来说,所有服务器都已重新启动(关闭电源并重新启动),您需要从最后一个已知良好的位置重新启动集群。我们将使用dba.reboot_cluster_from_complete_outage()方法重启集群。首先,登录到其中一个服务器,运行如下命令:

 MySQL  localhost:33060+ ssl  Py > cluster = dba.reboot_cluster_from_complete_outage('RPI_Cluster')
Reconfiguring the cluster 'RPI_Cluster' from complete outage...

The instance '192.168.42.244:3306' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The instance '192.168.42.243:3306' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The instance '192.168.42.242:3306' was part of the cluster configuration.
Would you like to rejoin it to the cluster? [y/N]: y

The cluster was successfully rebooted.

如果成功,您将看到消息,指示集群已重新启动。

最后手段

如果在尝试恢复集群后,您仍然有问题,并希望从头开始,您可以通过在每台服务器上发出以下命令来实现。这是一个相当激进的步骤,将删除所有元数据并重新启动组复制。但是,您应该将此作为最后的手段,因为它会中断停机事件期间发生的任何事务的正常流程(不太可能,但有可能)。

dba.drop_metadata_schema()
RESET SLAVE
RESET MASTER

第一个命令删除 InnoDB 集群元数据,有效地将服务器从集群中删除。接下来的两个 SQL 命令重置服务器上的复制。回想一下,我们可以通过连接到机器的 MySQL Shell 运行这些命令,但是我们必须对第一个命令使用 Python 模式(\py),对最后两个命令使用 SQL 模式(\sql)。

警告

这是一项非常危险的任务,建议仅作为最后手段使用。

在所有服务器上输入这些命令后,您可以通过创建集群并添加实例来重新开始。同样,这是最后一招,不推荐用于任何生产系统。

现在,让我们看看管理 MySQL InnoDB 集群的一些最佳实践。

最佳实践

现在,您已经看到了修复集群可能需要的常见维护任务以及一些故障排除任务的列表,让我们讨论几个管理 InnoDB 集群的关键最佳实践。本节中列出的实践既不完整,也不是您应该为 InnoDB 集群考虑的唯一实践。相反,这里列出的实践旨在提醒您应该采用的关键实践。此外,这些实践假设您也在为任何高可用性系统采用典型的最佳实践。

我们从一些对某些人来说似乎平凡但有价值的事情开始,包括对 InnoDB 集群或高可用性解决方案的新手。除了您在组织中开发的最佳实践之外,以下部分还包含您应该考虑的最佳实践。

没有关机和重启

高可用性系统不是为定期关闭和重启而设计的。事实上,AdminAPI 中没有“关闭”命令。此语句指的是整个集群,而不是单个实例;您可能需要让实例脱机进行维护或修复,但是集群不应该关闭(唯一的例外是实验或开发试验)。您应该规划您的 InnoDB 集群安装,这样您就不必经常关闭或重启它。

在进行更改之前,请备份您的数据

对于拥有值为 2 的数据的任何系统的任何管理员来说,一个主要的工具是在进行任何更改之前保存数据的能力。这里选择的工具是备份和恢复实用程序。幸运的是,MySQL 在社区版中附带了两个这样的工具:mysqlpumpmysqldump。这些是逻辑备份工具,允许您使用 SQL 语句创建文件,以便从一个或多个数据库中恢复数据。mysqlpump实用程序是mysqldump的替代品,工作得更好,特别是对于大型数据库,因为mysqlpump可以执行并行转储。但是,这两种方法都可以在本演示中使用。

如前所述,这些是逐行制作数据副本的逻辑备份实用程序,而不是制作数据二进制副本的二进制备份。因此,逻辑备份往往比二进制备份更慢,占用的空间也更多。二进制备份通常还允许服务器在短时间内锁定数据,允许在服务器在线并接受更改时进行备份(有时称为热备份)。逻辑备份通常需要在备份期间锁定数据库。对于本演示,我们没有大型数据集,也不担心在线备份操作,因此可以毫无问题地使用逻辑备份。

MySQL 的企业级备份

如果您计划在您的企业中使用 MySQL,尤其是如果您计划部署 InnoDB 集群,您应该考虑企业版。虽然它是 Oracle 的付费产品,但它包含了许多企业级工具,包括 MySQL 企业备份(MEB)。

MEB 是一个完整的二进制备份工具,允许提供 MySQL 服务器的备份和恢复所需的所有备份方式。MEB 可以执行完整备份、增量备份、加密备份、压缩备份等等。如果您有兴趣了解更多关于 MEB 的信息,请参阅在线 MySQL 企业备份参考手册 https://dev.mysql.com/doc/mysql-enterprise-backup/8.0/en/

在本节中,您将了解如何在执行任何维护操作之前对我们的数据进行逻辑备份。这对于允许您从禁用数据或以某种方式危及数据(或数据本身)访问的更改中恢复是必不可少的。一个好的系统管理员应该总是在进行任何维护之前进行备份。在确认更改是正确的之后,如果空间有问题,您可以随时删除备份文件。 3

如果集群处于活动状态,让我们首先备份我们创建的数据,以确保不会丢失。因为我们只有少量的数据,所以我们将使用名为mysqlpump的简单的逻辑备份工具。让我们从应用服务器开始工作。

下面显示了一个命令,您可以使用该命令对示例数据库执行逻辑备份,并将其存储在应用服务器上。我们必须提供用户连接信息、主机和端口。我们还提供了一个选项来生成位于文件顶部的DROP DATABASETABLE命令,这有助于通过首先删除数据来恢复数据。我们还添加了一个选项,将GTID_PURGED变量设置为OFF,这样我们就不会捕获 GTID 信息。回想一下,InnoDB 集群使用组复制,但是我们使用 InnoDB 集群和 AdminAPI 来管理集群,所以我们不需要 GTID 信息。我们将把这个文件命名为backup_pre_changes.sql。它被命名为.sql文件,因为mysqlpump创建 SQL 语句来保存模式和数据。

$ mysqlpump -uroot -p -h localhost --port=3306 --add-drop-database --add-drop-table --set-gtid-purged=OFF shopping > backup_pre_changes.sql

当您执行这个命令时,看起来什么也不会发生,因为我们将所有输出重定向到了该文件。这是因为该实用程序将 SQL 命令打印到标准输出。如果您感到好奇,请打开该文件并查看 SQL 命令。清单 9-5 是文件应该包含的内容的示例摘录。

-- Dump created by MySQL pump utility, version: 8.0.11, Linux (armv7l)
-- Dump start time: Fri Jun  1 15:23:00 2018
-- Server version: 8.0.11

SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE;
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
SET @@SESSION.SQL_LOG_BIN= 0;
SET @OLD_TIME_ZONE=@@TIME_ZONE;
SET TIME_ZONE='+00:00';
SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT;
SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS;
SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION;
SET NAMES utf8mb4;
SET @@GLOBAL.GTID_PURGED=/*!80000 '+'*/ '16cce4a6-53d1-11e8-badc-b827eb2bc4f3:1-2,
2d550a88-5453-11e8-9517-b827eb2bc4f3:1-27,
4127c169-520f-11e8-a834-b827ebcb9200:1-170,
4b2d0de2-5454-11e8-9517-b827eb2bc4f3:1-3,
de5cbffd-5455-11e8-85a7-b827eb2bc4f3:1-119:1000102,
f1cdcfd9-62a2-11e8-9e50-b827eb2bc4f3:1-37,
fc2bf2b7-53ca-11e8-a131-b827eb2bc4f3:1-2';
DROP DATABASE IF EXISTS `shopping`;
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `shopping` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */;
DROP TABLE IF EXISTS `shopping`.`list`;
CREATE TABLE `shopping`.`list` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`description` char(64) DEFAULT NULL,
`note` char(64) DEFAULT NULL,
`purchased` int(11) DEFAULT '0',
PRIMARY KEY (`rowid`)

) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
;
INSERT INTO `shopping`.`list` VALUES (1,"Milk","2%",1),(2,"Bread",”,0),(3,"Eggs","Free range",0),(4,"Cheese","Cheddar",0),(5,"Bagels",”,0);
Dump progress: 1/1 tables, 0/5 rows
mysqlpump: [WARNING] (3719) 'utf8' is currently an alias for the character set UTF8MB3, which will be replaced by UTF8MB4 in a future release. Please consider using UTF8MB4 in order to be unambiguous.
mysqlpump: [WARNING] (3719) 'utf8' is currently an alias for the character set UTF8MB3, which will be replaced by UTF8MB4 in a future release. Please consider using UTF8MB4 in order to be unambiguous.
SET TIME_ZONE=@OLD_TIME_ZONE;
SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT;
SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS;
SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
SET SQL_MODE=@OLD_SQL_MODE;
-- Dump end time: Fri Jun  1 15:23:11 2018
Dump completed in 10798

Listing 9-5Example Logical Backup (mysqlpump)

如果您需要恢复数据,您可以使用 MySQL Shell 通过在 SQL 模式下使用\source命令来读取文件,如清单 9-6 所示。但是,除非您不小心删除了数据,否则您不需要这样做。此外,为了恢复 InnoDB 集群中的数据,您应该在读/写服务器连接上运行该命令(6446)。

pi@cluster-rpi1:~ $ mysqlsh root@cluster-rpi1:3306 --sql
Creating a session to 'root@cluster-rpi1:3306'
Enter password: ****
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 93
Server version: 8.0.11 MySQL Community Server (GPL)
No default schema selected; type \use <schema> to set one.
MySQL Shell 8.0.11

Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type '\help' or '\?' for help; '\quit' to exit.

 MySQL  cluster-rpi1:3306 ssl  SQL > \source shopping_logical_backup.sql
Query OK, 0 rows affected (0.0022 sec)
Query OK, 0 rows affected (0.0018 sec)
Query OK, 0 rows affected (0.0017 sec)
Query OK, 0 rows affected (0.0017 sec)
Query OK, 0 rows affected (0.0016 sec)
Query OK, 0 rows affected (0.0017 sec)
Query OK, 0 rows affected (0.0019 sec)
Query OK, 0 rows affected (0.0017 sec)
Query OK, 0 rows affected (0.0017 sec)
Query OK, 0 rows affected (0.0017 sec)
Query OK, 0 rows affected (0.0019 sec)
Query OK, 1 row affected (0.2923 sec)
Query OK, 1 row affected (0.0121 sec)
Query OK, 0 rows affected, 1 warning (0.0083 sec)
Note (code 1051): Unknown table 'shopping.list'
Query OK, 0 rows affected (0.1067 sec)
Query OK, 5 rows affected (0.0187 sec)

Records: 5  Duplicates: 0  Warnings: 0
Query OK, 0 rows affected (0.0013 sec)
Query OK, 0 rows affected (0.0017 sec)
Query OK, 0 rows affected (0.0018 sec)
Query OK, 0 rows affected (0.0017 sec)
Query OK, 0 rows affected (0.0018 sec)
Query OK, 0 rows affected (0.0017 sec)
Query OK, 0 rows affected (0.0015 sec)

Listing 9-6Restoring a Logical Backup (mysqlsh)

小费

在对集群进行重大更改之前,请务必备份您的数据。

思考群体,而不是独立的服务器

学习如何部署和管理 InnoDB 集群的最大障碍之一是将集群视为一组相似的服务器,而不是一组单独的服务器。对于那些来自传统 MySQL 主/从复制的人来说,这是特别重要的一课。过去,人们通常认为拓扑中的服务器是必须单独使用的个体。这是因为我们有通过中继日志处理来自单个主服务器的更改的机器,而这些服务器(从服务器)从不互相通信。

但是,InnoDB Cluster(由于其组复制根)将集群中的机器视为一个组,其中一台(或多台)充当读/写服务器。事实上,管理哪台机器扮演哪种角色是自动的,所以我们不需要知道哪台服务器扮演哪种角色。MySQL 路由的加入为我们处理了集群中发生的变化。

在使用 InnoDB 集群时,我们必须将集群视为基础设施的高可用性组件,而不是一组松散耦合的服务器。这将允许您将 InnoDB Cluster 本身视为一个数据存储,这正是它应该成为的。

虽然硬件维护仍然是基于一台机器进行的,但是可以从集群中的任何机器或者不在集群中的另一台机器上使用 InnoDB 集群。这使得事情比旧形式的 MySQL 复制容易得多,使开发者能够专注于应用,而不是这种高可用性数据存储的细节。

保护 InnoDB 集群

最后一个最佳实践应该是任何人首先想到的—安全性!鉴于来自组织内外的威胁不断增加,提高安全性必须成为任何项目的首要目标。InnoDB 集群也不例外。

开发者不仅应该考虑保护他们的应用,还应该考虑保护幕后的数据和服务。事实上,所有使用互联网的解决方案都必须开发更好的安全实践。高可用性解决方案的独特方面使得计划和实施严格的安全实践变得特别困难,因为存在多个漏洞点。更具体地说,每个组件可能有不同类型的漏洞,从对设备的物理访问到对服务的远程攻击。

最近一连串的大规模数据泄露证明了他们的安全性不够。我们已经看到了从直接盗窃到利用从知名企业(如 Target)和政府机构(如美国人事管理办公室)窃取的数据(超过 4000 万个信用卡号可能已被泄露)的一切。有趣的是,违规的源头可以追溯到第三方承包商和服务。显然,没有人是安全的。我们需要一个革命性的步骤,而不是改进行之有效的机制。

可悲的是,在保护我们的解决方案方面,我们能走的路是有限的。任何信息技术(IT)专业人士都会告诉您,应用最好的、严格的密码策略和严密的安全措施会迫使用户危及旨在保护他们及其数据的策略。例如,考虑要求密码为 16 个或更多字符的密码策略,其中至少有四个大写字母、六个数字、三个特殊字符,并且没有英语词典单词;每 60 天过期一次;并且与以前的密码相同的字符不超过七个。在这种情况下,一些用户将被迫写下他们的密码,因为他们记不住字母、大写字母、数字和特殊字符的随机组合。

然而,让密码更难猜测或破解只是一种策略。事实上,关于如何正确保护系统存在各种不同的理念。尽管对所有技术的深入讨论超出了本书的范围,但是考虑如何提高 InnoDB 集群的安全性是很重要的。

那么,我们该怎么办?我们是否实施了良好的实践来确保系统不被轻易破坏,或者我们是否为了易用性而冒降低安全性的风险?底线是,您必须选择最能满足保护数据和服务需求的安全解决方案,而不强迫用户忍受繁重的实践,也不使他们的生活变得困难。

InnoDB Cluster 旨在与 MySQL 中最新的安全改进一起工作,包括新的身份验证插件( https://dev.mysql.com/doc/refman/8.0/en/caching-sha2-pluggable-authentication.html ),使其比 MySQL 的任何早期版本都更安全。然而,我们可以做更多的事情来保护我们的数据。幸运的是,MySQL 通过启用 SSL 连接来加密集群和客户端之间的通信(是的,甚至通过路由),使这一点变得很容易。

保护 InnoDB 集群有两个主要方面或工具。首先,我们应该使用 SSL 连接。其次,我们可以添加一个服务器白名单,只允许某些服务器访问集群。

SSL 连接

默认情况下,在 MySQL 8 的 GA 版本中,MySQL 服务器启用了 SSL 连接。如果您使用的是 MySQL 8.0.11 或更高版本,那么集群中服务器之间的连接已经配置了 SSL。但是,如果您加载了不同的身份验证插件,或者正在使用较旧但兼容的 MySQL 5.7 服务器,您可能需要考虑启用 SSL 连接。

小费

MySQL 可以配置为只使用安全连接。在 MySQL 中设置 SSL 的完整教程可以在在线参考手册的“使用加密连接”一节中找到。

通过在任何服务器上发出清单 9-7 中所示的查询,您可以快速确定您的服务器是否正在使用 SSL。注意,HAVE_SSL变量被设置为YES,, SSL 选项也相应地被填充,只有最少的一组选项。

SHOW VARIABLES LIKE '%ssl%';
+---------------------------------------------------+-----------------+
| Variable_name                                     | Value           |
+---------------------------------------------------+-----------------+
| group_replication_recovery_ssl_ca                 |                 |
| group_replication_recovery_ssl_capath             |                 |
| group_replication_recovery_ssl_cert               |                 |
| group_replication_recovery_ssl_cipher             |                 |
| group_replication_recovery_ssl_crl                |                 |
| group_replication_recovery_ssl_crlpath            |                 |
| group_replication_recovery_ssl_key                |                 |
| group_replication_recovery_ssl_verify_server_cert | OFF             |
| group_replication_recovery_use_ssl                | ON              |
| group_replication_ssl_mode                        | REQUIRED        |
| have_openssl                                      | YES             |

| have_ssl                                          | YES             |

| mysqlx_ssl_ca                                     |                 |
| mysqlx_ssl_capath                                 |                 |
| mysqlx_ssl_cert                                   |                 |
| mysqlx_ssl_cipher                                 |                 |
| mysqlx_ssl_crl                                    |                 |
| mysqlx_ssl_crlpath                                |                 |
| mysqlx_ssl_key                                    |                 |
| ssl_ca                                            | ca.pem          |
| ssl_capath                                        |                 |
| ssl_cert                                          | server-cert.pem |
| ssl_cipher                                        |                 |
| ssl_crl                                           |                 |
| ssl_crlpath                                       |                 |
| ssl_fips_mode                                     | OFF             |
| ssl_key                                           | server-key.pem  |
+---------------------------------------------------+-----------------+
27 rows in set (0.0415 sec)

Listing 9-7Displaying the SSL Variables

我们告诉 InnoDB Cluster 使用 SSL 的方式是在dba.create_cluster()方法期间向集群传递一个选项,就像我们设置其他元数据一样。在这种情况下,我们必须将memberSslMode键设置为以下值之一。如您所见,默认情况下在创建时使用 SSL,因此按照本书中的演示创建集群将确保集群配置为 SSL 连接。

  • AUTO:(默认)如果服务器实例支持 SSL 加密,则自动启用,如果服务器不支持,则禁用。

  • REQUIRED:集群中的种子实例启用了 SSL 加密。如果无法启用,则会引发错误。

  • DISABLED:确保对集群中的种子实例禁用 SSL 加密。

类似地,当我们使用cluster.add_instance()方法添加实例或使用cluster.rejoin_instance()方法允许实例重新加入集群时,实例上的 SSL 加密会根据种子实例(我们创建集群的第一个实例)的设置启用或禁用。但是,您可以对这些方法使用相同的memberSslMode选项来控制 SSL 连接,如下所示。正如您所看到的,默认情况下对实例使用 SSL,就像创建集群一样。

  • AUTO:(默认)SSL 加密根据种子实例(集群的其他成员)使用的设置和实例本身提供的可用 SSL 支持自动启用或禁用。

  • REQUIRED:强制为集群中的实例启用 SSL 加密。

  • DISABLED:确保对集群中的实例禁用 SSL 加密。

对于已经配置了组复制的 InnoDB 集群的部署,我们可以在创建集群时使用adoptFromGR键告诉 InnoDB 集群采用现有组的 SSL 设置。这样做时,所采用的集群上的 SSL 设置不会改变。

注意

memberSslMode键不能与adoptFromGR键一起使用。

服务器白名单

正如前面“创建实例白名单”一节中提到的,您可以创建一个服务器白名单,限制对某些服务器的访问,这样就没有其他服务器可以连接或加入集群。如果您有一个大型基础架构,并且希望将对集群的访问隔离到一组服务器,以实现划分或类似的组织目标,这是一个很好的选择。

回想一下,我们可以通过向create_cluster()add_instance()rejoin_instance()方法传递一个特殊的参数来显式地创建白名单。因此,在配置服务器之前,必须考虑使用白名单。但是,如果您想为一个现有的集群建立一个白名单,您可能需要删除该实例,然后将其添加回集群,传递元数据以将其添加到白名单中。

小费

如果计划使用白名单,应该在创建集群和添加实例时设置白名单选项。

示例:需要 SSL 连接

这些选项最安全的形式是将memberSslMode设置为REQUIRED。让我们看一个简短的例子来说明如何做到这一点。与此同时,我们还将创建一个服务器白名单,以进一步保护集群。回想一下,我们通过将ipWhitelist设置为子网 192.168.42 来实现这一点。否则,我们发出创建集群和添加实例的普通命令,但是我们将以下选项传递给这些方法。在本节中,您将看到使用该选项的演示。

{'memberSslMode': 'REQUIRED', 'ipWhitelist': '192.168.42.0/24'}

这些选项允许我们通过始终要求 SSL 连接和限制对指定子网上的那些服务器的访问来使我们的集群更加安全。如果集群正在运行(并且已经进行了备份),请继续解散集群,如下所示。执行此操作之前,请务必进行备份,以免意外丢失任何数据。

MySQL  cluster-rpi1:3306 ssl  Py > cluster.dissolve({'force':True})
The cluster was successfully dissolved.
Replication was disabled but user data was left intact.

集群解散后,您可以关闭所有服务器的 MySQL 实例。回想一下,我们可以通过 MySQL Shell 使用SHUTDOWN SQL 命令关闭 MySQL,如下所示。记得使用--sql选项!在集群中的所有计算机上运行以下命令:

$ mysqlsh root@cluster-rpi1:3306 --sql -e "SHUTDOWN"

接下来,如果没有在所有服务器上配置 SSL,我们应该现在就设置它。再说一遍,如果你用的是 MySQL 8.0.11 或者更高版本,那就已经这样了。如果不确定,可以运行以下命令来确定每台服务器上是否启用了 SSL:

SHOW VARIABLES LIKE 'have_ssl';

好了,现在我们准备好了。我们将这个示例集群命名为RPI_Cluster_SSL,以表明它需要 SSL 连接。我们发出以下命令来创建集群并添加所有实例:

cluster = dba.create_cluster('RPI_Cluster_SSL', {'memberSslMode':'REQUIRED', 'ipWhitelist':'192.168.42.0/24'})
cluster.add_instance('root@cluster-rpi2:3306', {'memberSslMode':'REQUIRED', 'ipWhitelist':'192.168.42.0/24'})
cluster.add_instance('root@cluster-rpi3:3306', {'memberSslMode':'REQUIRED', 'ipWhitelist':'192.168.42.0/24'})
cluster.add_instance('root@cluster-rpi4:3306', {'memberSslMode':'REQUIRED', 'ipWhitelist':'192.168.42.0/24'})

请注意,白名单值是 192.168.42.0/24,它允许从 192.168.42.1 到 192.168.42.255 的连接。如果想要更多的限制,可以用更宽的口罩。如果你不确定如何计算,你可以使用在线子网计算器,比如在 www.subnet-calculator.com 找到的那种。

让我们看一个演示。清单 9-8 展示了一个设置集群的例子,这个集群需要 SSL,并且已经指定了一个白名单。因为您已经看到了这些命令的运行,所以我们将跳过对这些命令的详细解释,而将注意力集中在清单末尾的状态报告上。

MySQL  cluster-rpi1:3306 ssl  Py > cluster = dba.create_cluster('RPI_Cluster_SSL', {'memberSslMode':'REQUIRED', 'ipWhitelist':'192.168.42.0/24'})
A new InnoDB cluster will be created on instance 'root@cluster-rpi1:3306'.

Validating instance at cluster-rpi1:3306...

This instance reports its own address as cluster-rpi1

Instance configuration is suitable.
Creating InnoDB cluster 'RPI_Cluster_SSL' on 'root@cluster-rpi1:3306'...
Adding Seed Instance...

Cluster successfully created. Use Cluster.add_instance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.add_instance('root@cluster-rpi2:3306', {'memberSslMode':'REQUIRED', 'ipWhitelist':'192.168.42.0/24'})
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@cluster-rpi2:3306': ****
Adding instance to the cluster ...

Validating instance at cluster-rpi2:3306...

This instance reports its own address as cluster-rpi2

Instance configuration is suitable.
The instance 'root@cluster-rpi2:3306' was successfully added to the cluster.

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.add_instance('root@cluster-rpi3:3306', {'memberSslMode':'REQUIRED', 'ipWhitelist':'192.168.42.0/24'})
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@cluster-rpi3:3306': ****
Adding instance to the cluster ...

Validating instance at cluster-rpi3:3306...

This instance reports its own address as cluster-rpi3

Instance configuration is suitable.
The instance 'root@cluster-rpi3:3306' was successfully added to the cluster.

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.add_instance('root@cluster-rpi4:3306', {'memberSslMode':'REQUIRED', 'ipWhitelist':'192.168.42.0/24'})
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@cluster-rpi4:3306': ****
Adding instance to the cluster ...

Validating instance at cluster-rpi4:3306...

This instance reports its own address as cluster-rpi4

Instance configuration is suitable.
The instance 'root@cluster-rpi4:3306' was successfully added to the cluster.

 MySQL  cluster-rpi1:3306 ssl  Py > cluster.status()
{
    "clusterName": "RPI_Cluster_SSL",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "cluster-rpi1:3306",
        "ssl": "REQUIRED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "cluster-rpi1:3306": {
                "address": "cluster-rpi1:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi2:3306": {
                "address": "cluster-rpi2:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi3:3306": {
                "address": "cluster-rpi3:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi4:3306": {
                "address": "cluster-rpi4:3306",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }
        }
    },
    "groupInformationSourceMember": "mysql://root@cluster-rpi1:3306"
}

Listing 9-8Example Cluster Setup with SSL = Required and Whitelist Defined

请注意,在状态报告中,我们看到 SSL 模式是必需的(这是默认的)。我们可以通过查询 SSL 选项来确认这一点:

MySQL  cluster-rpi1:3306 ssl  mysql_innodb_cluster_metadata  SQL > SHOW VARIABLES LIKE '%ssl%';
+---------------------------------------------------+-----------------+
| Variable_name                                     | Value           |
+---------------------------------------------------+-----------------+
| group_replication_recovery_ssl_ca                 |                 |
| group_replication_recovery_ssl_capath             |                 |
| group_replication_recovery_ssl_cert               |                 |
| group_replication_recovery_ssl_cipher             |                 |
| group_replication_recovery_ssl_crl                |                 |
| group_replication_recovery_ssl_crlpath            |                 |
| group_replication_recovery_ssl_key                |                 |
| group_replication_recovery_ssl_verify_server_cert | OFF             |
| group_replication_recovery_use_ssl                | ON              |
| group_replication_ssl_mode                        | REQUIRED        |
| have_openssl                                      | YES             |
| have_ssl                                          | YES             |
| mysqlx_ssl_ca                                     |                 |
| mysqlx_ssl_capath                                 |                 |
| mysqlx_ssl_cert                                   |                 |
| mysqlx_ssl_cipher                                 |                 |
| mysqlx_ssl_crl                                    |                 |
| mysqlx_ssl_crlpath                                |                 |
| mysqlx_ssl_key                                    |                 |
| ssl_ca                                            | ca.pem          |
| ssl_capath                                        |                 |
| ssl_cert                                          | server-cert.pem |
| ssl_cipher                                        |                 |
| ssl_crl                                           |                 |
| ssl_crlpath                                       |                 |
| ssl_fips_mode                                     | OFF             |
| ssl_key                                           | server-key.pem  |
+---------------------------------------------------+-----------------+
27 rows in set (0.0391 sec)

那么,白名单呢?我们可以通过检查组复制的变量来查看白名单,如清单 9-9 所示。我们可以看到白名单以及组复制的所有其他变量,所有这些都是 InnoDB Cluster 为我们设置的。

MySQL  cluster-rpi1:3306 ssl  mysql_innodb_cluster_metadata  Py > \sql
Switching to SQL mode... Commands end with ;

 MySQL  cluster-rpi1:3306 ssl  mysql_innodb_cluster_metadata  SQL > SHOW VARIABLES LIKE '%ssl%';
+---------------------------------------------------+-----------------+
| Variable_name                                     | Value           |
+---------------------------------------------------+-----------------+
| group_replication_recovery_ssl_ca                 |                 |
| group_replication_recovery_ssl_capath             |                 |
| group_replication_recovery_ssl_cert               |                 |
| group_replication_recovery_ssl_cipher             |                 |
| group_replication_recovery_ssl_crl                |                 |
| group_replication_recovery_ssl_crlpath            |                 |
| group_replication_recovery_ssl_key                |                 |
| group_replication_recovery_ssl_verify_server_cert | OFF             |
| group_replication_recovery_use_ssl                | ON              |
| group_replication_ssl_mode                        | REQUIRED        |
| have_openssl                                      | YES             |
| have_ssl                                          | YES             |
| mysqlx_ssl_ca                                     |                 |
| mysqlx_ssl_capath                                 |                 |
| mysqlx_ssl_cert                                   |                 |
| mysqlx_ssl_cipher                                 |                 |
| mysqlx_ssl_crl                                    |                 |
| mysqlx_ssl_crlpath                                |                 |
| mysqlx_ssl_key                                    |                 |
| ssl_ca                                            | ca.pem          |
| ssl_capath                                        |                 |
| ssl_cert                                          | server-cert.pem |
| ssl_cipher                                        |                 |
| ssl_crl                                           |                 |
| ssl_crlpath                                       |                 |
| ssl_fips_mode                                     | OFF             |
| ssl_key                                           | server-key.pem  |
+---------------------------------------------------+-----------------+
27 rows in set (0.0391 sec)

 MySQL  cluster-rpi1:3306 ssl  mysql_innodb_cluster_metadata  SQL > SHOW VARIABLES LIKE '%white%';
+--------------------------------+-----------------+
| Variable_name                  | Value           |
+--------------------------------+-----------------+
| group_replication_ip_whitelist | 192.168.42.0/24 |
+--------------------------------+-----------------+
1 row in set (0.0383 sec)

 MySQL  cluster-rpi1:3306 ssl  SQL > SHOW VARIABLES LIKE 'group_replication%';
+-----------------------------------------------------+-------------------+
| Variable_name                                       | Value             |
+-----------------------------------------------------+-------------------+
| group_replication_allow_local_lower_version_join    | OFF               |
| group_replication_auto_increment_increment          | 7                 |
| group_replication_bootstrap_group                   | OFF               |
| group_replication_communication_debug_options       | GCS_DEBUG_NONE    |
| group_replication_components_stop_timeout           | 31536000          |
| group_replication_compression_threshold             | 1000000           |
| group_replication_enforce_update_everywhere_checks  | OFF               |
| group_replication_flow_control_applier_threshold    | 25000             |
| group_replication_flow_control_certifier_threshold  | 25000             |
| group_replication_flow_control_hold_percent         | 10                |
| group_replication_flow_control_max_quota            | 0                 |
| group_replication_flow_control_member_quota_percent | 0                 |
| group_replication_flow_control_min_quota            | 0                 |
| group_replication_flow_control_min_recovery_quota   | 0                 |
| group_replication_flow_control_mode                 | QUOTA             |
| group_replication_flow_control_period               | 1                 |
| group_replication_flow_control_release_percent      | 50                |
| group_replication_force_members                     |                   |
| group_replication_group_name                        | a587a3ad-7002-11e8-8612-b827eb2bc4f3  |
| group_replication_group_seeds                       |                   |
| group_replication_gtid_assignment_block_size        | 1000000           |
| group_replication_ip_whitelist                      | 192.168.42.0/24   |
| group_replication_local_address                     |                   |
| group_replication_member_weight                     | 50                |
| group_replication_poll_spin_loops                   | 0                 |
| group_replication_recovery_complete_at              | TRANSACTIONS_APPLIED           |
| group_replication_recovery_get_public_key           | ON                |
| group_replication_recovery_public_key_path          |                   |
| group_replication_recovery_reconnect_interval       | 60                |
| group_replication_recovery_retry_count              | 10                |
| group_replication_recovery_ssl_ca                   |                   |
| group_replication_recovery_ssl_capath               |                   |
| group_replication_recovery_ssl_cert                 |                   |
| group_replication_recovery_ssl_cipher               |                   |
| group_replication_recovery_ssl_crl                  |                   |
| group_replication_recovery_ssl_crlpath              |                   |
| group_replication_recovery_ssl_key                  |                   |
| group_replication_recovery_ssl_verify_server_cert   | OFF               |
| group_replication_recovery_use_ssl                  | OFF               |
| group_replication_single_primary_mode               | ON                |
| group_replication_ssl_mode                          | DISABLED          |
| group_replication_start_on_boot                     | OFF               |
| group_replication_transaction_size_limit            | 150000000         |
| group_replication_unreachable_majority_timeout      | 0                 |
+-----------------------------------------------------+-------------------+
44 rows in set (0.0340 sec)

Listing 9-9Displaying the Group Replication Variables

保护路由

既然我们已经为 SSL 和白名单配置了 MySQL 服务器,您可能想知道路由需要做些什么。我们如何知道我们有从应用到路由的 SSL 连接?

因为我们使用的是 MySQL 8.0.11 或更高版本,我们知道我们默认使用 SSL 连接,即使设置为需要 SSL 连接。因为我们更改了集群的名称,所以我们只需要做一个更改:我们必须编辑路由配置文件以连接到正确的集群。打开路由配置文件(例如,/usr/local/mysql/lib/mysql-router/mysqlrouter.conf)并更改清单 9-10 中突出显示的集群名称。

# File automatically generated during MySQL Router bootstrap
[DEFAULT]
user=pi
logging_folder=/usr/local/mysql/lib/mysql-router/log
runtime_folder=/usr/local/mysql/lib/mysql-router/run
data_folder=/usr/local/mysql/lib/mysql-router/data
plugin_folder=/usr/local/mysql/lib/mysqlrouter
keyring_path=/usr/local/mysql/lib/mysql-router/data/keyring
master_key_path=/usr/local/mysql/lib/mysql-router/mysqlrouter.key
connect_timeout=30
read_timeout=30

[logger]
level = INFO

[metadata_cache:RPI_Cluster_SSL]

router_id=4
bootstrap_server_addresses=mysql://cluster-rpi1:3306,mysql://cluster-rpi2:3306,mysql://cluster-rpi3:3306,mysql://c
luster-rpi4:3306
user=mysql_router4_gnl6peiy2z2v

metadata_cluster=RPI_Cluster_SSL

ttl=5

[routing:RPI_Cluster_SSL_default_rw]

bind_address=0.0.0.0
bind_port=6446

destinations=metadata-cache://RPI_Cluster_SSL/default?role=PRIMARY

routing_strategy=round-robin
protocol=classic

[routing:RPI_Cluster_SSL_default_ro]

bind_address=0.0.0.0
bind_port=6447

destinations=metadata-cache://RPI_Cluster_SSL/default?role=SECONDARY

routing_strategy=round-robin
protocol=classic

[routing:RPI_Cluster_SSL_default_x_rw]

bind_address=0.0.0.0
bind_port=64460

destinations=metadata-cache://RPI_Cluster_SSL/default?role=PRIMARY

routing_strategy=round-robin
protocol=x

[routing:RPI_Cluster_SSL_default_x_ro]

bind_address=0.0.0.0
bind_port=64470

destinations=metadata-cache://RPI_Cluster_SSL/default?role=SECONDARY

routing_strategy=round-robin
protocol=x

Listing 9-10Change the Router Configuration File

请注意,我们将所有出现的RPI_Cluster更改为RPI_Cluster_SSL。您可以执行搜索并替换,或者,如果您想要创建新的路由配置文件,您可以使用 Bootstrap 选项并指定新的配置文件,如下所示。注意使用了--directory选项来指定存储配置的新目录,使用了--name选项来指定配置的新名称。

$ sudo /usr/local/mysql/bin/mysqlrouter --bootstrap root@cluster-rpi4:3306 \
   --directory=/usr/local/mysql/lib/mysql-router-ssl/mysqlrouter.conf --user=pi \
   --name=router_rpi_ssl

在我们做出这些更改之后,我们可以通过使用配置文件(修改过的或者新的)重启路由,并尝试我们的测试代码,如清单 9-11 所示。您可以将该文件保存为router_connect_test.py

import mysql.connector

# Simple function to display results from a cursor
def show_results(cur_obj):
  for row in cur_obj:
    print(row)

my_cfg = {
  'user':'root',
  'passwd':'secret',
  'host':'127.0.0.1',
  'port':6446
}

# Connecting to the server
conn = mysql.connector.connect(**my_cfg)

print("Listing the databases on the server.")
query = "SHOW DATABASES"
cur = conn.cursor()
cur.execute(query)
show_results(cur)

print("\nRetrieve the port for the server to which we're connecting.")
query = "SELECT @@port"
cur = conn.cursor()
cur.execute(query)
show_results(cur)

# Close the cursor and connection
cur.close()
conn.close()

Listing 9-11
Router Test Script

现在,我们运行 Python 脚本,应该会看到如下所示的输出。您可能认为代码会失败,但是在我们的集群中,客户端连接没有任何变化。因为我们使用的是 MySQL 8.0.11,SSL 连接在默认情况下是打开的。

$ python3 ./router_connect_test.py
('information_schema',)
('mysql',)
('mysql_innodb_cluster_metadata',)
('performance_schema',)
('shopping',)
('sys',)

就这样!我们刚刚通过启用 SSL 连接和使用白名单使我们的开发 InnoDB 集群更加安全。

摘要

对一个系统的真正考验是随着时间的推移它能被维护得多好。这种成功的一个重要因素是完成常规任务所需任务的复杂性。执行维护任务越容易,保持系统健康就越容易。同样,对于如何从导致需要修复或排除故障的错误或情况中恢复,有明确的任务也很重要。幸运的是,AdminAPI 拥有我们保持 InnoDB 集群良好运行所需的工具。

在本章中,您回顾了最常见的管理任务,查看了一组故障排除任务,并发现了一些与管理 InnoDB Cluster 相关的最佳实践。

下一章通过讨论部署 InnoDB Cluster 的注意事项以及为使用 MySQL 早期版本的用户准备迁移到 MySQL 8.0 来结束对 InnoDB Cluster 的介绍。

十、规划您的部署

这本书涵盖了很多内容,包括高可用性对您的意义以及 MySQL 如何满足这些需求的简要概述。我们重点介绍了 MySQL InnoDB 集群及其所有组件:InnoDB 存储引擎、组复制、AdminAPI、MySQL Shell 和 MySQL 路由。不仅如此,我们还通过使用沙箱和一组运行在不同机器上的 MySQL 服务器,完成了开发高可用性应用以及设置 InnoDB 集群的过程。

您还看到了一个独特的替代平台来试验 InnoDB 集群:Raspberry Pi。回想一下,您了解到尽管 Raspberry Pi 受限于相对较小的内存和较慢的处理器,但 MySQL 在该平台上运行得相当好——好到我们可以用 InnoDB Cluster 进行试验,而无需在一堆服务器硬件上花费大量资金。

然而,现在是时候了解如何在企业中采用 MySQL 8 和 MySQL InnoDB 集群了。本章接下来介绍规划集群部署的注意事项和技术。因为许多读者可能不熟悉 MySQL 8,所以我们也包括了如何计划在您的企业中采用 MySQL 8 的信息。

让我们首先简要讨论一些规划集群部署的策略。

规划您的集群

如果您想取得成功,系统管理和规划是不可分割的,在规划高可用性解决方案时更是如此。我们必须提前计划设置、配置和部署解决方案,否则将面临意外问题和延迟的危险。InnoDB Cluster 也不例外,您最好计划一下在您的环境中使用它的方式。

本节中的信息将帮助您深入了解特定于 InnoDB 集群的规划领域。您应该将本节视为一个额外的资源,与您已建立的策略、实践和工具一起用于规划系统。下面列出了 InnoDB 集群需要仔细规划的一些关键领域:

  • 为 InnoDB 集群管理员创建一个单独的用户

  • 配置主机名

  • 考虑您想要的元数据选项

  • 在 MySQL Shell 和 AdminAPI 中使用日志进行调试

  • 考虑读/写服务器的数量

  • 考虑只读服务器的数量

  • 规划物理部署

  • 从组复制部署规划升级

在本书的整个过程中,我们已经讨论了这些主题中的大部分,但是有些可能对您来说是新的,尤其是如果您没有大量部署 MySQL 无论是使用复制还是类似的配置。

以下部分通过提供建议(在某些情况下,提供示例)来阐述这些方面,以便您可以评估是否将它们纳入您的 InnoDB 集群计划中。不要认为这些是强制性的,而是强烈推荐的。

用户帐户

前一章介绍了使用 SSL 和服务器白名单来保护 InnoDB 集群。没有讨论的是用于管理 InnoDB 集群的用户帐户。到目前为止,我们使用的用户帐户是 root 帐户。尽管您可以继续有效地使用该帐户,但更安全的选择是为该任务指定一个特定的用户帐户。有些人可能不愿意这样做,因为这需要记住另一个用户帐户和密码,但这被认为比使用 root 帐户更安全,因为 root 帐户通常由多个人使用,执行各种与 MySQL 相关的操作。为 InnoDB Cluster 使用一个单独的帐户意味着潜在的更少的人会使用它来做其他事情。

用于管理的用户帐户除了拥有完整的 MySQL 管理员权限(与 root 相同:SUPERGRANT OPTIONCREATEDROP等等)之外,还必须拥有对 InnoDB Cluster 元数据数据库表的完全读写权限。).您可以使用CREATE USERGRANT SQL命令手动创建用户,如下所示:

CREATE USER idc_admin@'%' IDENTIFIED BY 'secret';
GRANT ALL ON mysql_innodb_cluster_metadata.* TO idc_admin@'%' WITH GRANT OPTION;

然而,首选方法是将clusterAdminclusterAdminPassword选项与dba.configure_instance()dba.configure_local_instance()cluster.add_instance()方法一起使用:

dba.configure_instance('root@localhost:3306', {'clusterAdmin': "idc_admin@'%'", 'clusterAdminPassword': 'secret'})

如果需要监控集群性能和元数据,可以使用更严格的权限创建一个只读帐户。以下示例创建用户并授予只读权限:

CREATE USER idc_mon@'%' IDENTIFIED BY 'secret';
GRANT SELECT ON mysql_innodb_cluster_metadata.* TO idc_mon@'%';
GRANT SELECT ON performance_schema.global_status TO idc_mon@'%';
GRANT SELECT ON performance_schema.replication_applier_configuration TO idc_mon@'%';
GRANT SELECT ON performance_schema.replication_applier_status TO idc_mon@'%';
GRANT SELECT ON performance_schema.replication_applier_status_by_coordinator TO idc_mon@'%';
GRANT SELECT ON performance_schema.replication_applier_status_by_worker TO idc_mon@'%';
GRANT SELECT ON performance_schema.replication_connection_configuration TO idc_mon@'%';
GRANT SELECT ON performance_schema.replication_connection_status TO idc_mon@'%';

GRANT SELECT ON performance_schema.replication_group_member_stats TO idc_mon@'%';
GRANT SELECT ON performance_schema.replication_group_members TO idc_mon@'%';
GRANT SELECT ON performance_schema.threads TO idc_mon@'%' WITH GRANT OPTION;

主机名

虽然选择主机名看起来不像是您想要计划的事情,但是考虑一下第 7 章中的示例部署,它使用了诸如cluster-rpi1cluster-rpi2之类的主机名。选择这些主机名是为了帮助识别群集中使用的服务器。在这种情况下,主机名被选为模拟集群的名称。但是,您可以使用带有前缀或后缀的集群名称来帮助进一步识别服务器。底线是,建议选择一个帮助您管理服务器的主机名。除此之外,主机名只是一个字符串,在系统或网络上定义主机名的范围内,您可以使用任何名称。

我们已经知道,集群中的每台机器都必须有自己的主机名。我们还看到了一种分配主机名和使用/etc/hosts文件来管理没有域名系统(DNS)的访问的技术。更具体地说,每台机器都必须能够解析集群中其他服务器的主机名。但是,如果您的网络上有 DNS,您可以通过使用该服务来分配主机名并相应地映射它们。

另外,记得为集群中的每个服务器配置report_host服务器选项。InnoDB 集群将使用它向集群报告主机名,并将其存储在元数据(和配置文件)中。该值应该与机器的主机名相匹配。在前面章节的演示中,您也看到了如何做到这一点。

[计]元数据

你在第 9 章中发现了如何改变元数据。当我们以 JSON 键/值对集的形式创建集群时,我们可以设置某些选项。以下是dba.create_cluster()方法中可用选项的列表。回想一下,我们还提供了集群的名称作为该方法的一个单独的参数。确保为集群选择一个描述性的名称,并且在您的基础架构中是唯一的。

  • multiMaster:如果True,用多个可写实例定义一个 InnoDB 集群。

  • force:如果True,确认必须应用multiMaster选项。

  • adoptFromGR:如果True,基于现有的复制组创建 InnoDB 集群。

  • memberSslMode:用于配置集群成员的 SSL 模式。

  • ipWhitelist:允许连接到实例进行组复制的主机列表。

  • clearReadOnly:如果True,确认super_read_only必须禁用。

  • groupName:要使用的组复制组名 UUID,而不是自动生成的名称。

  • localAddress:要使用的组复制本地地址,而不是自动生成的地址。

  • groupSeeds:要使用的组复制对等地址的逗号分隔列表,而不是自动生成的列表。

规划群集时,您应该查看此列表,看看是否需要设置这些选项中的任何一个。您已经看到了如何使用这些选项来设置 SSL 模式和建立白名单。如果您要从组复制部署进行迁移,您可能需要考虑其中的一些选项。我们将在后面的部分讨论从组复制迁移。我们还将在下一节讨论多主机选项。

记录

如果您做过任何形式的系统管理或问题诊断,包括调试代码,那么您应该熟悉日志和日志记录。MySQL Shell 提供了强大的调试级日志功能,可以帮助您管理集群。这在使用服务器实例时尤其有用。要在 MySQL Shell 中打开日志记录,请使用如下的–log-level选项:

$ mysqlsh root@cluster-rpi1:3306 --log-level=DEBUG3

DEBUG3值是最详细的选项,建议用于诊断。如果您想默认不进行额外的日志记录或使用不同的级别,您可以随时重新启动 shell。有关日志记录的更多详细信息,请参见 MySQL Shell 在线参考。

您还可以通过将dba.verbose成员变量设置为下列值之一来增加 AdminAPI 的详细程度(日志记录):

  • 0OFF:(默认)提供最小输出,是推荐的等级

  • 1ON:将每个调用的详细输出添加到 AdminAPI 中

  • 2:显示附加调试输出以及详细输出(developer-level)

清单 10-1 展示了一个使用带有日志记录和 verbosity 成员变量的 shell 的例子,以及您可以看到的输出。在这种情况下,我们使用级别 2,这是最详细的选项。该示例显示我们需要向集群中重新添加一个实例:一台机器出现了问题,需要修复,包括重新安装 MySQL。这个清单相当长,因为我们已经打开了最大详细度,但是它让我们深入了解了 shell 和 AdminAPI 中发生的许多事情。为了简洁起见,已经排除了一些更普通的部分。请花些时间仔细阅读这个列表。它揭示了很多关于add_instance()方法的工作方式。

 MySQL  cluster-rpi1:3306 ssl  Py > dba.verbose=2
 MySQL  cluster-rpi1:3306 ssl  Py > cluster = dba.get_cluster()
 MySQL  cluster-rpi1:3306 ssl  Py > cluster.add_instance('root@cluster-rpi2:3306')
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Please provide the password for 'root@cluster-rpi2:3306': ****
Adding instance to the cluster ...

Validating instance at cluster-rpi2:3306...

This instance reports its own address as cluster-rpi2
DBA: mysqlprovision: Executing printf '[{"server":{"host":"cluster-rpi2","passwd":"****","password":"****","port":3306,"scheme":"mysql","user":"root"},"verbose":2}]\n.\n' | /usr/local/mysql/bin/mysqlsh --log-level=8 --py -f /usr/local/mysql/share/mysqlsh/mysqlprovision.zip check
=========================== MySQL Provision Output ===========================

DEBUG: MySQL query: SHOW VARIABLES LIKE 'READ_ONLY'
Running check command.
DEBUG: MySQL query: SELECT GROUP_NAME FROM performance_schema.replication_connection_status where CHANNEL_NAME = 'group_replication_applier'
Checking Group Replication prerequisites.
DEBUG: The server: 'cluster-rpi2:3306' has been set to check
DEBUG: Option checking started: {'log_slave_updates': {'ONE OF': ('ON', '1')}, 'binlog_format': {'ONE OF': ('ROW',)}, 'relay_log_info_repository': {'ONE OF': ('TABLE',)}, 'binlog_checksum': {'ONE OF': ('NONE',)}, 'report_port': {'ONE OF': ('3306',)}, 'enforce_gtid_consistency': {'ONE OF': ('ON', '1')}, 'master_info_repository': {'ONE OF': ('TABLE',)}, 'log_bin': {'ONE OF': ('1', 'ON')}, 'gtid_mode': {'ONE OF': ('ON',)}, 'transaction_write_set_extraction': {'ONE OF': ('XXHASH64', '2', 'MURMUR32', '1')}}
DEBUG: Checking option: 'log_slave_updates'
DEBUG: MySQL query: SELECT @@log_slave_updates
DEBUG: Option current value: '1'
DEBUG: OK: value 1 is one of ('ON', '1')
DEBUG: Checking option: 'binlog_format'
DEBUG: MySQL query: SELECT @@binlog_format
DEBUG: Option current value: 'ROW'
DEBUG: OK: value ROW is one of ('ROW',)
...
DEBUG: MySQL query: SHOW VARIABLES LIKE 'VERSION'
DEBUG: Server version: [8, 0, 11]
DEBUG: Server version check result: True
* Checking server version... PASS
Server is 8.0.11
...
DEBUG: MySQL query: show plugins
* Verifying Group Replication plugin for server 'cluster-rpi2:3306' ...
DEBUG: MySQL query: show plugins
WARNING: The group_replication plugin has not been installed/loaded in 'cluster-rpi2:3306'
Group Replication plugin: Not loaded
DEBUG: MySQL query: SELECT PLUGIN_NAME, PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE 'group_replication%'
DEBUG: Plugin group_replication is not installed
===========================================================================

Instance configuration is suitable.
DBA: mysqlprovision: Executing printf '[{"group_seeds":"cluster-rpi3:33061,cluster-rpi4:33061,cluster-rpi1:33061","rep_user_passwd":"****","replication_user":"mysql_innodb_cluster_r0000124900","ssl_mode":"REQUIRED","verbose":2},{"host":"cluster-rpi2","passwd":"****","password":"****","port":3306,"user":"root"},{"host":"cluster-rpi1","passwd":"****","port":3306,"user":"root"}]\n.\n' | /usr/local/mysql/bin/mysqlsh --log-level=8 --py -f /usr/local/mysql/share/mysqlsh/mysqlprovision.zip join-replicaset
=========================== MySQL Provision Output ===========================
DEBUG: MySQL query: SHOW VARIABLES LIKE 'READ_ONLY'

Running join command on 'cluster-rpi2:3306'.
Checking Group Replication prerequisites.
DEBUG: MySQL query: SHOW VARIABLES LIKE 'READ_ONLY'
DEBUG: MySQL query: SELECT GROUP_NAME FROM performance_schema.replication_connection_status where CHANNEL_NAME = 'group_replication_applier'
DEBUG: MySQL query: SELECT MEMBER_STATE FROM performance_schema.replication_group_members as m JOIN performance_schema.replication_group_member_stats as s on m.MEMBER_ID = s.MEMBER_ID AND m.MEMBER_ID = @@server_uuid
DEBUG: MySQL query: SELECT @@have_ssl
DEBUG: MySQL query: SELECT @@group_replication_recovery_use_ssl
DEBUG: MySQL query: SELECT @@group_replication_ssl_mode
DEBUG: ->parse_server_address
  host: %
  address_type: host like
DEBUG: Using replication_user: mysql_innodb_cluster_r0000124900@'%'
DEBUG: ->rpl_user_dict {'replication_user': "mysql_innodb_cluster_r0000124900@'%'", 'rep_user_passwd': '******', 'host': '%', 'recovery_user': 'mysql_innodb_cluster_r0000124900', 'ssl_mode': u'REQUIRED'}
DEBUG: MySQL query: select MEMBER_HOST, MEMBER_PORT from performance_schema.replication_group_members
DEBUG: MySQL query: SHOW VARIABLES LIKE 'READ_ONLY'
DEBUG: MySQL query: SHOW VARIABLES LIKE 'READ_ONLY'
DEBUG: MySQL query: SHOW VARIABLES LIKE 'READ_ONLY'
DEBUG: The server: 'cluster-rpi2:3306' has been set to check
DEBUG: Option checking started: {'log_slave_updates': {'ONE OF': ('ON', '1')}, 'binlog_format': {'ONE OF': ('ROW',)}, 'relay_log_info_repository': {'ONE OF': ('TABLE',)}, 'binlog_checksum': {'ONE OF': ('NONE',)}, 'report_port': {'ONE OF': ('3306',)}, 'enforce_gtid_consistency': {'ONE OF': ('ON', '1')}, 'master_info_repository': {'ONE OF': ('TABLE',)}, 'log_bin': {'ONE OF': ('1', 'ON')}, 'gtid_mode': {'ONE OF': ('ON',)}, 'transaction_write_set_extraction': {'ONE OF': ('XXHASH64', '2', 'MURMUR32', '1')}}
DEBUG: Checking option: 'log_slave_updates'
DEBUG: MySQL query: SELECT @@log_slave_updates
DEBUG: Option current value: '1'
DEBUG: OK: value 1 is one of ('ON', '1')
DEBUG: Checking option: 'binlog_format'
DEBUG: MySQL query: SELECT @@binlog_format
DEBUG: Option current value: 'ROW'
DEBUG: OK: value ROW is one of ('ROW',)
DEBUG: Checking option: 'relay_log_info_repository'
DEBUG: MySQL query: SELECT @@relay_log_info_repository
DEBUG: Option current value: 'TABLE'
DEBUG: OK: value TABLE is one of ('TABLE',)
DEBUG: Checking option: 'binlog_checksum'
DEBUG: MySQL query: SELECT @@binlog_checksum
DEBUG: Option current value: 'NONE'
DEBUG: OK: value NONE is one of ('NONE',)
DEBUG: Checking option: 'report_port'
DEBUG: MySQL query: SELECT @@report_port
DEBUG: Option current value: '3306'
DEBUG: OK: value 3306 is one of ('3306',)
DEBUG: Checking option: 'enforce_gtid_consistency'
DEBUG: MySQL query: SELECT @@enforce_gtid_consistency
DEBUG: Option current value: 'ON'
DEBUG: OK: value ON is one of ('ON', '1')
DEBUG: Checking option: 'master_info_repository'
DEBUG: MySQL query: SELECT @@master_info_repository
DEBUG: Option current value: 'TABLE'
DEBUG: OK: value TABLE is one of ('TABLE',)
DEBUG: Checking option: 'log_bin'
DEBUG: MySQL query: SELECT @@log_bin
DEBUG: Option current value: '1'
DEBUG: OK: value 1 is one of ('1', 'ON')
DEBUG: Checking option: 'gtid_mode'
DEBUG: MySQL query: SELECT @@gtid_mode
DEBUG: Option current value: 'ON'
DEBUG: OK: value ON is one of ('ON',)
DEBUG: Checking option: 'transaction_write_set_extraction'
DEBUG: MySQL query: SELECT @@transaction_write_set_extraction
DEBUG: Option current value: 'XXHASH64'
DEBUG: OK: value XXHASH64 is one of ('XXHASH64', '2', 'MURMUR32', '1')
DEBUG: Options check result: True
* Comparing options compatibility with Group Replication... PASS
Server configuration is compliant with the requirements.
DEBUG: Checking option: 'transaction_write_set_extraction'
DEBUG: MySQL query: SELECT @@global.transaction_write_set_extraction
DEBUG: MySQL query: SELECT @@global.transaction_write_set_extraction
DEBUG: expected value: XXHASH64 found
* Comparing options compatibility with the group of the given peer-instance... PASS
Server configuration is compliant with current group configuration.
Option name                      Required Value   Current Value    Result
-------------------------------  ---------------  ---------------  -----
transaction_write_set_extraction  XXHASH64         XXHASH64         PASS
DEBUG: Server version checking: 5.7.17
DEBUG: MySQL query: SHOW VARIABLES LIKE 'VERSION'
DEBUG: Server version: [8, 0, 11]
DEBUG: Server version check result: True
* Checking server version... PASS
Server is 8.0.11

DEBUG: checking server id uniqueness
DEBUG: MySQL query: SELECT @@server_id
DEBUG: server id = 102
DEBUG: MySQL query: SELECT variable_source FROM performance_schema.variables_info WHERE variable_name="server_id"
DEBUG: MySQL query: SELECT @@server_id
DEBUG: Verifying the peer 'cluster-rpi3:3306' ...
DEBUG: The peer 'cluster-rpi3:3306' have a different server_id  103
DEBUG: MySQL query: SELECT @@server_id
DEBUG: Verifying the peer 'cluster-rpi4:3306' ...
DEBUG: The peer 'cluster-rpi4:3306' have a different server_id  104
DEBUG: MySQL query: SELECT @@server_id
DEBUG: Verifying the peer 'cluster-rpi1:3306' ...
DEBUG: The peer 'cluster-rpi1:3306' have a different server_id  101
* Checking that server_id is unique... PASS
The server_id is valid.

DEBUG: MySQL query: SELECT @@slave_parallel_workers
* Checking compatibility of Multi-Threaded Slave settings... PASS
Multi-Threaded Slave settings are compatible with Group Replication.

DEBUG: MySQL query: show plugins
* Verifying Group Replication plugin for server 'cluster-rpi2:3306' ...
DEBUG: MySQL query: SELECT @@global.super_read_only
Initializing group_replication plugin on 'cluster-rpi2:3306'
DEBUG: MySQL query: show plugins
DEBUG: MySQL query: SELECT @@version_compile_os
DEBUG: MySQL query: INSTALL PLUGIN group_replication SONAME 'group_replication.so'
DEBUG: The group_replication plugin has been successfully install in server: 'cluster-rpi2:3306'
DEBUG: MySQL query: SELECT PLUGIN_NAME, PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE 'group_replication%'
DEBUG: Plugin group_replication has state: ACTIVE and not the expected: DISABLED
WARNING: Not running locally on the server and can not access its error log.
DEBUG: MySQL query: SELECT MEMBER_STATE FROM performance_schema.replication_group_members as m JOIN performance_schema.replication_group_member_stats as s on m.MEMBER_ID = s.MEMBER_ID AND m.MEMBER_ID = @@server_uuid
DEBUG: local_address to use: cluster-rpi2:33061
DEBUG: MySQL query: show plugins
DEBUG: MySQL query: SELECT @@global.group_replication_local_address
DEBUG: MySQL query: SELECT @@group_replication_single_primary_mode
DEBUG: MySQL query: SELECT @@server_id
DEBUG: Trying to retrieve group replication name from peer server.
DEBUG: MySQL query: show plugins
DEBUG: MySQL query: SELECT GROUP_NAME FROM performance_schema.replication_connection_status WHERE CHANNEL_NAME="group_replication_applier"
DEBUG: Retrieved group replication name from peer server: dc7cb30b-701a-11e8-bc94-b827eb2bc4f3.
Joining Group Replication group: dc7cb30b-701a-11e8-bc94-b827eb2bc4f3
DEBUG: Setting Group Replication variables
DEBUG:   group_replication_group_seeds = cluster-rpi3:33061,cluster-rpi4:33061,cluster-rpi1:33061
DEBUG: MySQL query: SET SQL_LOG_BIN=0
DEBUG: MySQL query: SET PERSIST group_replication_group_seeds = ?, params (u'cluster-rpi3:33061,cluster-rpi4:33061,cluster-rpi1:33061',)
DEBUG: MySQL query: SET SQL_LOG_BIN=1
DEBUG:   group_replication_single_primary_mode = 'ON'
DEBUG: MySQL query: SET SQL_LOG_BIN=0
DEBUG: MySQL query: SET PERSIST group_replication_single_primary_mode = 'ON'
DEBUG: MySQL query: SET SQL_LOG_BIN=1
DEBUG:   group_replication_group_name = dc7cb30b-701a-11e8-bc94-b827eb2bc4f3
DEBUG: MySQL query: SET SQL_LOG_BIN=0
DEBUG: MySQL query: SET PERSIST group_replication_group_name = ?, params ('dc7cb30b-701a-11e8-bc94-b827eb2bc4f3',)
DEBUG: MySQL query: SET SQL_LOG_BIN=1
DEBUG:   group_replication_recovery_use_ssl = 'ON'
DEBUG: MySQL query: SET SQL_LOG_BIN=0
DEBUG: MySQL query: SET PERSIST group_replication_recovery_use_ssl = 'ON'
DEBUG: MySQL query: SET SQL_LOG_BIN=1
DEBUG:   auto_increment_offset = 2
DEBUG: MySQL query: SET SQL_LOG_BIN=0
DEBUG: MySQL query: SET PERSIST auto_increment_offset = ?, params (2,)
DEBUG: MySQL query: SET SQL_LOG_BIN=1
DEBUG:   group_replication_ssl_mode = 'REQUIRED'
DEBUG: MySQL query: SET SQL_LOG_BIN=0
DEBUG: MySQL query: SET PERSIST group_replication_ssl_mode = 'REQUIRED'
DEBUG: MySQL query: SET SQL_LOG_BIN=1
DEBUG:   group_replication_start_on_boot = ON
DEBUG: MySQL query: SET SQL_LOG_BIN=0
DEBUG: MySQL query: SET PERSIST group_replication_start_on_boot = ?, params ('ON',)
DEBUG: MySQL query: SET SQL_LOG_BIN=1
DEBUG:   group_replication_local_address = 'cluster-rpi2:33061'
DEBUG: MySQL query: SET SQL_LOG_BIN=0
DEBUG: MySQL query: SET PERSIST group_replication_local_address = 'cluster-rpi2:33061'
DEBUG: MySQL query: SET SQL_LOG_BIN=1
DEBUG:   auto_increment_increment = 1
DEBUG: MySQL query: SET SQL_LOG_BIN=0
DEBUG: MySQL query: SET PERSIST auto_increment_increment = ?, params (1,)
DEBUG: MySQL query: SET SQL_LOG_BIN=1
* Running change master command
DEBUG: MySQL query: SET SQL_LOG_BIN=0
DEBUG: MySQL query: CHANGE MASTER TO MASTER_USER = /*(*/ 'mysql_innodb_cluster_r0000124900' /*)*/, MASTER_PASSWORD = /*(*/ '******' /*)*/ FOR CHANNEL 'group_replication_recovery';
DEBUG: MySQL query: SET SQL_LOG_BIN=1
Attempting to join to Group Replication group...
DEBUG:
DEBUG: * Starting Group Replication plugin...
DEBUG: MySQL query: START group_replication
Server 'cluster-rpi2:3306' joined Group Replication group dc7cb30b-701a-11e8-bc94-b827eb2bc4f3.
===========================================================================
The instance 'root@cluster-rpi2:3306' was successfully added to the cluster.

Listing 10-1Using the Verbosity Setting in MySQL Shell

请注意来自 MySQL 供应脚本的许多语句。这是一个隐藏在 shell 中的特殊脚本,用于为您与服务器进行交互。如你所见,很多事情需要去做。如果您更深入地查看该列表,您还会看到关于配置组复制的陈述。现在,如果这还不能让您相信 InnoDB 集群更简单,那就更好了!

服务器部署

我们还没有谈到的一个领域是选择集群中应该有多少台服务器,以及应该如何在您的基础架构中部署它们。本节介绍了在确定您应该开始使用的主(读/写)和辅助(只读)服务器数量时的注意事项,以及物理和网络选择的注意事项。

主服务器

InnoDB 集群的大多数部署都基于默认的单主模式,在任何给定的时间,集群中都有一个主(读/写)服务器。然而,另一种称为多主服务器的模式为组中的多个读/写服务器提供支持。我们使用多主模式进行写入扩展,即将写入划分到两个或更多主模式,以提高写入性能。

在 InnoDB 集群中设置多主节点并没有很好的文档记录,但它是受支持的。联机参考手册的“组复制”一章中包含了大多数多主服务器文档。然而,InnoDB 集群关于多主服务器的文档指出,创建集群时所有服务器都作为读/写服务器参与,这可能不是您想要的。

如果您希望让服务器的子集作为读/写服务器参与,您应该考虑首先使用该配置设置组复制,然后将该配置应用到新集群中。请参阅下一节,了解如何采用“从组复制升级”到 InnoDB 集群。

要在 InnoDB 集群中启用多主模式,我们必须在创建集群时使用multiMasterforce选项,如下所示。同样,这将使所有服务器都能作为读/写服务器参与进来。

dba.create_cluster('RPI_Cluster_MP', {'multiMaster':True, 'force': True})

让我们来看看实际情况。清单 10-2 展示了在多主模式下创建 InnoDB 集群的摘录。请注意,所有服务器都列为读/写服务器。

 MySQL  cluster-rpi1:3306 ssl  Py > cluster = dba.create_cluster('RPI_Cluster_MP', {'multiMaster':True, 'force': True})
...
 MySQL  cluster-rpi1:3306 ssl  Py > cluster.add_instance('root@cluster-rpi4:3306')
...
 MySQL  cluster-rpi1:3306 ssl  Py > cluster.add_instance('root@cluster-rpi3:3306')
...
 MySQL  cluster-rpi1:3306 ssl  Py > cluster.add_instance('root@cluster-rpi2:3306')
...
 MySQL  cluster-rpi1:3306 ssl  Py > cluster.status()
{
    "clusterName": "RPI_Cluster_MP",
    "defaultReplicaSet": {
        "name": "default",
        "ssl": "REQUIRED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "cluster-rpi1:3306": {
                "address": "cluster-rpi1:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi2:3306": {
                "address": "cluster-rpi2:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi3:3306": {
                "address": "cluster-rpi3:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "cluster-rpi4:3306": {
                "address": "cluster-rpi4:3306",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }

        }
    },
    "groupInformationSourceMember": "mysql://root@cluster-rpi1:3306"
}

Listing 10-2Creating a Multi-Primary Mode Cluster

当您为多主模式集群引导路由时,您会得到一些有趣的结果。以下示例使用多主模式集群引导路由:

$ sudo /usr/local/mysql/bin/mysqlrouter --bootstrap root:root@cluster-rpi4:3306 --user=pi --directory=/usr/local/mysql/lib/test_mp --name=test_mp

Bootstrapping MySQL Router instance at '/usr/local/mysql/lib/test_mp'...
Module “ not registered with logger - logging the following message as 'main' instead
MySQL Router 'test_mp' has now been configured for the InnoDB cluster 'RPI_Cluster_MP' (multi-master).
The following connection information can be used to connect to the cluster.
Classic MySQL protocol connections to cluster 'RPI_Cluster_MP':

- Read/Write Connections: localhost:6446

X protocol connections to cluster 'RPI_Cluster_MP':

- Read/Write Connections: localhost:64460

与单主模式不同,我们只有一个用于读/写连接的端口,没有用于读连接的端口。同样,使用多基色是一个特殊的用例。InnoDB 集群部署的默认和推荐模式是单主模式。

小费

有关在 InnoDB 集群中使用多主模式的更多信息,请参见在线参考手册的“多主模式”一节,该节从组复制的角度描述了多主模式的工作原理。

辅助服务器

集群中需要的辅助(只读)服务器的初始数量被定义为您希望集群能够容忍的不可恢复故障数量的函数。服务器数量(S)等于您想要容忍的故障数量(f)的 2 倍,加上 1:

S = 2f + 1 因此,如果我们希望我们的集群能够容忍最多三个故障,我们应该在集群中至少有七个服务器。

小费

请参阅“如何计算一个组可以处理的故障数量?”第 3 章的侧边栏中有关于该公式的更多解释。

对于读取扩展,该功能是一个良好的开端,但是您可能需要添加更多的只读服务器来满足需求。需要多少将取决于各种因素,这些因素可以用下面的简化公式来概括: 1

AverageLoad = (∑ReadLoad + ∑WriteLoad) / ∑Capacity

服务器上的平均负载是读取负载加上写入负载的总和除以最大容量。这是因为在执行读请求时,每个只读服务器不仅必须满足读请求,还必须满足来自读/写(主)服务器的所有写请求。

让我们考虑一个例子。假设您有一个辅助服务器,通过试验或推荐(目标),它可以每秒处理 20,000 个事务,而没有明显的性能损失。假设集群有一个稳定的平均写入负载,每秒 5,000 个事务,一个只读服务器有一个平均读取负载,每秒 10,000 个读取(读写比为 2:1)。使用前面的公式,我们发现服务器只有 75%的容量,对于大多数应用来说,这是一个安全的操作余量:

(10,000 + 5,000) / 20,000 = 0.75 = 75%

但是,考虑到当有更多读取或写入请求时,平均负载可能会有峰值时间。发生这种情况时,可能会达到容量极限,您可能会遇到服务器之间的延迟。为了防止这种情况,您应该规划只读服务器的数量,使负载保持在 50–75%之间。您可以根据您的应用经历需求高峰期的频率(或是否)来调整该阈值。

那么,这对 InnoDB 集群有什么帮助呢?回想一下,默认情况下,路由循环访问只读服务器,在它们之间分配读取请求。如果我们考虑分配读取的循环算法,我们可以将公式修改如下:

AverageLoad = ((∑ReadLoad/NumSecondaries) + ∑WriteLoad) / ∑Capacity

在这里,我们可以在一组辅助节点上分配读取负载,从而更准确地估计集群的读取容量。回到我们的例子,如果我们有四个辅助服务器,那么只读服务器的平均负载要小得多:

((10,000 / 4) + 5,000) / 20,000 = 0.375 = 37.5%

因此,集群可以安全地处理只读服务器上平均 10,000 次读取。如果您监控平均向群集发出的读取请求的数量,您可以使用该公式来确定平均负载何时大于您选择的阈值,当出现这种情况时,您应该添加更多的只读服务器。

小费

有关读取横向扩展规划的精彩解释,请参见查尔斯·贝尔等人(O'Reilly,2014)在 MySQL 高可用性第二版中的“MySQL 复制横向扩展”。

物理部署

很少考虑参与 InnoDB 集群的服务器计算机的物理位置。最简单的方法是将服务器放在指定的气候控制实验室中,让管理这些实验室的服务员来决定和管理。尽管这对大多数人来说都很好,但你可能需要考虑某些方面。

首先要考虑硬件配置。任何形式的高可用性系统的最佳实践是为所有参与的服务器选择相似的硬件。虽然单个读/写(主)实例确实可能需要更多内存,且可以从更快的处理器中获益,但是这种说法并不适用于 InnoDB 集群,因为集群可以随时决定选择新的主实例。您应该计划使群集中的所有服务器具有相同的硬件配置。

此外,将机器放置在彼此靠近的位置,无论是在同一个机架中,作为同一个刀片服务器阵列的一部分,还是在同一个机架上,都有助于使机器的硬件维修更加容易。如果一台机器坏了,你可以用为该组购买的现有备件来修理它;您可能只需要一套备件。此外,如果你采用备有备用机器的做法(机器没有通电,但准备好交换),你可以这样做,而不必在实验室里拖着硬件到处跑。

网络注意事项

实验室中需要考虑的另一个方面是群集中服务器的网络配置。建议服务器位于自己的子网上,并切换以限制到群集的流量。与集群通信的路由应该能够通过高速连接访问该子网(或者它们也可以在该子网中)。如果这是您想要探索的一个选项,请咨询网络专家,以确保您拥有所需的设备,或者您获得了正确的设备来正确工作。

另一方面,如果您实验室的网络足够快,并且没有网络延迟,您可能要考虑以后再实施。不过,首先规划子网可能有助于避免将子网改造到集群部署中所需的停机时间。

从组复制升级

可能需要仔细规划的最后一个领域是通过使用现有的组复制部署来部署 InnoDB 集群。因为 InnoDB Cluster 使用组复制,所以这应该是一件容易的事情,对吗?答案是这很简单,但是只有当您创建集群时,将名为adoptFromGR的特殊选项设置为True,并使用dba.create_cluster()方法指定。但是,您应该了解三个注意事项。

首先,如果您的组复制拓扑包括 MyISAM(或其他存储引擎)表,您必须将它们转换为 InnoDB。这可以通过ALTER TABLE SQL 命令来完成。

其次,新的 InnoDB 集群将使用组复制模式进行配置。如果组复制是单主复制,InnoDB 集群也应该是单主复制。唯一的问题可能是如果你想改变模式。在这种情况下,从现有的组复制拓扑创建集群时,不可能指定multiMaster选项。

第三,如果任何实例的super_read_only设置为ON,那么当实例被添加到集群时,AdminAPI 会将其设置为OFF

让我们来看一个组复制拓扑,它作为单独的实例在一台机器上运行(不是在沙箱中)。清单 10-3 显示了在性能模式数据库上使用两个查询的现有组复制拓扑的示例。第一个选项选择组成员,第二个选项标识组中的主要成员。

> SELECT * FROM performance_schema.replication_group_members \G
*************************** 1\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: ab44d8c1-70c0-11e8-9776-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24801
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: PRIMARY
MEMBER_VERSION: 8.0.13
*************************** 2\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: b1bf7839-70c0-11e8-b7ee-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24802
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.13
*************************** 3\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: b83fcc48-70c0-11e8-9d84-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24803
  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.13
*************************** 4\. row ***************************
  CHANNEL_NAME: group_replication_applier
     MEMBER_ID: be81348f-70c0-11e8-8ce4-d4258b76e981
   MEMBER_HOST: oracle-pc
   MEMBER_PORT: 24804

  MEMBER_STATE: ONLINE
   MEMBER_ROLE: SECONDARY
MEMBER_VERSION: 8.0.13

> SELECT member_id, member_host, member_port FROM performance_schema.global_status JOIN performance_schema.replication_group_members ON VARIABLE_VALUE=member_id WHERE VARIABLE_NAME="group_replication_primary_member";
+--------------------------------------+-------------+-------------+
| member_id                            | member_host | member_port |
+--------------------------------------+-------------+-------------+
| ab44d8c1-70c0-11e8-9776-d4258b76e981 | oracle-pc   |       24801 |
+--------------------------------------+-------------+-------------+

Listing 10-3Existing Group Replication Topology

这里的主服务器是当前运行在端口 28401 上的服务器。现在,让我们看看如何将这个组复制拓扑转换为 InnoDB 集群。因为已经设置了组复制,所以我们只需要创建集群。清单 10-4 显示了将名为GR_Cluster的集群的组复制转换为 InnoDB 集群的脚本。

MySQL  192.168.1.80:24801 ssl  Py > cluster = dba.create_cluster('GR_Cluster', {'adoptFromGR':True})
A new InnoDB cluster will be created based on the existing replication group on instance 'root@192.168.1.80:24801'.

Creating InnoDB cluster 'GR_Cluster' on 'root@192.168.1.80:24801'...
Adding Seed Instance...
Adding Instance 'oracle-pc:24802'...
Adding Instance 'oracle-pc:24803'...
Adding Instance 'oracle-pc:24804'...

Cluster successfully created based on existing replication group.

 MySQL  192.168.1.80:24801 ssl  Py > cluster.status()
{
    "clusterName": "GR_Cluster",
    "defaultReplicaSet": {
        "name": "default",
        "primary": "oracle-pc:24801",
        "ssl": "DISABLED",
        "status": "OK",
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
        "topology": {
            "oracle-pc:24801": {
                "address": "oracle-pc:24801",
                "mode": "R/W",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }, 

            "oracle-pc:24802": {
                "address": "oracle-pc:24802",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "oracle-pc:24803": {
                "address": "oracle-pc:24803",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            },
            "oracle-pc:24804": {
                "address": "oracle-pc:24804",
                "mode": "R/O",
                "readReplicas": {},
                "role": "HA",
                "status": "ONLINE"
            }
        }
    },
    "groupInformationSourceMember": "mysql://root@192.168.1.80:24801"
}

Listing 10-4Converting an Exiting Group Replication Topology to InnoDB Cluster

create_cluster()方法自动找到实例并为集群配置它们。主服务器仍然是运行在端口 24801 上的服务器。还有一点需要注意:SSL 选项的状态。它目前已被禁用。这是意料之中的,因为组中的服务器运行时没有 SSL。因此,这个示例还演示了您不需要为了在 InnoDB 集群中使用服务器而将它们转换为使用 SSL(但是为了更好的安全性,建议这样做)。

我们还可以在状态输出中看到,我们确实有一个正在工作的集群。酷!因此,您不需要放弃当前的组复制拓扑来采用 InnoDB 集群。事实上,迁移到 InnoDB 集群更容易。

小费

有关使用组复制的更多信息,请参见在线参考手册中的“组复制”一章( https://dev.mysql.com/doc/refman/8.0/en/group-replication.html )。

限制

幸运的是,使用 InnoDB 集群只有一些限制。对于规划 InnoDB 集群部署来说,可能很重要的限制包括以下几点。请注意,该列表很短,适用于非关键领域:

  • 包含多字节字符的结果有时可能不会与列输出对齐。

  • 非标准字符集可能无法在结果中正确显示。

  • AdminAPI 不支持 UNIX 套接字连接。

  • 将非沙箱服务器实例添加到沙箱中的集群可能会阻止 MySQL Shell 将配置更改保存在实例的配置文件中。

  • InnoDB 集群服务器实例不支持使用--defaults-extra-file选项来指定选项文件。

小费

参见 https://dev.mysql.com/doc/refman/8.0/en/mysql-innodb-cluster-limitations.html 了解最新的限制列表和可能的解决方法。您还应该看到“组复制限制”一节,了解可能影响 InnoDB 集群的限制。

MySQL 8 的规划

虽然这本书不是升级到 MySQL 8 的教程,但在采用 MySQL InnoDB Cluster 之前,您应该考虑一些事情,这将很可能导致升级您现有的 MySQL 服务器。

有几种方法可以学习如何进行升级。最明显和推荐的途径是阅读在线参考手册,其中包含一个关于升级 MySQL 的章节(提供您必须知道的关键信息)。但是,一些更高级的或一般的实践适用于任何形式的升级或迁移。本节介绍的升级实践将帮助您避免升级像 MySQL 这样的主要系统时遇到的一些麻烦。

本节将介绍 MySQL 的升级类型,以及规划和执行升级的一般实践。我们以关于执行升级的原因的简短讨论来结束本节。我们将在最后讨论进行升级的原因,以便您能够更好地理解升级所涉及的内容,包括隐含的风险。

让我们先来看看您可能会遇到的升级类型。

升级类型

在线参考手册和类似出版物描述了两种基本的升级方法,即如何进行升级的策略和步骤。下面是这些方法的总结:

  • 就地 : MySQL 服务器实例通过使用现有的数据字典用二进制文件升级。这种方法使用各种实用程序和工具来确保平稳过渡到新版本。

  • 逻辑:在旧安装上安装新版本之前,数据被备份,升级之后,数据被恢复。

这两个升级 MySQL 的通用策略并没有涵盖所有可能的选项。事实上,您将在后面的章节中看到另一种方法。毕竟,您的安装可能会略有不同——特别是如果您已经使用 MySQL 很长时间了,或者有许多配置了高可用性的 MySQL 服务器,或者在您自己的应用中使用第三方应用和组件。这些因素会使遵循一个给定的、通用的程序成为问题。

与其尝试扩展升级方法,不如让我们从系统管理员的角度来看一下。具体来说,如果我们有 x.y.z 版本,想升级到 a.b.c,该怎么做?以下部分描述了基于版本的升级。

警告

Oracle 建议仅升级 GA 版本。不建议升级其他版本,这可能需要额外的时间进行迁移,并接受潜在的不兼容性。升级非 GA 版本的风险由您自行承担。

MySQL 版本号术语

MySQL 使用三位数的版本号,以主要版本号、次要版本号和修订版本号的形式表示(奇怪的是,它在文档中也被称为版本)。这通常用点符号表示。例如,版本 5.7.20 将主版本定义为 5,次版本定义为 7,修订版定义为 20。通常,版本号后面是文本(在文档中称为后缀),表示额外的版本历史、稳定性或一致性——例如通用可用性(GA)、发布候选(RC)等等。关于 MySQL 中版本号的完整解释,见 https://dev.mysql.com/doc/refman/8.0/en/which-version.html

版本升级

在最简单的升级形式中,只更改修订版号。这通常被称为 x.y.z 版本号中的 z,或者简称为主版本号的版本。例如,版本 5.7.20 是修订版 20,或 5.7 的版本 20。

在此版本级别升级通常是安全的,尽管不能保证工作完美,但风险很低。但是,您仍然应该在执行升级之前阅读发行说明。如果您正在使用非公开发行(GA)版本,这一点尤其正确。如果该版本不是 GA 版本,您必须注意参考手册中的发行说明和升级部分。尽管这种情况很少发生,但有时还是存在一些特殊的考虑因素,您必须为实现升级做好准备并克服这些因素。幸运的是,Oracle 在传达任何必要的步骤和过程方面做得非常好;你只需要阅读文档!例如,请参见 https://dev.mysql.com/doc/relnotes/mysql/8.0/en/ 的发行说明,以了解更多关于从一个版本到另一个版本的变更。

小升级

在下一次升级中,次要编号会发生变化。这通常被称为 x.y.z 版本号中的 y。例如,您可能从 5.6 升级到 5.7。

对于次要版本的个位数增量,升级通常是可接受的,并有记录。例如,支持从 5.6 升级到 5.7,但不直接支持从 5.0 升级到 5.7。这是因为版本之间存在太多差异,使得升级不可行(但并非不可能)。

然而,如果您有相应的计划,您可以升级较小的版本更改,风险可控。您将在后面的章节中看到更多关于管理风险的内容。

重大升级

在下一次升级中,主版本号会发生变化。除了不兼容的版本之外,这个类别是风险最大的类别,也是最有可能需要更多工作的类别。

主版本的版本升级很少发生,只有当 Oracle 发布了对服务器的一组新的、主要的更改(因此得名)时才会发生。MySQL Server 版本 8 包含了对 MySQL 5 的许多改进。大多数都带来了性能、高级特性和稳定性的巨大提高。但是,一些变化使得旧版本中的一些功能不兼容。

例如,MySQL 8.0 作为 GA 发布后,支持从 MySQL 5.7 升级到 MySQL 8.0,但您可能需要迁移某些功能才能完成升级。

幸运的是,Oracle 已经详细记录了所有问题领域,为迁移到新特性提供了建议。

不兼容的升级

正如您可能已经猜到的那样,有些升级是不推荐的,要么是因为缺少支持升级的功能,要么是因为主要的不兼容性。比如你不要考虑从 MySQL 5.0 升级到 MySQL 8.0。这仅仅是因为 8.0 不支持一些较老的 5.0 特性。这些类型的升级并不常见,因此我们在下面的列表中总结了一些不兼容的升级。不兼容的原因不是您要升级到的新版本;您要升级的是旧版本。

  • 跳过主要版本:升级主要版本可能会引入不兼容的变更。

  • 跳过次要版本:次要版本的某些升级可能会引入不兼容的变更。

  • 升级不兼容的硬件:升级一种字节序的硬件可能与另一种不兼容。例如,big-endian 到 little-endian 可能不兼容。

  • 更改 InnoDB 格式的版本:一些升级会导致 InnoDB 存储引擎内部发生变化。大多数计划用于兼容的次要修订版升级(例如,从 5.7.3 到 5.7.12),但有些需要一些额外的步骤来准备数据。

  • 新特性:新特性的引入很少会导致不兼容。例如,添加了数据字典,使得.frm元数据过时。

  • 平台变更:包括变更平台在内的一些升级可能需要额外的工作或引入潜在的不兼容性。例如,从文件系统中不支持区分大小写的平台转移到支持区分大小写的平台。

  • 升级非正式发布版本:不建议从非正式发布版本升级到正式发布版本,从正式发布版本升级到非正式发布版本,以及在非正式发布版本之间升级。

显然,不兼容性取决于某些功能、硬件或内部存储机制。在大多数情况下,在线文档概述了您可以做些什么来确保成功。有时这需要遵循特定的升级路径,例如在升级到目标版本之前先升级到一个版本。

如果我必须升级不兼容的版本怎么办?

如果您的升级策略属于这些不兼容升级类别之一,不要绝望。您可能仍然能够执行升级,但它可能更昂贵,需要更多的工作。例如,您可以通过使用带有mysqldumpmysqlpump的 SQL 语句来备份数据,安装新版本,然后使用 SQL 文件来调整它们以消除任何不兼容性,从而执行逻辑升级。尽管这确实带来了相当大的风险,即您仍然可以干净地导入所有数据,但这仍然是可能的。如果您发现自己处于这种情况,一定要花更多的时间使用并行安装和延长测试时间等策略来解决风险。

既然您对可能的升级类型有了很好的了解,那么让我们来看看执行升级的一些最佳实践。

升级实践

在升级任何系统时,我们都应该遵循一般惯例,或者至少将它们作为指南。本节描述了升级 MySQL 服务器时应该考虑的一些基本实践。虽然其中一些可能是熟悉的,但其他的可能不是您在升级 MySQL 时会考虑的。此外,在线参考手册中没有列出其中的一些内容。

正如您将看到的,这些实践不一定是下一个实践的顺序,甚至不一定是下一个实践的先决条件。例如,计划还应该包括测试时间。这里讨论的实践按照重要性的一般顺序排列,但是不应该按照这个顺序来考虑或实现。

检查先决条件

升级 MySQL 时,您应该做的第一件事是查看文档以了解任何先决条件。有时前提条件只是安全地备份数据,但也可能包括一些因素,如迁移某些功能(或数据)所需的实用程序和工具。在开始升级之前,请确保满足所有先决条件。

升级文档还将包括不兼容问题。这种情况通常发生在升级主要版本时,但有时次要版本也会出现不兼容的情况。幸运的是,在线参考手册中概述了这些内容。检查先决条件还可以帮助您提供可用于规划升级的详细信息。

警告

出现问题时,在线参考手册中关于升级的部分应该是您的第一站,而不是最后一站。阅读升级部分和发行说明可以帮助您在升级过程中避免不必要的返工和问题。

一旦通读了文档,作为先决条件,您需要做的一件事就是使用mysqlcheck实用程序来检查 MySQL 安装的兼容性。例如,升级到 MySQL 8 的一个先决条件是,根据在线参考手册,“不能有使用过时数据类型的表、过时函数、孤立的.frm文件、使用非本机分区的 InnoDB 表、缺少或空定义器的触发器或无效的创建上下文。”我们可以使用mysqlcheck实用程序来识别这些情况,如清单 10-5 所示。

$ mysqlcheck -u root -p --all-databases --check-upgrade
Enter password:
library_v1.authors                                 OK
library_v1.books                                   OK
library_v1.books_authors                           OK
library_v1.notes                                   OK
library_v1.publishers                              OK
library_v2.books                                   OK
library_v2.notes                                   OK
library_v2.publishers                              OK
library_v3.books                                   OK
...
mysql.user                                         OK
sys.sys_config                                     OK

Listing 10-5Using mysqlcheck to Identify Upgrade Issues

为了获得最佳结果,您应该使用您想要升级到的版本的mysqlcheck实用程序。这将确保该实用程序是最新的,并应识别更多的升级问题。

规划升级

当您规划好所有的先决条件并确定了需要特殊处理来解决不兼容问题的任何功能后,就该计划升级服务器了。如果您有数千台服务器,这可能是一件显而易见的事情,但对于只有几台(甚至一台)服务器需要升级的人来说,这就不那么明显了。

您应该抵制简单地运行升级而不计划要做什么的诱惑。我们希望通过降低(或消除)风险来确保升级顺利进行。这对于生产环境来说更加重要,但是任何潜在的可用性、性能或数据损失都会导致生产效率的损失。

您可以从文档中获得大部分需要规划的内容,但是文档并不特定于您的安装、服务器、平台等等。您必须填写这些空白,并根据您自己的安装修改文档中建议的步骤。通过阅读“MySQL 8.0 中的新特性”( https://dev.mysql.com/doc/refman/8.0/en/mysql-nutshell.html )一节,并关注《服务器在线参考手册》中标有“升级的后果”的小节,您可以学到很多东西。在那里,你会找到一些技巧,可以帮助你避免复杂的决定,或者更好地,避免复杂的维修。

这一步还包括确保您手头有合适的人员来进行升级,或者做好准备在出现问题时参与进来。 2 例如,不要忘记你的开发者、网站管理员和其他关键角色。

计划的形式由你决定;然而,建议你把你打算做的事情写下来,并与他人分享。这样,升级所有权链中的每个人都知道要做什么。你会惊讶地发现,小小的交流可以大大降低事情出错的风险。

警告

如果您正在使用或计划使用支持自动更新的平台,并且这些设施包括监控 MySQL 的存储库,那么您可能要考虑将 MySQL 排除在自动更新之外。对于生产环境来说尤其如此。您不应该在生产环境中为任何关键任务数据自动更新 MySQL。

记录您的结果

一些最优秀的管理者、建筑师和规划者拥有的品质之一是他们把一切都写下来。他们记下他们所尝试的和结果。这既包括正确的事情,也包括错误的事情。你应该总是考虑为你所有的项目保留一个工程师的笔记本。

有些人保留一个笔记本,按时间顺序记录笔记。其他人可能会保留几个笔记本,每个笔记本都专注于一个特定的项目或实验。笔记本不必是豪华的、用皮革包裹的、有三重安全锁的大部头,但它应该是典型的笔记本,因为它的书页不可移动,而且足够小,可以放在你的工作站旁边。

强烈建议您使用墨水来记录您的想法、实验、设置、配置和观察,这样您就不会试图删除它们。如果你绘图,你应该考虑使用铅笔,因为绘图经常改变或可能需要修改的准确性。你应该用日期和可选的时间标记你所有的条目。

此外,一定要把你的笔记本放在附近,这样你就不会想“以后再写下来”有时,诊断或实验时的激情会占据上风。只有常规和习惯才能确保你的笔记完整。

最后,要注意笔记本中信息的安全性。例如,如果您正在处理的项目包含不应被其他人看到的公司私人信息或其他信息,您应该将笔记本存放在公司政策为此类信息指定的地方。如果您无法做到这一点,或者您的公司政策中没有这样的机制,您应该避免写下任何此类信息。虽然这可能会使笔记不完整,但如果有人偶然发现你的笔记本上没有这些数据,这比偶然分享这些信息要好得多。 3

考虑并行部署

在升级需要大量工作的系统时,最有帮助的一种做法是与现有版本并行安装新版本。这是软件工程中的一种做法,旨在确保在安装和配置新系统时,现有的数据和应用保持不变。新版本(安装)将被视为一个开发平台,并且通常在完成充分的迁移测试后进行生产部署。

虽然这本身并不是一次升级(这是一次新的安装),但是并行运行 MySQL 的新版本在如何解决现有数据和应用的迁移方面提供了相当大的自由度。毕竟,如果出现问题,您的数据在旧系统上仍然是可操作的。

这种做法还提供了另一个好处:您可以更改平台或其他主要硬件,而不必拿现有数据冒险。如果您现有的服务器有要同时更新的硬件,您可以使用并行安装在新硬件上安装 MySQL,从而隔离新硬件的风险。

最后,采用并行安装可以确保现有系统完全能够运行,从而帮助您安排和规划迁移。更好的是,如果在迁移过程中出现问题,您可以随时回到旧系统。

并行部署通常包括让两个系统都运行一段时间。时间的长短可能取决于您愿意承担的风险大小,或者完全切换所有应用所需的时间。

不幸的是,有些人可能没有资源来考虑并行部署。考虑到同时运行两个 MySQL 安装可能会给开发者、管理员和支持人员带来更大的负担。考虑到并行开发的好处,在短时间内增加额外的资源或接受一些人员的低生产率可能是值得的。

然而,如果你没有进行足够的测试,即使这个安全网也是脆弱的。

测试测试测试!

测试和计划一起,经常被忽视或者被给予远远低于它应有的重要性。有时这是由于外部因素造成的,例如没有合适的人员可用,或者计划失败导致没有时间进行广泛的测试。不管借口是什么,未能充分测试您的升级会增加超出大多数人愿意忍受的风险。

测试应包括确保所有数据都已迁移,所有应用都能正常工作,以及所有访问权限(用户帐户、权限等)。)是功能性的。然而,不要就此止步。您还应该确保您的所有操作实践都已针对新版本进行了修改。更具体地说,您的维护脚本、过程和工具都可以在新版本中正常工作。

此外,您的测试应该导致接受升级的通过/不通过决定。如果事情不工作或有太多的问题,您可能需要决定保留或拒绝升级。并行安装实践可以以这种方式提供帮助,因为在确定一切正常之前,您不会破坏现有的数据或安装。将这些标准写入你的计划将确保成功。

小费

确保测试所有现有的操作程序,作为验收标准的一部分。

生产部署策略

如果您有一个生产和开发(或测试)环境,您还应该考虑如何将开发或测试部署转移到生产环境中。如果您使用并行安装,您可能会切换应用路由和类似的设备和应用。如果您使用就地安装,这个过程可能会更复杂。例如,您可能需要计划一段停机时间来完成迁移。

对于并行安装,计划停机时间可能更精确,并且涉及的时间更短,因为您有更多的时间来测试。但是,对于就地升级,您可能需要留出一段时间来完成迁移。自然,您会希望通过尽可能多地进行迁移来最大限度地减少停机时间。但在 MySQL 的基地里,这可能无非就是形成一个计划,聚集资源。底线是,不要放弃在你的计划中包括生产部署。

既然我们已经讨论了升级实践,那么让我们花点时间来讨论执行升级的原因,这显然是一个具有一定风险的复杂过程。

升级的原因

如果您像大多数平台或系统的狂热用户一样,每当有新版本发布时,您都会希望升级到最新、最好的版本。精明的管理员和规划者知道,在生产数据库环境中,这种行为几乎没有存在的空间。升级的理由将需要一些真正物有所值的东西。升级一定是值得的。升级 MySQL 的主要原因包括以下几点:

  • 特性:发布了一项新特性,可以改进您的应用或数据。示例包括文档存储、组复制和 InnoDB 集群。

  • 性能:新版本提高了性能,使您的应用更好。例如,最新的 5.7 版本比以前的版本快很多倍,MySQL 8 有望在此基础上有所改进。

  • 维护:新功能可以帮助你更好的维护系统。示例包括新的数据字典、组复制和 MySQL 企业备份等辅助工具。

  • 错误修复:旧版本中的缺陷可能需要解决方法或限制。较新的版本可能包含对关键错误的修复,因此您可以删除由缺陷引起的变通办法和限制。

  • 合规性:您的平台、标准操作程序或外部实体需要升级以实现合规性。例如,根据合同协议,您可能需要运行特定版本的 MySQL。

底线是你必须回答这个问题,“我为什么要升级?”这个答案必须给你、你的数据、客户、员工和公司的未来带来好处。将资源花费在几乎没有或根本没有好处的升级上是没有意义的,这也是公司经常跳过版本升级的另一个原因。唉,跳过太多升级会让以后的升级更成问题。然而,鉴于 MySQL 8.0 与 MySQL 5.7 和更早版本相比的所有改进,许多人会想升级到 MySQL 8。

小费

有关迁移到 MySQL 8 的更多详细信息,包括特定平台的步骤,请参见 http://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html

那么,我到底该不该升级到 MySQL 8?

本节的讨论可能会对您是否应该升级到 MySQL 8 产生一些疑问。那不是我的意图。这本书应该说服你尽快升级到 MySQL 8,只要你能以一种安全、无风险的方式升级。本节建议您需要仔细规划和执行升级,以确保成功。如果你现在开始迁移到 MySQL 8,等到 MySQL 8 有了正式发布的修订版或小升级版,你就可以升级了。

升级到 MySQL 8 的注意事项

MySQL 8.0 的在线参考手册中指出了几个兼容性问题。以下是您在规划 MySQL 8.0 升级时应该注意的一些事项:

  • 数据字典:新的元数据、事务存储机制是架构中的一个重大变化。如果您有处理.frm文件和其他元数据的 DevOps,您可能需要进行更改以迁移到使用数据字典。

  • 认证插件:默认认证插件已经改变。这可能会给使用旧身份验证机制的用户带来连接问题。

  • 错误代码:部分错误代码已更改。如果您的应用使用了错误代码,请研究这些更改以避免升级后出现应用错误。

  • 分区:默认的分区存储引擎支持已经被移除。如果您使用的是定制存储引擎(或旧引擎),请确保存在可用于 MySQL 8 的升级版本。

  • INFORMATION_SCHEMA :对视图做了微小的修改。如果您的应用或开发团队使用这些视图,请确保检查您正在使用的任何视图是否已被删除或更改。

  • SQL 命令:这个版本有一些新的和过时的 SQL 命令。请务必检查您的 SQL 语句,看看您是否在使用一些旧的、已删除的命令。

  • 默认字符集:默认字符集已更改为utf8mb4。如果您的应用支持字符集,您可能需要使用新的默认值进行测试,以确保兼容性。

同样,请务必阅读在线参考手册的“验证 MySQL 5.7 安装的升级先决条件”一节,以及“影响 MySQL 8.0 升级的更改”一节,了解升级到 MySQL 8.0 所需的这些和其他先决条件和迁移任务的最新信息。

其他优秀的资源还有 https://mysqlserverteam.com /的工程博客。这些博客通常在新功能正式发布之前就对其进行讨论,是关于这些功能如何工作以及工程团队已经发现或正在努力解决的任何升级问题的知识源泉。关注博客将会给变化一个很好的预警。

摘要

规划 InnoDB 集群安装的任务并不太复杂,但确实需要一些深谋远虑和周密的计划。与任何技术一样,我们应该从一个简单的实验装置开始,尽可能多地测试该技术的各个方面。这不仅包括设置集群的机制,还包括配置路由和应用以用于集群的细节。更重要的是,我们必须记录我们所有的结果——包括错误和不起作用的事情——以便我们在为生产部署 InnoDB Cluster 时能够避免它们。

在本章中,您了解了规划 InnoDB 集群的一些重要注意事项,包括规划 MySQL 8 的采用。这就完成了您对 InnoDB 集群的介绍。有了这些关于 InnoDB 集群工作方式的新知识,您就完全可以在自己的环境中采用 MySQL InnoDB 集群了。

对于 MySQL 用户来说,这是一个激动人心的时刻。Oracle 继续信守承诺,不仅继续开发 MySQL,还投入资源改进和扩展特性集。请密切关注更多优秀的特性以及进一步的改进和更新。MySQL 8 已经推出,现在是时候加入了。在 MySQL 8 上寻找更多的标题!

posted @ 2024-08-19 15:45  绝不原创的飞龙  阅读(6)  评论(0编辑  收藏  举报