Mysql主从gtid不一致问题的分类和查找

版权声明:本文为博主原创文章,转载请注明出处:https://twocups.cn/index.php/2021/07/02/44/

gtid主从不一致情况分类

  1. 从库gtid多于主库(一般是从库开启了写操作)
  2. 从库gtid少于主库(一般是发生了宕机)
  3. 从库上没有全部的主库已经删除的gtid

gtid主从不一致情况扫描脚本逻辑

我写了一个 gtid 主从不一致扫描脚本,运行在集群管控节点上,每天扫描一下集群内的 gtid 异常情况。企业实际环境中不是很推荐像我一样用 shell 脚本去写,因为太依赖机器环境了。最好使用 Go 之类的语言去写,而且 Go 本身也有很多封装好的库,写起来会更方便。并且,用 Go 写之后还可以和公司内部的一些巡检或监视平台配合。

我说一下脚本的扫描逻辑。首先,在集群管控节点上搜索集群中所有的 mysql 实例 id,以及主从库的 ip 地址和端口,然后对每个实例运行以下扫描逻辑。

1. 前置检查

  1. 检查gtid模式是否开启
  2. 检查数据库类型

首先,我们要检查gtid是否开启,如果没有开启就直接报错,并结束这个实例的扫描。

2. 采集gtid

  1. 采集从库gtid和uuid
  2. 停顿
  3. 采集主库gtid和uuid

采集从库 gtid 一定要在主库前面,从而保证主库的gtid比从库更新,否则可能会出现从库gtid跑在主库前面的奇怪情况。因为我们集群比较大,所以向主从库请求gtid会有一定程度的延迟,还是会有可能引发从库gtid比主库更新的情况,所以我还是在中间停顿了0.1s,保证了主库gtid一定比从库的更新。

3. 查找多的gtid

  1. 采集从库比主库多的gtid
  2. 第一次采集主库比从库多的gtid
  3. 停顿
  4. 第二次采集主库比从库多的gtid
  5. 根据两次采集的结果,计算出主库比从库多的gtid中异常的那部分
  6. 分析主库比从库多的gtid
  7. 分析从库比主库多的gtid

看到这个逻辑,一定会想到一个问题:为什么采集从库比主库多的 gtid 只需要采集一次,而采集主库比从库多的 gtid 要采集两次呢?

从库比主库多的 gtid 只需要采集一次是因为,从库的 gtid 本身就不应该比主库多,从库的 gtid 应该和主库是一样的,或者因为延迟,主库的 gtid 比主库稍微慢一些。如果从库的 gtid 比主库多,那么多出来的那部分一定是有问题的 gtid。

但是主库 gtid 比从库多是正常的,因为我们是先采集从库再采集主库,这就导致主库的 gtid 比从库的更新。在我们采集两者的间隔,主库也可能在源源不断地产生新的 gtid,所以主库的 gtid 会比从库更多。那我们可以先采集主库再采集从库吗?那更不行,否则你就会看到从库的 gtid 跑到主库前面去,这更离谱。

所以,我们需要采集两次主库比从库多出的 gtid,并且在两次采集中加一个时间间隔,目的是区分出所有的主库正在增长的 gtid。这里我们用到了 mysql 提供的一个关于 gtid 的函数。

select gtid_subtract('A', 'B')

这个函数的作用是求出 gtid 集合 A 与 gtid 集合 B 的差值,具体来说,是求出 A 集合中有、而 B 集合中没有的 gtid。换言之,这个函数可以在 A 集合中去掉 A 和 B 集合相同的 gtid。

好消息是,这个函数非常方便。如果没有这个函数,我们就需要去比较 gtid 的字符串了,非常麻烦。

坏消息是,关于 gtid 的函数,mysql 就提供了这么一个。我们能用的也只有它。

我们通过连续使用两次这个函数,就可以得到两次采集的主库比从库多的 gtid 结果中重合的那一部分,这就是主库 gtid 比从库多的 gtid 中异常的那部分。

这里其实存在一个问题,那就是两次采集重合的部分,不一定是异常的部分,还可能是正在正常增长的 gtid。所以我们采集两次的时间间隔可以长一些,我自己设置的是0.5s。如果两次采集的部分中仍然出现了正常增长的 gtid,那么说明主从库存在延迟大的问题。检测主从库延迟是否过大,也正是我们设置时间间隔的第二个原因。

4. 查找主库中已经删除但从库中未出现过的gtid

  1. 采集从库接收到但未执行的 gtid + 从库已执行的 gtid(两者是有重合的)
  2. 分离出从库收到但未执行的 gtid
  3. 采集从库已删除的 gtid
  4. 采集主库已删除的 gtid
  5. 从主库已删除的 gtid 中依次过滤掉从库中出现过的 gtid

我没找到单独采集从库接收到但未执行的 gtid 的方法,我只能从 mysql 指令“show slave status”里面的参数“Retrieved_Gtid_Set”里面得知该数据。但这个参数又不能单独采集,所以我只能连它和“Executed_Gtid_Set”一起采集了。但是它们俩的 gtid 一般来说都是有重合的,不能直接使用函数 gtid_subtract。

我又观察到它们集合中的 gtid 之间除了逗号还多有一个空格,并且“Retrieved_Gtid_Set”的值和“Executed_Gtid_Set”的值两段数据之间没有逗号只有空格。那我就可以通过处理字符串的方法,将从库接收到但未执行的 gtid 分离出来。

扫描脚本

#!/bin/sh
# Description: Scan for abnormalities of gtid
# Author: hayalin
# Date: 2021-07-01
​
source /root/.c 2>/dev/null
​
# 显示gtid多的异常情况
showMoreGtid(){
    echo "$1" | grep $2 > /dev/null
    if [ $? -eq 0 ];then 
        echo "$3 has more gtid: master:$4:$5, slave:$6:$7 instance:$8 gtid:$1"
    else
        echo "$3 has more gtid: master:$4:$5, slave:$6:$7 instance:$8 gtid:$1, but no belong to $3"
    fi
}
​
d -Ne "这里是从集群管控节点上搜索出所有mysql的实例id,和这些实例的ip和端口" |
while read mip mport sip sport inst_id;do
​
    # ------------------------ 1. 前置检查 ------------------------
    # 检查gtid模式是否开启
    #echo "master:${mip}:${mport} slave:${sip}:${sport}"
    gtid_mode=$(mysql -h${mip} -P${mport} -u账户 -Ne "show global variables like 'gtid_mode'" | awk '{print $2}')
    #echo "gtid_mode: ${gtid_mode}"
    if [ "${gtid_mode}" != "ON" ] && [ "${gtid_mode}" != "1" ];then
        #echo "no suport gtid"
        continue
    fi
    
    # 检查数据库类型是否为percona
    ver=$(mysql -h${mip} -P${mport} -u账户 -Ne "select @@version_comment")
    echo "${ver}" | grep Percona 
    if [ $? -eq 0 ];then
        echo "is percona inst, omit!!!!."
        continue
    fi
​
    # ------------------------ 2. 提取gtid ------------------------
    # 提取从库gtid
    sgtid=$(mysql -h${sip} -P${sport} -u账户 -Ne "select @@global.gtid_executed" | sed 's/\\n//g')
    #echo -e "sgtid:\n${sgtid}"
    suuid=$(mysql -h${sip} -P${sport} -u账户 -Ne "select @@server_uuid")
    #echo "suuid: ${suuid}"
    if [ "${suuid}" == "" ];then
        echo "slave has null suuid"
        continue
    fi
​
    # 考虑到服务器可能返回gtid慢的情况,所以必须要设置时间间隔,否则即使从库比主库先采样,从库gtid仍然可能会跑到主库前面去
    sleep 0.1
    
    # 提取主库gtid
    mgtid=$(mysql -h${mip} -P${mport} -u账户 -Ne "select @@global.gtid_executed" | sed 's/\\n//g')
    #echo -e "mgtid:\n${mgtid}"
    muuid=$(mysql -h${mip} -P${mport} -u账户 -Ne "select @@server_uuid")
    #echo "muuid: ${muuid}"
    if [ "${muuid}" == "" ];then
        echo "master has null muuid"
        continue
    fi
​
    # ------------------------ 3. 查找多的gtid ------------------------
    # 从库gtid多的情况只需要采样一次,而主库原本就比从库快,所以需要采样两次,利用时间差来排除主库gtid自然增长的情况,同时测试主从gtid传输延迟
    # 从库gtid采样
    slave_more_gtid=$(mysql -h${sip} -P${sport} -u账户 -Ne "select gtid_subtract('${sgtid}', '${mgtid}')")
    #echo "slave_more_gtid: ${slave_more_gtid}"
    # 第一次主库gtid多的采样
    master_more_gtid_time1=$(mysql -h${mip} -P${mport} -u账户 -Ne "select gtid_subtract('${mgtid}', '${sgtid}')")
    #echo "master_more_gtid_time1: ${master_more_gtid_time1}"
​
    # 两次采样主库多gtid情况的时间间隔,用于排除主库gtid自然增长慢的情况
    sleep 0.5
​
    # 第二次主库gtid多的采样
    sgtid=$(mysql -h${sip} -P${sport} -u账户 -Ne "select @@global.gtid_executed" | sed 's/\\n//g')
    # 考虑到服务器可能返回gtid慢的情况,所以必须要设置时间间隔,否则即使从库比主库先采样,从库gtid仍然可能会跑到主库前面去
    sleep 0.1
    mgtid=$(mysql -h${mip} -P${mport} -u账户 -Ne "select @@global.gtid_executed" | sed 's/\\n//g')
    master_more_gtid_time2=$(mysql -h${mip} -P${mport} -u账户 -Ne "select gtid_subtract('${mgtid}', '${sgtid}')")
    #echo "master_more_gtid_time2: ${master_more_gtid_time2}"
​
    # 两次主库比从库多出的gtid的交集,就是异常情况下主库比从库多出gtid
    # 如果自然增长序列也出现在交集当中,说明要么是gtid自然增长慢,需要调整两次采样的间隔;要么是出现了主从库gtid延迟过大的问题,延迟至少超过了我们两次采样的间隔
    master_more_gtid=$(mysql -h${mip} -P${mport} -u账户 -Ne "select gtid_subtract('${master_more_gtid_time1}', '${master_more_gtid_time2}')")
    master_more_gtid=$(mysql -h${mip} -P${mport} -u账户 -Ne "select gtid_subtract('${master_more_gtid_time1}', '${master_more_gtid}')")
    #echo "master_more_gtid: ${master_more_gtid}"
​
    # 分析主库多的gtid
    if [ "${master_more_gtid}" != "" ];then
        ha_tag=$(d -Ne "select 1 from tb_oss_job where job_type='switch_master_rw_inst' and data->'$.in.instanceid' = '${inst_id}'")
        if [ "${ha_tag}" != "" ];then
            echo "[ha task]master has more gtid: master:${mip}:${mport}, slave:${sip}:${sport} instance: ${inst_id} gtid:${master_more_gtid}"
        else
            showMoreGtid ${master_more_gtid} ${muuid} master ${mip} ${mport} ${sip} ${sport} ${inst_id}
        fi
        #echo -e "mgtid:\n${mgtid}"
        #echo -e "sgtid:\n${sgtid}"
    fi
    # 分析从库多的gtid
    if [ "${slave_more_gtid}" != "" ];then
        ha_tag=$(d -Ne "select 1 from tb_oss_job where job_type='switch_slave_rw_inst' and data->'$.in.instanceid' = '${inst_id}'")
        if [ "${ha_tag}" != "" ];then
            echo "[ha task]slave has more gtid: master:${mip}:${mport}, slave:${sip}:${sport} instance: ${inst_id} gtid:${slave_more_gtid}"
        else
            showMoreGtid ${slave_more_gtid} ${suuid} slave ${mip} ${mport} ${sip} ${sport} ${inst_id}
        fi
        #echo -e "mgtid:\n${mgtid}"
        #echo -e "sgtid:\n${sgtid}"
    fi
​
    # ------------------------ 4. 查找主库中已经purged但从库中未出现过的gtid ------------------------
​
    # 从库接收到但未执行的gtid + 从库已执行的gtid(两者是有重合的)
        slave_retrieved_gtid=$(mysql -h${sip} -P${sport} -utencentroot -e "show slave status\G" | grep Retrieved_Gtid_Set | awk '{print $2}' | sed 's/\\n//g')
        #echo -e "slave_retrieved_gtid:\n${slave_retrieved_gtid}"

        # 从库已经删除的gtid
        slave_purged_gtid=$(mysql -h${sip} -P${sport} -utencentroot -Ne "select @@gtid_purged" | sed 's/\\n//g')
        #echo -e "slave_purged_gtid:\n${slave_purged_gtid}"

        # 主库已经删除的gtid
        master_purged_gtid=$(mysql -h${mip} -P${mport} -utencentroot -Ne "select @@gtid_purged" | sed 's/\\n//g')
        #echo -e "master_purged_gtid:\n${master_purged_gtid}"

        # 逐次要筛掉从库中出现过的gtid
        lost_gtid=$(mysql -h${sip} -P${sport} -utencentroot -Ne "select gtid_subtract('${master_purged_gtid}', '${slave_retrieved_gtid}')" | sed 's/\\n//g')
        #echo -e "lost_gtid:\n${lost_gtid}"
        lost_gtid=$(mysql -h${sip} -P${sport} -utencentroot -Ne "select gtid_subtract('${lost_gtid}', '${sgtid}')" | sed 's/\\n//g')
        #echo -e "lost_gtid:\n${lost_gtid}"
        lost_gtid=$(mysql -h${sip} -P${sport} -utencentroot -Ne "select gtid_subtract('${lost_gtid}', '${slave_purged_gtid}')" | sed 's/\\n//g')
        #echo -e "lost_gtid:\n${lost_gtid}"

        if [ "${lost_gtid}" != "" ];then
            echo "rebuild: ${sip}:${sport} ${lost_gtid} | instance:${inst_id}, slave loses gtid: control platform:${own_ip} master:${mip}:${mport}"
            
        fi
done
暂无评论

请到【后台 - 用户 - 我的个人资料】中填写个人说明。

发表评论