├── README.md ├── mgr_Check_primary_class.php ├── mgr_MFailover_class.php ├── mgr_configure1.php └── mgr_master_ip_failover.php /README.md: -------------------------------------------------------------------------------- 1 | # mgr_failover_vip 2 | MySQL 8.0 MGR(组复制)高可用VIP故障转移脚本 3 | 4 | 视频演示:https://www.bilibili.com/video/BV18a4y197TG/ 5 | 6 | 简介:MGR(组复制)官方推荐用MySQL router中间件去做MGR高可用故障转移,但其多过了一层网络,性能会下降,并且需要额外维护一套中间件,运维成本过高,于是写了一个类似MHA的master_ip_failover脚本,实现VIP切换。 7 | 8 | 1)脚本会自动设置当前Primary和备选Primary参数group_replication_member_weight值为100(权重100,默认为50的Secondary不进行vip切换) 9 | 10 | 2)脚本会自动设置当前Primary和备选Primary参数group_replication_consistency值为BEFORE_ON_PRIMARY_FAILOVER(意思为当Primary挂了的时候,备选Primary只有把事务全部执行完毕,才提供客户端读写操作) 11 | 12 | 3)最好生产关闭限流模式set global group_replication_flow_control_mode = 'DISABLED',以防止高并发期间自动触发限流,造成主库不可写,引起生产事故。 13 | 14 | 15 | * 环境准备: 16 | 17 | 1) ```shell> yum install -y php-process php php-mysql``` 18 | 2) 开通监控管理机和MGR SSH互信(可用SSH主机批量互信认证脚本https://github.com/hcymysql/batch_sshkey) 19 | 20 | 3) 手工添加VIP地址 21 | ```shell> ip addr add 192.168.148.100/32 dev eth0 ; arping -q -c 2 -U -I eth0 192.168.148.100``` 22 | 23 | * 运行条件: 24 | * 1、MySQL 8.0版本 25 | * 2、single-primary mode(单主模式) 26 | * 3、Native Password Authentication(5.5/5.6/5.7传统用户认证模式) 27 | * 例: 28 | > CREATE USER 'hechunyang'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; 29 | 30 | > GRANT ALL ON *.* TO 'hechunyang'@'%' WITH GRANT OPTION; 31 | 32 | Usage: 33 | 34 | Options: 35 | 36 | -I interval time seconds 设置守护进程下间隔监测时间 37 | 38 | --daemon 1 开启后台守护进程,0关闭后台守护进程 39 | 40 | --conf 指定配置文件 41 | 42 | --help 帮助 43 | 44 | Example : 45 | 46 | 前台运行 47 | 48 | ```shell> php mgr_master_ip_failover.php --conf=mgr_configure1.php``` 49 | 50 | 后台运行 51 | 52 | ```shell> nohup /usr/bin/php mgr_master_ip_failover.php --conf=mgr_configure1.php -I 5 --daemon 1 > /dev/null 2>&1 &``` 53 | 54 | 关闭后台运行 55 | 56 | ```shell> php mgr_master_ip_failover.php --conf=mgr_configure1.php --daemon 0``` 57 | 58 | 59 | mgr_configure1.php为配置文件,你可以配置多个监控配置文件,监控多套MGR环境。 60 | 61 | 62 | -------------------------------------------------------------------------------- /mgr_Check_primary_class.php: -------------------------------------------------------------------------------- 1 | primary_ip = $primary_ip; 13 | $this->new_primary_ip = $new_primary_ip; 14 | $this->user = $user; 15 | $this->passwd = $passwd; 16 | $this->port = $port; 17 | $this->new_port = $new_port; 18 | $this->hostname = $hostname; 19 | $this->status = $status; 20 | } 21 | 22 | function check_primary(){ 23 | if($this->status == 0){ 24 | $con = mysqli_connect("{$this->primary_ip}","{$this->user}","{$this->passwd}","performance_schema","{$this->port}"); 25 | $p_ip = $this->primary_ip; 26 | } else { 27 | $con = mysqli_connect("{$this->new_primary_ip}","{$this->user}","{$this->passwd}","performance_schema","{$this->new_port}"); 28 | $p_ip = $this->new_primary_ip; 29 | } 30 | if(!mysqli_connect_errno($con)){ 31 | file_put_contents(dirname(__FILE__)."/".strstr($GLOBALS['filename'],'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n"."连接 Primary节点: $p_ip 成功"."\n\n", FILE_APPEND); 32 | echo "连接 Primary节点: $p_ip 成功".PHP_EOL.PHP_EOL; 33 | mysqli_query($con,"set global group_replication_member_weight = 100"); 34 | $get_primary_info = "SELECT MEMBER_HOST,MEMBER_ROLE,MEMBER_STATE FROM performance_schema.replication_group_members WHERE MEMBER_ROLE = 'PRIMARY' AND MEMBER_STATE = 'ONLINE'"; 35 | $result = mysqli_query($con,$get_primary_info); 36 | if(mysqli_num_rows($result) > 0){ 37 | $row = mysqli_fetch_array($result); 38 | if( $p_ip == $this->hostname[$row[0]]){ 39 | file_put_contents(dirname(__FILE__)."/".strstr($GLOBALS['filename'],'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n"."主机名: ".sprintf("%-15s",$row[0])." 角色: ".$row[1]."\n"."Primary IP: ".$p_ip."\n\n", FILE_APPEND); 40 | echo "主机名: ".sprintf("%-15s",$row[0])." 角色: ".$row[1].PHP_EOL; 41 | echo "Primary IP: ".$p_ip.PHP_EOL.PHP_EOL; 42 | 43 | //判断有无延迟事务 44 | mysqli_query($con,"set global group_replication_member_weight = 100"); 45 | mysqli_query($con,"set global group_replication_consistency = 'BEFORE_ON_PRIMARY_FAILOVER'"); 46 | $get_queue_count = "SELECT COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE FROM performance_schema.replication_group_member_stats WHERE MEMBER_ID IN (SELECT MEMBER_ID FROM performance_schema.replication_group_members WHERE MEMBER_ROLE = 'SECONDARY' AND MEMBER_STATE = 'ONLINE')"; 47 | $result2 = mysqli_query($con,$get_queue_count); 48 | $row2 = mysqli_fetch_array($result2); 49 | if ($row2[0] == 0){ 50 | file_put_contents(dirname(__FILE__)."/".strstr($GLOBALS['filename'],'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n无数据延迟"."\n\n", FILE_APPEND); 51 | echo "无数据延迟".PHP_EOL; 52 | return $c=1; 53 | } else { 54 | file_put_contents(dirname(__FILE__)."/".strstr($GLOBALS['filename'],'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n数据延迟 ".$row2[0]." 个事务,不能切换VIP"."\n\n", FILE_APPEND); 55 | echo "\e[38;5;196m数据延迟 ".$row2[0]." 个事务,不能切换VIP".PHP_EOL; 56 | return $c=2; 57 | } 58 | } else { 59 | file_put_contents(dirname(__FILE__)."/".strstr($GLOBALS['filename'],'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n"."检测到主节点{$this->primary_ip}当前已经不是Primary状态, 即将进行切换VIP...."."\n\n", FILE_APPEND); 60 | echo "\e[38;5;196m检测到主节点{$this->primary_ip}当前已经不是Primary状态, 即将进行切换VIP...".PHP_EOL.PHP_EOL; 61 | return $c=1; 62 | } 63 | } else { 64 | file_put_contents(dirname(__FILE__)."/".strstr($GLOBALS['filename'],'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n"."检测到主节点{$this->primary_ip}当前已经不是Primary状态, 即将进行切换VIP...."."\n\n", FILE_APPEND); 65 | echo "\e[38;5;196m检测到主节点{$this->primary_ip}当前已经不是Primary状态, 即将进行切换VIP...".PHP_EOL.PHP_EOL; 66 | return $c=1; 67 | } 68 | } else{ 69 | file_put_contents(dirname(__FILE__)."/".strstr($GLOBALS['filename'],'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n"."检测到主主节点{$this->primary_ip}已经无法连接, 即将进行切换VIP..."."\n\n", FILE_APPEND); 70 | echo "\e[38;5;196m检测到主节点{$this->primary_ip}已经无法连接, 即将进行切换VIP...".PHP_EOL.PHP_EOL; 71 | return $c=0; 72 | } 73 | 74 | } 75 | 76 | } 77 | 78 | 79 | 80 | ?> 81 | -------------------------------------------------------------------------------- /mgr_MFailover_class.php: -------------------------------------------------------------------------------- 1 | primary_ip = $primary_ip; 12 | $this->new_primary_ip = $new_primary_ip; 13 | $this->ssh_port = $ssh_port; 14 | $this->vip = $vip; 15 | $this->network_name = $network_name; 16 | } 17 | 18 | function execCommand(){ 19 | $del_vip="sudo /usr/bin/ssh -p $this->ssh_port root@$this->primary_ip -C '/usr/sbin/ip addr del {$this->vip}/32 dev $this->network_name'"; 20 | system("$del_vip"); 21 | sleep(1); 22 | $add_vip="sudo /usr/bin/ssh -p $this->ssh_port root@$this->new_primary_ip -C '/usr/sbin/ip addr add {$this->vip}/32 dev $this->network_name;/sbin/arping -q -c 2 -U -I $this->network_name $this->vip'"; 23 | system("$add_vip",$res); 24 | if($res == 0){ 25 | file_put_contents(dirname(__FILE__)."/".strstr($GLOBALS['filename'],'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n"."VIP: {$this->vip}已经切换成功"."\n\n", FILE_APPEND); 26 | echo "VIP: {$this->vip}已经切换成功".PHP_EOL.PHP_EOL; 27 | exit; 28 | } else { 29 | file_put_contents(dirname(__FILE__)."/".strstr($GLOBALS['filename'],'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n"."VIP: {$this->vip}切换失败,退出主程序!"."\n\n", FILE_APPEND); 30 | echo "VIP: {$this->vip}切换失败,退出主程序!".PHP_EOL.PHP_EOL; 31 | exit; 32 | } 33 | } 34 | } 35 | 36 | ?> 37 | 38 | -------------------------------------------------------------------------------- /mgr_configure1.php: -------------------------------------------------------------------------------- 1 | "192.168.148.41", 21 | "mgr2"=>"192.168.148.42", 22 | "mgr3"=>"192.168.148.43"); 23 | 24 | ?> 25 | -------------------------------------------------------------------------------- /mgr_master_ip_failover.php: -------------------------------------------------------------------------------- 1 | CREATE USER 'hechunyang'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; 13 | * > GRANT ALL ON *.* TO 'hechunyang'@'%' WITH GRANT OPTION; 14 | * 15 | * 环境准备: 16 | * shell> yum install -y php-process php php-mysql 17 | * 开通监控管理机和MGR SSH互信 18 | * 19 | */ 20 | 21 | ini_set('date.timezone','Asia/Shanghai'); 22 | 23 | function Usage(){ 24 | echo "\e[38;5;11m 25 | * MGR(组复制)高可用VIP切换脚本 26 | * https://github.com/hcymysql/mgr_failover_vip 27 | * 28 | * 运行条件: 29 | * Modified by: hcymysql 2019/07/24 30 | * 1、MySQL 8.0版本 31 | * 2、单主模式 32 | * 3、Native Password Authentication 33 | * 例:> CREATE USER 'hechunyang'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; 34 | * > GRANT ALL ON *.* TO 'hechunyang'@'%' WITH GRANT OPTION; 35 | 36 | Usage: 37 | Options: 38 | -I interval time seconds 设置守护进程下间隔监测时间 39 | --daemon 1 开启后台守护进程,0关闭后台守护进程 40 | --conf 指定配置文件 41 | --help 帮助 42 | 43 | Example : 44 | 前台运行 45 | shell> php mgr_master_ip_failover.php --conf=mgr_configure1.php 46 | 47 | 后台运行 48 | shell> nohup /usr/bin/php mgr_master_ip_failover.php --conf=mgr_configure1.php -I 5 --daemon 1 > /dev/null 2>&1 & 49 | 50 | 关闭后台运行 51 | shell> php mgr_master_ip_failover.php --conf=mgr_configure1.php --daemon 0 52 | \e[0m" .PHP_EOL; 53 | } 54 | 55 | $shortopts = "I:"; 56 | 57 | $longopts = array( 58 | "conf::", 59 | "daemon:", 60 | "help", 61 | ); 62 | 63 | $options = getopt($shortopts,$longopts); 64 | if(empty($options) || isset($options['help'])){ 65 | Usage(); 66 | exit; 67 | } 68 | 69 | if(!isset($options['daemon'])){ 70 | Check_MGR_Primary_Status(); 71 | exit; 72 | } else{ 73 | if($options['daemon'] != 0){ 74 | if(!isset($options['I'])){ 75 | die("请设置间隔时间 -I 5(单位秒)"."\n"); 76 | } 77 | } 78 | $deamon = new Daemon($options['I'],$options['conf']); 79 | $deamon->run($options['daemon']); 80 | } 81 | 82 | function Check_MGR_Primary_Status(){ 83 | 84 | global $options; 85 | 86 | if(isset($options['conf'])){ 87 | 88 | require $options['conf']; 89 | 90 | global $filename; 91 | $filename = $options['conf']; 92 | 93 | //记录监控日志 94 | file_put_contents(dirname(__FILE__)."/".strstr($filename,'.',true)."_master_status.health", "-----------------------------------------------------------------\n"); 95 | 96 | ####################################################### 97 | require_once('mgr_MFailover_class.php'); 98 | 99 | ####################################################### 100 | require_once('mgr_Check_primary_class.php'); 101 | 102 | //检测vip是否存在,如果不存在,退出监控程序。 103 | $check_vip="ssh -p $ssh_port $primary_ip -C 'ip addr list | grep $vip'"; 104 | exec("$check_vip",$output,$return); 105 | if($return == 0){ 106 | file_put_contents(dirname(__FILE__)."/".strstr($filename,'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n"."检测到VIP: ${vip}已经存在"."\n\n", FILE_APPEND); 107 | echo "检测到VIP: ${vip}已经存在".PHP_EOL.PHP_EOL; 108 | } else { 109 | system("ping -c 5 {$primary_ip}",$rs_ping); 110 | if($rs_ping == 0){ 111 | file_put_contents(dirname(__FILE__)."/".strstr($filename,'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n"."检测到VIP: ${vip}不存在"."\n\n"."退出主程序"."\n\n", FILE_APPEND); 112 | echo "检测到VIP: ${vip}不存在!".PHP_EOL.PHP_EOL; 113 | echo "退出主程序".PHP_EOL.PHP_EOL; 114 | unlink(strstr($filename,'.',true).".pid"); 115 | exit; 116 | } else { 117 | file_put_contents(dirname(__FILE__)."/".strstr($filename,'.',true)."_master_status.health", date('Y-m-d H:i:s')."\n\n"."检测到VIP: ${vip}不存在"."\n\n"."无法ping通Primary IP:$primary_ip"." 即将进行VIP切换...\n\n", FILE_APPEND); 118 | echo "\e[38;5;196m检测到VIP: ${vip}不存在! 无法ping通Primary IP:$primary_ip"." 即将进行VIP切换...".PHP_EOL.PHP_EOL; 119 | goto Failover_Vip; 120 | } 121 | } 122 | //--------------------------------------------------- 123 | 124 | Failover_Vip: 125 | $check_primary = new Check_primary($primary_ip,$new_primary_ip,$user,$passwd,$port,$new_port,$hostname,0); 126 | $r_status = $check_primary->check_primary(); 127 | 128 | //检测失败切换VIP, $r_status值等于0代表切换,1代表不切 129 | if ($r_status == 0){ 130 | $check_new_primary = new Check_primary($primary_ip,$new_primary_ip,$user,$passwd,$port,$new_port,$hostname,1); 131 | $r_new_status = $check_new_primary->check_primary(); 132 | if ($r_new_status == 1) { 133 | $failover_primary = new MFailover($primary_ip,$new_primary_ip,$ssh_port,$vip,$network_name); 134 | $failover_primary->execCommand(); 135 | } 136 | } 137 | } 138 | } 139 | 140 | 141 | 142 | //后台守护进程 143 | class Daemon { 144 | private $pidfile; 145 | private $sleep_time; 146 | function __construct($st,$fn) { 147 | $this->pidfile = dirname(__FILE__).'/'.strstr($fn,'.',true).'.pid'; 148 | $this->sleep_time = $st; 149 | } 150 | 151 | private function startDeamon() { 152 | if (file_exists($this->pidfile)) { 153 | echo "The file $this->pidfile exists." . PHP_EOL; 154 | exit(); 155 | } 156 | 157 | $pid = pcntl_fork(); 158 | if ($pid == -1) { 159 | die('could not fork\n'); 160 | } else if ($pid) { 161 | echo 'start ok' . PHP_EOL; 162 | exit($pid); 163 | } else { 164 | file_put_contents($this->pidfile, getmypid()); 165 | return getmypid(); 166 | } 167 | } 168 | 169 | private function start(){ 170 | $pid = $this->startDeamon(); 171 | while (true) { 172 | Check_MGR_Primary_Status(); 173 | sleep($this->sleep_time); 174 | } 175 | } 176 | 177 | private function stop(){ 178 | if (file_exists($this->pidfile)) { 179 | $pid = file_get_contents($this->pidfile); 180 | posix_kill($pid, 9); 181 | unlink($this->pidfile); 182 | } 183 | } 184 | 185 | public function run($param) { 186 | if($param == 1) { 187 | $this->start(); 188 | }else if($param == 0) { 189 | $this->stop(); 190 | echo "mgr_master_ip_failover.php后台守护进程已停止。". PHP_EOL; 191 | } 192 | else{ 193 | echo 'daemon传参错误,请输入0关闭后台进程,1开启后台线程。'. PHP_EOL; 194 | } 195 | } 196 | 197 | } 198 | 199 | ?> 200 | --------------------------------------------------------------------------------