- 論壇徽章:
- 0
|
我使用的可動(dòng)態(tài)遷移的 mysql 架構(gòu)。
mysql 的架構(gòu)已經(jīng)討論很多了,這里最為經(jīng)典的要算 ( 主 -> 從 ) 結(jié)構(gòu)了。( 下面用 M 表示Master S 表示Slave S1 S2 分別表示一級(jí)Slave 二級(jí)Slave )
這個(gè)架構(gòu)的優(yōu)點(diǎn)是 S 不唯一 分擔(dān)了查詢的壓力 , 即使 1兩個(gè) S 損壞也不會(huì)影響正常的使用 ,缺點(diǎn)是 M 是唯一的,一旦順壞,將影響所有寫(xiě)入的請(qǐng)求。
對(duì)于這個(gè)缺點(diǎn)又有很多不同的解決方案。
方案1:
這個(gè)結(jié)構(gòu),當(dāng) S 損壞不會(huì)影響任何業(yè)務(wù),只需要重裝即可,當(dāng) M 損壞,其中的一臺(tái) S 升級(jí)成為 M ,問(wèn)題是所有的 S 的同步這時(shí)候會(huì)有問(wèn)題。
![]()
方案2:
這個(gè)結(jié)構(gòu)使用雙 M 解決 M 損壞的問(wèn)題。 一般有 DRBD 或 MMM 這兩種。
在 MMM 這個(gè)方案中,如果其中一個(gè) M 在切換之前重啟過(guò)幾次,那么就會(huì)出現(xiàn)其中含有浮動(dòng) ip 的M在損壞以后雖然另一個(gè)切換接管了,但在沒(méi)有人工干預(yù)的情況下 S 的同步會(huì)斷掉。
在 DRBD 的這個(gè)解決方案中,更適用 Innodb ,在使用 Myisam 表的情況下會(huì)出現(xiàn)在切換之后表?yè)p壞,也在某些情況下需要人工干預(yù)。
![]()
方案3:
除了上面的兩個(gè)方案,還有一個(gè)方案3 做的是 M -> S1 -> S2 這樣的結(jié)構(gòu)。
這個(gè)方案解決了在 M 死亡以后 S2 的同步情況的問(wèn)題。但在 S1 死亡以后仍需要人工干預(yù),但在不干預(yù)的情況下既不會(huì)影響插入,也不會(huì)影響查詢,只不過(guò)數(shù)據(jù)不同步了。
情況 1 Master 宕機(jī) : S1 接管 M 的浮動(dòng) IP ,這時(shí)候任何都不需要改動(dòng),數(shù)據(jù)庫(kù)結(jié)構(gòu)完好
情況 2 Slave1 宕機(jī) : 既不會(huì)影響插入,也不會(huì)影響查詢,只不過(guò)數(shù)據(jù)不同步了。需要人工干預(yù)才能恢復(fù)。
情況 3 Slave2 宕機(jī) : 只要不是全宕了就沒(méi)事。
![]()
方案4 :
NDB cluster 集群,和這次要討論的沒(méi)啥關(guān)系。
優(yōu)點(diǎn): 多點(diǎn)寫(xiě)入,完全代替主從結(jié)構(gòu)。
缺點(diǎn): 增加節(jié)點(diǎn)的時(shí)候要集體拆遷。這對(duì)于數(shù)據(jù)增長(zhǎng)快的網(wǎng)站基本就是難以接受。
圖: 略 ,這里不詳細(xì)討論這個(gè)東西。
下面簡(jiǎn)單介紹一下我的情況。
我們是一個(gè)做求職招聘的公司,主要業(yè)務(wù)就是 企業(yè)搜索個(gè)人簡(jiǎn)歷,個(gè)人搜索企業(yè)職位,所以數(shù)據(jù)庫(kù)就是我們的核心業(yè)務(wù)。我們所采用用的全部都是 mysql 。
我們的特點(diǎn)是查詢量大,寫(xiě)入量小,我們采用了 標(biāo)準(zhǔn)的 M -> S 結(jié)構(gòu),并且全部引擎采用了 MYISAM 來(lái)滿足我們的查詢 使用這個(gè)結(jié)構(gòu)的同時(shí)也帶來(lái)了上面提到的問(wèn)題,M是一個(gè)單點(diǎn)故障的問(wèn)題。
為了解決這個(gè)問(wèn)題,我采用了一個(gè)類(lèi)似 方案2 的結(jié)構(gòu),沒(méi)用 DRBD,也沒(méi)用 MMM 而是使用了一個(gè)雙通道的盤(pán)陣來(lái)掛兩臺(tái)機(jī)器來(lái)做 HA
結(jié)構(gòu)圖如下:
![]()
在這個(gè)結(jié)構(gòu)里面我模仿了 MMM 和 DRBD 中的部分思想,使用一個(gè) 浮動(dòng) IP 給 Master ,使用 HA 軟件檢測(cè) Master 的存活,并切換。
使用盤(pán)陣的目的是,考慮我的 bin-log mysql 的數(shù)據(jù)文件是放在同一個(gè)地方的,我希望我在切換Master的時(shí)候 S 同步不會(huì)掉。
做完這個(gè)工作 因自己的小聰明 沾沾自喜 若干天 ……
之后災(zāi)難降臨了 ~~
首先是一次文件系統(tǒng)的 bug ,這讓我看到了盤(pán)陣是個(gè)單點(diǎn),同時(shí)也在安慰自己 用DRBD 碰到這個(gè)情況一樣~~
在來(lái)一次主板的bug 非正常斷電,造成Master 切換。很多表?yè)p壞了,光修表就好長(zhǎng)時(shí)間,然后同步也掉了。
緊接著就是一次意外的斷電,起來(lái)以后和上面的情況一樣、修表、重做同步 ~~。
經(jīng)過(guò)這些問(wèn)題我發(fā)現(xiàn)這個(gè)架構(gòu)簡(jiǎn)直就是災(zāi)難,看來(lái)耍小聰明是不行了,要來(lái)點(diǎn)實(shí)際的了。
痛定思痛之后認(rèn)真的讀了一遍 mysql 的官方手冊(cè)最終吧問(wèn)題集中在了兩個(gè)點(diǎn)上。
1、 mysql master 的HA
2、 當(dāng) master 死亡以后 Slave 如何處理。
針對(duì)這兩問(wèn)題我和我們二個(gè)同事一起討論了一下,最終我們確定了如下結(jié)構(gòu):
![]()
先說(shuō)這個(gè)結(jié)構(gòu)和上面提到的第三種情況非常類(lèi)似,只不過(guò)我多增加了一個(gè) S1 節(jié)點(diǎn)。然后分別在兩個(gè)節(jié)點(diǎn)里放置了 S2 節(jié)點(diǎn)。
這個(gè)結(jié)構(gòu)的巧妙之處就是我可以動(dòng)態(tài)的吧他們變成 方案1 方案3 的任何一種形式,下面就看我如何動(dòng)態(tài)遷移這個(gè)架構(gòu)。
情況 1: S2 宕機(jī), 解決方法:因S2 很多不會(huì)影響任何使用。
情況 2: S1 其中之一宕機(jī), 即使我什么都不修改也只是變成了 方案3 的情況。仍舊可以冗余。(斷掉的機(jī)器動(dòng)態(tài)遷移到完好的那個(gè)數(shù)的底下)
情況 3: Master 宕機(jī), 兩臺(tái)S1 中的1臺(tái)升級(jí)成為 Master 成為標(biāo)準(zhǔn) M -> S 的情況。(斷掉的機(jī)器動(dòng)態(tài)遷移到完好的那個(gè)數(shù)的底下)
為了不使機(jī)器浪費(fèi),我們需要能動(dòng)態(tài)遷移同步斷掉的機(jī)器到完好的那個(gè)樹(shù)的底下。
這里寫(xiě)一個(gè)動(dòng)態(tài)遷移的技巧。也就是我寫(xiě)這個(gè)文章的核心技術(shù)。
1、要?jiǎng)討B(tài)遷移 S1 要打開(kāi) binlog 并記錄同步的binlog日志,S2一級(jí)可以不必打開(kāi)。
在 S1 一級(jí)配置 my.cnf 打開(kāi)其中的
# 我一般喜歡把 log 和數(shù)據(jù)分開(kāi)存放。
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 我喜歡和數(shù)據(jù)放在一起。方便做Slave
master-info-file = /var/lib/mysql/master.info
2、在Master里面建立一個(gè)表用于打標(biāo)記和記錄整個(gè)樹(shù)的結(jié)構(gòu)。
首先添加一個(gè)用戶要對(duì) monitor_db 有 完全的控制權(quán),其次這個(gè)用戶還要有Select_priv,Reload_priv,File_priv,Super_priv,Lock_tables_priv,Repl_slave_priv這些權(quán)限。
后面會(huì)用這個(gè)用戶 改變結(jié)構(gòu),所以權(quán)限一定要加夠。
INSERTINTOmysql.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');
|
創(chuàng)建庫(kù)、表,并定期更新,具體看程序吧。
為了防止一臺(tái)機(jī)器死亡,我把這個(gè)分別放在了 M 和兩臺(tái) S1 上,并利用 crontab 執(zhí)行。
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 分鐘就有一次更新了,即使機(jī)器宕機(jī)了,最少也能保證 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 下面的,不同的情況請(qǐng)自己修改路徑。
初始化 monitor_db insert_uuid --install
然后在 crontab 里面添加即可。
3、寫(xiě)程序在 Slave 上面運(yùn)行,當(dāng)檢測(cè)到自己的父節(jié)點(diǎn)死亡以后,利用 change master 語(yǔ)句遷移為新的父節(jié)點(diǎn)。
這部是成功的關(guān)鍵,如果你上一步成功了那么這步也就很容易了。下面是 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(2 ,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)
|
這個(gè)腳本需要依賴 MySQL-python
一切正常之后我們 只需要在 Slave2 的任意一臺(tái)機(jī)器上測(cè)試即可.編輯上面的
DBuser='monitoruser'
DBpasswd='monitoruserpasswd'
用show slave status ,查看現(xiàn)在的 master 是誰(shuí),然后使用 change_master.py --masterip 另一 S1 的ip,等待大約 1~2 分鐘你就只要不報(bào)錯(cuò)你就可以查看你的 master 是否已經(jīng)改變了。
如果是新做的數(shù)據(jù)庫(kù)還不是一個(gè)從,你如果上面的 log 配置方式是按照我的方式配置的,你可以在裝一個(gè) rsync ,并配置這個(gè) rsync 到 /var/lib/mysql 目錄下 ,使用下面這個(gè)腳本快速制作一個(gè)從數(shù)據(jù)庫(kù)。
腳本中不同處請(qǐng)自行修改。
#!/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"
|
有了這些東西,我們就可以在聯(lián)機(jī)的情況下,動(dòng)態(tài)的遷移數(shù)據(jù)庫(kù)集群到任何一個(gè)樣子,如果是多機(jī)房的那就更適合不過(guò)了,在中心機(jī)房放置 mysql master,在其他的每個(gè)機(jī)房部署 S1 -> S2 就是一個(gè)標(biāo)準(zhǔn)的主從結(jié)構(gòu)。
即使某個(gè)節(jié)點(diǎn)的mysql壞掉了我們也可以動(dòng)態(tài)遷移到另一個(gè)節(jié)點(diǎn)去繼續(xù)做同步,而不影響整體集群。
本文寫(xiě)完了,為了部署方便,我對(duì)上述腳本都使用 bash 重寫(xiě)了一次。在這后面我將貼出我所有配置文件和 腳本。
如果你有什么意見(jiàn)或建議歡迎與我聯(lián)系 xin.yv@163.com
因字?jǐn)?shù)限制其他的部分腳本貼在 http://blog.chinaunix.net/u/31455/showart.php?id=2242965 |
|