MySQL 专家级教程(四)
八、扩展 MySQL 高可用性
MySQL 的一个高级特性是它能够提供高可用性数据库解决方案。负责获得高可用性的服务器组件是复制。有些可能包括其他特性,如分区和许多更小的特性,但是支持高可用性的最重要的特性是复制。
在这一章中,你将通过一个关于 MySQL 复制的简短教程来学习什么是复制以及它的基本配置。有了这些基本技能和我们在前面章节中学到的技能,您将会浏览复制源代码,并学习如何通过示例项目来扩展复制。首先,让我们了解什么是复制以及它是如何工作的。
什么是复制?
MySQL 复制是将一台服务器上发生的数据变化复制到另一台服务器上的能力。不是直接复制数据(当应用UPDATE或DELETE语句时,这可能会很慢且复杂),而是以元数据和命令的形式传输更改,因此是事件,它们被复制到第二个服务器并在那里执行。
这些事件被写入原始服务器上称为二进制日志的有序顺序文件,第二个服务器通过远程连接读取这些事件,并将它们存储在称为中继日志的相同格式的文件中。然后在第二个服务器上从中继日志中一次读取一个事件并执行。
这提供了从原始服务器精确复制数据更改的能力,因为它保留了事件的顺序,并确保使用通过服务器的相同路径。因此我们称这个过程为复制,因为它在变化发生时复制变化。我们把服务器之间的连接称为复制拓扑。
- 主服务器—该服务器是所有 write 和 DML 语句被发送到的原始服务器。
- slave—此服务器是通过复制事件维护数据副本的服务器。
- 中继从服务器—该服务器执行从服务器的角色,同时也是一个或多个从服务器的主服务器。
实际上,Slaves 是只读的。这是为了确保只有一个位置可以引发事件(数据更改)。因此,主机是您的所有写入都应该指向的位置。从机使用两个线程 2 :一个从主机读取事件的输入/输出(IO)线程和一个从中继日志执行事件的 SQL 线程。我将在后面的部分解释这些线程如何工作的细节。
注意MySQL 的最新版本包括一个多线程从模块,它使用一个 IO 线程来读取主模块的二进制日志条目,并使用多个 SQL 线程来执行事件。对于允许事件并行运行(例如,由数据库隔离)的复制安装,多线程从属可以提高复制性能。有关多线程从机(mt)的更多详细信息,请参见在线参考手册。
下面几节将解释为什么要使用复制,除了复制日期之外它还提供了什么功能,以及使用复制需要什么。完整地解释复制系统的每个方面和细微差别需要一整本书。我没有试图解释关于复制的所有知识,而是从更广的角度介绍复制,这将使您能够快速入门。
如果您计划试验复制,本章的内容应该足够了。如果您计划将复制用于高级高可用性解决方案,您应该阅读本章,并仔细研究在线参考手册中的复制章节。
为什么使用复制?
使用复制有很多原因。我上面所描述的——一个主机和一个从机——是构建拓扑的最基本的构建模块。这种简单的主从拓扑结构在从设备上提供了数据的冗余副本,使您能够保留一份副本,以防主设备发生问题,或者如果您希望将写入和读取分开以获得更好的应用性能。
但是你能做的远不止这些。如果主服务器出现故障,您可以将从服务器用作热备用服务器,并且可以使用从服务器来运行备份和维护操作,这些操作通常需要使服务器脱机。在这种情况下,您可以暂时停止从属服务器处理事件(一旦条件允许),进行备份,然后重新启动从属服务器。从模块将开始读取事件,从主模块二进制日志中的下一个事件开始。
具有许多客户端的应用可以看到,通过使用多个从设备来允许同时读取,读取操作有了显著的改进。这一过程称为横向扩展,是高可用性解决方案的构造块。
这些只是复制的一些用途。表 8-1 总结了复制可以实现的主要功能。
表 8-1 。复制的多种用途
| 使用 | 描述 |
|---|---|
| 支持 | 运行需要使服务器离线(或不离线)的备份操作。 |
| 超过尺寸范围 | 添加更多从机以提高读取吞吐量。 |
| 热备用 | 为主设备提供替代品,以显著减少停机时间。 |
| 数据分析 | 在从设备而不是主设备上执行资源密集型操作,以避免与其他活动应用发生冲突。 |
| 排除故障 | 对复杂查询进行潜在的侵入性诊断,并在不影响生产数据库的情况下优化数据库设计。 |
| 发展 | 为新应用的开发提供接近生产质量的数据,以帮助避免制造可能不代表实际数据值、范围和大小的数据。 |
正如您所猜测的,复制会对资源提出更多的要求。令人高兴的是,复制可以在任何支持 MySQL 的机器上运行。在采用多层主设备和许多从设备的大型安装中,主设备通常安装在具有更大内存、网络连接和更快磁盘系统的机器上。这是因为在主设备上执行了大量的写操作(在大多数情况下,写操作比读操作花费更多的时间)。
另一方面,从机通常安装在为读操作而优化的机器上。在您可能希望将从服务器用作主服务器的热备用服务器的情况下,您应该使用与主服务器相同的硬件。这使得改变角色不太可能遇到性能问题。
本节用最简洁的术语简要介绍了 MySQL 复制。有关复制及其所有细微差别的更深入的内容,请参见在线参考手册。
复制如何实现高可用性?
您可能想知道复制与高可用性有什么关系,因为大多数人认为高可用性是一种很少停机的状态(除了短暂的、有意计划的事件)。复制通过提供将主节点的角色从主节点切换到另一个有能力的从节点的能力来实现高可用性。我们说有能力,是因为并不是所有的奴隶都适合担当主人的角色。如果从设备的硬件与主设备有很大的不同(较慢),您不会希望选择该从设备作为新的主设备。有多种方法可以将存储作为主服务器的备用服务器,也有多种方法可以切换角色。通常,我们认为有两种改变角色的方法:切换和故障转移。
如果主服务器是健康的,但是您需要改变角色,因为您需要在主服务器上执行维护,或者主服务器上发生了一些事情,但是没有完全禁用它,我们将这种切换称为,因为您正在将角色从主服务器切换到从服务器。
如果主服务器崩溃或脱机,我们必须选择一个从服务器来创建新的主服务器。在这种情况下,我们称之为故障转移,因为失败是改变角色的动力。
这就是高可用性发挥作用的地方。如果您有几个能够接管主服务器的从服务器,特别是当主服务器出现故障时,您可以通过将主服务器的角色快速切换到其中一个从服务器来避免潜在的长时间停机以及更重要的数据丢失。
最近已经投入了一些努力来试图实现几乎零停机时间,即使在完全失去主设备的情况下。Oracle 为复制添加了新功能,使管理员能够设置自动故障转移。这是通过服务器中一个名为全局事务标识符(GTID) 的特性和 MySQL 实用程序套件(见下文)中一个名为mysqlfailover 的脚本的组合来实现的。虽然对 GTID 和自动故障转移的深入研究超出了本书的范围,但是请参阅侧栏“什么是 GTID?”以获得该过程的概述。
什么是 GTID?
GTIDs 使服务器能够为每个事件集或组分配一个唯一的标识符,从而可以知道每个从服务器上应用了哪些事件。要使用 GTIDs 执行故障转移,可以选择最好的从设备(丢失事件最少的设备,并且硬件与主设备最匹配),并使其成为所有其他从设备的从设备。我们称这个从设备为候选从设备。GTID 机制将确保只应用那些没有在候选从设备上执行的事件。通过这种方式,候选从设备成为最新的,因此成为主设备的替代。
mysqlfailover命令行工具监控原来的主服务器,通过执行上述事件序列来执行自动故障转移,并负责将剩余的从服务器重定向到新的主服务器。
因此,GTIDs 使得复制更有能力提供高可用性数据库解决方案。有关 GTIDs 和使用 MySQL 实用程序的自动解决方案的更多信息,请访问:
http://dev.mysql.com/doc/refman/5.6/en/replication-gtids.html
http://dev.mysql.com/doc/workbench/en/mysqlfailover.html
http://drcharlesbell.blogspot.com/2012/04/mysql-utilities-and-global-transaction.html
在 GTIDs 之前,在从设备上执行并存储在从设备的本地二进制日志中的复制事件将被重新应用,因此,如果没有读取和执行这些事件的从设备试图建立复制,则会导致错误。虽然这听起来有点奇怪,但在线参考手册对这个主题的讨论要详细得多。
正如你所看到的,你可以通过复制完成很多事情。到目前为止,我所讨论的实际上只是基础。您可以在联机参考手册中找到有关复制的更深入的信息。如果您想探索复制的高级功能并配置您的系统以获得最大的高可用性,我推荐我的书 *MySQL 高可用性,*由 O'Reilly Media 出版。
在下一节中,我将演示如何设置复制,并讨论建立复制拓扑所需的一些一般原则和命令。
基本复制设置
您可能认为复制可能很难设置或者很复杂。情况也不是这样。复制非常容易设置,命令也很少。此外,如果设置正确,复制非常稳定,很少出现与数据配置无关的问题、用户引发的问题(例如意外的DROP TABLE)或数据损坏。尽管如此,Oracle 仍在不断改进复制,增加了一些功能以实现更可靠的复制,例如使用校验和检查传输过程中的数据损坏。
在这一节中,我将介绍复制的要求,解释在设置复制时使用的命令,并介绍在主设备和从设备之间建立复制的标准方法。
复制的要求
为了设置复制,您至少需要两台服务器:一台作为应用应用更改的原始数据的主服务器(主服务器),另一台作为应用读取数据的位置(从服务器)。
最严格的要求是拓扑中的每台服务器都必须有唯一的服务器标识符。这可以在选项文件中用server-id=N设置,作为命令行用- server-id=N 设置,或者用SET GLOBAL server_id=N SQL 命令设置。您也可以使用命令的替代形式;SET @@GLOBAL.server_id=N
主机必须启用二进制日志记录。最简单的方法是使用--log-bin选项。此选项允许您指定用于二进制日志的路径和文件名。只包括文件名,不包括扩展名。复制系统将附加一个由六位数字组成的扩展名,代表二进制日志的序列号。每次轮换二进制日志时,该数字都会增加。
使用FLUSH BINARY LOGS命令可以完成二进制日志(和中继日志)的旋转。当发出这个命令时,当前文件被关闭(在事务完成之后),并且创建一个具有递增序列号的新文件。
您可以在标准的 my . CNF(Windows 的 my.ini)文件中找到如何打开二进制日志记录的示例。我在下面提供了一个例子,展示了如何打开二进制日志并设置二进制日志格式。
# Uncomment the following if you want to log updates
log-bin=mysql-bin
# binary logging format - mixed recommended
binlog_format=mixed
循环意味着日志被刷新(缓存的事件被写入磁盘)和关闭,并打开一个新的日志文件。旋转二进制日志可以使用以下命令手动完成:
mysql> FLUSH BINARY LOGS;
您还需要设置一个拥有复制事件权限的特殊用户。更具体地说,它用于从主服务器的二进制日志中读取事件。您可以在主服务器上使用以下命令来实现这一点。您只需要运行这个命令一次。该用户和密码用于从设备上的特殊命令,以连接到主设备。
mysql> GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'%' IDENTIFIED BY 'secret';
你还需要所谓的主二进制日志的坐标。这些包括当前二进制日志的名称和最近事件的位置,通过使用SHOW MASTER STATUS命令发现,如下所示。
mysql> SHOW MASTER STATUS;
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
| mysql-bin.000001 | 245 | | | |
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
1 row in set (0.00 sec)
请注意此视图中显示的列。您可以看到当前的二进制日志文件和当前位置。还有两列显示运行中的任何二进制日志过滤器。不鼓励使用二进制日志筛选器,因为在激活时,没有通过筛选器的事件不会写入二进制日志。有关从属服务器上的二进制日志过滤器和复制过滤器的更多信息,请参见联机参考手册。
最后,如果您在主服务器上有任何数据,请将当前状态的数据复制到从服务器;这通常通过备份和恢复过程来完成。如果打开了二进制日志记录,您需要在进行复制时锁定表,以便在复制数据时没有事件被写入二进制日志。如果您以独占方式使用 InnoDB 存储引擎,您可以使用一致的读锁来锁定表,但仍然允许读。从包含数据的主机向从机复制数据的基本过程是:。
- 用
FLUSH TABLES WITH READ LOCK锁定主机上的表。 - 复制数据。你可以用任何你想用的方法。对于少量数据,可以使用 mysqldbexport(来自 MySQL 实用程序)或 mysqldump 客户端应用。
- 用
SHOW MASTER STATUS记录主日志文件和位置。 - 用
UNLOCK TABLES解锁主机上的桌子。 - 将数据导入从机。例如,使用 mysqldbimport 导入由 msyqldbexport 生成的文件,或者通过 mysql 客户端中的 source 命令读取 mysqldump 输出。
- 使用(3)中的值开始复制。
现在我已经解释了设置复制的要求,让我们看看如何配置主服务器和从服务器。以下部分假设您有两台安装了 MySQL 的兼容硬件的服务器。这些例子没有使用 GTIDs,但是我注意到了下面的区别。
还假设您已经将任何现有数据从要配置为主服务器的服务器复制到要配置为从服务器的服务器,并且主服务器不会因为锁定表或因为没有连接客户端而出现写操作(数据更改)。这一点很重要,因为在建立复制时发生写入可能意味着您为从属服务器选择了错误的主日志文件和位置(坐标)。
配置主机
如果服务器正在运行,检查它是否打开了二进制日志记录。执行一个类似' log_bin '的显示变量。您应该会看到类似的结果:
mysql> SHOW VARIABLES LIKE 'log_bin';
+−−---------------------------------------+−−--------------------+
| Variable_name | Value |
+−−---------------------------------------+−−--------------------+
| log_bin | ON |
+−−---------------------------------------+−−--------------------+
1 rows in set (0.00 sec)
注意变量log_bin的值。在这个例子中,它被打开(ON)。如果您的服务器的这个值是OFF,关闭服务器并打开二进制日志记录,或者通过命令行,如果您以这种方式启动服务器,或者通过选项文件。对于打算用于应用的服务器,最好将设置放在配置文件中。
要通过命令行设置变量,请在启动服务器时添加以下选项。第一个选项告诉服务器对事件使用行格式(这是可选的,但建议使用),第二个选项是打开二进制日志记录并使用mysql_bin 作为文件名(不带扩展名),第三个选项用于设置唯一的server_id 。一定要检查你的奴隶,以确保它有一个不同的server_id值。
--binlog-format=row --log-bin=mysql_bin --server-id=5
注意您可以动态设置
server_id,但这不会将值保存到选项文件中。因此,当服务器重新启动时,该值将恢复为默认值或从选项文件中读取的值。
要在配置文件中设置变量,打开名为 my.cnf 的配置文件(或任何您命名的文件)并添加以下行。对于使用预定义配置文件的 MySQL 安装,您可能会看到这些条目被注释掉。在这种情况下,只需取消注释即可。将这些值放入[mysqld]部分。
binlog_format=row
log_bin=mysql_bin
server_id = 5
注确保找到您的服务器正在使用的选项文件。这通常出现在 Mac、Linux 和 Unix 系统的
/etc./my.cnf、/etc./mysql/my.cnf、/usr/local/mysql/etc./my.cnf或∼/.my.cnf中。Windows 系统可能会将该文件命名为 my.ini,它通常位于c:\windows\my.ini、c:\my.ini、、>\my.ini或、>\.mylogin.cnf中。有关更多信息,请参考在线参考手册的“使用选项文件”一节。
对配置文件进行更改后,重新启动服务器并再次检查以确保二进制日志记录已打开。
要允许从服务器连接到主服务器并读取事件,您必须定义一个复制并发布适当的权限。该用户必须拥有REPLICATION SLAVE权限,并且可以使用如下GRANT语句创建。请确保根据您的域或使用 IP 地址来设置主机名。此外,根据您的信息安全策略设置密码。
mysql> GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'%.mydomain.com' IDENTIFIED BY 'secret';
稍后,当我们将从服务器连接到主服务器并开始复制时,我们将使用这些凭据。
提示如果您已经配置您的服务器不允许使用
GRANT语句自动创建用户帐户,您必须在GRANT命令之前发出CREATE USER命令。
您需要在主服务器上做的另一件事是发现二进制日志文件的名称及其当前位置。如果您还没有这样做,或者还没有采取措施来确保主服务器上没有发生写操作,请如下所示锁定表。
mysql> FLUSH TABLES WITH READ LOCK;
一旦您确定不再有写入发生,您可以使用如下所示的SHOW MASTER STATUS命令。当我们将从机连接到主机时,我们将使用这些信息。
mysql> SHOW MASTER STATUS;
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
| mysql-bin.000152 | 243 | | | |
+−−----------------+−−--------+−−------------+−−----------------+−−------------------+
1 row in set (0.00 sec)
注意,在这个例子中,二进制日志已经增加了很多次。对于长期运行的服务器来说,这种情况并不少见。如果您继续操作并设置一个新的主机,您可能会看到二进制对数的值低得多,位置的值也可能更小。这是完全正常的。只需记录这些值以备后用。
如果在发出 SHOW MASTER STATUS 命令之前锁定了表,现在可以使用以下命令解锁它们:
mysql> UNLOCK TABLES;
注意如果你正在使用 GTIDs,你不需要知道主坐标。GTID 特性会自动为您解析二进制日志中的起始位置。
配置从机
配置从机要简单一些。您只需要为从机设置server_id。与主服务器一样,如果以这种方式启动服务器(??),可以通过命令行设置该值,但最好的方法是在配置文件中更改该值。打开从属服务器的选项文件,添加或取消注释以下内容。将这些值放入[mysqld]部分。
[mysqld]
...
server_id = 7
将从设备连接到主设备
现在,您已经用二进制日志配置了您的主服务器,并将两个服务器的server_id设置为一个惟一的值,您已经准备好将从服务器连接到主服务器。这需要按顺序发出两个命令。第一个用于指示从设备如何连接到哪个主设备,第二个用于启动从设备上的 IO 和 SQL 线程。
CHANGE MASTER命令建立从机到主机的连接。您可以指定几种选项和连接形式,包括 SSL 连接。对于这个例子,我们使用最基本的选项,并使用普通的 MySQL 身份验证来连接从服务器。有关 SSL 连接和许多选项的更多信息,请参见在线参考手册。
以下是使用上述SHOW MASTER STATUS中的信息将从机连接到主机的CHANGE MASTER命令示例。当你建立你自己的主人和奴隶时,从你的主人那里得到的值。
mysql> CHANGE MASTER TO MASTER_HOST='localhost', MASTER_USER='rpl',
MASTER_PASSWORD='pass', MASTER_LOG_FILE='mysql_bin.000152',
MASTER_LOG_POS=243;
如果使用 GTIDs,可以省略主二进制对数坐标,使用特殊选项,如下所示。这告诉服务器开始协商要在从服务器上执行的起始事务。
mysql> CHANGE MASTER TO MASTER_HOST='localhost', MASTER_USER='rpl',
MASTER_PASSWORD='pass', MASTER_AUTO_POSITION = 1;
下一个命令用于启动从服务器上的线程,并开始从主服务器复制事件。
mysql> START SLAVE;
伴随命令STOP SLAVE,停止从线程,从而停止复制。还有一个命令RESET SLAVE,,用于删除从服务器上的所有复制文件,并重置主服务器的从服务器连接信息。在需要清除从机连接信息的情况下,很少使用 reset 命令。一个等效的RESET MASTER命令删除所有二进制日志文件并清除二进制日志索引。
注意确保您确实想要销毁复制信息(
RESET SLAVE)或二进制日志信息(RESET MASTER),并且没有从设备连接到主设备(RESET MASTER)。无意中发出这些命令可能会导致复制拓扑发生错误,数据复制停止。
当发出START SLAVE命令时,您可能会看到警告,或者在某些情况下会看到错误。不幸的是,这并不总是那么有用。精明的数据库管理员使用一个特殊的命令——SHOW SLAVE STATUS命令——来检查从命令的状态,该命令在语法上类似于SHOW MASTER命令。这个命令将生成一个单行的非常长(宽)的视图,其中包含许多关于从属服务器内部正在进行的复制的确切信息。查看这些信息的最佳方式是以垂直格式显示(使用\G)。清单 8-1 显示了一个没有错误的从机的典型输出。我用粗体突出显示了要检查的更重要的属性。
清单 8-1。 显示奴隶状态报告
mysql> SHOW SLAVE STATUS \G
*************************** 1\. row ***************************
Slave_IO_State:
Waiting for master to send event
Master_Host: localhost
Master_User: rpl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File:
my_log.000152
Read_Master_Log_Pos:
243
Relay_Log_File: clone-relay-bin.000002
Relay_Log_Pos: 248
Relay_Master_Log_File: my_log.000152
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: 243
Relay_Log_Space: 403
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:
1 row in set (0.00 sec)
养成在设置复制时总是发出这个命令的习惯。它将提供丰富的信息,包括任何问题和错误的更多细节。
后续步骤
如果您已经发出了上面的所有命令,并且在SHOW SLAVE STATUS的输出或命令本身中没有错误,那么恭喜您!您刚刚建立了您的第一个复制拓扑。
如果您想要测试您的复制设置,那么在主服务器上创建一个新的数据库和表,并检查从服务器以确保这些事件在那里被重复。用 SHOW SLAVE STATUS 检查从站,直到它报告它正在等待主站的新事件。否则,您可能会在从属服务器完成从中继日志中读取事件之前检查它。我没有通过示例向您展示这一点,而是将测试您的复制设置作为一个练习。
在下一节中,我将更深入地研究复制最重要的支持特性—二进制日志。
但是等等,没有更好的办法吗?
如果您已经阅读了前面的部分并在您自己的服务器上执行了这些步骤,您可能会认为虽然这个过程很简单,但是有很多要执行的步骤和要检查的内容。您可能想知道为什么没有一个简单的命令来“照做”复制。
我有好消息。有这样一个命令,但它是以与 MySQL Workbench 捆绑在一起的 Python 实用程序的形式。被称为 MySQL 实用程序,它包括两个非常有用的命令:mysqlreplicate,自动设置主服务器和从服务器;和mysqlrplcheck,,其在复制建立之前或之后检查主设备和从设备的先决条件。我将在下面更详细地解释这些工具和 MySQL 工具。
二进制日志
如果不详细查看二进制日志,关于复制的任何讨论都是不完整的。在本节中,我们将进一步了解二进制日志是如何工作的,以及如何使用外部客户端和特殊的 SHOW 命令来读取二进制日志和中继日志。
有些人可能认为二进制日志是复制,但这并不完全正确。任何服务器,包括充当从属服务器的服务器,都可以启用二进制日志记录。在从属服务器的情况下,服务器将包含中继日志和二进制日志。这使得将其他服务器作为从服务器的从服务器可以执行主服务器和从服务器的角色(有时称为中间从服务器)。
二进制日志最初有另一个目的。它可用于数据丢失时的恢复。例如,如果服务器启用了二进制日志,则可以重放二进制日志以将数据恢复到丢失点。这被称为时间点恢复,之所以成为可能,是因为 mysql 客户端可以读取事件并执行它们。您既可以获取文件,也可以将单个事件(或一系列事件)复制并粘贴到 mysql 客户端中。有关时间点恢复的更多信息,请参见在线参考手册。
提示要发现用于二进制日志记录的所有变量,发出命令
SHOW VARIABLES LIKE '%binlog%'。类似地,要发现用于中继日志的变量,发出命令SHOW VARIABLES LIKE '%relay%'。
如前所述,用户可以使用--log-bin选项 来设置二进制日志的名称,通过该选项可以指定一个用于所有二进制日志文件的文件名。旋转二进制日志时,会附加一个六位数递增的新文件作为文件扩展名。还有一个索引文件,其名称是为二进制日志指定的,扩展名为.index 。该文件维护当前的二进制日志文件。服务器在启动时使用这个文件来知道从哪里开始附加事件。因此,二进制日志是文件的集合,而不是单个文件。每当您执行维护或希望归档二进制日志(或中继日志)时,请包含所有相关的文件,包括索引文件。
提示您可以使用- relay-log 启动选项 更改中继日志的名称。
二进制日志是使复制成为可能的机制,也是管理员在出现问题时关注的地方。但是首先,让我们检查二进制日志的格式。
行格式
二进制日志是一个顺序文件,其中事件(对数据的更改)被写入文件的末尾。文件位置用于确定下一个二进制日志事件在文件中的偏移量。因此,服务器维护当前二进制日志的名称和下一个位置指针(如上面的SHOW MASTER STATUS所示)。这样,服务器就可以知道下一个事件写在哪里。
虽然有点用词不当,但二进制日志可以被认为是两种主要格式之一:基于语句的或基于行的。还有一个混合版本叫做混合格式。虽然这听起来像是文件的格式不同,但这种格式指的是事件本身。无论事件的格式如何,二进制日志文件本身仍然是一个顺序文件,并且使用简单的文件名加偏移量的概念来读写它。不同的格式有:
- 基于语句的复制(SBR)—事件包含在主服务器上执行的实际 SQL 语句。这些代码被打包,运送到从机,并在那里执行。
- 基于行的复制(RBR)—事件包含更改后产生的二进制行。这允许从属服务器简单地将事件应用于行,而不是通过 SQL 接口执行语句。
- 混合格式 SBR 和 RBR 的这种组合由存储引擎和服务器的其他元素以及正在执行的命令类型控制。默认情况下,混合格式使用 SBR。例如,如果存储引擎支持 RBR,则事件将采用 RBR 格式。同样,如果有理由不使用 RBR,SBR 将被用于该事件。
请参阅在线参考手册,了解哪些命令强制事件采用 SBR 或 RBR 格式。
mysqlbinlog 客户端
所有 MySQL 安装都包括一个用于读取二进制日志的特殊客户端。客户端被令人信服地命名为 mysqlbinlog。客户端的目的是以人类可读的形式显示二进制日志的内容。对于 SBR 格式,实际的查询包含在事件的有效负载中,因此易于阅读。RBR 格式事件是二进制形式的。对于 RBR 事件,客户端将显示任何人类可读的信息,以 ASCII 形式显示行格式。
mysqlbinlog 客户端有许多控制输出的选项。您可以以十六进制格式显示输出,跳过前 N 个事件,在一系列位置显示事件,或者使用更多选项。要读取二进制日志中的一系列事件,可以指定开始日期时间和结束日期时间或者开始和停止位置。对于客户机来说,最强大的特性可能是连接到远程服务器并读取其二进制日志的能力。此功能使管理多台服务器变得更加容易。
清单 8-2 展示了一个针对典型的二进制日志文件运行mysqlbinlog客户端的例子。在这种情况下,该文件来自一个其日志已经轮换过一次的主服务器。我们可以通过用于二进制日志文件扩展名的序列号来判断这一点。每次旋转日志时,该值都会递增。回想一下,这个过程是由FLUSH LOGS启动的,导致现有的二进制日志文件被关闭,一个新文件被打开,一个新的头被写入新文件。
***清单 8-2。***mysqlbinlog 客户端示例输出
$ mysqlbinlog /usr/local/mysql/data/mysql-bin.000002
/*!40019 SET @@session.max_insert_delayed_threads=0*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#121016 20:50:47 server id 1 end_log_pos 107 Start: binlog v 4, server v 5.6.7-log created 121016 20:50:47 at startup
ROLLBACK/*!*/;
BINLOG '
5wB+UA8BAAAAZwAAAGsAAAABAAQANS41LjIzLWxvZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAADnAH5QEzgNAAgAEgAEBAQEEgAAVAAEGggAAAAICAgCAA==
'/*!*/;
# at 107
#121016 20:54:23 server id 1 end_log_pos 198 Query thread_id=108 exec_time=0 error_code=0
SET TIMESTAMP=1350435263/*!*/;
SET @@session.pseudo_thread_id=108/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=0/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C utf8 *//*!*/;
SET @@session.character_set_client=33,@@session.collation_connection=33,@@session.collation_server=8/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
CREATE DATABASE example1
/*!*/;
# at 198
#121016 20:54:42 server id 1 end_log_pos 303 Query thread_id=108 exec_time=0 error_code=0
SET TIMESTAMP=1350435282/*!*/;
CREATE TABLE example1.t1(a int, b varchar(20))
/*!*/;
# at 303
#121016 20:55:05 server id 1 end_log_pos 367 Query thread_id=108 exec_time=0 error_code=0
SET TIMESTAMP=1350435305/*!*/;
BEGIN
/*!*/;
# at 367
#121016 20:55:05 server id 1 end_log_pos 493 Query thread_id=108 exec_time=0 error_code=0
SET TIMESTAMP=1350435305/*!*/;
insert into example1.t1 values (1, 'one'), (2, 'two'), (3, 'three')
/*!*/;
# at 493
#121016 20:55:05 server id 1 end_log_pos 520 Xid = 141
COMMIT/*!*/;
# at 520
#121016 20:55:17 server id 1 end_log_pos 584 Query thread_id=108 exec_time=0 error_code=0
SET TIMESTAMP=1350435317/*!*/;
BEGIN
/*!*/;
# at 584
#121016 20:55:17 server id 1 end_log_pos 682 Query thread_id=108 exec_time=0 error_code=0
SET TIMESTAMP=1350435317/*!*/;
DELETE FROM example1.t1 where b = 'two'
/*!*/;
# at 682
#121016 20:55:17 server id 1 end_log_pos 709 Xid = 148
COMMIT/*!*/;
DELIMITER ;
# End of log file
ROLLBACK /* added by mysqlbinlog */;
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
请注意,在输出中,我们看到了许多关于每个事件的元数据,以及一个包含二进制日志文件信息的标题。另外,请注意,每个事件(本例中为 SBR 格式)都显示了日志位置、线程 id 和其他相关信息。时间戳条目是一种特殊形式的事件,服务器使用它来维护日志中的时间戳数据(因此在从属服务器上)。
提示二进制日志和中继日志具有相同的布局,因此两者都可以被
mysqlbinlog客户端读取。
使用 mysqlbinlog 客户端从二进制或中继日志中读取事件是非常强大的,但是还有一个方便的 SHOW 命令,它以表格形式显示事件。
显示 BINLOG 事件命令
MySQL 服务器包含一个特殊的 SHOW 命令 SHOW BINLOG EVENTS,它允许您查看位于二进制日志中的最新事件。让我们来看看这个命令的运行情况。清单 8-3 显示了 SHOW BINLOG 事件运行的结果,该服务器与前一节中 mysqlbinlog 示例所用的服务器相同。我使用竖排格式是为了更容易阅读。
清单 8-3。 展示 BINLOG 事件示例 1
cbell$ mysql -uroot
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 508
Server version: 5.6.7-m9 MySQL Community Server (GPL)
Copyright (c) 2000, 2012, 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 BINLOG EVENTS \G
*************************** 1\. row ***************************
Log_name: mysql-bin.000001
Pos: 4
Event_type: Format_desc
Server_id: 1
End_log_pos: 107
Info: Server ver: 5.6.7-m9, Binlog ver: 4
*************************** 2\. row ***************************
Log_name: mysql-bin.000001
Pos: 107
Event_type: Query
Server_id: 1
End_log_pos: 245
Info: SET PASSWORD FOR 'root'@'localhost'='*81F5E21E35407D884A6CD4A731AEBFB6AF209E1B'
*************************** 3\. row ***************************
Log_name: mysql-bin.000001
Pos: 245
Event_type: Stop
Server_id: 1
End_log_pos: 264
Info:
3 rows in set (0.00 sec)
您可能想知道前一个示例中的事件发生了什么。这揭示了不熟悉二进制日志记录的用户经常犯的错误。默认情况下,SHOW BINLOG EVENTS命令显示第一个二进制日志中的事件。在上一节中,我使用了第二个二进制日志。要查看二进制日志而不是第一个日志中的事件,可以使用IN子句,如下所示。我使用竖排格式是为了更容易阅读。
清单 8-4。 展示 BINLOG 事件示例 2
mysql> SHOW BINLOG EVENTS IN 'mysql-bin.000002' \G
*************************** 1\. row ***************************
Log_name: mysql-bin.000002
Pos: 4
Event_type: Format_desc
Server_id: 1
End_log_pos: 107
Info: Server ver: 5.6.7-m9, Binlog ver: 4
*************************** 2\. row ***************************
Log_name: mysql-bin.000002
Pos: 107
Event_type: Query
Server_id: 1
End_log_pos: 198
Info: CREATE DATABASE example1
*************************** 3\. row ***************************
Log_name: mysql-bin.000002
Pos: 198
Event_type: Query
Server_id: 1
End_log_pos: 303
Info: CREATE TABLE example1.t1(a int, b varchar(20))
*************************** 4\. row ***************************
Log_name: mysql-bin.000002
Pos: 303
Event_type: Query
Server_id: 1
End_log_pos: 367
Info: BEGIN
*************************** 5\. row ***************************
Log_name: mysql-bin.000002
Pos: 367
Event_type: Query
Server_id: 1
End_log_pos: 493
Info: insert into example1.t1 values (1, 'one'), (2, 'two'), (3, 'three')
*************************** 6\. row ***************************
Log_name: mysql-bin.000002
Pos: 493
Event_type: Xid
Server_id: 1
End_log_pos: 520
Info: COMMIT /* xid=141 */
*************************** 7\. row ***************************
Log_name: mysql-bin.000002
Pos: 520
Event_type: Query
Server_id: 1
End_log_pos: 584
Info: BEGIN
*************************** 8\. row ***************************
Log_name: mysql-bin.000002
Pos: 584
Event_type: Query
Server_id: 1
End_log_pos: 682
Info: DELETE FROM example1.t1 where b = 'two'
*************************** 9\. row ***************************
Log_name: mysql-bin.000002
Pos: 682
Event_type: Xid
Server_id: 1
End_log_pos: 709
Info: COMMIT /* xid=148 */
9 rows in set (0.00 sec)
mysql>
该命令显示日志名称、日志中事件的开始和结束位置(文件偏移量)、事件类型和负载(SBR 事件的查询)。
看起来这些工具 msyqlbinlog 客户端和 SHOW BINLOG EVENTS 命令——就像一个有限的集合,但事实是它们是成为运行二进制日志记录和复制拓扑的服务器的优秀管理员的关键。优先学习这些工具。下面列出了几个资源(除了在线参考手册之外),您可以使用它们来了解有关二进制日志记录和事件复制的更多信息。
额外资源
任何想了解二进制日志及其格式的详细信息的人都可能会对一些相当晦涩的资源感兴趣。当然,在线参考手册有大量的文档,应该是您的主要来源,但是下面包含一些在其他来源找不到的关键信息。
- dev.mysql.com/doc/interna…
- http://dev . MySQL . com/doc/internals/en/replication-protocol . html
- http://dev . MySQL . com/doc/internals/en/row-based-replication . html
在接下来的小节中,我将深入探讨复制的细节,首先是复制架构的概述,然后是复制源代码的简要介绍。
复制架构
复制源代码非常大,事实上,它是服务器代码中最大的部分之一。由于复制渗透到服务器代码的这么多层次,而且它已经发展了很多年,所以有时很难看出它是如何适应服务器的其他部分的。
因此,花一点时间查看架构本身的高级视图来了解复制中的各个部分是如何工作的是很有帮助的。这也是介绍复制源代码的主要部分的好方法。图 8-1 显示了复制架构的简化框图。该图后面的部分更详细地解释了这些组件。
图 8-1。复制架构
上图中带编号的箭头显示了描述复制体系结构的简化事件序列。首先,主机将事件写入二进制日志。其次,从设备的IO_THREAD通过专用网络连接从主设备的二进制日志中读取事件,并将其写入其中继日志。第三,从服务器的SQL_THREAD从中继日志中读取事件,并将其应用(执行)到数据库。
让我们放慢一点,更详细地检查序列。下面是代码序列的概括。在主服务器中,有些事件遵循稍微不同的路径,但一般来说,这就是事件被传送到从服务器的方式。
当用户或应用在主服务器上发出写入二进制日志类型的 SQL 语句时(SHOW命令是一个不被记录的命令),服务器代码调用代码写入binlog.cc,中的二进制日志,然后调用rpl_master.cc中的代码创建一个日志事件类的实例。当写入二进制日志时,调用 log-event 类的pack_info()方法来格式化数据块,以便存储在二进制日志中。这里特别有趣的是,二进制日志类更新它的日志位置,以便服务器和用户知道二进制日志文件中下一个事件的位置。
当从机通过rpl_slave.cc中的代码向主机请求更多事件时,它连接到主机并发出特殊命令COM_BINLOG_DUMP,,该命令通过sql_parse.cc中的大开关执行,并导致 rpl_master.cc 中的代码发送自从机读取的最后一个位置以来二进制日志中的任何事件。读取这些事件时,通过 rpl_slave.cc 中的代码将它们写入从机的二进制日志文件。该过程由从机通过 IO 线程执行。同时,从服务器有另一个线程,SQL 线程,它从中继日志中读取事件并执行它们。这个代码也在rpl_slave.cc里面。
虽然这种解释忽略了许多细节,但它准确地描述了复制体系结构如何实现其目标。这个过程涉及许多行代码,因此将其概括为简单是一种保守的说法。复制源代码一点也不简单,但是它执行起来很优雅,掩盖了它的复杂性。
在下一节中,我将简要介绍复制源代码。由于组成复制功能的元素很多,所以我将重点放在有助于您理解复制工作原理的一个关键方面——日志事件。
复制源代码简介
正如我前面提到的,复制源涉及到服务器代码的许多部分。考虑一下在二进制日志中记录一个事件所涉及的所有内容。显然,我们希望复制的每个命令都必须调用复制代码(在本例中是二进制日志记录代码)来启动事件并将其记录在二进制日志中。将该事件发送到从属服务器并在那里执行,引入代码将该事件放入服务器以供执行——通过执行查询(SBR)或应用结果行(RBR)。
在接下来的几节中,我将带您浏览复制源代码,重点是围绕日志事件的代码。在我们开始这个旅程之前,让我们看一下复制的源代码文件。
复制源代码文件
此外,超过 48 个单独的源代码文件组成了复制源代码。可以想象,这是一个巨大的代码量,因此,很难记住(或学习)所有文件做什么以及每段代码驻留在哪里。
我最近采访了一些开发者(匿名),发现了一个有趣的趋势。虽然有些人对大多数源文件及其功能了如指掌,但即使是最有经验的人也不得不搜索他们的记忆来回忆每个文件包含的内容。可以肯定地说,只有 Oracle 复制开发者自己可以被视为这方面的专家。
我在这里展示的是复制代码最重要的一个方面的私人旅行。对所有代码以及每个类和方法所做的事情的完整浏览将会消耗一整本书,并且需要作者和复制开发者的共同努力。
这里的好消息是,我已经安排了这个教程,为您提供什么是日志事件、它们是如何编码的、它们在哪里编码以及它们如何工作以支持复制的实用知识。
在我们踏上这条荆棘之路之前,通读一下表 8-2 中 48 个复制源文件的概述以及每个文件包含的内容。我相信你会同意这是一份令人生畏的清单。这些文件中的大多数都在源代码树的/sql 文件夹中。
表 8-2 。复制源文件列表
| 源文件 | 描述 |
|---|---|
| binlog.h/。直流(DC) | 包含二进制日志代码 |
| log_event.h/。复写的副本 | 为每个定义日志事件和操作 |
| rpl_constants.h/。直流(DC) | 复制的全局常数和定义 |
| rpl_filter.h/.cc 版 | 实现用于二进制日志记录和复制的过滤器 |
| rpl_gtid_cache.cc | 类 Gtid_cache,保存线程当前提交的事务中的 Gtid。 |
| rpl_gtid_execution.cc | 从机如何重新执行 GTIDs 的逻辑。 |
| rpl_gtid.h | 所有 GTID 事物的类定义。 |
| rpl_gtid_misc.cc | 将 GTID 转换为字符串,反之亦然。 |
| rpl_gtid_mutex_cond_array.cc | 数据结构‘class Mutex _ cond _ array’,这是一个可增长的数组,其中每个元素包含一个互斥体和一个与该互斥体相关联的条件变量。 |
| rpl_gtid_owned.cc | 数据结构‘类 Gtid _ owned’,它保存 Gtid 所有权的当前状态。 |
| rpl_gtid_set.cc | 数据结构“类 Gtid_set”,它包含一组 Gtid。 |
| rpl_gtid_sid_map.cc | 数据结构“类 Sid_map”,它保存 Sid 和数字之间的双向映射。 |
| rpl_gtid_specification.cc | 数据结构“类 Gtid_specification”,它保存 GTID_NEXT 的数据类型,即 Gtid 或“匿名”或“自动”。 |
| rpl_gtid_state.cc | 数据结构“类 gtid_state”,它保存 Gtid 的全局状态,即提交的 Gtid 集(@ @ global . Gtid _ done/Gtid _ state::logged _ Gtid)、Gtid 所有权状态(@ @ global . Gtid _ owned/Gtid _ state::owned _ Gtid)、丢失的 Gtid(@ @ global . Gtid _ lost/Gtid _ state::lost _ Gtid) |
| rpl _ handler . h/cc | 处理程序接口的帮助函数 |
| rpl_info.h/。直流(DC) | 用于存储从属主机信息的基类 |
| rpl_info_dummy.h/.cc 版 | rpl_info.h 的虚拟版本 |
| rpl_info_factory.h/。复写的副本 | 工厂,用于生成工作线程和对类的引用,以操作从属主机的信息 |
| rpl_info_file.h/。直流(DC) | 主信息文件操作 |
| rpl _ info _ handler . h/cc | 对从设备的主设备信息进行存储、文件刷新和类似操作 |
| rpl_info_table_access.h/。复写的副本 | 对从机主信息的表级访问 |
| rpl_info_table.h/。直流(DC) | 从属主机信息的表级 I/O 操作 |
| rpl_info_values.h/。复写的副本 | 用于处理从从属主机信息中读取的值的类 |
| rpl_injector.h/。复写的副本 | 由 NDB(集群)存储引擎用于将事件注入二进制日志 |
| rpl_master.h/.cc 版 | 定义主角色的复制操作 |
| rpl_mi.h/.直流(DC) | 封装从设备的主设备信息 |
| rpl_record.h/。复写的副本 | 行记录方法 |
| rpl_record_old.h/。复写的副本 | 5.1 基于行的事件正式发布前旧格式的行记录方法 |
| rpl_reporting.h/。复写的副本 | 用于在二进制日志文件中报告错误和显示从属状态输出的基类 |
| rpl_rli.h/.直流(DC) | 中继日志信息处理 |
| S7-1200 可编程控制器。直流(DC) | 中继日志帮助器类,包括哈希和队列 |
| rpl_slave.h/。直流(DC) | 定义从属角色的复制操作 |
| rpl_tblmap.h/.直流(DC) | 实现表映射事件类型 |
| rpl_utility.h/。复写的副本 | 包含用于复制的杂项帮助器方法 |
| sql_binlog.h/。直流(DC) | SQL 语句 BINLOG,由 mysqlbinlog 生成,用于执行行事件和格式化描述日志事件。 |
您将在其中工作的主要文件是log_event.h/.cc文件。这些包含日志事件的所有代码。我将在下一节解释主要的日志事件类。还需要研究的是rpl_master.h/.cc文件。这是主角色的代码所在的位置,包括将事件写入二进制日志的代码。如果您正在开发一个解决方案来扩展主服务器角色的复制,请从这些文件开始研究。你可能也想检查一下rpl_slave.h/.cc的文件。这是在从属服务器上执行事件的代码所在的位置。当处理 slave 角色的扩展时,从这些文件开始研究。现在让我们开始浏览日志事件。
日志事件 解释
我们首先检查所有日志事件的基类- Log_event。该类包含通过二进制日志和中继日志存储、传送和执行事件所需的所有方法和属性。打开文件/sql/log_event.h,向下滚动到大约第 962 行,到Log_event类的开头。清单 8-5 显示了log_event.h文件中log-event类的摘录。
为了便于阅读,我省略了一些细节。代码中有很多文档,从枚举的解释到日志事件的字节布局的描述。如果您对这些和类似的细节感兴趣,请参阅源代码中的优秀文档。花点时间浏览一下这段代码。在这一章的后面,我将解释一些关于创建你自己的日志事件类时使用的关键方法的细节。从上面的清单中可以看出,这个类非常复杂。
注意有两个
log_event*头文件和类文件。log_event_old*文件是那些用于 RBR 事件格式的旧格式的日志事件类。在这一章中,我们将重点讨论更新的日志事件格式。作为练习,您可以自由检查旧的格式。
清单 8-5。 Log_event 类声明
class Log_event
{
public:
enum enum_skip_reason {
EVENT_SKIP_NOT,
EVENT_SKIP_IGNORE,
EVENT_SKIP_COUNT
};
protected:
enum enum_event_cache_type
{
EVENT_INVALID_CACHE= 0,
EVENT_STMT_CACHE,
EVENT_TRANSACTIONAL_CACHE,
EVENT_NO_CACHE,
EVENT_CACHE_COUNT
};
enum enum_event_logging_type
{
EVENT_INVALID_LOGGING= 0,
EVENT_NORMAL_LOGGING,
EVENT_IMMEDIATE_LOGGING,
EVENT_CACHE_LOGGING_COUNT
};
public:
typedef unsigned char Byte;
my_off_t log_pos;
char *temp_buf;
struct timeval when;
ulong exec_time;
ulong data_written;
uint32 server_id;
uint32 unmasked_server_id;
uint16 flags;
ulong slave_exec_mode;
enum_event_cache_type event_cache_type;
enum_event_logging_type event_logging_type;
ha_checksum crc;
ulong mts_group_idx;
Relay_log_info *worker;
ulonglong future_event_relay_log_pos;
#ifdef MYSQL_SERVER
THD* thd;
db_worker_hash_entry *mts_assigned_partitions[MAX_DBS_IN_EVENT_MTS];
Log_event(enum_event_cache_type cache_type_arg= EVENT_INVALID_CACHE,
enum_event_logging_type logging_type_arg= EVENT_INVALID_LOGGING);
Log_event(THD* thd_arg, uint16 flags_arg,
enum_event_cache_type cache_type_arg,
enum_event_logging_type logging_type_arg);
static Log_event* read_log_event(IO_CACHE* file,
mysql_mutex_t* log_lock,
const Format_description_log_event
*description_event,
my_bool crc_check);
static int read_log_event(IO_CACHE* file, String* packet,
mysql_mutex_t* log_lock, uint8 checksum_alg_arg);
static void init_show_field_list(List<Item>* field_list);
#ifdef HAVE_REPLICATION
int net_send(Protocol *protocol, const char* log_name, my_off_t pos);
virtual int pack_info(Protocol *protocol);
#endif /* HAVE_REPLICATION */
virtual const char* get_db()
{
return thd ? thd->db : 0;
}
#else // ifdef MYSQL_SERVER
Log_event(enum_event_cache_type cache_type_arg= EVENT_INVALID_CACHE,
enum_event_logging_type logging_type_arg= EVENT_INVALID_LOGGING)
: temp_buf(0), event_cache_type(cache_type_arg),
event_logging_type(logging_type_arg)
{ }
/* avoid having to link mysqlbinlog against libpthread */
static Log_event* read_log_event(IO_CACHE* file,
const Format_description_log_event
*description_event, my_bool crc_check);
/* print*() functions are used by mysqlbinlog */
virtual void print(FILE* file, PRINT_EVENT_INFO* print_event_info) = 0;
void print_timestamp(IO_CACHE* file, time_t* ts);
void print_header(IO_CACHE* file, PRINT_EVENT_INFO* print_event_info,
bool is_more);
void print_base64(IO_CACHE* file, PRINT_EVENT_INFO* print_event_info,
bool is_more);
#endif // ifdef MYSQL_SERVER ... else
uint8 checksum_alg;
static void *operator new(size_t size)
{
return (void*) my_malloc((uint)size, MYF(MY_WME|MY_FAE));
}
static void operator delete(void *ptr, size_t)
{
my_free(ptr);
}
static void *operator new(size_t, void* ptr) { return ptr; }
static void operator delete(void*, void*) { }
bool wrapper_my_b_safe_write(IO_CACHE* file, const uchar* buf,
ulong data_length);
#ifdef MYSQL_SERVER
bool write_header(IO_CACHE* file, ulong data_length);
bool write_footer(IO_CACHE* file);
my_bool need_checksum();
virtual bool write(IO_CACHE* file)
{
return(write_header(file, get_data_size()) ||
write_data_header(file) ||
write_data_body(file) ||
write_footer(file));
}
virtual bool write_data_header(IO_CACHE* file)
{ return 0; }
virtual bool write_data_body(IO_CACHE* file __attribute__((unused)))
{ return 0; }
inline time_t get_time()
{
if (!when.tv_sec && !when.tv_usec) /* Not previously initialized */
{
THD *tmp_thd= thd ? thd : current_thd;
if (tmp_thd)
when= tmp_thd->start_time;
else
my_micro_time_to_timeval(my_micro_time(), &when);
}
return (time_t) when.tv_sec;
}
#endif
virtual Log_event_type get_type_code() = 0;
virtual bool is_valid() const = 0;
void set_artificial_event() { flags |= LOG_EVENT_ARTIFICIAL_F; }
void set_relay_log_event() { flags |= LOG_EVENT_RELAY_LOG_F; }
bool is_artificial_event() const { return flags & LOG_EVENT_ARTIFICIAL_F; }
bool is_relay_log_event() const { return flags & LOG_EVENT_RELAY_LOG_F; }
bool is_ignorable_event() const { return flags & LOG_EVENT_IGNORABLE_F; }
bool is_no_filter_event() const { return flags & LOG_EVENT_NO_FILTER_F; }
inline bool is_using_trans_cache() const
{
return (event_cache_type == EVENT_TRANSACTIONAL_CACHE);
}
inline bool is_using_stmt_cache() const
{
return(event_cache_type == EVENT_STMT_CACHE);
}
inline bool is_using_immediate_logging() const
{
return(event_logging_type == EVENT_IMMEDIATE_LOGGING);
}
Log_event(const char* buf, const Format_description_log_event
*description_event);
virtual ∼Log_event() { free_temp_buf();}
void register_temp_buf(char* buf) { temp_buf = buf; }
void free_temp_buf()
{
if (temp_buf)
{
my_free(temp_buf);
temp_buf = 0;
}
}
virtual int get_data_size() { return 0;}
static Log_event* read_log_event(const char* buf, uint event_len,
const char **error,
const Format_description_log_event
*description_event, my_bool crc_check);
static const char* get_type_str(Log_event_type type);
const char* get_type_str();
#if defined(MYSQL_SERVER) && defined(HAVE_REPLICATION)
private:
enum enum_mts_event_exec_mode
{
EVENT_EXEC_PARALLEL,
EVENT_EXEC_ASYNC,
EVENT_EXEC_SYNC,
EVENT_EXEC_CAN_NOT
};
bool is_mts_sequential_exec()
{
return
get_type_code() == START_EVENT_V3 ||
get_type_code() == STOP_EVENT ||
get_type_code() == ROTATE_EVENT ||
get_type_code() == LOAD_EVENT ||
get_type_code() == SLAVE_EVENT ||
get_type_code() == CREATE_FILE_EVENT ||
get_type_code() == DELETE_FILE_EVENT ||
get_type_code() == NEW_LOAD_EVENT ||
get_type_code() == EXEC_LOAD_EVENT ||
get_type_code() == FORMAT_DESCRIPTION_EVENT||
get_type_code() == INCIDENT_EVENT;
}
enum enum_mts_event_exec_mode get_mts_execution_mode(ulong slave_server_id,
bool mts_in_group)
{
if ((get_type_code() == FORMAT_DESCRIPTION_EVENT &&
((server_id == (uint32) ::server_id) || (log_pos == 0))) ||
(get_type_code() == ROTATE_EVENT &&
((server_id == (uint32) ::server_id) ||
(log_pos == 0 /* very first fake Rotate (R_f) */
&& mts_in_group /* ignored event turned into R_f at slave stop */))))
return EVENT_EXEC_ASYNC;
else if (is_mts_sequential_exec())
return EVENT_EXEC_SYNC;
else
return EVENT_EXEC_PARALLEL;
}
Slave_worker *get_slave_worker(Relay_log_info *rli);
virtual List<char>* get_mts_dbs(MEM_ROOT *mem_root)
{
List<char> *res= new List<char>;
res->push_back(strdup_root(mem_root, get_db()));
return res;
}
virtual void set_mts_isolate_group()
{
DBUG_ASSERT(ends_group() ||
get_type_code() == QUERY_EVENT ||
get_type_code() == EXEC_LOAD_EVENT ||
get_type_code() == EXECUTE_LOAD_QUERY_EVENT);
flags |= LOG_EVENT_MTS_ISOLATE_F;
}
public:
bool contains_partition_info(bool);
virtual uint8 mts_number_dbs() { return 1; }
bool is_mts_group_isolated() { return flags & LOG_EVENT_MTS_ISOLATE_F; }
virtual bool starts_group() { return FALSE; }
virtual bool ends_group() { return FALSE; }
int apply_event(Relay_log_info *rli);
int update_pos(Relay_log_info *rli)
{
return do_update_pos(rli);
}
enum_skip_reason shall_skip(Relay_log_info *rli)
{
return do_shall_skip(rli);
}
virtual int do_apply_event(Relay_log_info const *rli)
{
return 0; /* Default implementation does nothing */
}
virtual int do_apply_event_worker(Slave_worker *w);
protected:
enum_skip_reason continue_group(Relay_log_info *rli);
virtual int do_update_pos(Relay_log_info *rli);
virtual enum_skip_reason do_shall_skip(Relay_log_info *rli);
#endif
};
首先,请注意这段代码中的条件编译指令。这些是必要的,因为 MySQL 源代码的其他部分使用了log-event类。例如,mysqlbinlog客户端应用将针对这段代码进行编译。因此,有些部分被标记为专门在服务器中使用(MYSQL_SERVER),有些部分被标记为如果不使用复制就要删除(HAVE_REPLICATION)。 3 编译mysqlbinlog等外部代码时,使用不符合这些指令的部分(#else)。
该类包括许多帮助函数,例如获取事件数据库的方法、创建和销毁实例(新建、删除)的方法等等。一些更有趣的助手方法是write_*()方法。这些用于将事件(也称为序列化)写入二进制日志,与负责将有效负载嵌入事件的pack_info()方法结合使用。
同样,也有从二进制日志文件中读取事件的方法。注意read_event()方法的重复出现。其中几个位于这个类中。这是因为,根据您读取事件的位置或方式,您可能需要不同形式的此方法。因此,此方法是重载的,调用哪个取决于参数的上下文。我们最感兴趣的是从二进制日志中读取后返回事件实例的版本。在本节的稍后部分,我们将看到这一点。
最后,服务器使用do_apply_event() 来执行事件。每种日志事件类型都有该方法的特定实现,因此需要使它和许多其他方法成为虚拟的。
请注意其他被标记为虚拟的方法。当您检查不同的日志事件时,您会看到这些事件的类实现要小得多,并且通常只包括那些在这里标记为虚拟的方法。
在下一节中,我将描述一些类型的日志事件以及每种日志事件的功能。接下来简单看一下日志事件的执行路径。
日志事件的类型
为从主设备向从设备复制命令定义了 30 多个日志事件。如果您检查二进制日志或中继日志,很可能无法找到所有这些事件。事实上,有些类是为特定格式(SBR 或 RBR)定义的,除非您使用混合二进制日志格式,否则您不会找到这两类事件。
SBR 赛事的基本看点是Query_log_event 。这包含用户在主服务器上发出的 SQL 语句。当应用此事件时,SQL 语句按编写的那样执行。查询使用的任何变量——包括用户定义的变量、随机数或与时间相关的值——在Query_log_event之前被写入二进制日志,因此在查询本身之前在从机上执行。这就是复制在主服务器和从服务器上保持时间戳、随机数、增量列和类似的特殊事件或代码相同的方式。例如,在随机数的情况下,主节点上使用的种子被传输到从节点,导致RAND函数在从节点上返回与主节点相同的值。
RBR 的情况有所不同。这些使用一个基类Rows_log_event 来封装所有 RBR 事件的基本功能。因为 RBR 事件只包含行图像或在主服务器上应用查询的结果,所以每种类型都有唯一的日志事件。有针对插入、更新(有一个 before image 事件)和删除的日志事件。
特别值得注意的是Incident_log_event 。此事件存储主服务器上遇到的任何异常情况或状态,如生成事件的错误、另一个错误或警告,从轻微到严重不等。创建此事件是为了在主服务器上发生异常情况时,从服务器在应用(执行)该事件时,可以决定该事件是否严重到需要停止复制。对复制进行故障排除时,请检查事件事件以了解更多详细信息。
表 8-3 显示了更频繁遇到的日志事件列表。我包括了对每个事件的描述,以及在这些事件可能出现的地方使用的二进制日志格式。
表 8-3。重要日志事件类型
| 事件类型 | 描述 | 格式 |
|---|---|---|
Ignorable_log_event | 忽略从属服务器上的事件,并且不将其写入中继日志以供执行。 | 全部 |
Incident_log_event | 记录异常事件,如主服务器上发生的错误或警告。会导致从属服务器停止复制。排除复制故障时,请检查这些事件。 | 全部 |
Intvar_log_event | 在 Query_log_event 之前创建,以包含查询使用的任何变量,如 LAST_INSERT_ID。 | 小触须(同 small bristles) |
User_var_log_event | 在 Query_log_event 之前创建,以包含查询定义和使用的任何用户变量。 | 小触须(同 small bristles) |
Query_log_event | 用户在主服务器上发出的查询。 | 小触须(同 small bristles) |
Rand_log_event | 从主服务器计算或传输一个随机种子。 | 小触须(同 small bristles) |
Rotate_log_event | 启动原木旋转。 | 全部 |
Rows_log_event | RBR 事件的基类。 | 规则的推理 |
Write_rows_log_event | 包括表的一个或多个插入或更新行。 | 规则的推理 |
Update_rows_log_event | 包括一个或多个行更新。 | 规则的推理 |
Delete_rows_log_event | 包括要删除的行图像。 | 规则的推理 |
Table_map_log_event | 信息性事件,包含有关当前正在被前面的事件修改或操作的表的信息。这些信息包括数据库名、表名和列定义。 | 规则的推理 |
Unknown_log_event | 用于捕获未知事件或在以后版本中定义的事件的虚拟事件。 | 全部 |
在接下来的部分中,我们将使用一个新的日志事件,该事件旨在将信息嵌入到二进制日志中,以便进行诊断。这个事件与从机无关,所以我们将创建一个类似于Ignorable_log_event的事件。
现在我们已经看到了一些更重要的事件类型,让我们来看看事件是如何在从属服务器上执行的。
日志事件的执行
通过首先从中继日志中读取日志事件并实例化类实例,从模块执行日志事件。这就是日志事件类型至关重要的地方。从机必须知道它正在读取什么类型的日志事件。当我们探索扩展复制时,您将会看到,每个日志事件都被分配了一个特殊的枚举(代码)。这就是从属服务器知道实例化哪个类的方法。读取日志事件并实例化它们的代码位于Log_event::next_event()方法 中的 rpl_slave.cc 中。
该方法包含一个无限循环,其工作是读取事件、检查先决条件、错误和管理中继日志的特殊命令(如 purge 和 rotate)以及实例化事件。
事件通过Log_event::read_log_event()方法实例化。该方法还将在创建事件实例之前进行错误检查(如校验和验证)。该代码位于log_event.cc中第 1386 行附近。如果您打开该文件并向下滚动该方法,您将看到用于实例化事件的 switch 语句。清单 8-6 显示了突出 switch 语句的方法摘录
***清单 8-6。***Log _ event::read _ Log _ event()方法
Log_event* Log_event::read_log_event(const char* buf, uint event_len,
const char **error,
const Format_description_log_event *description_event,
my_bool crc_check)
{
Log_event* ev;
...
if (alg != BINLOG_CHECKSUM_ALG_UNDEF &&
(event_type == FORMAT_DESCRIPTION_EVENT ||
alg != BINLOG_CHECKSUM_ALG_OFF))
event_len= event_len - BINLOG_CHECKSUM_LEN;
switch(event_type) {
case QUERY_EVENT:
ev = new Query_log_event(buf, event_len, description_event,
QUERY_EVENT);
break;
case LOAD_EVENT:
ev = new Load_log_event(buf, event_len, description_event);
break;
case NEW_LOAD_EVENT:
ev = new Load_log_event(buf, event_len, description_event);
break;
case ROTATE_EVENT:
ev = new Rotate_log_event(buf, event_len, description_event);
break;
case CREATE_FILE_EVENT:
ev = new Create_file_log_event(buf, event_len, description_event);
break;
case APPEND_BLOCK_EVENT:
ev = new Append_block_log_event(buf, event_len, description_event);
break;
case DELETE_FILE_EVENT:
ev = new Delete_file_log_event(buf, event_len, description_event);
break;
case EXEC_LOAD_EVENT:
ev = new Execute_load_log_event(buf, event_len, description_event);
break;
case START_EVENT_V3: /* this is sent only by MySQL <=4.x */
ev = new Start_log_event_v3(buf, description_event);
break;
case STOP_EVENT:
ev = new Stop_log_event(buf, description_event);
break;
case INTVAR_EVENT:
ev = new Intvar_log_event(buf, description_event);
break;
case XID_EVENT:
ev = new Xid_log_event(buf, description_event);
break;
case RAND_EVENT:
ev = new Rand_log_event(buf, description_event);
break;
case USER_VAR_EVENT:
ev = new User_var_log_event(buf, description_event);
break;
case FORMAT_DESCRIPTION_EVENT:
ev = new Format_description_log_event(buf, event_len, description_event);
break;
#if defined(HAVE_REPLICATION)
case PRE_GA_WRITE_ROWS_EVENT:
ev = new Write_rows_log_event_old(buf, event_len, description_event);
break;
case PRE_GA_UPDATE_ROWS_EVENT:
ev = new Update_rows_log_event_old(buf, event_len, description_event);
break;
case PRE_GA_DELETE_ROWS_EVENT:
ev = new Delete_rows_log_event_old(buf, event_len, description_event);
break;
case WRITE_ROWS_EVENT_V1:
ev = new Write_rows_log_event(buf, event_len, description_event);
break;
case UPDATE_ROWS_EVENT_V1:
ev = new Update_rows_log_event(buf, event_len, description_event);
break;
case DELETE_ROWS_EVENT_V1:
ev = new Delete_rows_log_event(buf, event_len, description_event);
break;
case TABLE_MAP_EVENT:
ev = new Table_map_log_event(buf, event_len, description_event);
break;
#endif
case BEGIN_LOAD_QUERY_EVENT:
ev = new Begin_load_query_log_event(buf, event_len, description_event);
break;
case EXECUTE_LOAD_QUERY_EVENT:
ev= new Execute_load_query_log_event(buf, event_len, description_event);
break;
case INCIDENT_EVENT:
ev = new Incident_log_event(buf, event_len, description_event);
break;
case ROWS_QUERY_LOG_EVENT:
ev= new Rows_query_log_event(buf, event_len, description_event);
break;
case SLAVE_CONNECT_LOG_EVENT:
ev= new Slave_connect_log_event(buf, event_len, description_event);
break;
case GTID_LOG_EVENT:
case ANONYMOUS_GTID_LOG_EVENT:
ev= new Gtid_log_event(buf, event_len, description_event);
break;
case PREVIOUS_GTIDS_LOG_EVENT:
ev= new Previous_gtids_log_event(buf, event_len, description_event);
break;
#if defined(HAVE_REPLICATION)
case WRITE_ROWS_EVENT:
ev = new Write_rows_log_event(buf, event_len, description_event);
break;
case UPDATE_ROWS_EVENT:
ev = new Update_rows_log_event(buf, event_len, description_event);
break;
case DELETE_ROWS_EVENT:
ev = new Delete_rows_log_event(buf, event_len, description_event);
break;
#endif
default:
/*
Create an object of Ignorable_log_event for unrecognized sub-class.
So that SLAVE SQL THREAD will only update the position and continue.
*/
if (uint2korr(buf + FLAGS_OFFSET) & LOG_EVENT_IGNORABLE_F)
{
ev= new Ignorable_log_event(buf, description_event);
}
else
{
DBUG_PRINT("error",("Unknown event code: %d",
(int) buf[EVENT_TYPE_OFFSET]));
ev= NULL;
}
break;
}
}
...
DBUG_RETURN(ev);
}
一旦事件被读取,从属代码调用Log_event::apply_event()方法,该方法又调用 log-event-class 实例的*_log_event::do_apply_event()。此方法负责执行事件。如前一节所述,所有日志事件类都有一个*_log_event::apply_event()方法。下面的清单 8-7 显示了该事件的一个示例实现。这显示了执行Intvar_log_event时执行的代码。
清单 8-7。 示例 do_apply_event()方法
/*
Intvar_log_event::do_apply_event()
*/
int Intvar_log_event::do_apply_event(Relay_log_info const *rli)
{
/*
We are now in a statement until the associated query log event has
been processed.
*/
const_cast<Relay_log_info*>(rli)->set_flag(Relay_log_info::IN_STMT);
if (rli->deferred_events_collecting)
return rli->deferred_events->add(this);
switch (type) {
case LAST_INSERT_ID_EVENT:
thd->stmt_depends_on_first_successful_insert_id_in_prev_stmt= 1;
thd->first_successful_insert_id_in_prev_stmt= val;
break;
case INSERT_ID_EVENT:
thd->force_one_auto_inc_interval(val);
break;
}
return 0;
}
请注意代码是如何设计来操作自动递增值的属性的。这就是从机如何确保为执行的查询正确设置自动增量值。这主要用于 SBR,因为 RBR 包含了主数据行的结果图像。
既然我们已经了解了日志事件是如何在从属服务器上执行的,现在是卷起袖子,再次深入源代码进行修改复制源代码的实验的好时机。接下来的部分将向您展示一些基本的方法,您可以通过这些方法来扩展复制以满足您独特的高可用性需求。
扩展复制
本节提供了一些示例项目,您可以使用它们来研究 MySQL 复制源代码。虽然有些人可能认为这些示例本质上是学术性的,但是您可能会发现它们很有用,可以作为您环境中实际应用的模板。
注意修改复制代码应该非常认真。它是最复杂的子系统之一,也是最健壮和可靠的子系统之一。确保你的修改是合理的。如果您的代码带来了副作用,或者更糟糕的是,导致服务器崩溃,那么复制的数据可能会过期或损坏。在任何生产环境中尝试使用它们之前,最好仔细计划您的修改并广泛测试它们。
如果上面的警告让你害怕,那是好的,因为复制子系统设计得很好,具有长期稳定的历史。显然,如果您想要使用复制或者依赖复制来实现备用、备份或高可用性,那么谨慎是合理的。
不要让这阻止你探索这些例子。事实上,有时候了解一件事的最好方法就是先打破它。如果你已经研究过前面章节的例子,你可能已经体验过了。
既然已经陈述了必要的告诫,让我们修改一些代码吧!
全局从站停止命令
我将从一个不太复杂的扩展开始。假设您有一个包含许多从属服务器的复制拓扑,这时您需要停止复制。在这种情况下,你必须访问每个从机并发出STOP SLAVE命令 。在主服务器上使用一个命令来告诉所有从服务器停止复制不是更容易吗?
这并不是说没有办法做到这一点——有几种方法。您可以锁定主服务器上的所有表,从而停止事件流。您也可以关闭二进制日志,但是这可能会导致从属服务器抛出错误。
然而,拜访每个奴隶可能还不够。假设有一个主动主服务器不断从客户端接收更新。还要考虑到不可能同时向每个从机发出停止从机命令。在访问每个从服务器之前,您仍然需要停止主服务器上的事件复制。
如果主设备上有一个命令同时被复制到每个从设备,这将确保所有从设备在复制过程中的同一点停止。
因此,如果有一个 SQL 命令——比方说,STOP ALL SLAVES——将STOP SLAVE命令复制到所有从站,那么您可以确保所有从站同时停止。让我们看看如何着手制定这样的命令。
代码修改
这个扩展需要修改解析器,并为 sql_parse.cc 中的大开关添加 case 语句。
表 8-4 。停止所有从属命令的文件已更改
| 文件 | 变更摘要 |
|---|---|
| sql/lex.h | 为新命令添加新符号 |
| sql/sql_cmd.h | 添加新的枚举 |
| sql/sql_yacc.yy | 为新命令添加新的解析器规则 |
| sql/sql_parse.cc | 为大开关添加新案例,以发送从属停止命令 |
现在我们知道了哪些文件需要更改,让我们开始修改。首先,打开sql/lex.h文件并添加以下代码。我们正在为命令添加新符号。该文件包含按字母顺序存储的符号数组。因此,我们将在第 523 行附近添加SLAVES符号。清单 8-8 描述了上下文中的修改。
清单 8-8。 向 sql/lex.h 添加奴隶符号
{ "SIGNED", SYM(SIGNED_SYM)},
{ "SIMPLE", SYM(SIMPLE_SYM)},
{ "SLAVE", SYM(SLAVE)},
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add SLAVES keyword */
{ "SLAVES", SYM(SLAVES)},
/* END CAB MODIFICATION */
{ "SLOW", SYM(SLOW)},
{ "SNAPSHOT", SYM(SNAPSHOT_SYM)},
{ "SMALLINT", SYM(SMALLINT)},
接下来,我们需要修改 sql_cmd.h 文件,为大交换机添加一个新的枚举。打开 sql_cmd.h 文件,找到文件顶部附近的 enum enum_sql_command 定义。清单 8-9 显示了为新命令添加新枚举的代码。
清单 8-9。 为停止所有奴隶命令添加枚举
SQLCOM_FLUSH, SQLCOM_KILL, SQLCOM_ANALYZE,
SQLCOM_ROLLBACK, SQLCOM_ROLLBACK_TO_SAVEPOINT,
SQLCOM_COMMIT, SQLCOM_SAVEPOINT, SQLCOM_RELEASE_SAVEPOINT,
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add SQLCOM_STOP_SLAVES enum */
SQLCOM_SLAVE_START, SQLCOM_SLAVE_STOP, SQLCOM_STOP_SLAVES,
/* END CAB MODIFICATION */
SQLCOM_BEGIN, SQLCOM_CHANGE_MASTER,
SQLCOM_RENAME_TABLE,
SQLCOM_RESET, SQLCOM_PURGE, SQLCOM_PURGE_BEFORE, SQLCOM_SHOW_BINLOGS,
接下来,我们需要添加一个新令牌用于新规则。同样,令牌列表是按字母顺序排列的。打开 sql_yacc.yy 文件,找到定义新标记的部分。在这种情况下,我们需要为新命令添加一个令牌定义。我们将把它命名为奴隶。清单 8-10 显示了要添加的上下文中的代码。您可以在第 1497 行附近找到此代码。
清单 8-10。 添加代币
%token SIGNED_SYM
%token SIMPLE_SYM /* SQL-2003-N */
%token SLAVE
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add SLAVES token */
%token SLAVES
/* END CAB MODIFICATION */
%token SLOW
%token SMALLINT /* SQL-2003-R */
%token SNAPSHOT_SYM
接下来,修改%type 定义所在的部分。我们需要将新的令牌添加到这个定义中。您可以在第 1813 行附近找到这一部分。清单 8-11 展示了上下文中的代码。
清单 8-11。 向无定义类型添加令牌
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add stop to list of NONE types */
repair analyze check start stop checksum
/* END CAB MODIFICATION */
field_list field_list_item field_spec kill column_def key_def
keycache_list keycache_list_or_parts assign_to_keycache
assign_to_keycache_parts
...
我们快完成了。接下来,我们向命令列表中添加一个新的命令定义,这样解析器就可以将控制指向新的规则。同样,这个列表是按字母顺序排列的。您可以在第 2035 行附近找到要修改的位置。清单 8-12 显示了修改。请注意,我们向要评估的新规则添加了一个新的“or”条件映射。在这种情况下,我们将规则命名为 stop。
清单 8-12。 添加新的规则定义
| show
| slave
| start
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add stop to list of statement targets */
| stop
/* END CAB MODIFICATION */
| truncate
| uninstall
| unlock
最后,我们将添加新规则来处理 STOP ALL SLAVES 命令。我将这段代码放在第 8027 行附近现有的 start 规则附近。清单 8-13 展示了新的规则。该规则只是将新的枚举保存到lex->sql_command属性中。这就是代码如何将规则的结果(以及命令的处理)映射到 big switch 到等于枚举值的情况。
清单 8-13。 向解析器添加停止所有奴隶规则
}
;
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add rule for STOP ALL SLAVES command */
stop:
STOP_SYM ALL SLAVES
{
LEX *lex= Lex;
lex->sql_command= SQLCOM_STOP_SLAVES;
}
;
/* END CAB MODIFICATION */
start:
START_SYM TRANSACTION_SYM opt_start_transaction_option_list
{
完成对 YACC 文件的更改后,我们可以为大开关添加一个新的案例,以确保命令一旦被解析器捕获,就被定向到代码,以将事件写入二进制日志。正常情况下,停止从命令不会被复制。我们的代码也需要覆盖这个限制。让我们添加 case 语句。打开 sql_parse.cc 文件,找到包含复制语句的部分。这是在 3054 号线附近。清单 8-14 显示了新的 case 语句。
清单 8-14。 增加了备自投大开关
mysql_mutex_unlock(&LOCK_active_mi);
break;
}
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add case statement for STOP ALL SLAVES command */
case SQLCOM_STOP_SLAVES:
{
if (!lex->no_write_to_binlog)
res= write_bin_log(thd, TRUE, "STOP SLAVE IO_THREAD", 20);
break;
}
/* END CAB MODIFICATION */
#endif /* HAVE_REPLICATION */
case SQLCOM_RENAME_TABLE:
花点时间检查一下这段代码。第一条语句旨在检查服务器是否能够写入二进制日志。如果是这样,我们添加一个新的日志事件,传递给它一个 SQL 命令STOP SLAVE。注意,我们使用了特定版本的STOP SLAVE命令。在这种情况下,我们只停止 IO 线程。这有两个原因。首先,我们只需要停止 IO 线程来停止将事件复制到从属线程。这仍然允许从线程处理从主线程读取的任何事件(就像SQL线程所做的那样)。第二,STOP SLAVE命令更复杂,因为它停止两个线程,这样做时,它有几个关键部分需要被互斥体保护。在执行日志事件(日志事件本身就是STOP SLAVE)的过程中执行 STOP SLAVE 命令会导致与互斥体的冲突。因此,只停止 IO 线程是在从机上成功执行命令的唯一方法。
编译代码
要编译新代码,只需从源代码树的根执行 make。由于 cmake 文件中没有新文件或更改,我们只需要重新构建可执行文件。如果有错误,返回并修复它们,直到服务器代码编译成功。
示例执行
执行全局 slave-stop 命令需要一个至少有一个从属服务器的复制拓扑。为了演示该命令的工作情况,我将创建一个使用三个从设备的拓扑。
第一步是建立一个简单的复制拓扑。我使用 MySQL 实用程序命令来快速设置我的测试条件,而不是按照上面列出的所有步骤来设置主服务器和从服务器。
我首先为主服务器和每个从服务器分别克隆一个正在运行的服务器的实例。mysqlserverclone 实用程序旨在创建一个关闭的或运行中的从服务器的新实例。我们只需使用 server 选项连接到服务器,为新服务器(mysqld)传递任何选项,并定义新的数据目录、端口和 server_id。清单 8-15 显示了这些步骤的结果。
清单 8-15。 设置简单的复制拓扑
cbell@ubuntu:$ mysqlserverclone.py --basedir=/source/mysql-5.6 \
--mysqld="--log-bin=mysql-bin" --new-port=3310 –new-data=/source/temp_3310 \
--new-id=100 --delete-data
# WARNING: Root password for new instance has not been set.
# Cloning the MySQL server located at /source/mysql-5.6.
# Creating new data directory...
# Configuring new instance...
# Locating mysql tools...
# Setting up empty database and mysql tables...
# Starting new instance of the server...
# Testing connection to new instance...
# Success!
# Connection Information:
# -uroot --socket=/source/temp_3310/mysql.sock
#...done.
cbell@ubuntu:$ mysqlserverclone.py --basedir=/source/mysql-5.6 \
--mysqld="--log-bin=mysql-bin --report-port=3311 --report-host=localhost" \
--new-port=3311 –new-data=/source/temp_3311 \
--new-id=101 --delete-data
# WARNING: Root password for new instance has not been set.
# Cloning the MySQL server located at /source/mysql-5.6.
# Creating new data directory...
# Configuring new instance...
# Locating mysql tools...
# Setting up empty database and mysql tables...
# Starting new instance of the server...
# Testing connection to new instance...
# Success!
# Connection Information:
# -uroot --socket=/home/cbell/source/temp_3311/mysql.sock
#...done.
cbell@ubuntu:$ mysqlserverclone.py --basedir=/source/mysql-5.6 \
--mysqld="--log-bin=mysql-bin --report-port=3312 --report-host=localhost" \
--new-port=3312 –new-data=/source/temp_3312 \
--new-id=102 --delete-data
# WARNING: Root password for new instance has not been set.
# Cloning the MySQL server located at /source/mysql-5.6.
# Creating new data directory...
# Configuring new instance...
# Locating mysql tools...
# Setting up empty database and mysql tables...
# Starting new instance of the server...
# Testing connection to new instance...
# Success!
# Connection Information:
# -uroot --socket=/home/cbell/source/temp_3311/mysql.sock
#...done.
cbell@ubuntu:$ mysqlserverclone.py --basedir=/source/mysql-5.6 \
--mysqld="--log-bin=mysql-bin --report-port=3313 --report-host=localhost" \
--new-port=3313 –new-data=/source/temp_3313 \
--new-id=103 --delete-data
# WARNING: Root password for new instance has not been set.
# Cloning the MySQL server located at /source/mysql-5.6.
# Creating new data directory...
# Configuring new instance...
# Locating mysql tools...
# Setting up empty database and mysql tables...
# Starting new instance of the server...
# Testing connection to new instance...
# Success!
# Connection Information:
# -uroot --socket=/home/cbell/source/temp_3311/mysql.sock
#...done.
一旦我们运行了主设备和从设备,我们就可以在主设备和每个从设备之间建立复制。mysqlreplicate 实用程序使从设备到主设备的连接只需一步。清单 8-16 显示了结果。请注意,该实用程序只需要一个到主机的连接和一个到从机的连接。
清单 8-16。 设置复制
cbell@ubuntu:$ mysqlreplicate.py --master=root@localhost:3310 \
--slave=root@localhost:3311
# master on localhost: ... connected.
# slave on localhost: ... connected.
# Checking for binary logging on master...
# Setting up replication...
# ...done.
cbell@ubuntu:$ mysqlreplicate.py --master=root@localhost:3310 \
--slave=root@localhost:3312
# master on localhost: ... connected.
# slave on localhost: ... connected.
# Checking for binary logging on master...
# Setting up replication...
# ...done.
cbell@ubuntu:$ mysqlreplicate.py --master=root@localhost:3310 \
--slave=root@localhost:3313
# master on localhost: ... connected.
# slave on localhost: ... connected.
# Checking for binary logging on master...
# Setting up replication...
# ...done.
什么是 MYSQL 实用程序?
MySQL 工具是 MySQL 工作台工具的子项目。MySQL Utilities 包含许多有用的命令行工具,用于管理 MySQL 服务器,重点是复制。
当您下载 Workbench 时,您还会获得 MySQL 实用程序。您可以通过 Workbench 中的插件访问这些实用程序。也可以直接从 Launchpad 下载 MySQL 实用程序。
MySQL 工作台下载:http://dev.mysql.com/downloads/workbench/5.2.html
从 Launchpad 下载 MySQL 实用程序:https://launchpad.net/mysql-utilities
有关 MySQL 实用程序的更多信息,请参见联机文档:
http://dev.mysql.com/doc/workbench/en/mysql-utilities.html
让我们回顾一下到目前为止的设置。上面的命令允许我从源代码树(由--basedir指定)中创建一个服务器的运行实例,向它传递参数,比如要使用的端口及其数据目录的位置。mysqlserverclone 实用程序将为我做所有的工作,然后告诉我如何连接到服务器。我这样做了两次:一次用于主设备,一次用于从设备,使用不同的端口和数据目录。然后,我使用自动化复制设置实用程序mysqlreplicate,在主设备和三个从设备之间设置复制。注意,我在--mysqld选项中为每个奴隶设置了--report-port和--report-host。此选项允许您指定服务器启动的选项。
正如你所看到的,这是非常容易和非常快速的设置。您甚至可以将这些命令放在脚本中,以便您可以在任何时候创建测试拓扑。
现在我们有了测试拓扑,让我们检查每个从机 IO 线程的状态,然后发出命令并再次检查状态。第一步是查看附属于主设备的从设备列表。为此,我使用了mysqlrplshow命令。清单 8-17 显示了该命令的输出;它打印出了我们的拓扑结构的漂亮图形。在此过程中,我们还显示了每个从机的从机状态,以确保每个从机都在主动复制主机的数据。为了简洁起见,我提供了输出的摘录。
清单 8-17。 检查拓扑
cbell@ubuntu:$ mysqlrplshow.py --master=root@localhost:3310
# master on localhost: ... connected.
# Finding slaves for master: localhost:3310
# Replication Topology Graph
localhost:3310 (MASTER)
|
+−−- localhost:3311 - (SLAVE)
|
+−−- localhost:3312 - (SLAVE)
|
+−−- localhost:3313 - (SLAVE)
cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3311
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 5
Server version: 5.6.6-m9-log Source distribution
Copyright (c) 2000, 2011, 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
*************************** 1\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: localhost
Master_User: rpl
Master_Port: 3310
Connect_Retry: 60
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 325
Relay_Log_File: clone-relay-bin.000002
Relay_Log_Pos: 283
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)
cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3312
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.6.6-m9-log Source distribution
Copyright (c) 2000, 2011, 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
*************************** 1\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: localhost
Master_User: rpl
Master_Port: 3310
Connect_Retry: 60
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 325
Relay_Log_File: clone-relay-bin.000002
Relay_Log_Pos: 283
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)
cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3313
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.6.6-m9-log Source distribution
Copyright (c) 2000, 2011, 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
*************************** 1\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: localhost
Master_User: rpl
Master_Port: 3310
Connect_Retry: 60
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 325
Relay_Log_File: clone-relay-bin.000002
Relay_Log_Pos: 283
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)
既然我们已经建立了新的拓扑并验证了一切正常,我们可以测试新的 STOP ALL SLAVES 命令了。清单 8-18 显示了结果。
清单 8-18。 演示停止所有奴隶的命令
cbell@ubuntu$ mysql -uroot -h 127.0.0.1 --port=3310
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 10
Server version: 5.6.6-m9-log Source distribution
Copyright (c) 2000, 2011, 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> STOP ALL SLAVES;
Query OK, 0 rows affected (0.00 sec)
你可能在想,“是这样吗?”对主人来说,是。什么也没发生,因为(回想一下大开关的代码)我们只将命令写入二进制日志。在主服务器上不做任何其他事情,它会不间断地继续执行。
当然,想要的效果是让奴隶停下来。让我们检查从设备上的从设备状态,看看是否确实发生了这种情况。记住,我们正在停止 IO 线程,所以我们应该在 slave-status 输出中寻找它。清单 8-19 显示了每个从状态的输出,为简洁起见,摘录如下。请注意,在每种情况下,从属 IO 线程确实已经停止(Slave_IO_Running = No)。
清单 8-19。 结果对奴隶发出停止所有奴隶的命令
cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3311
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.6.6-m9-log Source distribution
Copyright (c) 2000, 2011, 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
*************************** 1\. row ***************************
Slave_IO_State:
Master_Host: localhost
Master_User: rpl
Master_Port: 3310
Connect_Retry: 60
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 408
Relay_Log_File: clone-relay-bin.000002
Relay_Log_Pos: 366
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: No
Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)
cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3312
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.6.6-m9-log Source distribution
Copyright (c) 2000, 2011, 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
*************************** 1\. row ***************************
Slave_IO_State:
Master_Host: localhost
Master_User: rpl
Master_Port: 3310
Connect_Retry: 60
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 408
Relay_Log_File: clone-relay-bin.000002
Relay_Log_Pos: 366
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: No
Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)
cbell@ubuntu: $ mysql -uroot -h 127.0.0.1 --port=3313
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.6.6-m9-log Source distribution
Copyright (c) 2000, 2011, 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
*************************** 1\. row ***************************
Slave_IO_State:
Master_Host: localhost
Master_User: rpl
Master_Port: 3310
Connect_Retry: 60
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 408
Relay_Log_File: clone-relay-bin.000002
Relay_Log_Pos: 366
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: No
Slave_SQL_Running: Yes
...
1 row in set (0.00 sec)
此示例演示了如何创建一个独特的复制命令,该命令可以在主服务器上执行,并发送到所有从服务器,以停止复制事件流。如您所见,这个扩展很容易添加,并显示了二进制日志事件的强大功能。如果你发挥你的想象力,你可以想到其他有用的命令,你可能想发送给你所有的奴隶。
在下一个例子中,难度水平显著增加。我将向您展示如何创建一个新的日志事件,在二进制日志中记录信息。在这种情况下,我们创建一个新的日志事件,记录从设备何时连接到主设备。如果您需要确定何时或者是否有新的从站加入拓扑,这可能会很有用。
从机连接日志
精明的管理员知道定期检查复制拓扑是否有错误。这可以包括检查SHOW SLAVE STATUS 的结果,以及检查错误、从延迟和类似事件的值。遇到错误或必须诊断复制问题的管理员缺少一个关键数据点。在大型拓扑中,如果有许多从属服务器为了向外扩展而联机,则复制可能会脱机。这个过程可能一周发生几次,在某些情况下,可能一天发生多次。小型甚至中型复制拓扑的管理员可能会在从属服务器连接和断开时保留一个日志(至少在心里),但是对于大型复制拓扑来说,这可能是不可能的。在这种情况下,可能无法确定从设备何时连接到主设备,只能确定从设备是否连接到主设备。
在诊断和修复复制的情况下,了解从机连接的时间有助于将该事件的时间映射到同时发生的其他事件。也许这些事件是有关联的。如果你不知道从机何时连接到主机,你可能永远也不会知道。
在本节中,我们将创建一个新的日志事件,记录从设备何时连接到主设备。这将作为永久条目写入二进制日志。虽然它也被发送到从机,但我们使该事件成为一个可忽略的事件,允许从机跳过该事件。因此,如果使用了二进制日志记录,从属服务器将不会采取任何行动,不会将事件写入其中继日志,也不会将事件写入其二进制日志。
让我们从查看需要修改的源文件开始。虽然事件本身在设计和目的上非常简单,但是创建新事件所需的代码非常广泛,并且分散在整个复制源文件中。正如您将看到的,在主服务器和从服务器中有许多地方都处理了事件。
代码修改
此扩展将需要修改许多复制源文件。表 8-5 列出了需要修改的文件。所有这些文件都位于/sql 文件夹中。
表 8-5。从机连接事件的文件已更改
| 文件 | 变更摘要 |
|---|---|
| 日志 _ 事件. h | 新的Slave_connect_log_event类声明 |
| log_event.cc | 新的Slave_connect_log_event类定义 |
| binlog . h .- | 新的MYSQL_BIN_LOG::write_slave_connect()方法声明 |
| binlog.cc | 新的MYSQL_BIN_LOG::write_slave_connect()定义 |
| rpl_master.cc | 修改register_slave()以调用write_slave_connect() |
| rpl_rli(消歧义) | 添加对Slave_connect_log_event类的引用 |
| rpl_rli.cc | 添加删除Slave_connect_log_event类的代码以丢弃它 |
| rpl_rli_pdb.cc | 添加删除Slave_connect_log_event类的代码以丢弃它 |
| rpl_slave.cc | 添加删除Slave_connect_log_event类的代码以丢弃它 |
| sql_binlog.cc | 添加删除Slave_connect_log_event类的代码以丢弃它 |
乍看之下,这份修改清单似乎令人望而生畏。幸运的是,大多数源文件只需要很小的改动。主要工作是创建新的 log-event 类,并将其连接到主服务器的register_slave()方法。为了使它更容易,并保持与服务器代码(服务器的主要源文件)相同的一般隔离,不要直接创建日志事件;而是使用在MYSQL_BIN_LOG类中定义的方法。我将创建一个新的方法来做到这一点。
源代码中有几个地方我们需要丢弃新事件。我们镜像了Rows_query_log_event动作,这样我们就可以确保覆盖所有需要忽略和删除事件的情况。
现在我们知道了哪些文件需要更改,让我们开始修改。我们从新的日志事件开始。打开位于中的 log_event.h 文件。/sql 文件夹。首先,将新事件的新枚举添加到enum Log_event_type列表中。我们需要将枚举添加到列表末尾的ENUM_END_EVENT标记之前。将新条目命名为SLAVE_CONNECT_LOG_EVENT ,并赋予它列表中的下一个值。清单 8-20 显示了上下文中的修改。
提示这是源代码中大多数枚举列表中的标准机制。结束标记的使用允许循环限制以及越界检查。
清单 8-20。 为 log_event.h 中的新事件添加新的枚举
enum Log_event_type
{
/*
Every time you update this enum (when you add a type), you have to
fix Format_description_log_event::Format_description_log_event().
*/
UNKNOWN_EVENT= 0,
START_EVENT_V3= 1,
QUERY_EVENT= 2,
STOP_EVENT= 3,
ROTATE_EVENT= 4,
INTVAR_EVENT= 5,
LOAD_EVENT= 6,
...
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add new log event enumeration */
SLAVE_CONNECT_LOG_EVENT= 36,
/* END CAB MODIFICATION */
ENUM_END_EVENT /* end marker */
};
同样在 log_event.h 文件中,我们需要为新事件添加一个新的类声明。将事件命名为 Slave_connect_log_event,并从 log_event 基类派生它。将此代码添加到文件中包含其他日志事件类声明的部分。
您可以复制任何其他日志事件声明,并相应地更改名称。使用“搜索和替换”时要小心,因为您不想更改原始日志事件。清单 8-21 显示了完整的类声明。我将在本节后面更详细地描述每个元素。
清单 8-21。 在 log_event.h 中声明 Slave_connect_log_event 类
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add new log event class declaration */
class Slave_connect_log_event : public Log_event {
public:
#ifndef MYSQL_CLIENT
Slave_connect_log_event(THD *thd_arg, const char * query, ulong query_len)
: Log_event(thd_arg, LOG_EVENT_IGNORABLE_F,
Log_event::EVENT_STMT_CACHE,
Log_event::EVENT_IMMEDIATE_LOGGING)
{
DBUG_ENTER("Slave_connect_log_event::Slave_connect_log_event");
if (!(m_slave_connect= (char*) my_malloc(query_len + 1, MYF(MY_WME))))
return;
my_snprintf(m_slave_connect, query_len + 1, "%s", query);
DBUG_PRINT("enter", ("%s", m_slave_connect));
DBUG_VOID_RETURN;
}
#endif
#ifndef MYSQL_CLIENT
int pack_info(Protocol*);
#endif
Slave_connect_log_event(const char *buf, uint event_len,
const Format_description_log_event *descr_event);
virtual ∼Slave_connect_log_event();
bool is_valid() const { return 1; }
#ifdef MYSQL_CLIENT
virtual void print(FILE *file, PRINT_EVENT_INFO *print_event_info);
#endif
virtual bool write_data_body(IO_CACHE *file);
virtual Log_event_type get_type_code() { return SLAVE_CONNECT_LOG_EVENT; }
virtual int get_data_size()
{
return IGNORABLE_HEADER_LEN + 1 + (uint) strlen(m_slave_connect);
}
#if defined(MYSQL_SERVER) && defined(HAVE_REPLICATION)
virtual int do_apply_event(Relay_log_info const *rli);
#endif
private:
char *m_slave_connect;
};
/* END CAB MODIFICATION */
请注意,有些部分受到条件编译标志的保护。这是因为该代码与服务器源代码的其他部分共享。例如,它用在 mysqlbinlog 客户端工具中。显然,该应用不需要某些代码。因此,我们屏蔽掉条件编译不需要的部分。
我想提醒大家注意一面非常重要但不为人知的旗帜。注意 LOG_EVENT_IGNORABLE_F 标志是基类构造函数的第二个参数。顾名思义,这个标志告诉服务器忽略这个事件。二进制日志和中继日志代码旨在忽略任何带有此标志的事件。这意味着我们只需要处理写入二进制日志(和打印事件)的普通方法。在许多地方,我们还必须添加代码来跳过销毁事件实例,但幸运的是,我们有一个模型可以遵循。我将在本节稍后描述这些。
在类声明的底部,我放置了一个指针来包含将作为有效负载添加到日志事件中的消息。需要在类中定义的关键方法包括构造函数和析构函数、pack_info()、print()、write_body()和apply_event()。大多数剩下的方法都是不言自明的。注意get_type_code()和get_data_size(方法。当我们将代码添加到 log_event.cc 文件中时,我将解释其中的每一个。
现在我们有了类声明,打开位于。/sql 文件夹。首先,定位Log_event::get_type_str()方法 ,并为Slave_connect_log_event添加一个新的 case 语句。case 语句使用我们之前创建的枚举。这种方法在很多地方被用来描述视图中的事件,比如SHOW BINLOG EVENTS。清单 8-22 显示了上下文的变化。该代码位于第 686 行附近。
***清单 8-22。***Log _ event . cc 中 Log_event::get_type_str()的 Case 语句
case INCIDENT_EVENT: return "Incident";
case IGNORABLE_LOG_EVENT: return "Ignorable";
case ROWS_QUERY_LOG_EVENT: return "Rows_query";
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add case to return name of new log event */
case SLAVE_CONNECT_LOG_EVENT: return "Slave_connect";
/* END CAB MODIFICATION */
case WRITE_ROWS_EVENT: return "Write_rows";
case UPDATE_ROWS_EVENT: return "Update_rows";
case DELETE_ROWS_EVENT: return "Delete_rows";
我们还需要为新事件添加一个 case 语句到Log_event::read_log_event() 。这个方法负责创建一个新事件。它还使用前面定义的新枚举。添加一个新的 case 语句来创建一个Slave_connect_log_event类的新实例。清单 8-23 显示了上下文的变化。该代码位于第 1579 行附近。请注意,除了类名之外,它与其他事件的代码相同。
***清单 8-23。***Log _ event . cc 中 Log_event::read_log_event()的 Case 语句
case ROWS_QUERY_LOG_EVENT:
ev= new Rows_query_log_event(buf, event_len, description_event);
break;
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add case to create new log event */
case SLAVE_CONNECT_LOG_EVENT:
ev= new Slave_connect_log_event(buf, event_len, description_event);
break;
/* END CAB MODIFICATION */
case GTID_LOG_EVENT:
case ANONYMOUS_GTID_LOG_EVENT:
还有一个方法需要修改。Format _ description _ log _ event()返回每个日志事件的头长度。在这种情况下,我们需要返回新日志事件的头长度。清单 8-24 在上下文中显示了这段代码。该代码位于行号 5183 附近。在这种情况下,我们返回代码中已经定义的可忽略日志事件头的长度。
***清单 8-24。***log _ event . cc 中 Format_description_log_event()的新查找
post_header_len[HEARTBEAT_LOG_EVENT-1]= 0;
post_header_len[IGNORABLE_LOG_EVENT-1]= IGNORABLE_HEADER_LEN;
post_header_len[ROWS_QUERY_LOG_EVENT-1]= IGNORABLE_HEADER_LEN;
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Return header length for the new log event */
post_header_len[SLAVE_CONNECT_LOG_EVENT-1]= IGNORABLE_HEADER_LEN;
/* END CAB MODIFICATION */
post_header_len[WRITE_ROWS_EVENT-1]= ROWS_HEADER_LEN_V2;
post_header_len[UPDATE_ROWS_EVENT-1]= ROWS_HEADER_LEN_V2;
post_header_len[DELETE_ROWS_EVENT-1]= ROWS_HEADER_LEN_V2;
现在我们可以为类方法本身添加代码。我列出了清单 8-25 中的所有代码,然后解释了每个方法。
清单 8-25。??【Slave _ connect _ log _ event 方法 in log_event.cc
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Class method definitions for the new log event */
Slave_connect_log_event::Slave_connect_log_event(const char *buf,
uint event_len,
const Format_description_log_event *descr_event)
: Log_event(buf, descr_event)
{
DBUG_ENTER("Slave_connect_log_event::Slave_connect_log_event");
uint8 const common_header_len= descr_event->common_header_len;
uint8 const post_header_len=
descr_event->post_header_len[SLAVE_CONNECT_LOG_EVENT-1];
DBUG_PRINT("info",
("event_len: %u; common_header_len: %d; post_header_len: %d",
event_len, common_header_len, post_header_len));
/*
m_slave_connect length is stored using only one byte, but that length is
ignored and the complete query is read.
*/
int offset= common_header_len post_header_len 1;
int len= event_len - offset;
if (!(m_slave_connect= (char*) my_malloc(len1, MYF(MY_WME))))
return;
strmake(m_slave_connect, buf offset, len);
DBUG_PRINT("info", ("m_slave_connect: %s", m_slave_connect));
DBUG_VOID_RETURN;
}
Slave_connect_log_event::∼Slave_connect_log_event()
{
my_free(m_slave_connect);
}
#ifndef MYSQL_CLIENT
int Slave_connect_log_event::pack_info(Protocol *protocol)
{
char *buf;
size_t bytes;
ulong len= sizeof("# SLAVE_CONNECT = ") (ulong) strlen(m_slave_connect);
if (!(buf= (char*) my_malloc(len, MYF(MY_WME))))
return 1;
bytes= my_snprintf(buf, len, "# SLAVE_CONNECT = %s", m_slave_connect);
protocol->store(buf, bytes, &my_charset_bin);
my_free(buf);
return 0;
}
#endif
#ifdef MYSQL_CLIENT
void
Slave_connect_log_event::print(FILE *file,
PRINT_EVENT_INFO *print_event_info)
{
IO_CACHE *const head= &print_event_info->head_cache;
IO_CACHE *const body= &print_event_info->body_cache;
char *slave_connect_copy= NULL;
if (!(slave_connect_copy= my_strdup(m_slave_connect, MYF(MY_WME))))
return;
my_b_printf(head, "# Slave Connect:\n# %s\n", slave_connect_copy);
print_header(head, print_event_info, FALSE);
my_free(slave_connect_copy);
print_base64(body, print_event_info, true);
}
#endif
bool
Slave_connect_log_event::write_data_body(IO_CACHE *file)
{
DBUG_ENTER("Slave_connect_log_event::write_data_body");
/*
m_slave_connect length will be stored using only one byte, but on read
that length will be ignored and the complete query will be read.
*/
DBUG_RETURN(write_str_at_most_255_bytes(file, m_slave_connect,
(uint) strlen(m_slave_connect)));
}
#if defined(MYSQL_SERVER) && defined(HAVE_REPLICATION)
int Slave_connect_log_event::do_apply_event(Relay_log_info const *rli)
{
DBUG_ENTER("Slave_connect_log_event::do_apply_event");
DBUG_ASSERT(rli->info_thd == thd);
/* Set query for writing Slave_connect log event into binlog later.*/
thd->set_query(m_slave_connect, (uint32) strlen(m_slave_connect));
DBUG_ASSERT(rli->slave_connect_ev == NULL);
const_cast<Relay_log_info*>(rli)->slave_connect_ev= this;
DBUG_RETURN(0);
}
#endif
以下部分将更详细地描述每种方法。我解释了为什么我们需要这个方法,如何使用这个方法,以及代码中实现的任何细节。
Slave _ connect _ log _ event::Slave _ connect _ log _ event()
这是类实例的构造函数。这里需要特别注意的是,当写入二进制日志时,我们为事件的消息或负载分配内存。它也有设置头长度的代码。
Slave _ connect _ log _ event::∞Slave _ connect _ log _ event()
这是类实例的析构函数。这里,我们只是释放了在构造函数中分配的字符串。
slave _ connect _ log _ event::pack _ info()
此方法用于存储要写入二进制日志的事件数据。我们格式化一个字符串,使其包含一个描述事件的标签,以及一个包含连接到主服务器的从服务器的主机名、端口和 server_id 的字符串。
奴隶 _ 连接 _ 日志 _ 事件::打印()
客户端使用此方法以人类可读的形式打印事件。请注意条件编译指令,它确保代码只在客户端编译。这告诉我们这个方法与 pack_info()事件非常不同。
在这种方法中,打印头,后面是包含主机名、端口和 server_id 的类似格式的事件数据。在这个方法中,我们还复制了类中的字符串,以便客户端应用可以在类之外使用它。
slave _ connect _ log _ event::write _ data _ body()
此方法与 pack_info()方法结合使用。它会将事件数据写入二进制日志。
slave _ connect _ log _ event::do _ apply _ event()??
此方法用于将事件写入中继日志。由于该事件被忽略,因此不会被写入中继日志。我没有将这个方法留空,而是将它作为一个例子来完成,以防您希望创建自定义事件。在这种情况下,您可以使用这里的代码作为示例。
现在我们已经定义了类,我们可以向二进制日志类添加一个方法,可以更容易地从主代码中调用该方法。我们首先向类中添加一个新的方法声明。打开 binlog.h 文件,并在行号 541 附近找到 write_incident()事件。添加一个名为 write_slave_connect()的新方法,该方法带有当前线程实例、主机名、端口和 server_id 的参数。我们将使用最后三个参数作为 Slave_connect_log_event 类的信息性消息或负载。清单 8-26 显示了新的方法声明。
***清单 8-26。***binlog . h 中用于 write_slave_connect() 的方法声明
bool write_incident(Incident_log_event *ev, bool need_lock_log,
bool do_flush_and_sync= true);
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Method declaration for writing the slave connect event to the binlog */
bool write_slave_connect(THD *thd, char *host, int port, int server_id);
/* END CAB MODIFICATION */
void start_union_events(THD *thd, query_id_t query_id_param);
void stop_union_events(THD *thd);
bool is_query_in_union(THD *thd, query_id_t query_id_param);
现在我们可以将方法定义添加到 binlog.cc 文件中。与头文件一样,我们将把新方法放在 write_incident()方法附近。打开此文件,将此方法放在行号 5154 附近。清单 8-27 显示了完整的方法。
***清单 8-27。***binlog . cc 中的 write_slave_connect()方法定义
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add new method definition to write slave connect event to binlog */
bool MYSQL_BIN_LOG::write_slave_connect(THD *thd, char *host, int port, int server_id)
{
char buffer[255];
binlog_cache_data* cache_data= NULL;
DBUG_ENTER("MYSQL_BIN_LOG::write_slave_connect");
/* Record slave connection in the binary log */
sprintf(buffer, "Host: %s Port: %d Server_Id: %d", host, port, server_id);
Slave_connect_log_event ev(thd, buffer, (int)strlen(buffer));
if (thd->binlog_setup_trx_data())
DBUG_RETURN(1);
cache_data= &thd_get_cache_mngr(thd)->trx_cache;
if (cache_data->write_event(thd, &ev))
DBUG_RETURN(1);
cache_data->finalize(thd, NULL);
ordered_commit(thd, true);
DBUG_RETURN(0);
}
/* END CAB MODIFICATION */
在方法中,我们首先创建新事件的新实例,然后调用方法来设置事务,检索缓存管理器(用于将事件写入二进制日志缓存的设备),并写入事件。finalize()方法确保为事件设置了正确的标志,ordered_commit()方法完成事务,从而确保在将缓存刷新到磁盘时将事件写入二进制日志。
接下来,我们将为调用新的二进制日志方法的主服务器更改代码。rpl_master.cc 文件包含主设备的代码,包括从设备连接时使用的代码。当从机连接时,调用 register_slave()方法。我们将使用这个方法作为通过 write_slave_connect()方法启动 Slave_connect_log_event 的起点。
打开 rpl_master.cc 文件,在第 147 行附近找到 register_slave()方法。添加清单 8-28 中的代码。这段代码应该出现在互斥锁调用之后和 unregister_slave()调用之前。
清单 8-28。 对 rpl_master.cc 中的 register_slave()的修改
mysql_mutex_lock(&LOCK_slave_list);
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Write a new Slave_connect_log_event to binary log when slave connects */
/* If this is a new slave registration, log the slave connect message. */
if (my_hash_search(&slave_list, (uchar*)&thd->server_id, 4) == NULL)
{
DBUG_PRINT("info", ("Logging slave connect for host: %s", si->host));
mysql_bin_log.write_slave_connect(thd, si->host, si->port, si->server_id);
}
/* END CAB MODIFICATION */
unregister_slave(thd, false, false/*need_lock_slave_list=false*/);
res= my_hash_insert(&slave_list, (uchar*) si);
mysql_mutex_unlock(&LOCK_slave_list);
您可能会注意到这段代码有些奇怪。如果你和我一样,当我第一次查看register_slave()方法时,我得出结论,每次从设备连接到主设备时,该方法都会被调用一次。然而,我没有意识到的是,每次从设备向主设备请求数据时,都会调用这个方法。因此,register_slave()方法可以被多次调用。如果我们希望仅在从设备第一次连接时记录从设备连接事件,我们必须首先搜索从设备散列。如果我们找不到,我们可以打电话给write_slave_connect()。
现在我们有了新的事件类和二进制日志类中的新方法来编写事件,并将其链接到主代码,我们可以处理扩展的次要部分来完成这个特性。
我们从中继日志代码开始。打开rpl_rli.h文件并添加一个新的成员变量来包含该类的一个实例。当事件被忽略时,它仍然被从机的 IO 线程读取,并被继电器日志代码询问。因此,中继日志代码需要创建一个实例,以确定它是否可忽略。清单 8-29 显示了添加新变量的代码。这位于 480 号线附近,但是在Relay_log_info级的任何地方都可以。
清单 8-29。 为 rpl_rli.h 中的 Slave_connect_log_event 类添加新变量
bool deferred_events_collecting;
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Add a new variable for a Slave_connect_log_event instance */
Slave_connect_log_event* slave_connect_ev;
/* END CAB MODIFICATION */
/*****************************************************************************
接下来,我们添加代码,以便在 IO 线程读取事件时初始化新变量。打开 rpl_rli.cc 文件,找到构造函数 Relay_log_info::Relay_log_info()。添加新变量的初始化,并将其设置为 NULL。我们这样做是为了防止在尚未初始化(实例化)的类实例上调用方法或属性。这样做很好,但是需要在调用任何方法或属性之前检查变量是否为 NULL。清单 8-30 显示了添加到构造函数中的代码。
***清单 8-30。***rpl _ rli . cc 中 slave_connect_ev 变量的初始化
retried_trans(0),
tables_to_lock(0), tables_to_lock_count(0),
rows_query_ev(NULL), last_event_start_time(0), deferred_events(NULL),
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Initialize the slave_connect_ev variable */
slave_connect_ev(NULL), slave_parallel_workers(0),
/* END CAB MODIFICATION */
recovery_parallel_workers(0), checkpoint_seqno(0),
checkpoint_group(opt_mts_checkpoint_group),
recovery_groups_inited(false), mts_recovery_group_cnt(0),
接下来要修改的几个文件主要是将新事件添加到现有的销毁实例和错误处理代码中。我选择将这段代码放在与 Rows_query_log_event 相同的位置。这是一个不错的选择,因为 Rows_query_log_event 也是一个可忽略的事件。
同样在 rpl_rli.cc 文件中,我们需要添加代码来销毁实例(如果它之前被实例化的话)。我们在 cleanup_context()方法 中这样做。清单 8-31 显示了删除(销毁)实例的代码。注意,我们首先检查实例是否已经被实例化。这避免了在试图销毁不存在的类实例时出现特别严重的错误。
清单 8-31。 代码销毁 rpl_rli 中 cleanup_context()中的变量..复写的副本
rows_query_ev= NULL;
info_thd->set_query(NULL, 0);
}
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Destroy the Slave_connect_log_event instance */
if (slave_connect_ev)
{
delete slave_connect_ev;
slave_connect_ev= NULL;
info_thd->set_query(NULL, 0);
}
/* END CAB MODIFICATION */
m_table_map.clear_tables();
slave_close_thread_tables(thd);
下一个要更改的文件是rpl_rli_pdb.cc。打开文件,找到文件中的最后一个方法slave_worker_exec_job() 。我们需要在该方法的错误处理代码中添加另一个排除。清单 8-32 显示了新的变化。注意,我们以与Rows_query_log_event 事件相同的方式添加了另一个条件。
清单 8-32。 添加条件删除 rpl_rli_pdb.cc 中的 Slave_connect_log_event
// todo: simulate delay in delete
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Destroy the Slave_connect_log_event instance */
if (ev && ev->worker && ev->get_type_code() != ROWS_QUERY_LOG_EVENT &&
ev->get_type_code() != SLAVE_CONNECT_LOG_EVENT)
/* END CAB MODIFICATION */
{
delete ev;
}
接下来,我们将新事件添加到 rpl_slave.cc 代码中的另一个排除中。打开文件,并在 exec_relay_log_event()方法中找到执行后破坏事件的条件。这位于 3654 号线附近。清单 8-33 显示了这种情况下所需的改变。注意,我们在 if 语句中添加了另一个条件。
清单 8-33。 从 rpl_slave.cc 中的销毁条件中排除 Slave_connect_log_event
clean-up routine.
*/
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Exclude the Slave_connect_log_event from destruction */
if (ev->get_type_code() != FORMAT_DESCRIPTION_EVENT &&
ev->get_type_code() != ROWS_QUERY_LOG_EVENT &&
ev->get_type_code() != SLAVE_CONNECT_LOG_EVENT)
/* END CAB MODIFICATION */
{
DBUG_PRINT("info", ("Deleting the event after it has been executed"));
delete ev;
最后要修改的文件是 sql_binlog.cc,有两个地方需要修改代码。首先,我们必须将 SLAVE_CONNECT_LOG_EVENT 添加到排除项中,以便删除已经执行的事件。其次,我们必须添加代码,以便在出现错误时销毁新的事件实例。
打开 sql_binlog.cc 文件,找到第 292 行附近的 Rows_query_log_event 的错误条件,并进行如清单 8-34 所示的更改。请注意,我们只是在 if 语句中添加了另一个条件。
清单 8-34。 从 sql_binlog.cc 的销毁条件中排除 Slave_connect_log_event
of the event.
*/
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Exclude the Slave_connect_log_event from destruction */
if (ev->get_type_code() != FORMAT_DESCRIPTION_EVENT &&
ev->get_type_code() != ROWS_QUERY_LOG_EVENT &&
ev->get_type_code() != SLAVE_CONNECT_LOG_EVENT)
/* END CAB MODIFICATION */
{
delete ev;
ev= NULL;
我们必须在错误条件中添加一个条件,在出现错误的情况下销毁新事件。和前面的例子一样,我们将模拟 Rows_query_log_event 的代码。这位于 320 号线附近。清单 8-35 显示了新的代码。我们检查错误,如果有错误并且新事件存在(指针不为空或 0),我们删除实例。
清单 8-35。 如果 sql_binlog.cc 中的错误条件则销毁 Slave_connect_log_event
delete rli->rows_query_ev;
rli->rows_query_ev= NULL;
}
/* BEGIN CAB MODIFICATION */
/* Reason for Modification: */
/* Destroy the Slave_connect_log_event instance if there is an error */
if ((error || err) && rli->slave_connect_ev)
{
delete rli->slave_connect_ev;
rli->slave_connect_ev= NULL;
}
/* END CAB MODIFICATION */
rli->slave_close_thread_tables(thd);
}
thd->variables.option_bits= thd_options;
这就完成了对代码的修改,将新的可忽略事件添加到二进制日志的事件列表中。显然,这不是一个微不足道的变化,日志事件涉及到几个源代码文件。
您现在已经准备好编译代码了。检查你修改的所有文件的正确性。您还应该从源代码树的根开始编译服务器,这样所有组件都可以根据更改进行构建。这是必要的,以便 mysqlbinlog 客户端应用可以解密新事件。
编译代码
现在,编译服务器代码。如果所有的修改都如清单所示,代码应该可以编译了。如果没有,返回并检查修改。下一节演示新事件的运行。
示例执行
在本节中,我们将看到新日志事件的运行演示。无论在主服务器还是从服务器上,都没有多少输出,因为日志事件隐藏在复制中。但是,有一个 SQL 命令 SHOW BINLOG EVENTS ,可以用来显示服务器上二进制日志事件的摘录。我们之前讨论过的一个客户端工具 mysqlbinlog 可以用来检查二进制或中继日志的内容。
我们正在建立一个测试环境。不像在本章的第一个例子中那样使用实用程序,我将向您展示一个有趣的技巧,使用现有的服务器测试环境快速设置主服务器和从服务器。
我们同时也想开始一个奴隶。如果您提供一个测试套件,特别是复制套件,mysql-test-run 将创建额外的服务器。对于- suite=rpl,mysql-test-run 将创建两个预配置用于复制的服务器。
显然,这比为主服务器和从服务器启动一个新实例要容易得多。对于需要一个主机和一个从机的情况,我们可以使用一个命令,而不是两个命令——这已经比手动启动 mysqld 实例更容易了。清单 8-36 显示了一个使用 mysql-test-run 来启动主服务器和从服务器的例子。
清单 8-36。 开始一个新的主从使用 MTR
cbell@ubuntu:$ ./mysql-test-run.pl --start-and-exit --suite=rpl
Logging: ./mysql-test-run.pl --start-and-exit --suite=rpl
121003 18:26:01 [Note] Plugin 'FEDERATED' is disabled.
121003 18:26:01 [Note] Binlog end
121003 18:26:01 [Note] Shutting down plugin 'CSV'
121003 18:26:01 [Note] Shutting down plugin 'MyISAM'
MySQL Version 5.6.6
Checking supported features...
- skipping ndbcluster
- SSL connections supported
- binaries are debug compiled
Using suites: rpl
Collecting tests...
Checking leftover processes...
Removing old var directory...
Creating var directory '/mysql-test/var'...
Installing system database...
Using server port 54781
==============================================================================
TEST RESULT TIME (ms) or COMMENT
------------------------------------------------------------------------------
worker[1] Using MTR_BUILD_THREAD 300, with reserved ports 13000..13009
worker[1]
Started [mysqld.1 - pid: 2300, winpid: 2300] [mysqld.2 - pid: 2330, winpid: 2330]
worker[1] Using config for test rpl.rpl_000010
worker[1] Port and socket path for server(s):
worker[1] mysqld.1 13000 /mysql-test/var/tmp/mysqld.1.sock
worker[1] mysqld.2 13001 /mysql-test/var/tmp/mysqld.2.sock
worker[1] Server(s) started, not waiting for them to finish
我们现在可以使用 mysqlreplicate 实用程序快速地将从设备连接到主设备,并开始复制数据。清单 8-37 显示了运行这个实用程序的输出。
清单 8-37。 设置复制
cbell@ubuntu:$ python ./scripts/mysqlreplicate.py --master=root@localhost:13000 --slave=root@localhost:13001
# master on localhost: ... connected.
# slave on localhost: ... connected.
# Checking for binary logging on master...
# Setting up replication...
# ...done.
你在期待什么吗?也许是一个信息或无处不在的“冰”声?这些都不应该发生。复制背后的代码是广泛而稳定的,并且大部分是完全静默的。要了解复制发生了什么,以及在这种情况下刚刚发生了什么,您必须查询主服务器或从服务器的状态。
如果您还记得,当从机连接时,扩展应该向主机上的二进制日志中写入一个日志事件。因为我们用 mysqlreplicate 设置了复制,所以没有显示任何内容。我们必须去大师那里看看这个事件是否真的发生了。
提示如果你在控制台模式下启动服务器,你会在控制台中看到任何消息和错误。这包括来自复制的错误和警告。例如,如果从主机读取或执行事件时出现问题,从主机将显示与连接到主机相关的消息和一个错误。通过直接启动 mysqld 可执行文件,可以在 Linux 和 Mac 系统上以控制台模式启动服务器。在 Windows 上,您必须使用- console 选项。
检查从机的错误可能是个好主意。在从机上执行SHOW SLAVE STATUS并检查错误。我把这个留给你作为练习。你应该看到的是一个正常健康的奴隶。
让我们回到主人身上。SHOW BINLOG EVENTS命令将显示写入二进制日志的最新事件。如果Slave_connect_log_event工作正常,您应该在视图中看到它。清单 8-38 显示了在主服务器上运行SHOW BINLOG EVENTS的结果。记住,这个命令只显示第一个二进制日志中的事件。使用IN子句指定特定的二进制日志。
注意在任何没有启用二进制日志记录的服务器上运行 SHOW BINLOG 事件将导致空的结果集。如果您在对这段代码运行测试时看到这种情况,请检查以确保您不是在从属代码上运行。
清单 8-38。 二进制主机上的日志事件
cbell@ubuntu:$ mysql -uroot -h 127.0.0.1 --port=13000
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 4
Server version: 5.6.6-m9-debug-log Source distribution
Copyright (c) 2000, 2011, 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 binlog events \G
*************************** 1\. row ***************************
Log_name: master-bin.000001
Pos: 4
Event_type: Format_desc
Server_id: 1
End_log_pos: 121
Info: Server ver: 5.7.0-m10-debug-log, Binlog ver: 4
*************************** 2\. row ***************************
Log_name: master-bin.000001
Pos: 121
Event_type: Query
Server_id: 1
End_log_pos: 326
Info: GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'localhost'
*************************** 3\. row ***************************
Log_name: master-bin.000001
Pos: 326
Event_type: Slave_connect
Server_id: 2
End_log_pos: 390
Info: # SLAVE_CONNECT = Host: 127.0.0.1 Port: 13001 Server_Id: 2
3 rows in set (0.00 sec)
mysql>
注意,上面的清单在输出中显示了新的日志事件。我们看到事件的有效负载(Info 字段)包括从属服务器的主机名、端口和 server_id。它显示了环回地址,因为复制拓扑(主和从)运行在本地机器上(localhost = 127.0.0.1)。
现在,让我们检查 mysqlbinlog 客户端应用的输出。仅使用主服务器的二进制日志作为唯一参数来运行应用。因为我使用 mysql-test-run 设置了服务器,所以我在/mysql-test/var 文件夹下找到了这些文件。在本例中,我在寻找主服务器的二进制文件,主服务器是第一台服务器,因此,文件夹名为 mysqld.1,数据目录名为 data。因此,mysql-test 的相对路径是。/var/mysqld.1/data。
清单 8-39 显示了 mysqlbinlog 客户端应用在测试拓扑中转储主服务器的二进制日志的输出。
清单 8-39。 用 mysqlbinlog 检查主服务器上的二进制日志
cbell@ubuntu:$ ../client/mysqlbinlog ./var/mysqld.1/data/master-bin.000001
/*!40019 SET @@session.max_insert_delayed_threads=0*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#121003 16:03:59 server id 1 end_log_pos 121 CRC32 0x83c06bae Start: binlog v 4, server v 5.6.6-m9-debug-log created 121003 16:03:59 at startup
# Warning: this binlog is either in use or was not closed properly.
ROLLBACK/*!*/;
BINLOG '
X8RsUA8BAAAAdQAAAHkAAAABAAQANS43LjAtbTEwLWRlYnVnLWxvZwAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAABfxGxQEzgNAAgAEgAEBAQEEgAAXQAEGggAAAAICAgCAAAACgoKGRkAAAGu
a8CD
'/*!*/;
# at 121
#121003 16:05:23 server id 1 end_log_pos 326 CRC32 0x559a612a Query thread_id=1 exec_time=0 error_code=0
SET TIMESTAMP=1349305523/*!*/;
SET @@session.pseudo_thread_id=1/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1073741824/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C latin1 *//*!*/;
SET @@session.character_set_client=8,@@session.collation_connection=8,@@session.collation_server=8/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'localhost'
/*!*/;
# at 326
# Slave Connect:
# Host: 127.0.0.1 Port: 13001 Server_Id: 2
#121003 16:05:23 server id 2 end_log_pos 390 CRC32 0x262bb144 DELIMITER ;
# End of log file
ROLLBACK /* added by mysqlbinlog */;
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
输出可能看起来像一堆随机的、神奇的字符串和位,但是相信我,这一切都是有意义的。在列表底部附近,您可以看到 Slave_connect_log_event 的结果。请记住,mysqlbinlog 客户端应用使用 Slave_connect_log_event::print()方法来显示二进制日志事件。print()方法被设计为为事件打印两行:标题和有效负载,显示从服务器的主机名、端口和 server_id。
如果这个二进制日志来自一个有许多从服务器的主服务器,其中一些从服务器已经被添加、删除和再次添加,那么运行 mysqlbinlog 客户端应用的输出将显示所有的从服务器连接事件。事实上,您可以通过管道将输出传递给诸如grep这样的程序,以便在文件中找到' # Slave Connect:'行的所有位置。
除了这个实用程序之外,复制特性的这个扩展还演示了日志事件代码的位置,以及如何将日志事件写入二进制日志和从二进制日志中读取日志事件。我希望这个练习已经为您提供了一些解决您自己独特的高级复制挑战的思路。
摘要
在这一章中,我介绍了 MySQL 复制的一个简短教程,包括为什么你会使用它,它的架构,以及复制源代码之旅。您了解了如何设置复制以及几种扩展 MySQL 复制特性集的方法。
在下一章中,我将展示 MySQL 的另一个强大特性——可插拔架构。我通过探索另一个高级特性——可插拔身份验证来探索这个体系结构。我将简要介绍 MySQL 中的可插拔工具,并展示一个使用 RFID 标签进行用户验证的示例身份验证插件。
有些人觉得这些角色的名字带有贬义,并暗示它们在某些文化中可能具有攻击性。因此,在未来,这些名称可能会改变,但服务器的角色或工作不太可能改变。
2 MySQL 有一个多线程从机特性,实现多线程读取中继日志,从而提高从机在某些用例中的性能。关于这个特性的更深入的讨论超出了本文的范围,但是可以在在线参考手册中找到。
3HAVE _ REPLICATION条件编译主要用于嵌入式服务器。