MySQL 作为使用范围最广泛的数据库之一,官网一直没有出故障转移和高可用。无论是 MHA , 半同步,还是增强半同步,Xenon 等, 都是围绕 binlog 这个逻辑复制展开的, 要么是官网对逻辑复制的增强,要么是第三方做的故障转移和高可用。终于在 MySQL5.7 官网出了 MGR 故障转移方案, 但是要注意,这里只有故障转移,并没有高可用。MGR 的原理图如下:
当事务得到组内多个节点认证时,主节点就可以提交事务。当主节点宕机时,集群内部会选出一个新的主节点, 实现了故障转移。MGR 集群保证了数据库的数据安全和数据的一致性, 对于应用来说,应用并不知道 primary 节点已经转移了,于是应用的连接会大量的报错。而现在的 MySQL 驱动也没有想 MongoDB 那样,允许连接多个集群内节点的 IP,驱动自己判断并连接到 primary 上。对于使用 MGR 的同学来说, 怎么让应用能适中保持主节点的连接 ,就是我们要解决的问题了。
上篇文章实现了 MGR+自研脚本的高可用。详细参见:
https://xie.infoq.cn/article/d5082aa1c4d111bf9902ac0de
本篇分享只读节点的高可用。对写节点的高可用,我么主要检测主节点的变化,当主节点变化时,我们重新绑定 vip(或者 dns)即可。对于只读节点的高可用,情况会复杂一些。可以分成两种情况。我们举例来说。
假如有一个三节点的 MGR 集群 A,B,C;
其中 A 是主节点绑定 vip(10.10.10.10 或者域名);
C 是绑定只读的 vip(10.10.10.11 或者域名);
第一种情况,C 节点挂了, 此时我们需要把 只读的 vip(10.10.10.11 或者域名), 重新绑定的 B 上;
第二种情况,C 正常, 检测的时候,发现 C 已经成为了 primary (这里我们不用关心 C 是怎么成为主节点的), 此时我们需要把(10.10.10.11 或者域名)重新绑定的一台 Secondary 节点上。
咱们一个一个来。
第一种情况,C 节点挂了, 此时我们需要把 只读的 vip(10.10.10.11 或者域名), 重新绑定的 B 上;
核心代码如下:
首先我们只查询只读的从节点,z.GetRow(SQLText) 方法是循环查询三个节点(防止某个节点宕机),直到正常返回(没有查询到数据的返回也是正常返回)。
若是正常返回,且没有查询到数据,说明该节点已经因故障,退出了集群,我们就需要 vip(或者 dns)的切换了。
SQLText = "select MEMBER_HOST,MEMBER_STATE,MEMBER_ROLE from performance_schema.replication_group_members where MEMBER_HOST='" + z.Vip.ReadHost + "';"
rows = z.GetRow(SQLText)
...
...
// irows ==0 说明,没有查询到数据,也就是说负责只读的mysqld 已经挂了
// 将只读vip 绑定到另外一个从节点上
if irows == 0 {
SQLText = "select MEMBER_HOST,MEMBER_STATE,MEMBER_ROLE from performance_schema.replication_group_members where MEMBER_ROLE='SECONDARY';"
rows = z.GetRow(SQLText)
for rows.Next() {
err := rows.Scan(&member.MEMBER_HOST, &member.MEMBER_STATE, &member.MEMBER_ROLE)
if err != nil {
z.Log.Error(" rows scan error : [%v]", err.Error())
}
newReadIP := mysqlnode[member.MEMBER_HOST]
z.Log.Warning("read only mysqld[%v] stop. prepare bind vip[%v] to secondary [%v]", z.CurrentReadIP, z.Vip.ReadVip, newReadIP)
z.unbind(z.CurrentReadIP, z.Vip.ReadVip)
z.bind(newReadIP, z.Vip.ReadVip)
z.CurrentReadIP = newReadIP
z.Vip.ReadHost = member.MEMBER_HOST
}
}
复制代码
第二种情况,C 正常, 检测的时候,发现 C 已经成为了 primary (这里我们不用关心 C 是怎么成为主节点的), 此时我们需要把(10.10.10.11 或者域名)重新绑定的一台 Secondary 节点上。
这种情况下,查询一定正常返回了 C 节点的数据。
我们进一步判断,MEMBER_ROLE 的值,若是 SECONDARY, 则是正常状态,写下日志返回。
若 MEMBER_ROLE 的是 PRIMARY, 则说明我们的只读 vip 指定的节点已经成为主节点, 就需要把只读的 vip 重新一个 SECONDAR 节点上。
SQLText = "select MEMBER_HOST,MEMBER_STATE,MEMBER_ROLE from performance_schema.replication_group_members where MEMBER_HOST='" + z.Vip.ReadHost + "';"
rows = z.GetRow(SQLText)
...
...
for rows.Next() {
irows = 1
err := rows.Scan(&member.MEMBER_HOST, &member.MEMBER_STATE, &member.MEMBER_ROLE)
if err != nil {
z.Log.Error(" rows scan error : [%v]", err.Error())
}
// 若只读节点还活着,,则判断它的role是不是primary;
if member.MEMBER_ROLE == "SECONDARY" {
z.Log.Info("secondary node [%v] is ok ", z.CurrentReadIP)
rows.Close()
return
}
if member.MEMBER_ROLE == "PRIMARY" {
// 若我们的只读的节点已经变成primary,,则需要把只读vip 绑定到一个从节点上
z.Log.Warning("secondary node [%v] is switch to primary", z.CurrentReadIP)
z.Log.Warning("prepare unbind [%v] from [%v]", z.Vip.ReadVip, z.CurrentReadIP)
SQLText = "select MEMBER_HOST,MEMBER_STATE,MEMBER_ROLE from performance_schema.replication_group_members where MEMBER_ROLE='SECONDARY';"
rows = z.GetRow(SQLText)
for rows.Next() {
err := rows.Scan(&member.MEMBER_HOST, &member.MEMBER_STATE, &member.MEMBER_ROLE)
if err != nil {
z.Log.Error(" rows scan error : [%v]", err.Error())
}
newReadIP := mysqlnode[member.MEMBER_HOST]
z.Log.Warning("read only mysqld[%v] switch to primary. prepare bind vip[%v] to secondary [%v]", z.CurrentReadIP, z.Vip.ReadVip, newReadIP)
z.unbind(z.CurrentReadIP, z.Vip.ReadVip)
z.bind(newReadIP, z.Vip.ReadVip)
z.CurrentReadIP = newReadIP
z.Vip.ReadHost = member.MEMBER_HOST
}
}
}
复制代码
到现在,我们已经使用脚本检测 MGR 状态的变化, 分别绑定读写 vip(或者 dns)和只读 vip(或者 dns), 实现了基于 MGR 的高可用和读写分离。 这里其实可以优化一下, 启动两个 go routinue, 一个负责写 vip 的高可用,一个负责只读 vip 的高可用。
评论