一、前言
隨著系統(tǒng)應(yīng)用訪問量逐漸增大,單臺數(shù)據(jù)庫讀寫訪問壓力也隨之增大,當(dāng)讀寫訪問達到一定瓶頸時,將數(shù)據(jù)庫的讀寫效率驟然下降,甚至不可用。
為了解決此類問題,通常會采用MySQL集群,當(dāng)主庫宕機后,集群會自動將一個從庫升級為主庫,繼續(xù)對外提供服務(wù);那么主庫和從庫之間的數(shù)據(jù)是如何同步的呢?本文將分理論和實踐兩個角度分別進行介紹。
二、概念篇
2.1 MySQL復(fù)制是什么?(WHAT?)
為了減輕主庫的壓力,應(yīng)該在系統(tǒng)應(yīng)用層面做讀寫分離,寫操作走主庫,讀操作走從庫。下圖為MySQL官網(wǎng)給出的主從復(fù)制的原理圖,從圖中可以簡單的了解讀寫分離及主從同步的過程,分散了數(shù)據(jù)庫的訪問壓力,提升整個系統(tǒng)的性能和可用性,降低了大訪問量引發(fā)數(shù)據(jù)庫宕機的故障率。
復(fù)制的結(jié)果是集群(Cluster)中的所有數(shù)據(jù)庫服務(wù)器得到的數(shù)據(jù)理論上都是一樣的,都是同一份數(shù)據(jù),只是有多個copy。MySQL默認內(nèi)建的復(fù)制策略是異步的,基于不同的配置,Slave不一定要一直和Master保持連接不斷的復(fù)制或等待復(fù)制,我們可以指定復(fù)制所有的數(shù)據(jù)庫,一部分數(shù)據(jù)庫,甚至是某個數(shù)據(jù)庫的某部分的表。
2.1.1 復(fù)制策略
MySQL復(fù)制支持多種不同的復(fù)制策略,包括同步、半同步、異步和延遲策略等。
- 同步策略:Master要等待所有Slave應(yīng)答之后才會提交(MySql對DB操作的提交通常是先對操作事件進行二進制日志文件寫入然后再進行提交)。
- 半同步策略:Master等待至少一個Slave應(yīng)答就可以提交。
- 異步策略:Master不需要等待Slave應(yīng)答就可以提交。
- 延遲策略:Slave要至少落后Master指定的時間。
2.1.2 復(fù)制模式
根據(jù)binlog日志格式的不同,MySQL復(fù)制同時支持多種不同的復(fù)制模式:
- 基于語句的復(fù)制,即Statement Based Replication(SBR):記錄每一條更改數(shù)據(jù)的sql
- 優(yōu)點:binlog文件較小,節(jié)約I/O,性能較高。
- 缺點:不是所有的數(shù)據(jù)更改都會寫入binlog文件中,尤其是使用MySQL中的一些特殊函數(shù)(如LOAD_FILE()、UUID()等)和一些不確定的語句操作,從而導(dǎo)致主從數(shù)據(jù)無法復(fù)制的問題。
- 基于行的復(fù)制,即Row Based Replication(RBR):不記錄sql,只記錄每行數(shù)據(jù)的更改細節(jié)
- 優(yōu)點:詳細的記錄了每一行數(shù)據(jù)的更改細節(jié),這也意味著不會由于使用一些特殊函數(shù)或其他情況導(dǎo)致不能復(fù)制的問題。
- 缺點:由于row格式記錄了每一行數(shù)據(jù)的更改細節(jié),會產(chǎn)生大量的binlog日志內(nèi)容,性能不佳,并且會增大主從同步延遲出現(xiàn)的幾率。
- 混合復(fù)制(Mixed)
一般的語句修改使用statment格式保存binlog,如一些函數(shù),statement無法完成主從復(fù)制的操作,則采用row格式保存binlog,MySQL會根據(jù)執(zhí)行的每一條具體的sql語句來區(qū)分對待記錄的日志形式,也就是在Statement和Row之間選擇一種。
2.2 MySQL的復(fù)制有什么好處?(WHY)
- 性能方面:MySQL復(fù)制是一種Scale-out方案,也即“水平擴展”,將原來的單點負載擴散到多臺Slave機器中去,從而提高總體的服務(wù)性能。在這種方式下,所有的寫操作,當(dāng)然包括UPDATE操作,都要發(fā)生在Master服務(wù)器上。讀操作發(fā)生在一臺或者多臺Slave機器上。這種模型可以在一定程度上提高總體的服務(wù)性能,Master服務(wù)器專注于寫和更新操作,Slave服務(wù)器專注于讀操作,我們同時可以通過增加Slave服務(wù)器的數(shù)量來提高讀服務(wù)的性能?!獙崿F(xiàn)“讀”與“寫”分離
- 故障恢復(fù):同時存在多臺Slave提供讀操作服務(wù),如果有一臺Slave掛掉之后我們還可以從其他Slave讀取,如果配置了主從切換的話,當(dāng)Master掛掉之后我們還可以選擇一臺Slave作為Master繼續(xù)提供寫服務(wù),這大大增加了應(yīng)用的可靠性。
- 數(shù)據(jù)分析:實時數(shù)據(jù)可以存儲在Master,而數(shù)據(jù)分析可以從Slave讀取,這樣不會影響Master的性能。
2.3 MySQL如實實現(xiàn)主從復(fù)制?(HOW)
mysql主從復(fù)制需要三個線程,master(binlog dump thread)、slave(I/O thread 、SQL thread)。
- master
- binlog dump線程:當(dāng)主庫中有數(shù)據(jù)更新時,那么主庫就會根據(jù)按照設(shè)置的binlog格式,將此次更新的事件類型寫入到主庫的binlog文件中,此時主庫會創(chuàng)建log dump線程通知slave有數(shù)據(jù)更新,當(dāng)I/O線程請求日志內(nèi)容時,會將此時的binlog名稱和當(dāng)前更新的位置同時傳給slave的I/O線程。
- slave
- I/O線程:該線程會連接到master,向log dump線程請求一份指定binlog文件位置的副本,并將請求回來的binlog存到本地的relay log中,relay log和binlog日志一樣也是記錄了數(shù)據(jù)更新的事件,它也是按照遞增后綴名的方式,產(chǎn)生多個relay log( host_name-relay-bin.000001)文件,slave會使用一個index文件( host_name-relay-bin.index)來追蹤當(dāng)前正在使用的relay log文件。
- SQL線程:該線程檢測到relay log有更新后,會讀取并在本地做redo操作,將發(fā)生在主庫的事件在本地重新執(zhí)行一遍,來保證主從數(shù)據(jù)同步。此外,如果一個relay log文件中的全部事件都執(zhí)行完畢,那么SQL線程會自動將該relay log 文件刪除掉。

- 主庫將更新以事件的形式記錄到二進制日志(Binary log)文件中;
- 從庫開啟IO線程,與主庫建立普通連接;主庫開啟二進制轉(zhuǎn)儲線程,將二進制日志中的內(nèi)容發(fā)送到從庫;從庫IO線程收到主庫的內(nèi)容后,寫入到從庫的中繼日志(Relay log)文件中;
需要注意的是,二進制轉(zhuǎn)儲線程不是輪詢的,如果該線程追改上主庫的更新線程,則進入sleep狀態(tài),由主庫進行喚醒;二進制日志與中繼日志相同;
- 從庫開啟SQL線程,讀取并且重放中繼日志中的事件,實現(xiàn)與主庫的一致;
2.4 主從同步的延遲
mysql的主從復(fù)制都是單線程的操作,主庫對所有DDL和DML產(chǎn)生binlog,binlog是順序?qū)?,所以效率很高,slave的I/O線程到主庫取日志,效率也比較高,但是,slave的SQL線程將主庫的DDL和DML操作在slave實施。DML和DDL的IO操作是隨即的,不是順序的,成本高很多,還可能存在slave上的其他查詢產(chǎn)生lock爭用的情況,由于SQL也是單線程的,所以一個DDL卡住了,需要執(zhí)行很長一段事件,后續(xù)的DDL線程會等待這個DDL執(zhí)行完畢之后才執(zhí)行,這就導(dǎo)致了延時。當(dāng)主庫的TPS并發(fā)較高時,產(chǎn)生的DDL數(shù)量超過slave一個sql線程所能承受的范圍,延時就產(chǎn)生了,除此之外,還有可能與slave的大型query語句產(chǎn)生了鎖等待導(dǎo)致。
由于主從同步延遲是客觀存在的,我們只能從我們自己的架構(gòu)上進行設(shè)計, 盡量讓主庫的DDL快速執(zhí)行。下面列出幾種常見的解決方案:
- 業(yè)務(wù)的持久化層的實現(xiàn)采用分庫架構(gòu),mysql服務(wù)可平行擴展,分散壓力。
- 服務(wù)的基礎(chǔ)架構(gòu)在業(yè)務(wù)和mysql之間加入memcache或者Redis的cache層。降低mysql的讀壓力;
- 使用比主庫更好的硬件設(shè)備作為slave;
- sync_binlog在slave端設(shè)置為0。該選項控制mysql怎么刷新二進制日志到磁盤,默認是0,意味著mysql并不刷新,由操作系統(tǒng)自己決定什么時候刷新緩存到持久化設(shè)置,如果這個值比0大,它指定了兩次刷新到磁盤的動作之間間隔多少次二進制日志寫操作;
- 禁用slave庫的binlog。
通常情況,從服務(wù)器從主服務(wù)器接收到的更新不記入它的二進制日志。該選項告訴從服務(wù)器將其SQL線程執(zhí)行的更新記入到從服務(wù)器自己的二進制日志。為了使該選項生效,還必須用--logs-bin選項啟動從服務(wù)器以啟用二進制日志。如果想要應(yīng)用鏈?zhǔn)綇?fù)制服務(wù)器,應(yīng)使用
--logs-slave-updates。
例如,可能你想要這樣設(shè)置:
graph LR
A-->B
B-->C
也就是說,A為從服務(wù)器B的主服務(wù)器,B為從服務(wù)器C的主服務(wù)器。為了能工作,B必須既為主服務(wù)器又為從服務(wù)器。你必須用--logs-bin啟動A和B以啟用二進制日志,并且用--logs-slave-updates選項啟動B。
2.5 其他問題
問題一:通過復(fù)制模型雖然讀能力可以通過擴展slave機器來達到提高,而寫能力卻不能,如果寫達到瓶頸我們應(yīng)該怎么做呢?
答:我們首先會得出結(jié)論,這種復(fù)制模型對于寫少讀多型應(yīng)用是非常有優(yōu)勢的,其次,當(dāng)遇到這種問題的時候我們可以對數(shù)據(jù)庫進行分庫操作,所謂分庫,就是將業(yè)務(wù)相關(guān)性比較大的表放在同一個數(shù)據(jù)庫中,例如之前數(shù)據(jù)庫有A,B,C,D四張表,A表和B表關(guān)系比較大,而C表和D表關(guān)系比較大,這樣我們把C表和D表分離出去成為一個單獨的數(shù)據(jù)庫,通過這種方式,我們可以將原有的單點寫變成雙點寫或多點些,從而降低原有主庫的寫負載。
問題二:因為復(fù)制是有延遲的,肯定會發(fā)生主庫寫了,但是從庫還沒有讀到的情況,遇到這種問題怎么辦?
答:MySQL支持不同的復(fù)制策略,基于不同的復(fù)制策略達到的效果也是不一樣的,如果是異步復(fù)制,MySQL不能保證從庫立馬能夠讀到主庫實時寫入的數(shù)據(jù),這個時候我們要權(quán)衡選擇不同復(fù)制策略的利弊來進行取舍。所謂利弊,就是我們是否對從庫的讀有那么高的實時性要求,如果真的有,我們可以考慮使用同步復(fù)制策略,但是這種策略相比于異步復(fù)制策略會大大降低主庫的響應(yīng)時間和性能。我們是否可以在應(yīng)用的設(shè)計層面去避開這個問題?
問題三:復(fù)制的不同模式有什么優(yōu)缺點?我們?nèi)绾芜x擇?
答:基于語句的復(fù)制實際上是把主庫上執(zhí)行的SQL在從庫上重新執(zhí)行一遍,這么做的好處是實現(xiàn)起來簡單,當(dāng)前也有缺點,比如我們SQL里面使用了NOW(),當(dāng)同一條SQL在從庫中執(zhí)行的時候顯然和在主庫中執(zhí)行的結(jié)果是不一樣的,諸如此類問題可以類推。其次問題就是這種復(fù)制必須是串行的,為了保證串行執(zhí)行,就需要更多的鎖。
基于行的復(fù)制的時候二進制日志中記錄的實際上是數(shù)據(jù)本身,這樣從庫可以得到正確的數(shù)據(jù),這種方式缺點很明顯,數(shù)據(jù)必須要存儲在二進制日志文件中,這無疑增加的二進制日志文件的大小,同時增加的IO線程的負載和網(wǎng)絡(luò)帶寬消耗。而相比于基于語句的復(fù)制還有一個優(yōu)點就是基于行的復(fù)制無需重放查詢,省去了很多性能消耗。
無論哪種復(fù)制模式都不是完美的,日志如何選擇,這個問題可以在理解他們的優(yōu)缺點之后進行權(quán)衡。
三、主從同步配置實戰(zhàn)
3.1 準(zhǔn)備工作
演示之前,需要兩臺安裝有mysql的機器。有條件的搞兩臺阿里云主機,沒條件的裝個vmare,裝兩臺Linux系統(tǒng)的虛擬機,Ubuntu或者centos可隨意。再或者一臺機器跑兩個MySQL的實例,跑在兩個不同的端口3306和3307上。
使用ifconfig指令查看主機的ip地址。
| 主機1(主庫) | 主機2(從庫) |
|---|---|
| 192.168.91.134 | 192.168.91.137 |
3.2 主庫配置
首先,修改主的配置:
find / -name 'my.cnf'
#假定查找到的地址為/etc/mysql/my.cnf
sudo vim /etc/mysql/my.cnf
在[mysqld]下添加如下的配置信息:
server-id=1
log-bin=master-bin
log-bin-index=master-bin.index
修改完成后保存退出,接著使用如下的指令重啟mysql:
service mysql restart
通過如下指令觀察mysql是否重啟成功:
ps -ef|grep mysqld
mysql 5887 1 0 18:52 ? 00:00:00 /usr/sbin/mysqld
root 6091 4169 0 18:52 pts/13 00:00:00 grep --color=auto mysqld
看到如上的顯示說明重啟成功,成功重啟之后登陸mysql
mysql -u root -p 123
查看下主mysql的當(dāng)前狀態(tài):
show master status;
顯示信息如下:
+-------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+-------------------+----------+--------------+------------------+
| master-bin.000001 | 107 | | |
+-------------------+----------+--------------+------------------+
1 row in set (0.00 sec)
接下來創(chuàng)建一個用來復(fù)制主信息的賬戶,并賦予其備份的權(quán)限:
create user repl;
grant replication slave on *.* to 'repl'@'192.168.91.137' identified by 'mysql';
flush privileges;
接下來創(chuàng)建一個測試的數(shù)據(jù)庫,用于查看同步的結(jié)果:
create database o2o;
3.3 從庫配置
下面開始配置從庫
首先從修改從庫的配置文件開始:
sudo vim /etc/mysql/my.cnf
在mysqld下添加如下的配置信息:
server-id=2
relay-log=slave-relay-bin
relay-log-index=slave-relay-bin.index
保存退出后重啟mysql,重啟的方法除了上面的方法之外,這里再介紹另外一種方法:
/etc/init.d/mysql restart
同樣通過上述的ps指令查看mysql的啟動狀態(tài):
ps -ef|grep mysqld
下面登錄mysql:
mysql -u root -p 123
下面為從庫配置主庫連接的相關(guān)信息:
change master to master_host='192.168.91.134',master_port=3306,master_user='repl',master_password='mysql',master_log_file='master-bin.000001',master_log_pos=0;
備注:這里的master_log_pos設(shè)置為0而不是107的原因在于,當(dāng)前主庫日志文件已經(jīng)到位置107了,而從庫尚未開始復(fù)制,自然是從0位置作為起始的復(fù)制點,而不是主庫的當(dāng)前位置107,否則主庫通過指令create database o2o;新建庫o2o的操作就無法同步過來了,后面基于這個庫的指令重放可能會出問題。
執(zhí)行
change master to master_host='ip', master_port=3306, master_user='xxx',master_password='xxx', master_log_file='mysql-bin.000012',master_log_pos=107;
拋出錯誤
ERROR 1201 (HY000): Could not initialize master info structure; more error messages can be found in the MySQL error log
解決辦法:
stop slave;
reset slave;
完成之后查看一下當(dāng)前salave的狀態(tài):
show slave status \G;
顯示信息如下:
*************************** 1. row ***************************
Slave_IO_State:
Master_Host: 192.168.91.134
Master_User: repl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: master-bin.000001
Read_Master_Log_Pos: 4
Relay_Log_File: slave-relay-bin.000001
Relay_Log_Pos: 4
Relay_Master_Log_File: master-bin.000001
Slave_IO_Running: No
Slave_SQL_Running: No
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 4
Relay_Log_Space: 107
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: NULL
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 0
1 row in set (0.00 sec)
這里重點關(guān)注到IO線程和SQL線程都沒有啟動:
Slave_IO_Running: No
Slave_SQL_Running: No
考慮重啟下mysql服務(wù):
service mysql restart
進入再次進入從庫查看從庫的運行狀態(tài):
show alave status;
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.91.134
Master_User: repl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: master-bin.000001
Read_Master_Log_Pos: 492
Relay_Log_File: slave-relay-bin.000003
Relay_Log_Pos: 639
Relay_Master_Log_File: master-bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 492
Relay_Log_Space: 795
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 1
1 row in set (0.00 sec)
此時發(fā)現(xiàn),上面提到的兩個線程都處于工作狀態(tài)了。
接下來查看下從庫的數(shù)據(jù)庫:
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| o2o |
| performance_schema |
| scm |
| shopping_mall |
+--------------------+
可以看到,從庫這邊已經(jīng)成功將主庫的數(shù)據(jù)庫信息同步過來了。