复制的管理与维护

监视复制状态

复制增加了监视 MySQL 的复杂程度。虽然复制事实上在上都在发生,但大部分工作还是上进行的,所以多数问题都会出在上。

所有的复制都在工作吗?有没有发生了错误?最慢的落后了多远?MySQL 可以提供多数信息来回答这些问题,但要实现监视的自动化,使复制变得更加强劲,则需要管理员的工作。

查看当前二进制日志的位置和配置

在主服务器上,可以使用 SHOW MASTER STATUS 来查看当前二进制日志的位置和配置

查看当前磁盘中有哪些日志文件

mysql> SHOW MASTER LOGS;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000220 | 425605 |
| mysql-bin.000221 | 1134128 |
| mysql-bin.000222 | 13653 |
| mysql-bin.000223 | 13634 |
+------------------+-----------+

该信息可用于判断应该给 PURGE MASTER LOGS 命令什么参数。

查看日志事件

mysql> SHOW BINLOG EVENTS;
+------------+-----+----------------+-----------+-------------+-----------------------------------+
| Log_name   | Pos | Event_type     | Server_id | End_log_pos | Info                              |
+------------+-----+----------------+-----------+-------------+-----------------------------------+
| bin.000001 |   4 | Format_desc    |        10 |         124 | Server ver: 8.0.11, Binlog ver: 4 |
| bin.000001 | 124 | Previous_gtids |        10 |         155 |                                   |
| bin.000001 | 155 | Stop           |        10 |         178 |                                   |
+------------+-----+----------------+-----------+-------------+-----------------------------------+
3 rows in set (0.00 sec)

因为我用的是两个虚拟机,而且并没有创建任何数据库,所以当前的事件很少。

查看特定位置的事件

通过以上命令可以得到某个事件的具体位置,即 Pos 字段的值。继而可以详细查看该位置的事件:

mysql> SHOW BINLOG EVENTS IN 'mysql-bin.000223' FROM 13634\G
*************************** 1. row ***************************
Log_name: mysql-bin.000223
Pos: 13634
Event_type: Query
Server_id: 1
End_log_pos: 13723
Info: use `test`; CREATE TABLE test.t(a int)

计算复制延迟

最常需要监视的就是,一个落后了多远。虽然 SHOW SLAVE STATUS 返回的 Seconds_Behind_Master 字段理论上可以查看的延迟,事实上这个数字并不总是准确的,原因如下:

  • 从服务器是通过把自己当前的时间戳与二进制日志事件中记录的时间戳相比较,来计算 Seconds_Behind_Master 的,因此,从服务器只有进行一次查询才能进行计算。
  • 如果复制进程没有运行,从服务器通常会返回 NULL
  • 主从之间不匹配的 max_allowed_packet 设置,或不稳定的网络,这样的错误会导致复制被破坏,线程被终止,而 Seconds_Behind_Master 却只会显示为 0,而不会提示错误。
  • 即使有些时候复制进程明明正常运行,从服务器仍然无法计算延迟,此时,它返回的要么是 0,要么是 NULL
  • 一个非常长的事务会导致计算出来的延迟产生波动。例如,某个更新数据的事务用了一个小时,然后才提交,于是在真正发生数据更新之后的一个小时,日志才被更新。当处理该语句时,此时的报告会是自己落后一小时,处理完成后立即变成落后 0 秒。
  • 如果一个分主落后了,它下面的都与它进度相同,这些会跟它说我们当前零延迟,它们并不知道自己与 的延迟是多少。

计算延迟的最好办法 - heartbeat

这些问题的解决办法就是忽略 Seconds_Behind_Master,直接用一些可以观察和测量的手段来监视复制延迟。

最好的办法是 heartbeat record,是指在主服务器上 每秒钟更新一次的时间戳,就像心跳一样。计算延迟时,只需要在上用 当前时间戳减掉心跳时间 就行了。

这种方法可以避免以上所有问题,还有额外的好处:可以创建一个时间戳,用来查看的数据当前在什么时间点上。

pt-heartbeat 脚本包含于 Percona Toolkit,是复制心跳的最流行的实现。

心跳的另一好处:保存在二进制日志中的复制心跳记录可以用于多个操作,比如灾难恢复。

判断从与主是否一致

理论上讲,服务器始终应该是的完美副本,但实际使用中,复制中的错误会造成上的数据变的不同步。即使没有明显的错误,也仍然有可能不同步,原因有可能是 MySQL 的某些功能无法正确复制,或是因为 MySQL 的 bug,或是网络问题,系统崩溃,非正常关机等其他因素。

检查主从的数据是否一致应该被作为一项例行工作,如果要用复制做为备份的话,这一点尤其重要。

MySQL 没有内置的办法来判断 主从 是否同步。

pt-table-checksum

Percona Toolkit 有一个工具叫 pt-table-checksum 可解决该问题。它的主要特点是,它可以确认某个是否与其同步,它通过在上运行 INSERT ... SELECT 查询来实现。

这些查询会对数据进行检验,然后把结果插入表格。这些语句在复制链上流动,在上再次执行。之后就可以把上运行的结果进行比较,看是否有区别。因为这个过程是空越复制链进行的,所以它会得到一致的结果,无需同时锁定两端服务器。

通常的做法是在上运行,使用以下参数:

$ pt-table-checksum --replicate=test.checksum <master_host>

该命令会校验所有的表格,并把结果插入 test.checksum 表格中。

所有的都执行完查询以后,可以用一个简单的查询来检查每个的区别。

pt-table-checksum 能够发现每个从服务器,并在每个上面都运行查询,然后自动输出结果。

从主服务器重新同步

实际生产中,有时会需要处理失去同步的从服务器,也许使用检验技术找到了差异,也许发现忽略了一个查询,或上的数据被人为修改了。

全盘重新复制

要想修复失去同步的从服务器,通常的建议是放弃,从主服务器重新复制。如果数据不一致的对你来说是个很严重的问题,一旦发现就应该将其停止,并尽快从生产中撤下来。然后找个备份专门为其恢复数据。

但该方法很不方便,尤其是在数据量特别大的情况下。

使用 mysqldump 进行局部复制

如果能找到哪些数据存在差异,就能比全盘复制更高效地复制。如果发现的不一致并不是特别严重,也许不用动它,直接在线把受影响的数据同步就行了。

最简单的解决办法是使用 mysqldump ,仅针对受影响的数据进行复制并重新加载,只要这期间该数据没有发生改变,这种方法就很管用。只需要在上锁定表格,然后复制表格,等待同步完成,再在上导入该表格即可。

虽然这种方法在很多情况下很管用,但如果是特别繁忙的服务器就无法完成了。

该方法还有一个缺点,就是复制是在复制链之外进行的。我们知道,只有在复制链上从上至下地复制才是最安全的,因为可以避免发生竞争和其它意外。如果表格很大,网络带宽却很有限,复制和重新加载也会带来过高的开销。假如一个有百万行的表格中,每一千行就有一个差异的话,复制、重载整个表格将带来特别大的开销。

使用 pt-table-sync

pt-table-sync 能够高效地查找并解决表格之间的差异,还能穿越复制链进行,通过在中执行查询来重新同步,这样,就不会发生竞争。

它还集成了由 pt-table-checksum 创建的校验表格,以便只在有差异的表格的位置进行操作。

但它并不适用于所有的场景,它需要保证复制处于正常运行状态,以便正确地同步主从。因此,如果发生了复制错误,它就无法正常工作。

它不适用于特别大的数据,因为这会难以避免地为两端服务器造成巨大的压力。

修改主服务器

在生产中经常会需要把指向一个新的,原因也许是升级服务器,或是失效了,需要把一个提升为,或只是重新分配吞吐量。不管什么原因,都必须告诉它的新是谁。

只要提前规划好,只需要在上使用 CHANGE MASTER TO 命令。会忽略其当前的配置以及中继日志,开始从新的进行复制。同时还会更新 master.info 文件的内容,这样修改在重启后也会生效。

最难的部分在于,在新上如何能找到需要的位置,这样,就可以在原终止的同一个逻辑位置开始了。

把一个 提升为 则有一点难度。主要有两种情况:按计划提升和非计划提升。

计划内提升

原理

提升为理论上很简单:

  1. 停止向原的写入
  2. 视需要让所有的完成同步
  3. 把其中一个配置为新
  4. 指向新,开始向其写入
实际步骤

在实际操作中,根据不同的拓扑可能稍有区别。

  1. 在当前上停止所有写入。如果可能的话,可以考虑强制所有客户端程序退出。如果使用虚拟 IP 地址,只需要关掉这些虚拟 IP,然后杀掉所有客户端连接,从而关闭它们打开的事务。
  2. 上,视需要使用 FLUSH TABLES WITH READ LOCK 停止所有写入活动,还可以用 read_only 选项把设置为只读。从这个时间点开始,就应该在上禁止一切写入,因为一旦它不再是,向其写入数据就意味着丢失数据。不过只读的设置不会影响现有事务的提交。要想更加保险,也可以把所有打开的事务杀掉,这样会真正实现停止所有写入。
  3. 选择一个来做新,并确保它已经同步完成,一定要完成执行所有的中继日志。
  4. 视需要确认新与旧包含相同的数据。
  5. 在新上执行 STOP SLAVE,翻身农奴把歌唱。
  6. 在新上执行 CHANGE MASTER TO MASTER_HOST='',再执行 RESET SLAVE,这样就从其原断开了,并会忽略 master.info 的定义。(如果连接信息是在 my.cnf 指定,这一步就会失败,因此建议不要放到配置文件里)
  7. SHOW MASTER STATUS 查看新的二进制日志坐标,记下来。
  8. 确保所有其它都同步完成。
  9. 关掉旧
  10. 在 MySQL 5.1 之后的版本上,视需要在新上激活事件。
  11. 让客户端连接到新
  12. 在每个上运行 CHANGE MASTER TO 命令,指向新,在此使用第 7 步记下来的坐标。

提升为时,一定要确保其中的专用的数据库、表格、权限都被彻底移除,同时还需要修改原有针对的配置参数,如 innodb_flush_log_at_trx_commit 选项。同样道理,如果把降级为,也需要如此操作。

当然,如果最初给是完全一样的配置,就不需要修改任何东西了。

计划外提升

如果崩溃了,必须提升一个来代替,这个过程的难度就大一些。如果只有一个,直接用就行了,但如果有多个,还得多做几步工作。

这期间的操作有可能会丢失一些复制事件。

有可能上发生的更新在还没来得及进行。还可能上执行了语句之后又回滚了,而上没有回滚,于是实际上的逻辑复制位置比要超前。如果能恢复的数据,有可能可以把丢失的语句提取出来,手工运行一遍。

以下步骤中,计算时一定要使用 Master_Log_FileRead_Master_Log_Pos 的值。

以下为 “一主多从” 拓扑中,提升的步骤:

  1. 判断哪个的数据最新。在每个上查看 SHOW SLAVE STATUS 的输出,看 Master_Log_FileMaster_Log_Pos 的坐标哪个最新。
  2. 让所有的把中继日志都执行完毕。
  3. 在新上执行 STOP SLAVE,翻身农奴把歌唱。
  4. 在新上执行 CHANGE MASTER TO MASTER_HOST='',再执行 RESET SLAVE,这样就从其原断开了,并会忽略 master.info 的定义。(如果连接信息是在 my.cnf 指定,这一步就会失败,因此建议不要放到配置文件里)
  5. SHOW MASTER STATUS 查看新的二进制日志坐标,记下来。
  6. 把每个Master_Log_File/Read_Master_Log_Pos 的坐标与新的进行比较。
  7. 在 MySQL 5.1 之后的版本上,视需要在新上激活事件。
  8. 让客户端连接到新
  9. 在每个上运行 CHANGE MASTER TO 命令,指向新,在此使用第 5 步记下来的坐标。

以上步骤假设在所有上都启用了 log_binlog_slave_updates,只有启用这两个选项,才能把所有的都恢复到同一个时间点。

定位到需要的日志位置

如果有些新主不在同一个位置,就必须去新的二进制日志中,查找所执行的最后一个事件的位置,用它来执行 CHANGE MASTER TO

假设日志事件都有对应的渐增的数字 ID。新刚刚获取了第 100 号事件,此时旧崩溃了。同级还有另外两个,即 replica2 和 replica3。replica2 刚刚获取了 99 号事件,replica3 刚获取了 98 号事件。如果把这两个都指向新当前的日志位置,它们会从 101 号事件开始复制,这就发生了同步偏移。然而,只要新的二进制日志启用了 log_slave_updates,就能在其中找到 99 号和 100 号事件,就可以把这两个带回到一致的状态。

方法一

由于服务器的重启、不同的配置、日志滚动、FLUSH LOGS 命令等因素,有些事件在不同的服务器中的字节偏移量很有可能不同。要想定位这些事件,只需在的二进制日志或中继日志中,用 mysqlbinlog 查找最后运行的事件,然后在新的二进制日志中用 mysqlbinlog 找到相同的查询,结果会返回该查询的字节偏移量,将其用于 CHANGE MASTER TO

方法二

另一个办法会更快一些,就是用新的字节偏移量减去的字节偏移量,得到的是它们字节位置的差值。用新当前二进制日志的位置减去这个差值,很有可能就定位到了想要找的那个查询。

范例
场景一

假设 server1 是,server2 和 server3 是它的,server1 现在崩溃了。

image-center

在 server2 和 server3 上用 SHOW SLAVE STATUS 查看 Master_Log_File/Read_Master_Log_Pos,结果表明 server2 完成了彻底的复制,而 server3 没有。具体来说,server2 当前的位置是与相同的 1582,因此可以把它提升为新

那么,在 server3 执行 CHANGE MASTER TO 命令时,要给哪些参数呢?这里需要一点计算。server3 停止的偏移量是 1493,比 server2 的 1582 落后 89 字节

server2 当前在自己的二进制日志中正在向位置 8167 写入,在它身上计算上一个事件的位置:

8167 - 89 = 8078

因此,理论上我们应该把 server3 指向 server2 日志的 8078 位置,为了保险,应该查看一下,以确认该位置确实有正确的事件。

假设在检查之后已经确认,server2 日志中 8078 位置的事件确实是 server3 最后一个事件。此时就可以把 server3 的指向 server2 了:

server2> CHANGE MASTER TO MASTER_HOST="server2", MASTER_LOG_FILE="mysql-bin.000009",
MASTER_LOG_POS=8078;
场景二

假如在崩溃之前,server1 在其日志 1582 位置之后,实际上执行完了另一个事件,并已经保存进日志,又当如何呢?

因为 server2 读取并执行到了偏移量 1582,你可能就永远丢失了一个事件。然而,如果旧的磁盘没有损坏,还是可以从其二进制日志中恢复这个事件的,可以用 mysqlbinlog 或借助一个日志服务器。

如果需要从旧中恢复丢失的事件,建议的时机是在提升新之后、客户端连接到新之前。这样,就不必在每个上执行丢失的事件了,让复制链自己去进行。

日志文件的保存

建议以一种 可靠的方式 保存的二进制日志。

  • 可以使用 SAN 或分布式复制块设备。这样,即使彻底损坏,你仍然保留了完整的二进制日志文件。
  • 可以配置一个日志服务器,然后把指向它,让所有的都可以有机会完整地同步到最后的位置。

把一个提升为时,不要把其 Server ID 改成旧的,如果这样做了,就无法使用一个日志服务器来重演事件了,因为新与日志中的旧的 ID 相同,复制时事件会被忽略。因此,Server ID 始终要保持固定不变。

在 主-主 的配置中切换角色

主-主 的拓扑优点之一,就是可以轻易地切换二者的主动与被动的角色,因为它们使用了对称的配置。

切换角色时,最重要的事是:确保在任何时刻只有一个在写入。如果二者交叉写入,容易产生冲突。因此,切换角色以后,被动服务器一定不能从对方接收任何二进制日志事件。要想保证这一点,在让被动服务器可以写入之前,需要确保其 SQL 线程与主动服务器是同步的。

以下切换角色的步骤:

  1. 在主动服务器上停止所有写入。
  2. 在主动服务器上执行 SET GLOBAL read_only=1,并在配置文件中也配置 read_only,以保证重启后有效。记住,即使这样也无法阻止拥有 SUPER 权限的用户对数据进行修改。要想防止所有用户的修改,应该使用 FLUSH TABLES WITH READ LOCK。如果不这么做,就必须杀掉所有客户端连接,以确保没有留下长时间运行的语句或未提交的事务。
  3. 在主动服务器上执行 SHOW MASTER STATUS ,记下二进制日志的坐标。
  4. 使用刚刚记下的坐标,在被动服务器上执行 SELECT MASTER_POS_WAIT(),执行该命令以后,会一直保持阻塞,确保被动服务器能够读取并执行事件,一直到该坐标为止,同步完毕命令才会返回。
  5. 在被动服务器上执行 SET GLOBAL read_only=0,使其成为主动服务器。
  6. 重新配置应用程序,使其向新的主动服务器写入。