本文是翻译MySQL InnoDB Cluster – how to manage a split-brain situation[1]这篇文章,如有翻译不妥或不对的地方,敬请谅解与指正。请尊重原创和翻译劳动成果,转载的时候请注明出处。谢谢!
每次我展示MySQL InnoDB Cluster时,在创建集群的演示中,很多人都不明白为什么当我集群中已有2个成员时,我的集群还不能容忍任何故障。 事实上,当您创建MySQL InnoDB集群时,只要您添加了第二个实例,您就可以看到状态信息:
"status": "OK_NO_TOLERANCE",
"statusText": "Cluster is NOT tolerant to any failures.",
仲裁(Quorum)
[译者注释] 这里Quorum翻译成仲裁,直译是法定人数。个人认为翻译成仲裁更好一些。
这是为什么呢?这是因为,要成为网络主分区(网络主分区指包含服务的网络分区,在默认的单主模式中指具有主节点的网络分区)的一部分,您的网络分区必须达到大多数节点(法定人数)。在MySQL InnoDB集群(和许多其他集群解决方案一样)中,要达到法定人数,分区中的成员数量必须大于50%。
[译者注释]:这里所谓的网络主分区(primary partition),其实是指集群中的节点可能由于网络故障导致变成了2个或多个隔离区域。是一个网络拓扑概念。
因此,当我们已有2个节点时,如果两个服务器之间出现网络问题,集群将分裂为2个分区。每个分区将拥有总成员数量的 50%(2 个中的1个)。那么50% > 50% 吗?不!在MySQL InnoDB集群的环境下,没有一个网络分区会达到法定人数,也没有一个网络分区允许查询。这就是原因。
事实上,第一台服务器会发现它再也无法与第二台服务器通信了……但为什么呢?是第二台服务器宕机了吗?是不是网络接口出了问题?我们不知道,所以我们无法决定。
让我们看一下这个由3名成员组成的集群 (3/3 = 100%):
![](http://dev-img.mos.moduyun.com/20240813/b829c657-46c7-4bde-8efe-6c98cdb43de8.png)
如果我们看一下cluster.status()命令的输出信息,我们可以看到,有3个节点时可以容忍一个节点出现故障:
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
现在让我们想象一下,我们遇到一个网络问题,它会隔离其中一个成员/节点:
![](http://dev-img.mos.moduyun.com/20240813/b544647c-f503-469e-a8c0-d98b802c963a.png)
我们可以在cluster.status()命令的输出信息中看到节点丢失: 只要一个分区仍然具有法定人数(2/3 = 66%,大于 50%),我们的集群就仍然能够提供交易服务。
"mysql6:3306": {
"address": "mysql6:3306",
"mode": "n/a",
"readReplicas": {},
"role": "HA",
"status": "(MISSING)"
}
这里我想介绍一个非常重要的概念,因为这并不总是显而易见的。InnoDB Cluster 和 Group Replication 中的集群概念是不同的。实际上,InnoDB Cluster 依赖于DBA使用 MySQL Shell创建的元数据。这些元数据描述了集群的设置方式。Group Replication 以不同的方式看待集群。它以上次检查时的状态以及现在的状态来看待集群……并更新该视图。这通常被称为世界观(view of the world)。 因此,在上面的示例中,InnoDB 集群看到 3 个节点:2 个在线,1 个丢失。对于组复制,在短时间内,网络分区中节点处于UNREACHABLE状态,几秒钟后,在被多数人从组中逐出后(因此只有在仍有多数人的情况下),该节点不再是集群的一部分。组大小现在是 2/2(2/2 而不是 2/3)。此信息可以通过performance_schema.replication_group_members获取。
如果我们的网络问题更加严重,并将我们的集群分成 3 个,如下图所示,那么集群将处于“离线”状态,因为这 3 个分区均未达到法定多数,即 1/3 = 33% (<50%):
![](http://dev-img.mos.moduyun.com/20240813/6dc8eb1f-20e2-410e-bc99-6f7cab2f992a.png)
在这种情况下,MySQL 服务将无法正常工作,直到人工修复该问题。
解决问题
当集群中不再有网络主分区时(如上例所示),DBA 需要恢复服务。和往常一样,MySQL 错误日志中已经有一些信息:
2019-04-10T13:34:09.051391Z 0 [Warning] [MY-011493] [Repl] Plugin group_replication
reported: 'Member with address mysql4:3306 has become unreachable.'
2019-04-10T13:34:09.065598Z 0 [Warning] [MY-011493] [Repl] Plugin group_replication
reported: 'Member with address mysql5:3306 has become unreachable.'
2019-04-10T13:34:09.065615Z 0 [ERROR] [MY-011495] [Repl] Plugin group_replication
reported: 'This server is not able to reach a majority of members in
the group. This server will now block all updates. The server will
remain blocked until contact with the majority is restored. It is
possible to use group_replication_force_members to force a new group
membership.
从消息中我们可以看到这正是我们在这里解释的情况/场景。我们可以从命令cluster.status()的输出信息看到集群被“阻止”了:
"status": "NO_QUORUM",
"statusText": "Cluster has no quorum as visible from 'mysql4:3306'
and cannot process write transactions.
2 members are not active",
我们有两个解决方案来解决这个问题:
-
使用SQL和组复制变量 -
使用 MySQL Shell 的 adminAPI
使用SQL和组复制变量进行修复
手册[2]中解释了这个过程(组复制:网络分区)。 在DBA想要用来恢复服务的节点上,如果只剩下一个节点,我们可以使用全局变量group_replication_force_members,并使用您可以在group_replication_local_address找到的服务器的GCS地址(如果有多个服务器在线但未达到大多数,则应将所有服务器添加到此变量中):
set global group_replication_force_members=@@group_replication_local_address;
请注意,最佳做法是关闭其他节点,以避免在强制仲裁过程中再次出现任何类型的冲突。
集群将再次可用。我们可以在错误日志中看到情况已解决:
2019-04-10T14:41:15.232078Z 0 [Warning] [MY-011498] [Repl] Plugin group_replication
reported:
'The member has resumed contact with a majority of the members in the group.
Regular operation is restored and transactions are unblocked.'
当节点上线后不要忘记删除变量group_replication_force_members的值
set global group_replication_force_members='';
当网络问题解决后,节点将尝试重新连接,但由于我们强制成员身份/资格,这些节点将被拒绝。您需要通过以下方式将其重新加入群组:
-
重新启动 mysqld服务 -
重新启动组复制(stop group_replication; start group_replication) -
使用 MySQL Shell ( cluster.rejoinInstance())
使用 MySQL Shell的adminAPI
另一个选项是使用MySQL Shell的adminAPI。这当然是更好的选择!使用 AdminAPI,您甚至不需要知道用于 GCS 的端口即可恢复仲裁。 在下面的示例中,我们将使用名为mysql4的服务器来重新激活我们的集群:
cluster.forceQuorumUsingPartitionOf('clusteradmin@mysql4')
并且当网络问题解决后,Shell 还可以用于重新加入其他实例(在本例中为mysql6):
cluster.rejoinInstance('clusteradmin@mysql6')
结论
如果您遇到任何原因在 MySQL InnoDB 群集上失去仲裁的情况,请不要惊慌。选择要使用的节点(或仍可相互通信的节点列表),如果可以的话,请关闭或停止其他节点上的mysqld服务。然后 MySQL Shell再次帮助你,你可以使用 adminAPI 强制仲裁并用一个命令中重新激活您的群集!
Bonus
如果您想知道您的 MySQL 服务器是否属于网络主分区(占多数的分区),您可以运行以下命令:
mysql> SELECT IF( MEMBER_STATE='ONLINE' AND ((
SELECT COUNT(*) FROM performance_schema.replication_group_members
WHERE MEMBER_STATE NOT IN ('ONLINE', 'RECOVERING')) >=
((SELECT COUNT(*)
FROM performance_schema.replication_group_members)/2) = 0), 'YES', 'NO' )
` in primary partition`
FROM performance_schema.replication_group_members
JOIN performance_schema.replication_group_member_stats
USING(member_id) where member_id=@@global.server_uuid;
+----------------------+
| in primary partition |
+----------------------+
| NO |
+----------------------+
或者使用addition_to_sys_GR.sql这个SQL来确认。
USE sys;
DELIMITER $$
CREATE FUNCTION my_id() RETURNS TEXT(36) DETERMINISTIC NO SQL RETURN (SELECT @@global.server_uuid as my_id);$$
-- new function, contribution from Bruce DeFrang
CREATE FUNCTION gr_member_in_primary_partition()
RETURNS VARCHAR(3)
DETERMINISTIC
BEGIN
RETURN (SELECT IF( MEMBER_STATE='ONLINE' AND ((SELECT COUNT(*) FROM
performance_schema.replication_group_members WHERE MEMBER_STATE NOT IN ('ONLINE', 'RECOVERING')) >=
((SELECT COUNT(*) FROM performance_schema.replication_group_members)/2) = 0),
'YES', 'NO' ) FROM performance_schema.replication_group_members JOIN
performance_schema.replication_group_member_stats USING(member_id) where member_id=my_id());
END$$
CREATE VIEW gr_member_routing_candidate_status AS SELECT
sys.gr_member_in_primary_partition() as viable_candidate,
IF( (SELECT (SELECT GROUP_CONCAT(variable_value) FROM
performance_schema.global_variables WHERE variable_name IN ('read_only',
'super_read_only')) != 'OFF,OFF'), 'YES', 'NO') as read_only,
Count_Transactions_Remote_In_Applier_Queue as transactions_behind, Count_Transactions_in_queue as 'transactions_to_cert'
from performance_schema.replication_group_member_stats where member_id=my_id();$$
DELIMITER ;
SQL>select gr_member_in_primary_partition();
+----------------------------------+
| gr_member_in_primary_partition() |
+----------------------------------+
| YES |
+----------------------------------+
1 row in set (0.0288 sec)
1: https://lefred.be/content/mysql-innodb-cluster-how-to-manage-a-split-brain-situation/
[2]2: https://dev.mysql.com/doc/refman/8.0/en/group-replication-network-partitioning.html