当前位置: > 数据库 > MySQL >

可动态迁移的 mysql 架构

时间:2015-03-09 22:29来源:linuxtone.org 作者:IT

mysql 的架构已经讨论很多了,这里最为经典的要算 ( 主  -> 从 ) 结构了。( 下面用 M 表示Master  S 表示Slave  S1 S2 分别表示一级Slave  二级Slave )

这个架构的优点是 S 不唯一 分担了查询的压力 , 即使 1两个 S 损坏也不会影响正常的使用 ,缺点是 M 是唯一的,一旦顺坏,将影响所有写入的请求。

对于这个缺点又有很多不同的解决方案。

方案1:
这个结构,当 S 损坏不会影响任何业务,只需要重装即可,当 M 损坏,其中的一台 S 升级成为 M ,问题是所有的 S 的同步这时候会有问题。

 

方案2:
这个结构使用双 M 解决 M 损坏的问题。 一般有 DRBD 或 MMM 这两种。
在 MMM 这个方案中,如果其中一个 M 在切换之前重启过几次,那么就会出现其中含有浮动 ip 的M在损坏以后虽然另一个切换接管了,但在没有人工干预的情况下 S 的同步会断掉。
在 DRBD 的这个解决方案中,更适用 Innodb ,在使用 Myisam 表的情况下会出现在切换之后表损坏,也在某些情况下需要人工干预。

 

 

方案3:
除了上面的两个方案,还有一个方案3 做的是 M -> S1 -> S2 这样的结构。
这个方案解决了在 M 死亡以后 S2 的同步情况的问题。但在 S1 死亡以后仍需要人工干预,但在不干预的情况下既不会影响插入,也不会影响查询,只不过数据不同步了。
情况 1 Master 宕机 : S1 接管 M 的浮动 IP ,这时候任何都不需要改动,数据库结构完好
情况 2 Slave1 宕机 :  既不会影响插入,也不会影响查询,只不过数据不同步了。需要人工干预才能恢复。
情况 3 Slave2 宕机 :  只要不是全宕了就没事。

 

 

方案4 :
NDB cluster 集群,和这次要讨论的没啥关系。
优点: 多点写入,完全代替主从结构。
缺点: 增加节点的时候要集体拆迁。这对于数据增长快的网站基本就是难以接受。
图: 略 ,这里不详细讨论这个东西。

下面简单介绍一下我的情况。
我们是一个做求职招聘的公司,主要业务就是 企业搜索个人简历,个人搜索企业职位,所以数据库就是我们的核心业务。我们所采用用的全部都是 mysql 。
我们的特点是查询量大,写入量小,我们采用了 标准的 M -> S 结构,并且全部引擎采用了 MYISAM 来满足我们的查询 使用这个结构的同时也带来了上面提到的问题,M是一个单点故障的问题。
为了解决这个问题,我采用了一个类似 方案2 的结构,没用 DRBD,也没用 MMM 而是使用了一个双通道的盘阵来挂两台机器来做 HA
结构图如下:

 

 

在这个结构里面我模仿了 MMM 和 DRBD 中的部分思想,使用一个 浮动 IP 给 Master ,使用 HA 软件检测 Master 的存活,并切换。
使用盘阵的目的是,考虑我的 bin-log mysql 的数据文件是放在同一个地方的,我希望我在切换Master的时候 S 同步不会掉。
做完这个工作 因自己的小聪明 沾沾自喜 若干天 ……

之后灾难降临了 ~~

首先是一次文件系统的 bug ,这让我看到了盘阵是个单点,同时也在安慰自己 用DRBD 碰到这个情况一样~~
在来一次主板的bug 非正常断电,造成Master 切换。很多表损坏了,光修表就好长时间,然后同步也掉了。
紧接着就是一次意外的断电,起来以后和上面的情况一样、修表、重做同步 ~~。
经过这些问题我发现这个架构简直就是灾难,看来耍小聪明是不行了,要来点实际的了。
痛定思痛之后认真的读了一遍 mysql 的官方手册最终吧问题集中在了两个点上。
1、 mysql master 的HA
2、 当 master 死亡以后 Slave 如何处理。
针对这两问题我和我们二个同事一起讨论了一下,最终我们确定了如下结构:

 

 

先说这个结构和上面提到的第三种情况非常类似,只不过我多增加了一个 S1 节点。然后分别在两个节点里放置了 S2 节点。
这个结构的巧妙之处就是我可以动态的吧他们变成 方案1 方案3 的任何一种形式,下面就看我如何动态迁移这个架构
情况 1: S2 宕机, 解决方法:因S2 很多不会影响任何使用。
情况 2: S1 其中之一宕机, 即使我什么都不修改也只是变成了 方案3 的情况。仍旧可以冗余。(断掉的机器动态迁移到完好的那个数的底下)
情况 3: Master 宕机,  两台S1 中的1台升级成为 Master 成为标准 M -> S 的情况。(断掉的机器动态迁移到完好的那个数的底下)
为了不使机器浪费,我们需要能动态迁移同步断掉的机器到完好的那个树的底下。

这里写一个动态迁移的技巧。也就是我写这个文章的核心技术。
1、要动态迁移 S1 要打开 binlog 并记录同步的binlog日志,S2一级可以不必打开。
    在 S1 一级配置 my.cnf 打开其中的
    # 我一般喜欢把 log 和数据分开存放。
    log-bin = /var/log/mysqllog/bin-log/mysql-bin
    log-slow-queries = /var/log/mysqllog/db-slow.log
    log-error = /var/log/mysqllog/db.err
    log-slave-updates
    # 我的 relay log 也存放在 /var/log/mysql/log
    relay-log = /var/log/mysqllog/relay-log
    relay-log-index = /var/log/mysqllog/relay-log-index
    relay-log-info-file = /var/log/mysqllog/relay-log.info
    # master info 我喜欢和数据放在一起。方便做Slave
    master-info-file = /var/lib/mysql/master.info

2、在Master里面建立一个表用于打标记和记录整个树的结构。
    首先添加一个用户要对 monitor_db 有 完全的控制权,其次这个用户还要有Select_priv,Reload_priv,File_priv,Super_priv,Lock_tables_priv,Repl_slave_priv这些权限。
     后面会用这个用户 改变结构,所以权限一定要加够。
     

INSERTINTO mysql.user VALUES('%','monitoruser',password("monitoruserpasswd"),'Y','N','N','N','N','N','Y','N','N','Y','N','N','N','N','N','Y','N','Y','N','Y','N','N','N','N','N','N','','','','',0,0,0,0);
INSERT INTO mysql.db VALUES
('%','monitor_db','monitoruser','Y','Y','Y','Y','Y','Y','N','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y');

     创建库、表,并定期更新,具体看程序吧。
     为了防止一台机器死亡,我把这个分别放在了 M 和两台 S1 上,并利用 crontab 执行。
     M 和 S 的 crontab 分别配置
     0-54/6 * * * * /usr/local/bin/insert_uuid
     2-56/6 * * * * /usr/local/bin/insert_uuid
     4-58/6 * * * * /usr/local/bin/insert_uuid
     这样就能保证 2 分钟就有一次更新了,即使机器宕机了,最少也能保证 4-6 分钟有一次更新。
     insert_uuid 代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uuid/uuid.h>
#include <mysql.h>

int main(int argc,char *argv[])
{
    char *server="127.0.0.1",*user="monitoruser",*password="monitoruserpasswd";
    MYSQL *conn;
    MYSQL_RES *res;
    MYSQL_ROW row;
    conn = mysql_init(NULL);
    if (!mysql_real_connect(conn, server,user, password, "monitor_db", 0, NULL, 0)) {
        fprintf(stderr, "%s/n", mysql_error(conn));
        exit(EXIT_FAILURE);
    }
    uuid_t uu;
    char myuuid[37];
    uuid_generate(uu);
    uuid_unparse(uu,myuuid);
    char sqlstr[512];
    if (argc == 2) {
        char *opt = "--install";
        if (!strcmp(argv[1],opt))
        {
            strcpy(sqlstr,"DROP DATABASE monitor_db");
            mysql_query(conn,sqlstr);
            strcpy(sqlstr,"CREATE DATABASE monitor_db");
            mysql_query(conn,sqlstr);
           strcpy(sqlstr,"CREATE TABLE monitor_db.monitor_uuid (`uuid` char(41)NOT NULL,`time` int(11) NOT NULL) ENGINE=MyISAM DEFAULT CHARSET=utf8");
            mysql_query(conn,sqlstr);
            strcpy(sqlstr,"INSERT INTO monitor_db.monitor_uuid VALUES ('UUID_00000000-0000-0000-0000-0000000',0000000000)");
            mysql_query(conn,sqlstr);
            if(mysql_affected_rows(conn) != 1)
                printf("install monitor_db error./n");
        }
    } else {
    strcpy(sqlstr,"UPDATE monitor_db.monitor_uuid SET uuid=/"UUID_");
    strcat(strcat(sqlstr,myuuid),"/",time=UNIX_TIMESTAMP(NOW())");
    mysql_query(conn,sqlstr);
    if(mysql_affected_rows(conn) <= 0)
       printf("update uuid error.");
    }
    mysql_close(conn);
    exit(EXIT_SUCCESS);
}

首先编辑 char *server="127.0.0.1",*user="你的用户名",*password="你的密码"; 吧这里的 127.0.0.1 改成你 M 的ip ,user 改成 用户名,passowrd 改成密码
使用 gcc -O2 -I/usr/local/mysql/include/mysql -rdynamic-L/usr/local/mysql/lib/mysql -lmysqlclient -lz -lcrypt -lnsl -lm -luuid-o insert_uuid insert_uuid.c 编译
我的 mysql 是安装在 /usr/local/mysql 下面的,不同的情况请自己修改路径。
初始化 monitor_db  insert_uuid --install
然后在 crontab 里面添加即可。

3、写程序在 Slave 上面运行,当检测到自己的父节点死亡以后,利用 change master 语句迁移为新的父节点。
这部是成功的关键,如果你上一步成功了那么这步也就很容易了。下面是 change master 的脚本。

#!/usr/bin/python
"""Change the master from mysql slave"""

import os,sys,re,string,time,struct,MySQLdb
DBuser='monitoruser'
DBpasswd='monitoruserpasswd'

def main(masterip):
    try:
        slave_link = MySQLdb.connect(host='127.0.0.1',user=DBuser,passwd=DBpasswd,db='mysql')
    except Exception, e:
        print e
        sys.exit(1)

    slave_connect = slave_link.cursor()
   
    # slave stop
    sql = "slave stop"
    slave_connect.execute(sql)

    # get uuid
    sql = "select uuid from monitor_db.monitor_uuid"
    slave_connect.execute(sql)
    UUID=slave_connect.fetchall()[0][0]

    # get Exec_Master_Log_Pos
    sql = "show slave status"
    slave_connect.execute(sql)
    local_var = slave_connect.fetchall()[0]
    Exec_Master_Log_Pos=local_var[21]

    # get uuid form relaylog
    fp = open("/var/log/mysqllog/"+local_var[7],"rb")
    binlogstr=struct.unpack("4s",fp.read(4))
    if binlogstr[0] != chr(0xfe) + chr(0x62) + chr(0x69) + chr(0x6e):
        print "binlogfile error !"
        sys.exit(1)
    relay_log_pos = None
    while relay_log_pos == None:
        try:
            binlogstr=struct.unpack("4s 5x 4s 4s 2x",fp.read(19))
        except:
            print "Error: get uuid from relaylog failed"
            sys.exit(1)
       event_length = ord(binlogstr[1][0]) + ord(binlogstr[1][1])*256 +ord(binlogstr[1][2])*65536 + ord(binlogstr[1][3])*16777216
        event = struct.unpack(str(event_length-19)+"s",fp.read(event_length-19))
        if re.search(UUID,event[0]):
           relay_log_pos = ord(binlogstr[2][0]) + ord(binlogstr[2][1])*256 +ord(binlogstr[2][2])*65536 + ord(binlogstr[2][3])*16777216
            break
    fp.close()

    # connect mysql master
    try:
        master_link = MySQLdb.connect(host=masterip,user=DBuser,passwd=DBpasswd,db='mysql')
    except Exception, e:
        print e
        sys.exit(1)
    master_connect = master_link.cursor()

    # get master binlog file and size
    sql = "show binary logs"
    master_connect.execute(sql)
    file_size = master_connect.fetchall()

    # get uuid from master binlog
    pos = 0
    sql_cmd = None
    for fz in file_size[::-1]:
        sql = "show binlog events in '%s'" % fz[0]
        master_connect.execute(sql)
        for remote_var in master_connect.fetchall():
            if re.search(UUID,remote_var[5]):
                pos = Exec_Master_Log_Pos - relay_log_pos + remote_var[4]
                for fixsize in file_size[list(file_size).index(fz)::]:
                    if pos <= fixsize[1]:
                        sql_cmd = "change master to MASTER_HOST='%s',/
                                                    MASTER_USER='%s',/
                                                    MASTER_PASSWORD='%s',/
                                                    MASTER_LOG_FILE='%s',/
                                                    MASTER_LOG_POS=%s"/
                                  % (masterip,DBuser,DBpasswd,fixsize[0],pos)
                        break
                    else:
                        pos = pos - fixsize[1] + 117
                break
        if sql_cmd is not None: break

    master_connect.close()
    master_link.close()
   
    # change master
    for sql in  ["flush tables","reset slave",sql_cmd,"slave start"]:
        slave_connect.execute(sql)

    time.sleep(1)
   event_name=['Slave_IO_State:','Master_Host:','Master_User:','Master_Port:','Connect_Retry:','Master_Log_File:','Read_Master_Log_Pos:',
                'Relay_Log_File:','Relay_Log_Pos:','Relay_Master_Log_File:','Slave_IO_Running:','Slave_SQL_Running:','Replicate_Do_DB:',
                'Replicate_Ignore_DB:','Replicate_Do_Table:','Replicate_Ignore_Table:','Replicate_Wild_Do_Table:','Replicate_Wild_Ignore_Table:',
                'Last_Errno:','Last_Error:','Skip_Counter:','Exec_Master_Log_Pos:','Relay_Log_Space:','Until_Condition:','Until_Log_File:',
                'Until_Log_Pos:','Master_SSL_Allowed:','Master_SSL_CA_File:','Master_SSL_CA_Path:','Master_SSL_Cert:','Master_SSL_Cipher:',
                 'Master_SSL_Key:','Seconds_Behind_Master:']
   
    sql = "show slave status"
    slave_connect.execute(sql)
    event_status = slave_connect.fetchall()[0]
    for i in range(0,33): print event_name.rjust(28),event_status
  
    slave_connect.close()
    slave_link.close()

if (len(sys.argv) == 3):
    if (sys.argv[1] == '--masterip'):
        iplist=string.split(sys.argv[2],'.')
        if len(iplist) == 4:
           if ( 0 <= int(iplist[0]) < 256 ) and ( 0 <= int(iplist[1])< 256 ) and ( 0 <= int(iplist[2]) < 256 ) and ( 0 <=int(iplist[3]) < 256 ):
                main(sys.argv[2])
                sys.exit(0)
else:
    print "%s --masterip xx.xx.xx.xx" % sys.argv[0]
    sys.exit(1)

这个脚本需要依赖 MySQL-python
一切正常之后我们 只需要在 Slave2 的任意一台机器上测试即可.编辑上面的
DBuser='monitoruser'
DBpasswd='monitoruserpasswd'
用show slave status ,查看现在的 master 是谁,然后使用 change_master.py --masterip 另一 S1 的ip,等待大约 1~2 分钟你就只要不报错你就可以查看你的 master 是否已经改变了。

如果是新做的数据库还不是一个从,你如果上面的 log 配置方式是按照我的方式配置的,你可以在装一个 rsync ,并配置这个 rsync 到 /var/lib/mysql 目录下 ,使用下面这个脚本快速制作一个从数据库。
脚本中不同处请自行修改。

#!/bin/bash
Slave_IP="192.168.1.100"
PATH=${PATH}:/usr/local/bin
/etc/init.d/mysqld stop
rm -rf /var/log/mysqllog/relay*
mysql -u'monitoruser' -p'monitoruserpasswd' -h${Slave_IP} -e "slave stop;flush tables;"
Exec_pos=($(mysql-u'replication_user' -p'800HRreplication' -h${Slave_IP} -e "show slavestatus/G"|awk '$0~/Exec_Master_Log_Pos/{print $2}'))
typeset -x RSYNC_PASSWORD='rsyncpassword';
rsync -av --delete mysql-data@${Slave_IP}::mysql-data /var/lib/mysql/
sed '3 c/'${Exec_pos} -i /var/lib/mysql/master.info
mysql -u'monitoruser' -p'monitoruserpasswd' -h${Slave_IP} -e "slave start;"
/etc/init.d/mysqld start
sleep 3
mysql -u'monitoruser' -p'monitoruserpasswd' -h127.0.0.1 -e "show slave status/G"

有了这些东西,我们就可以在联机的情况下,动态的迁移数据库集群到任何一个样子,如果是多机房的那就更适合不过了,在中心机房放置 mysql master,在其他的每个机房部署  S1 -> S2 就是一个标准的主从结构。
即使某个节点的mysql坏掉了我们也可以动态迁移到另一个节点去继续做同步,而不影响整体集群。


转载 http://bbs.linuxtone.org/forum-viewthread-tid-6514-highlight-%E6%9E%B6%E6%9E%84.html


(责任编辑:IT)
------分隔线----------------------------
栏目列表
推荐内容