写点什么

MySQL MGR + 自研脚本实现高可用

作者:lixiaofeng
  • 2021 年 12 月 13 日
  • 本文字数:2806 字

    阅读完需:约 9 分钟

MySQL MGR + 自研脚本实现高可用

MySQL 作为使用范围最广泛的数据库之一,官网一直没有出故障转移和高可用。 无论是 MHA , 半同步,还是增强半同步,Xenon 等, 都是围绕 binlog 这个逻辑复制展开的, 要么是官网对逻辑复制的增强,要么是第三方做的故障转移和高可用。

终于在 MySQL5.7 官网出了 MGR 故障转移方案, 但是要注意,这里只有故障转移,并没有高可用。 MGR 的原理图如下:


当事务得到组内多个节点认证时,主节点就可以提交搞事务。 当主节点宕机时,集群内部会选出一个新的主节点, 实现了故障转移。 MGR 集群保证了数据库的安全切数据的一致性, 对于应用来说,应用并不知道 primary 节点已经转移了,于是应用的连接会大量的报错。 而现在的 MySQL 驱动也没有想 MongoDB 那样,允许连接多个集群内节点的 IP,驱动自己判断并连接到 primary 上。

对于使用 MGR 的同学来说, 怎么让应用能适中保持主节点的连接 ,就是我们要解决的问题了。

其实 MGR 已经实现了 故障转移 和数据的一致性, 剩下的工作就比较简单了。我们只要使用脚本去定时探测主节点了,是否改变了就可以了。

应用连接数据库,要么使用虚拟 IP,要是使用 DNS 域名。 当我们探测到 MGR 主节点已经转移,比如我们探测到 MGR 主节点已经从 A 转移到了 B,那么:

  • 当应用使用虚拟 IP 连接数据库, 我们需要把虚拟 IP 从原来的主节点 A 上 解绑, 然后把虚拟 IP 绑定的新的主节点 B 上(这里需要有连接主机,执行 shell 命令的操作);

  • 当应用使用 DNS 域名连接数据库时,会更简单一些, 我们只要把新主节点 B 的 IP 绑定到 DNS 域名上即可;

具体怎么做呢,其实比较简单,两个关键点, 一个是定期探测主节点是否有变化, 另一个是主节点变化时的切换。

通过一下 SQL 语句可以检测,当前集群各个节点的角色:

select MEMBER_HOST,MEMBER_STATE,MEMBER_ROLE from performance_schema.replication_group_members;
复制代码




通过鞋面的 shell 命令可以在 MGR 节点上绑定或者解绑虚拟 IP(不通的 Linux 版本会,下面命令有所不通):

ip addr add 10.50.133.241/24 dev ens192:1ip addr dele 10.50.133.240/24 dev ens192:1
复制代码


使用 golang 实现 MGR HA 的关键代码如下。

1, 定期探测集群的主节点


z.Log.Info("start CheckMaster")    // z.Schedule.Step  从配置文件取得探测频率,比如1秒一次    step := time.Millisecond * time.Duration(z.Schedule.Step)    myTimer := time.NewTimer(step) // 启动定时器    for {        select {        case <-myTimer.C:            // CheckMaster 主函数            z.CheckMaster()            myTimer.Reset(step)        }    }
复制代码

2,z.MgrNodes 里面是所有的 MGR 节点 , 需要在 MGR 节点的循环找 replication_group_members 的信息, 查询出结果就跳出循环。

SQLText := "select MEMBER_HOST,MEMBER_STATE,MEMBER_ROLE from performance_schema.replication_group_members;"    mysqluser := z.MysqlLogin.MysqlUser    mysqlpass := z.MysqlLogin.MysqlPassword    mysqlport := strconv.Itoa(z.MysqlLogin.MysqlPort)    // 循环连接节点, z.MgrNodes 里面是所有的MGR 节点    for _, v := range z.MgrNodes {        db, err := sql.Open("mysql", mysqluser+":"+mysqlpass+"@tcp("+v.Ip+":"+mysqlport+")/performance_schema?charset=utf8")        if err != nil {            z.Log.Error("connect mysql [%v] error : [%v] ", v.Ip, err.Error())            continue        }        rows, errrows = db.Query(SQLText)        if errrows != nil {            z.Log.Error("select replication_group_members error. ip : [%v];  error : [%v]", v.Ip, errrows.Error())        }        if rows != nil {            break        }    }
复制代码


3, 查出结果以后,对比主节点是否发生的变化, 若是已发生变化,则调用 bind,unbind 函数,进行绑定和解绑 IP。

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())                continue            }            //z.Log.Info("mgr member : [%v], [%v], [%v]", member.MEMBER_HOST, member.MEMBER_ROLE, member.MEMBER_STATE)            if member.MEMBER_ROLE == "PRIMARY" {                member.Role_Type = "Write"                member.WriteVip = z.Vip.WriteVip                if z.CurrentWriteIP != mysqlnode[member.MEMBER_HOST] {                    z.Log.Warning("master switched; new master is [%v]", mysqlnode[member.MEMBER_HOST])                    // 若当前记录的 CurrentWriteIP  和 数据库取出来的不一样,, 则说明已经切换; 调用解绑ip, 绑定ip的程序                    newWriteIP := mysqlnode[member.MEMBER_HOST]                    z.unbind(z.CurrentWriteIP, z.Vip.WriteVip)                    z.bind(newWriteIP, z.Vip.WriteVip)                    z.CurrentWriteIP = newWriteIP                } else {                    z.Log.Info("master [%v] is OK", z.CurrentWriteIP)                }                break            }        }
复制代码


4, 绑定和解绑函数

func (z *Zhongkui) bind(hostip, vip string) {    shellClient = *runshell.New(hostip, z.LinuxLogin.LinuxUser, z.LinuxLogin.LinuxPassword, 22)    strResult, err := shellClient.RunCmd("ip addr add " + vip + "/24 dev ens192:1")    if err != nil {        z.Log.Error("bind vip [%v] to [%v] error.  result : [%v], error : [%v]", vip, hostip, strResult, err.Error())    } else {        z.Log.Warning("bind vip [%v] to [%v] success. result : [%v] ", vip, hostip, strResult)    }}func (z *Zhongkui) unbind(hostip, vip string) {    shellClient = *runshell.New(hostip, z.LinuxLogin.LinuxUser, z.LinuxLogin.LinuxPassword, 22)    strResult, err := shellClient.RunCmd("ip addr dele " + vip + "/24 dev ens192:1")    if err != nil {        z.Log.Error("unbind vip [%v] from [%v] error.  result : [%v], error : [%v]", vip, hostip, strResult, err.Error())    } else {        z.Log.Warning("unbind vip [%v] from [%v] success. result : [%v] ", vip, hostip, strResult)    }}
复制代码


测试结果如下:


半天写完的脚本,功能已经实现。 其实这里,还可以添加一个只读的虚拟 IP。 也就是说,一个 MGR 集群,对外提供两个虚拟 IP,一个连接到主节点,执行读写; 一个连接到从节点,执行只读操作。 对只读节点也进行监控,保证虚拟只读 IP 高可用且只在从节点上。 上面的程序稍加修改即可实现。

发布于: 11 小时前阅读数: 9
用户头像

lixiaofeng

关注

还未添加个人签名 2018.04.25 加入

还未添加个人简介

评论

发布
暂无评论
MySQL MGR + 自研脚本实现高可用