本文非常詳細地介紹MySQL復制相關的內容,包括基本概念、復制原理、如何配置不同類型的復制(傳統復制)等等。在此文章之后,還有幾篇文章分別介紹GTID復制、半同步復制、實現MySQL的動靜分離,以及MySQL 5.7.17引入的革命性功能:組復制(MGR)。
本文是MySQL Replication的基礎,但卻非常重要。對於MySQL復制,如何搭建它不是重點(因為簡單,網上資源非常多),如何維護它才是重點(網上資源不集中)。以下幾個知識點是掌握MySQL復制所必備的:
- 復制的原理
- 將master上已存在的數據恢復到slave上作為基准數據
- 獲取正確的binlog坐標
- 深入理解
show slave status中的一些狀態信息

本文對以上內容都做了非常詳細的說明。希望對各位初學、深入MySQL復制有所幫助。
mysql replication官方手冊:https://dev.mysql.com/doc/refman/5.7/en/replication.html。
1.復制的基本概念和原理
mysql復制是指從一個mysql服務器(MASTER)將數據通過日志的方式經過網絡傳送到另一台或多台mysql服務器(SLAVE),然后在slave上重放(replay或redo)傳送過來的日志,以達到和master數據同步的目的。
它的工作原理很簡單。首先確保master數據庫上開啟了二進制日志,這是復制的前提。
- 在slave准備開始復制時,首先要執行
change master to語句設置連接到master服務器的連接參數,在執行該語句的時候要提供一些信息,包括如何連接和要從哪復制binlog,這些信息在連接的時候會記錄到slave的datadir下的master.info文件中,以后再連接master的時候將不用再提供這新信息而是直接讀取該文件進行連接。 - 在slave上有兩種線程,分別是IO線程和SQL線程。
- IO線程用於連接master,監控和接受master的binlog。當啟動IO線程成功連接master時,master會同時啟動一個dump線程,該線程將slave請求要復制的binlog給dump出來,之后IO線程負責監控並接收master上dump出來的二進制日志,當master上binlog有變化的時候,IO線程就將其復制過來並寫入到自己的中繼日志(relay log)文件中。
- slave上的另一個線程SQL線程用於監控、讀取並重放relay log中的日志,將數據寫入到自己的數據庫中。如下圖所示。
站在slave的角度上看,過程如下:

站在master的角度上看,過程如下(默認的異步復制模式,前提是設置了sync_binlog=1,否則binlog刷盤時間由操作系統決定):

所以,可以認為復制大致有三個步驟:
- 數據修改寫入master數據庫的binlog中。
- slave的IO線程復制這些變動的binlog到自己的relay log中。
- slave的SQL線程讀取並重新應用relay log到自己的數據庫上,讓其和master數據庫保持一致。
從復制的機制上可以知道,在復制進行前,slave上必須具有master上部分完整內容作為復制基准數據。例如,master上有數據庫A,二進制日志已經寫到了pos1位置,那么在復制進行前,slave上必須要有數據庫A,且如果要從pos1位置開始復制的話,還必須有和master上pos1之前完全一致的數據。如果不滿足這樣的一致性條件,那么在replay中繼日志的時候將不知道如何進行應用而導致數據混亂。也就是說,復制是基於binlog的position進行的,復制之前必須保證position一致。(注:這是傳統的復制方式所要求的)
可以選擇對哪些數據庫甚至數據庫中的哪些表進行復制。默認情況下,MySQL的復制是異步的。slave可以不用一直連着master,即使中間斷開了也能從斷開的position處繼續進行復制。
MySQL 5.6對比MySQL 5.5在復制上進行了很大的改進,主要包括支持GTID(Global Transaction ID,全局事務ID)復制和多SQL線程並行重放。GTID的復制方式和傳統的復制方式不一樣,通過全局事務ID,它不要求復制前slave有基准數據,也不要求binlog的position一致。
MySQL 5.7.17則提出了組復制(MySQL Group Replication,MGR)的概念。像數據庫這樣的產品,必須要盡可能完美地設計一致性問題,特別是在集群、分布式環境下。Galera就是一個MySQL集群產品,它支持多主模型(多個master),但是當MySQL 5.7.17引入了MGR功能后,Galera的優勢不再明顯,甚至MGR可以取而代之。MGR為MySQL集群中多主復制的很多問題提供了很好的方案,可謂是一項革命性的功能。
復制和二進制日志息息相關,所以學習本章必須先有二進制日志的相關知識。
2.復制的好處
圍繞下面的拓撲圖來分析:

主要有以下幾點好處:
1.提供了讀寫分離的能力。
replication讓所有的slave都和master保持數據一致,因此外界客戶端可以從各個slave中讀取數據,而寫數據則從master上操作。也就是實現了讀寫分離。
需要注意的是,為了保證數據一致性,寫操作必須在master上進行。
通常說到讀寫分離這個詞,立刻就能意識到它會分散壓力、提高性能。
2.為MySQL服務器提供了良好的伸縮(scale-out)能力。
由於各個slave服務器上只提供數據檢索而沒有寫操作,因此"隨意地"增加slave服務器數量來提升整個MySQL群的性能,而不會對當前業務產生任何影響。
之所以"隨意地"要加上雙引號,是因為每個slave都要和master建立連接,傳輸數據。如果slave數量巨多,master的壓力就會增大,網絡帶寬的壓力也會增大。
3.數據庫備份時,對業務影響降到最低。
由於MySQL服務器群中所有數據都是一致的(至少幾乎是一致的),所以在需要備份數據庫的時候可以任意停止某一台slave的復制功能(甚至停止整個mysql服務),然后從這台主機上進行備份,這樣幾乎不會影響整個業務(除非只有一台slave,但既然只有一台slave,說明業務壓力並不大,短期內將這個壓力分配給master也不會有什么影響)。
4.能提升數據的安全性。
這是顯然的,任意一台mysql服務器斷開,都不會丟失數據。即使是master宕機,也只是丟失了那部分還沒有傳送的數據(異步復制時才會丟失這部分數據)。
5.數據分析不再影響業務。
需要進行數據分析的時候,直接划分一台或多台slave出來專門用於數據分析。這樣OLTP和OLAP可以共存,且幾乎不會影響業務處理性能。
3.復制分類和它們的特性
MySQL支持兩種不同的復制方法:傳統的復制方式和GTID復制。MySQL 5.7.17之后還支持組復制(MGR)。
- (1).傳統的復制方法要求復制之前,slave上必須有基准數據,且binlog的position一致。
- (2).GTID復制方法不要求基准數據和binlog的position一致性。GTID復制時,master上只要一提交,就會立即應用到slave上。這極大地簡化了復制的復雜性,且更好地保證master上和各slave上的數據一致性。
從數據同步方式的角度考慮,MySQL支持4種不同的同步方式:同步(synchronous)、半同步(semisynchronous)、異步(asynchronous)、延遲(delayed)。所以對於復制來說,就分為同步復制、半同步復制、異步復制和延遲復制。
3.1 同步復制
客戶端發送DDL/DML語句給master,master執行完畢后還需要等待所有的slave都寫完了relay log才認為此次DDL/DML成功,然后才會返回成功信息給客戶端。同步復制的問題是master必須等待,所以延遲較大,在MySQL中不使用這種復制方式。

例如上圖中描述的,只有3個slave全都寫完relay log並返回ACK給master后,master才會判斷此次DDL/DML成功。
3.2 半同步復制
客戶端發送DDL/DML語句給master,master執行完畢后還要等待一個slave寫完relay log並返回確認信息給master,master才認為此次DDL/DML語句是成功的,然后才會發送成功信息給客戶端。半同步復制只需等待一個slave的回應,且等待的超時時間可以設置,超時后會自動降級為異步復制,所以在局域網內(網絡延遲很小)使用半同步復制是可行的。

例如上圖中,只有第一個slave返回成功,master就判斷此次DDL/DML成功,其他的slave無論復制進行到哪一個階段都無關緊要。
3.3 異步復制
客戶端發送DDL/DML語句給master,master執行完畢立即返回成功信息給客戶端,而不管slave是否已經開始復制。這樣的復制方式導致的問題是,當master寫完了binlog,而slave還沒有開始復制或者復制還沒完成時,slave上和master上的數據暫時不一致,且此時master突然宕機,slave將會丟失一部分數據。如果此時把slave提升為新的master,那么整個數據庫就永久丟失這部分數據。

3.4 延遲復制
顧名思義,延遲復制就是故意讓slave延遲一段時間再從master上進行復制。
4.配置一主一從
此處先配置默認的異步復制模式。由於復制和binlog息息相關,如果對binlog還不熟悉,請先了解binlog,見:詳細分析二進制日志。
mysql支持一主一從和一主多從。但是每個slave必須只能是一個master的從,否則從多個master接受二進制日志后重放將會導致數據混亂的問題。
以下是一主一從的結構圖:

在開始傳統的復制(非GTID復制)前,需要完成以下幾個關鍵點,這幾個關鍵點指導后續復制的所有步驟。
- 為master和slave設定不同的
server-id,這是主從復制結構中非常關鍵的標識號。到了MySQL 5.7,似乎不設置server id就無法開啟binlog。設置server id需要重啟MySQL實例。 - 開啟master的binlog。剛安裝並初始化的MySQL默認未開啟binlog,建議手動設置binlog且為其設定文件名,否則默認以主機名為基名時修改主機名后會找不到日志文件。
- 最好設置master上的變量
sync_binlog=1(MySQL 5.7.7之后默認為1,之前的版本默認為0),這樣每寫一次二進制日志都將其刷新到磁盤,讓slave服務器可以盡快地復制。防止萬一master的二進制日志還在緩存中就宕機時,slave無法復制這部分丟失的數據。 - 最好設置master上的redo log的刷盤變量
innodb_flush_log_at_trx_commit=1(默認值為1),這樣每次提交事務都會立即將事務刷盤保證持久性和一致性。 - 在slave上開啟中繼日志relay log。這個是默認開啟的,同樣建議手動設置其文件名。
- 建議在master上專門創建一個用於復制的用戶,它只需要有復制權限
replication slave用來讀取binlog。 - 確保slave上的數據和master上的數據在"復制的起始position之前"是完全一致的。如果master和slave上數據不一致,復制會失敗。
- 記下master開始復制前binlog的position,因為在slave連接master時需要指定從master的哪個position開始復制。
- 考慮是否將slave設置為只讀,也就是開啟
read_only選項。這種情況下,除了具有super權限(mysql 5.7.16還提供了super_read_only禁止super的寫操作)和SQL線程能寫數據庫,其他用戶都不能進行寫操作。這種禁寫對於slave來說,絕大多數場景都非常適合。
4.1 一主一從
一主一從是最簡單的主從復制結構。本節實驗環境如下:

- 配置master和slave的配置文件。
[mysqld] # master
datadir=/data
socket=/data/mysql.sock
log-bin=master-bin
sync-binlog=1
server-id=100
[mysqld] # slave
datadir=/data
socket=/data/mysql.sock
relay-log=slave-bin
server-id=111
- 重啟master和slave上的MySQL實例。
service mysqld restart
- 在master上創建復制專用的用戶。
create user 'repl'@'192.168.100.%' identified by 'P@ssword1!';
grant REPLICATION SLAVE on *.* to 'repl'@'192.168.100.%';
- 將slave恢復到master上指定的坐標。
這是備份恢復的內容,此處用一個小節來簡述操作過程。詳細內容見MySQL備份和恢復(一)、(二)、(三)。
4.2 將slave恢復到master指定的坐標
對於復制而言,有幾種情況:
- (1).待復制的master沒有新增數據,例如新安裝的mysql實例。這種情況下,可以跳過恢復這個過程。
- (2).待復制的master上已有數據。這時需要將這些已有數據也應用到slave上,並獲取master上binlog當前的坐標。只有slave和master的數據能匹配上,slave重放relay log時才不會出錯。
第一種情況此處不贅述。第二種情況有幾種方法,例如使用mysqldump、冷備份、xtrabackup等工具,這其中又需要考慮是MyISAM表還是InnoDB表。
在實驗開始之前,首先在master上新增一些測試數據,以innodb和myisam的數值輔助表為例。
DROP DATABASE IF EXISTS backuptest;
CREATE DATABASE backuptest;
USE backuptest;
# 創建myisam類型的數值輔助表和插入數據的存儲過程
CREATE TABLE num_isam (n INT NOT NULL PRIMARY KEY) ENGINE = MYISAM ;
DROP PROCEDURE IF EXISTS proc_num1;
DELIMITER $$
CREATE PROCEDURE proc_num1 (num INT)
BEGIN
DECLARE rn INT DEFAULT 1 ;
TRUNCATE TABLE backuptest.num_isam ;
INSERT INTO backuptest.num_isam VALUES(1) ;
dd: WHILE rn * 2 < num DO
BEGIN
INSERT INTO backuptest.num_isam
SELECT rn + n FROM backuptest.num_isam;
SET rn = rn * 2 ;
END ;
END WHILE dd;
INSERT INTO backuptest.num_isam
SELECT n + rn
FROM backuptest.num_isam
WHERE n + rn <= num;
END ;
$$
DELIMITER ;
# 創建innodb類型的數值輔助表和插入數據的存儲過程
CREATE TABLE num_innodb (n INT NOT NULL PRIMARY KEY) ENGINE = INNODB ;
DROP PROCEDURE IF EXISTS proc_num2;
DELIMITER $$
CREATE PROCEDURE proc_num2 (num INT)
BEGIN
DECLARE rn INT DEFAULT 1 ;
TRUNCATE TABLE backuptest.num_innodb ;
INSERT INTO backuptest.num_innodb VALUES(1) ;
dd: WHILE rn * 2 < num DO
BEGIN
INSERT INTO backuptest.num_innodb
SELECT rn + n FROM backuptest.num_innodb;
SET rn = rn * 2 ;
END ;
END WHILE dd;
INSERT INTO backuptest.num_innodb
SELECT n + rn
FROM backuptest.num_innodb
WHERE n + rn <= num ;
END ;
$$
DELIMITER ;
# 分別向兩個數值輔助表中插入100W條數據
CALL proc_num1 (1000000) ;
CALL proc_num2 (1000000) ;
所謂數值輔助表是只有一列的表,且這個字段的值全是數值,從1開始增長。例如上面的是從1到100W的數值輔助表。
mysql> select * from backuptest.num_isam limit 10;
+----+
| n |
+----+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
| 10 |
+----+
4.2.1 獲取master binlog的坐標
如果master是全新的數據庫實例,或者在此之前沒有開啟過binlog,那么它的坐標位置是position=4。之所以是4而非0,是因為binlog的前4個記錄單元是每個binlog文件的頭部信息。
如果master已有數據,或者說master以前就開啟了binlog並寫過數據庫,那么需要手動獲取position。為了安全以及沒有后續寫操作,必須先鎖表。
mysql> flush tables with read lock;
注意,這次的鎖表會導致寫阻塞以及innodb的commit操作。
然后查看binlog的坐標。
mysql> show master status; # 為了排版,簡化了輸出結果
+-------------------+----------+--------------+--------+--------+
| File | Position | Binlog_Do_DB | ...... | ...... |
+-------------------+----------+--------------+--------+--------+
| master-bin.000001 | 623 | | | |
+-------------------+----------+--------------+--------+--------+
記住master-bin.000001和623。
4.2.2 備份master數據到slave上
下面給出3種備份方式以及對應slave的恢復方法。建議備份所有庫到slave上,如果要篩選一部分數據庫或表進行復制,應該在slave上篩選(篩選方式見后文篩選要復制的庫和表),而不應該在master的備份過程中指定。
- 方式一:冷備份直接cp。這種情況只適用於沒有新寫入操作。嚴謹一點,只適合拷貝完成前master不能有寫入操作。
- 如果要復制所有庫,那么直接拷貝整個datadir。
- 如果要復制的是某個或某幾個庫,直接拷貝相關目錄即可。但注意,這種冷備份的方式只適合MyISAM表和開啟了
innodb_file_per_table=ON的InnoDB表。如果沒有開啟該變量,innodb表使用公共表空間,無法直接冷備份。 - 如果要冷備份innodb表,最安全的方法是先關閉master上的mysql,而不是通過表鎖。
所以,如果沒有涉及到innodb表,那么在鎖表之后,可以直接冷拷貝。最后釋放鎖。
mysql> flush tables with read lock;
mysql> show master status; # 為了排版,簡化了輸出結果
+-------------------+----------+--------------+--------+--------+
| File | Position | Binlog_Do_DB | ...... | ...... |
+-------------------+----------+--------------+--------+--------+
| master-bin.000001 | 623 | | | |
+-------------------+----------+--------------+--------+--------+
shell> rsync -avz /data 192.168.100.150:/
mysql> unlock tables;
此處實驗,假設要備份的是整個實例,因為涉及到了innodb表,所以建議關閉MySQL。因為是冷備份,所以slave上也應該關閉MySQL。
# master和slave上都執行
shell> mysqladmin -uroot -p shutdown
然后將整個datadir拷貝到slave上(當然,有些文件是不用拷貝的,比如master上的binlog、mysql庫等)。
# 將master的datadir(/data)拷貝到slave的datadir(/data)
shell> rsync -avz /data 192.168.100.150:/
需要注意,在冷備份的時候,需要將備份到目標主機上的DATADIR/auto.conf刪除,這個文件中記錄的是mysql server的UUID,而master和slave的UUID必須不能一致。
然后重啟master和slave。因為重啟了master,所以binlog已經滾動了,不過這次不用再查看binlog坐標,因為重啟造成的binlog日志移動不會影響slave。
- 方式二:使用mysqldump進行備份恢復。
這種方式簡單的多,而且對於innodb表很適用,但是slave上恢復時速度慢,因為恢復時數據全是通過insert插入的。因為mysqldump可以進行定時點恢復甚至記住binlog的坐標,所以無需再手動獲取binlog的坐標。
shell> mysqldump -uroot -p --all-databases --master-data=2 >dump.db
注意,--master-data選項將再dump.db中加入change master to相關的語句,值為2時,change master to語句是注釋掉的,值為1或者沒有提供值時,這些語句是直接激活的。同時,--master-data會鎖定所有表(如果同時使用了--single-transaction,則不是鎖所有表,詳細內容請參見mysqldump)。
因此,可以直接從dump.db中獲取到binlog的坐標。記住這個坐標。
[root@xuexi ~]# grep -i -m 1 'change master to' dump.db
-- CHANGE MASTER TO MASTER_LOG_FILE='master-bin.000002', MASTER_LOG_POS=154;
然后將dump.db拷貝到slave上,使用mysql執行dump.db腳本即可。也可以直接在master上遠程連接到slave上執行。例如:
shell> mysql -uroot -p -h 192.168.100.150 -e 'source dump.db'
- 方式三:使用xtrabackup進行備份恢復。
這是三種方式中最佳的方式,安全性高、速度快。因為xtrabackup備份的時候會記錄master的binlog的坐標,因此也無需手動獲取binlog坐標。
xtrabackup詳細的備份方法見:xtrabackup
注意:master和slave上都安裝percona-xtrabackup。
以全備份為例:
innobackupex -u root -p /backup
備份完成后,在/backup下生成一個以時間為名稱的目錄。其內文件如下:
[root@xuexi ~]# ll /backup/2018-05-29_04-12-15
total 77872
-rw-r----- 1 root root 489 May 29 04:12 backup-my.cnf
drwxr-x--- 2 root root 4096 May 29 04:12 backuptest
-rw-r----- 1 root root 1560 May 29 04:12 ib_buffer_pool
-rw-r----- 1 root root 79691776 May 29 04:12 ibdata1
drwxr-x--- 2 root root 4096 May 29 04:12 mysql
drwxr-x--- 2 root root 4096 May 29 04:12 performance_schema
drwxr-x--- 2 root root 12288 May 29 04:12 sys
-rw-r----- 1 root root 22 May 29 04:12 xtrabackup_binlog_info
-rw-r----- 1 root root 115 May 29 04:12 xtrabackup_checkpoints
-rw-r----- 1 root root 461 May 29 04:12 xtrabackup_info
-rw-r----- 1 root root 2560 May 29 04:12 xtrabackup_logfile
其中xtrabackup_binlog_info中記錄了binlog的坐標。記住這個坐標。
[root@xuexi ~]# cat /backup/2018-05-29_04-12-15/xtrabackup_binlog_info
master-bin.000002 154
然后將備份的數據執行"准備"階段。這個階段不要求連接mysql,因此不用給連接選項。
innobackupex --apply-log /backup/2018-05-29_04-12-15
最后,將/backup目錄拷貝到slave上進行恢復。恢復的階段就是向MySQL的datadir拷貝。但注意,xtrabackup恢復階段要求datadir必須為空目錄。否則報錯:
[root@xuexi ~]# innobackupex --copy-back /backup/2018-05-29_04-12-15/
180529 23:54:27 innobackupex: Starting the copy-back operation
IMPORTANT: Please check that the copy-back run completes successfully.
At the end of a successful copy-back run innobackupex
prints "completed OK!".
innobackupex version 2.4.11 based on MySQL server 5.7.19 Linux (x86_64) (revision id: b4e0db5)
Original data directory /data is not empty!
所以,停止slave的mysql並清空datadir。
service mysqld stop
rm -rf /data/*
恢復時使用的模式是"--copy-back",選項后指定要恢復的源備份目錄。恢復時因為不需要連接數據庫,所以不用指定連接選項。
[root@xuexi ~]# innobackupex --copy-back /backup/2018-05-29_04-12-15/
180529 23:55:53 completed OK!
恢復完成后,MySQL的datadir的文件的所有者和屬組是innobackupex的調用者,所以需要改回mysql.mysql。
shell> chown -R mysql.mysql /data
啟動slave,並查看恢復是否成功。
shell> service mysqld start
shell> mysql -uroot -p -e 'select * from backuptest.num_isam limit 10;'
+----+
| n |
+----+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
| 10 |
+----+
4.3 slave開啟復制
經過前面的一番折騰,總算是把該准備的數據都准備到slave上,也獲取到master上binlog的坐標(154)。現在還欠東風:連接master。
連接master時,需要使用change master to提供連接到master的連接選項,包括user、port、password、binlog、position等。
mysql> change master to
master_host='192.168.100.20',
master_port=3306,
master_user='repl',
master_password='P@ssword1!',
master_log_file='master-bin.000002',
master_log_pos=154;
完整的change master to語法如下:
CHANGE MASTER TO option [, option] ...
option:
| MASTER_HOST = 'host_name'
| MASTER_USER = 'user_name'
| MASTER_PASSWORD = 'password'
| MASTER_PORT = port_num
| MASTER_LOG_FILE = 'master_log_name'
| MASTER_LOG_POS = master_log_pos
| MASTER_AUTO_POSITION = {0|1}
| RELAY_LOG_FILE = 'relay_log_name'
| RELAY_LOG_POS = relay_log_pos
| MASTER_SSL = {0|1}
| MASTER_SSL_CA = 'ca_file_name'
| MASTER_SSL_CAPATH = 'ca_directory_name'
| MASTER_SSL_CERT = 'cert_file_name'
| MASTER_SSL_CRL = 'crl_file_name'
| MASTER_SSL_CRLPATH = 'crl_directory_name'
| MASTER_SSL_KEY = 'key_file_name'
| MASTER_SSL_CIPHER = 'cipher_list'
| MASTER_SSL_VERIFY_SERVER_CERT = {0|1}
然后,啟動IO線程和SQL線程。可以一次性啟動兩個,也可以分開啟動。
# 一次性啟動、關閉
start slave;
stop slave;
# 單獨啟動
start slave io_thread;
start slave sql_thread;
至此,復制就已經可以開始工作了。當master寫入數據,slave就會從master處進行復制。
例如,在master上新建一個表,然后去slave上查看是否有該表。因為是DDL語句,它會寫二進制日志,所以它也會復制到slave上。
4.4 查看slave的信息
change master to后,在slave的datadir下就會生成master.info文件和relay-log.info文件,這兩個文件隨着復制的進行,其內數據會隨之更新。
4.4.1 master.info
master.info文件記錄的是IO線程相關的信息,也就是連接master以及讀取master binlog的信息。通過這個文件,下次連接master時就不需要再提供連接選項。
以下是master.info的內容,每一行的意義見官方手冊
[root@xuexi ~]# cat /data/master.info
25 # 本文件的行數
master-bin.000002 # IO線程正從哪個master binlog讀取日志
154 # IO線程讀取到master binlog的位置
192.168.100.20 # master_host
repl # master_user
P@ssword1! # master_password
3306 # master_port
60 # master_retry,slave重連master的超時時間(單位秒)
0
0
30.000
0
86400
0
4.4.2 relay-log.info
relay-log.info文件中記錄的是SQL線程相關的信息。以下是relay-log.info文件的內容,每一行的意義見官方手冊
[root@xuexi ~]# cat /data/relay-log.info
7 # 本文件的行數
./slave-bin.000001 # 當前SQL線程正在讀取的relay-log文件
4 # SQL線程已執行到的relay log位置
master-bin.000002 # SQL線程最近執行的操作對應的是哪個master binlog
154 # SQL線程最近執行的操作對應的是master binlog的哪個位置
0 # slave上必須落后於master多長時間
0 # 正在運行的SQL線程數
1 # 一種用於內部信息交流的ID,目前值總是1
4.4.3 show slave status
在slave上執行show slave status可以查看slave的狀態信息。信息非常多,每個字段的詳細意義可參見官方手冊
mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: # slave上IO線程的狀態,來源於show processlist
Master_Host: 192.168.100.20
Master_User: repl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: master-bin.000002
Read_Master_Log_Pos: 154
Relay_Log_File: slave-bin.000001
Relay_Log_Pos: 4
Relay_Master_Log_File: master-bin.000002
Slave_IO_Running: No # IO線程的狀態,此處為未運行且未連接狀態
Slave_SQL_Running: No # SQL線程的狀態,此處為未運行狀態
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: 154
Relay_Log_Space: 154
Until_Condition: None # start slave語句中指定的until條件,
# 例如,讀取到哪個binlog位置就停止
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 # SQL線程執行過的位置比IO線程慢多少
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 # master的server id
Master_UUID:
Master_Info_File: /data/master.info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: # slave SQL線程的狀態
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set:
Executed_Gtid_Set:
Auto_Position: 0
Replicate_Rewrite_DB:
Channel_Name:
Master_TLS_Version:
1 row in set (0.01 sec)
因為太長,后面再列出show slave status時,將裁剪一些意義不大的行。
再次回到上面show slave status的信息。除了那些描述IO線程、SQL線程狀態的行,還有幾個log_file和pos相關的行,如下所列。
Master_Log_File: master-bin.000002
Read_Master_Log_Pos: 154
Relay_Log_File: slave-bin.000001
Relay_Log_Pos: 4
Relay_Master_Log_File: master-bin.000002
Exec_Master_Log_Pos: 154
理解這幾行的意義至關重要,前面因為排版限制,描述看上去有些重復。所以這里完整地描述它們:
Master_Log_File:IO線程正在讀取的master binlog;Read_Master_Log_Pos:IO線程已經讀取到master binlog的哪個位置;Relay_Log_File:SQL線程正在讀取和執行的relay log;Relay_Log_Pos:SQL線程已經讀取和執行到relay log的哪個位置;Relay_Master_Log_File:SQL線程最近執行的操作對應的是哪個master binlog;Exec_Master_Log_Pos:SQL線程最近執行的操作對應的是master binlog的哪個位置。
所以,(Relay_Master_Log_File, Exec_Master_log_Pos)構成一個坐標,這個坐標表示slave上已經將master上的哪些數據重放到自己的實例中,它可以用於下一次change master to時指定的binlog坐標。
與這個坐標相對應的是slave上SQL線程的relay log坐標(Relay_Log_File, Relay_Log_Pos)。這兩個坐標位置不同,但它們對應的數據是一致的。
最后還有一個延遲參數Seconds_Behind_Master需要說明一下,它的本質意義是SQL線程比IO線程慢多少。如果master和slave之間的網絡狀況優良,那么slave的IO線程讀速度和master寫binlog的速度基本一致,所以這個參數也用來描述"SQL線程比master慢多少",也就是說slave比master少多少數據,只不過衡量的單位是秒。但需要注意,這個參數的描述並不標准,只有在網速很好的時候做個大概估計,很多種情況下它的值都是0,即使SQL線程比IO線程慢了很多也是如此。
4.4.4 slave信息匯總
上面的master.info、relay-log.info和show slave status的狀態都是剛連接上master還未啟動IO thread、SQL thread時的狀態。下面將顯示已經進行一段正在執行復制的slave狀態。
首先查看啟動io thread和sql thread后的狀態。使用show processlist查看即可。
mysql> start slave;
mysql> show processlist; # slave上的信息,為了排版,簡化了輸出
+----+-------------+---------+--------------------------------------------------------+
| Id | User | Command | State |
+----+-------------+---------+--------------------------------------------------------+
| 4 | root | Sleep | |
| 7 | root | Query | starting |
| 8 | system user | Connect | Waiting for master to send event |
| 9 | system user | Connect | Slave has read all relay log; waiting for more updates |
+----+-------------+---------+--------------------------------------------------------+
可以看到:
Id=8的線程負責連接master並讀取binlog,它是IO 線程,它的狀態指示"等待master發送更多的事件";Id=9的線程負責讀取relay log,它是SQL線程,它的狀態指示"已經讀取了所有的relay log"。
再看看此時master上的信息。
mysql> show processlist; # master上的信息,為了排版,經過了修改
+----+------+-----------------------+-------------+--------------------------------------+
| Id | User | Host | Command | State |
+----+------+-----------------------+-------------+--------------------------------------+
| 4 | root | localhost | Query | starting |
|----|------|-----------------------|-------------|--------------------------------------|
| 16 | repl | 192.168.100.150:39556 | Binlog Dump | Master has sent all binlog to slave; |
| | | | | waiting for more updates |
+----+------+-----------------------+-------------+--------------------------------------+
master上有一個Id=16的binlog dump線程,該線程的用戶是repl。它的狀態指示"已經將所有的binlog發送給slave了"。
現在,在master上執行一個長事件,以便查看slave上的狀態信息。
仍然使用前面插入數值輔助表的存儲過程,這次分別向兩張表中插入一億條數據(盡管去抽煙、喝茶,夠等幾分鍾的。如果機器性能不好,請大幅減少插入的行數)。
call proc_num1(100000000);
call proc_num2(100000000);
然后去slave上查看信息,如下。因為太長,已經裁剪了一部分沒什么用的行。
mysql> show slave status\G
mysql: [Warning] Using a password on the command line interface can be insecure.
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.100.20
Master_User: repl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: master-bin.000003
Read_Master_Log_Pos: 512685413
Relay_Log_File: slave-bin.000003
Relay_Log_Pos: 336989434
Relay_Master_Log_File: master-bin.000003
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Exec_Master_Log_Pos: 336989219
Slave_SQL_Running_State: Reading event from the relay log
從中獲取到的信息有:
- IO線程的狀態
- SQL線程的狀態
- IO線程讀取到master binlog的哪個位置:512685413
- SQL線程已經執行到relay log的哪個位置:336989434
- SQL線程執行的位置對應於master binlog的哪個位置:336989219
可以看出,IO線程比SQL線程超前了很多很多,所以SQL線程比IO線程的延遲較大。
4.5 MySQL復制如何實現斷開重連
很多人以為change master to語句是用來連接master的,實際上這種說法是錯的。連接master是IO線程的事情,change master to只是為IO線程連接master時提供連接參數。
如果slave從來沒連接過master,那么必須使用change master to語句來生成IO線程所需要的信息,這些信息記錄在master.info中。這個文件是change master to成功之后立即生成的,以后啟動IO線程時,IO線程都會自動讀取這個文件來連接master,不需要先執行change master to語句。
例如,可以隨時stop slave來停止復制線程,然后再隨時start slave,只要master.info存在,且沒有人為修改過它,IO線程就一定不會出錯。這是因為master.info會隨着IO線程的執行而更新,無論讀取到master binlog的哪個位置,都會記錄下這個位置,如此一來,IO線程下次啟動的時候就知道從哪里開始監控master binlog。
前面還提到一個文件:relay-log.info文件。這個文件中記錄的是SQL線程的相關信息,包括讀取、執行到relay log的哪個位置,剛重放的數據對應master binlog的哪個位置。隨着復制的進行,這個文件的信息會即時改變。所以,通過relay-log.info,下次SQL線程啟動的時候就能知道從relay log的哪個地方繼續讀取、執行。
如果不小心把relay log文件刪除了,SQL線程可能會丟失了一部分相比IO線程延遲的數據。這時候,只需將relay-log.info中第4、5行記錄的"SQL線程剛重放的數據對應master binlog的坐標"手動修改到master.info中即可,這樣IO線程下次連接master就會從master binlog的這個地方開始監控。當然,也可以將這個坐標作為change master to的坐標來修改master.info。
此外,當mysql實例啟動時,默認會自動start slave,也就是MySQL一啟動就自動開啟復制線程。如果想要禁止這種行為,在配置文件中加上:
[mysqld]
skip-slave-start
4.6 一些變量
默認情況下,slave連接到master后會在slave的datadir下生成master.info和relay-log.info文件,但是這是可以通過設置變量來改變的。
master-info-repository={TABLE|FILE}:master的信息是記錄到文件master.info中還是記錄到表mysql.slave_master_info中。默認為file。relay-log-info-repository={TABLE|FILE}:slave的信息是記錄到文件relay-log.info中還是記錄到表mysql.slave_relay_log_info中。默認為file。
IO線程每次從master復制日志要寫入到relay log中,但是它是先放在內存的,等到一定時機后才會將其刷到磁盤上的relay log文件中。刷到磁盤的時機可以由變量控制。
另外,IO線程每次從master復制日志后都會更新master.info的信息,也是先更新內存中信息,在特定的時候才會刷到磁盤的master.info文件;同理SQL線程更新realy-log.info也是一樣的。它們是可以通過變量來設置更新時機的。
sync-relay-log=N:設置為大於0的數表示每從master復制N個事件就刷一次盤。設置為0表示依賴於操作系統的sync機制。sync-master-info=N:依賴於master-info-repository的設置,如果為file,則設置為大於0的值時表示每更新多少次master.info將其寫入到磁盤的master.info中,設置為0則表示由操作系統來決定何時調用fdatasync()函數刷到磁盤。如果設置為table,則設置為大於0的值表示每更新多少次master.info就更新mysql.slave_master_info表一次,如果設置為0則表示永不更新該表。sync-relay-log-info=N:同上。
5.一主多從
一主多從有兩種情況,結構圖如下。
以下是一主多從的結構圖(和一主一從的配置方法完全一致):

以下是一主多從,但某slave是另一群MySQL實例的master:

配置一主多從時,需要考慮一件事:slave上是否要開啟binlog? 如果不開啟slave的binlog,性能肯定要稍微好一點。但是開啟了binlog后,可以通過slave來備份數據,也可以在master宕機時直接將slave切換為新的master。此外,如果是上面第二種主從結構,這台slave必須開啟binlog。可以將某台或某幾台slave開啟binlog,並在mysql動靜分離的路由算法上稍微減少一點到這些slave上的訪問權重。
上面第一種一主多從的結構沒什么可解釋的,它和一主一從的配置方式完全一樣,但是可以考慮另一種情況:向現有主從結構中添加新的slave。所以,稍后先介紹這種添加slave,再介紹第二種一主多從的結構。
5.1 向現有主從結構中添加slave
官方手冊:https://dev.mysql.com/doc/refman/5.7/en/replication-howto-additionalslaves.html
例如在前文一主一從的實驗環境下添加一台新的slave。
因為新的slave在開始復制前,要有master上的基准數據,還要有master binlog的坐標。按照前文一主一從的配置方式,當然很容易獲取這些信息,但這樣會將master鎖住一段時間(因為要備份基准數據)。
深入思考一下,其實slave上也有數據,還有relay log以及一些倉庫文件標記着數據復制到哪個地方。所以,完全可以從slave上獲取基准數據和坐標,也建議這樣做。
仍然有三種方法從slave上獲取基准數據:冷備份、mysqldump和xtrabackup。方法見前文將slave恢復到master指定的坐標。
其實臨時關閉一個slave對業務影響很小,所以我個人建議,新添加slave時采用冷備份slave的方式,不僅備份恢復的速度最快,配置成slave也最方便,這一點和前面配置"一主一從"不一樣。但冷備份slave的時候需要注意幾點:
- 可以考慮將slave1完全shutdown再將整個datadir拷貝到新的slave2上。
- 建議新的slave2配置文件中的"relay-log"的值和slave1的值完全一致,否則應該手動從slave2的relay-log.info中獲取IO線程連接master時的坐標,並在slave2上使用
change master to語句設置連接參數。
方法很簡單,所以不做演示了。
5.2 配置一主多從(從中有從)
此處實現的一主多從是下面這種結構:

這種結構對MySQL復制來說,是一個很好的提升性能的方式。對於只有一個master的主從復制結構,每多一個slave,意味着master多發一部分binlog,業務稍微繁忙一點時,這種壓力會加劇。而這種一個主master、一個或多個輔助master的主從結構,非常有助於MySQL集群的伸縮性,對壓力的適應性也很強。
除上面一主多從、從中有從的方式可提升復制性能,還有幾種提升MySQL復制性能的方式:
- 將不同數據庫復制到不同slave上。
- 可以將master上的事務表(如InnoDB)復制為slave上的非事務表(如MyISAM),這樣slave上回放的速度加快,查詢數據的速度在一定程度上也會提升。
回到這種主從結構,它有些不同,master只負責傳送日志給slave1、slave2和slave3,slave 2_1和slave 2_2的日志由slave2負責傳送,所以slave2上也必須要開啟binlog選項。此外,還必須開啟一個選項--log-slave-updates讓slave2能夠在重放relay log時也寫自己的binlog,否則slave2的binlog僅接受人為的寫操作。
問:slave能否進行寫操作?重放relay log的操作是否會記錄到slave的binlog中?
- 在slave上沒有開啟
read-only選項(只讀變量)時,任何有寫權限的用戶都可以進行寫操作,這些操作都會記錄到binlog中。注意,read-only選項對具有super權限的用戶以及SQL線程執行的重放寫操作無效。默認這個選項是關閉的。
mysql> show variables like "read_only";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| read_only | OFF |
+---------------+-------+
- 在slave上沒有開啟
log-slave-updates和binlog選項時,重放relay log不會記錄binlog。
所以如果slave2要作為某些slave的master,那么在slave2上必須要開啟log-slave-updates和binlog選項。為了安全和數據一致性,在slave2上還應該啟用read-only選項。
環境如下:

以下是master、slave1和slave2上配置文件內容。
# master上的配置
[mysqld]
datadir=/data
socket=/data/mysql.sock
server_id=100
sync-binlog=1
log_bin=master-bin
log-error=/data/err.log
pid-file=/data/mysqld.pid
# slave1上的配置
[mysqld]
datadir=/data
socket=/data/mysql.sock
server_id=111
relay-log=slave-bin
log-error=/data/err.log
pid-file=/data/mysqld.pid
log-slave-updates # 新增配置
log-bin=master-slave-bin # 新增配置
read-only=ON # 新增配置
# slave2上的配置
[mysqld]
datadir=/data
socket=/data/mysql.sock
server_id=123
relay-log=slave-bin
log-error=/data/err.log
pid-file=/data/mysqld.pid
read-only=ON
因為slave2目前是全新的實例,所以先將slave1的基准數據備份到slave2。由於slave1自身就是slave,臨時關閉一個slave對業務影響很小,所以直接采用冷備份slave的方式。
# 在slave2上執行
shell> mysqladmin -uroot -p shutdown
# 在slave1上執行:
shell> mysqladmin -uroot -p shutdown
shell> rsync -az --delete /data 192.168.100.19:/
shell> service mysqld start
冷備份時,以下幾點千萬注意:
- 因為slave2是slave1的從,所以在啟動MySQL前必須將備份到slave2上的和復制有關的文件都刪除。包括:
- (1).master.info。除非配置文件中指定了
skip-slave-start,否則slave2將再次連接到master並作為master的slave。 - (2).relay-log.info。因為slave1啟動后會繼續執行relay log中的內容(如果有未執行的),這時slave1會將這部分寫入binlog並傳送到slave2。
- (3).刪除relay log文件。其實不是必須刪除,但建議刪除。
- (4).刪除relay log index文件。
- (5).刪除DATADIR/auto.conf。這個文件必須刪除,因為這里面保留了mysql server的UUID,而master和slave的UUID必須不能一致。在啟動mysql的時候,如果沒有這個文件會自動生成自己的UUID並保存到auto.conf中。
- (1).master.info。除非配置文件中指定了
- 檢查slave1上從master復制過來的專門用於復制的用戶
repl是否允許slave2連接。如果不允許,應該去master上修改這個用戶。 - 因為slave1是剛開啟的binlog,所以slave2連接slave1時的binlog position應該指定為4。即使slave1不是剛開啟的binlog,它在重啟后也會滾動binlog。
所以,在slave2上繼續操作:
shell> ls /data
auto.cnf ib_buffer_pool ib_logfile1 performance_schema slave-bin.000005
backuptest ibdata1 master.info relay-log.info slave-bin.index
err.log ib_logfile0 mysql slave-bin.000004 sys
shell> rm -f /data/{master.info,relay-log.info,auto.conf,slave-bin*}
shell> service mysqld start
最后連上slave2,啟動復制線程。
shell> mysql -uroot -p
mysql> change master to
master_host='192.168.100.150',
master_port=3306,
master_user='repl',
master_password='P@ssword1!',
master_log_file='master-slave-bin.000001',
master_log_pos=4;
mysql> start slave;
mysql> show slave status\G
6.MySQL復制中一些常用操作
6.1 篩選要復制的庫和表
默認情況下,slave會復制master上所有庫。可以指定以下變量顯式指定要復制的庫、表和要忽略的庫、表,也可以將其寫入配置文件。
Replicate_Do_DB: 要復制的數據庫
Replicate_Ignore_DB: 不復制的數據庫
Replicate_Do_Table: 要復制的表
Replicate_Ignore_Table: 不復制的表
Replicate_Wild_Do_Table: 通配符方式指定要復制的表
Replicate_Wild_Ignore_Table: 通配符方式指定不復制的表
如果要指定列表,則多次使用這些變量進行設置。
需要注意的是,盡管顯式指定了要復制和忽略的庫或者表,但是master還是會將所有的binlog傳給slave並寫入到slave的relay log中,真正負責篩選的slave上的SQL線程。
另外,如果slave上開啟了binlog,SQL線程讀取relay log后會將所有的事件都寫入到自己的binlog中,只不過對於那些被忽略的事件只記錄相關的事務號等信息,不記錄事務的具體內容。所以,如果之前設置了被忽略的庫或表,后來取消忽略后,它們在取消忽略以前的變化是不會再重放的,特別是基於gtid的復制會嚴格比較binlog中的gtid。
總之使用篩選的時候應該多多考慮是否真的要篩選,是否是永久篩選。
6.2 reset slave和reset master
reset slave會刪除master.info/relay-log.info和relay log,然后新生成一個relay log。但是change master to設置的連接參數還在內存中保留着,所以此時可以直接start slave,並根據內存中的change master to連接參數復制日志。
reset slave all除了刪除reset slave刪除的東西,還刪除內存中的change master to設置的連接信息。
reset master會刪除master上所有的二進制日志,並新建一個日志。在正常運行的主從復制環境中,執行reset master很可能導致異常狀況。所以建議使用purge來刪除某個時間點之前的日志(應該保證只刪除那些已經復制完成的日志)。
6.3 show slave hosts
如果想查看master有幾個slave的信息,可以使用show slave hosts。以下為某個master上的結果:
mysql> show slave hosts;
+-----------+------+------+-----------+--------------------------------------+
| Server_id | Host | Port | Master_id | Slave_UUID |
+-----------+------+------+-----------+--------------------------------------+
| 111 | | 3306 | 11 | ff7bb057-2466-11e7-8591-000c29479b32 |
| 1111 | | 3306 | 11 | 9b119463-24d2-11e7-884e-000c29867ec2 |
+-----------+------+------+-----------+--------------------------------------+
可以看到,該show中會顯示server-id、slave的主機地址和端口號、它們的master_id以及這些slave獨一無二的uuid號。
其中show結果中的host顯示結果是由slave上的變量report_host控制的,端口是由report_port控制的。
例如,在slave2上修改其配置文件,添加report-host項后重啟MySQL服務。
[mysqld]
report_host=192.168.100.19
在slave1(前文的實驗環境,slave1是slave2的master)上查看,host已經顯示為新配置的項。
mysql> show slave hosts;
+-----------+----------------+------+-----------+--------------------------------------+
| Server_id | Host | Port | Master_id | Slave_UUID |
+-----------+----------------+------+-----------+--------------------------------------+
| 111 | 192.168.100.19 | 3306 | 11 | ff7bb057-2466-11e7-8591-000c29479b32 |
| 1111 | | 3306 | 11 | 9b119463-24d2-11e7-884e-000c29867ec2 |
+-----------+----------------+------+-----------+--------------------------------------+
6.4 多線程復制
在老版本中,只有一個SQL線程讀取relay log並重放。重放的速度肯定比IO線程寫relay log的速度慢非常多,導致SQL線程非常繁忙,且實現到從庫上延遲較大。沒錯,多線程復制可以解決主從延遲問題,且使用得當的話效果非常的好(關於主從復制延遲,是生產環境下最常見的問題之一,且沒有很好的辦法來避免。后文稍微介紹了一點方法)。
在MySQL 5.6中引入了多線程復制(multi-thread slave,簡稱MTS),這個多線程指的是多個SQL線程,IO線程還是只有一個。當IO線程將master binlog寫入relay log中后,一個稱為"多線程協調器(multithreaded slave coordinator)"會對多個SQL線程進行調度,讓它們按照一定的規則去執行relay log中的事件。
需要謹記於心的是,如果對多線程復制沒有了解的很透徹,千萬不要在生產環境中使用多線程復制。它的確帶來了一些復制性能的提升,並且能解決主從超高延遲的問題,但隨之而來的是很多的"疑難雜症",這些"疑難雜症"並非是bug,只是需要多多了解之后才知道為何會出現這些問題以及如何解決這些問題。稍后會簡單介紹一種多線程復制問題:gaps。
通過全局變量slave-parallel-workers控制SQL線程個數,設置為非0正整數N,表示多加N個SQL線程,加上原有的共N+1個SQL線程。默認為0,表示不加任何SQL線程,即關閉多線程功能。
mysql> show variables like "%parallel%";
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| slave_parallel_workers | 0 |
+------------------------+-------+
顯然,多線程只有在slave上開啟才有效,因為只有slave上才有SQL線程。另外,設置了該全局變量,需要重啟SQL線程才生效,否則內存中還是只有一個SQL線程。
例如,初始時slave上的processlist如下:

設置slave_parallel_workers=2。
mysql> set @@global.slave_parallel_workers=2;
mysql> stop slave sql_thread;
msyql> start slave sql_thread;
mysql> show full processlist;

可見多出了兩個線程,其狀態信息是"Waiting for an event from Coordinator"。
雖然是多個SQL線程,但是復制時每個庫只能使用一個線程(默認情況下,可以通過--slave-parallel-type修改並行策略),因為如果一個庫可以使用多個線程,多個線程並行重放relay log,可能導致數據錯亂。所以應該設置線程數等於或小於要復制的庫的數量,設置多了無效且浪費資源。
6.4.1 多線程復制帶來的不一致問題
雖然多線程復制帶來了一定的復制性能提升,但它也帶來了很多問題,最嚴重的是一致性問題。完整的內容見官方手冊。此處介紹其中一個最重要的問題。
關於多線程復制,最常見也是開啟多線程復制前最需要深入了解的問題是:由於多個SQL線程同時執行relay log中的事務,這使得slave上提交事務的順序很可能和master binlog中記錄的順序不一致(除非指定變量slave_preserve_commit_order=1)。(注意:這里說的是事務而不是事件。因為MyISAM的binlog順序無所謂,只要執行完了就正確,而且多線程協調器能夠協調好這些任務。所以只需考慮innodb基於事務的binlog)
舉個簡單的例子,master上事務A先於事務B提交,到了slave上因為多SQL線程的原因,可能事務B提交了事務A卻還沒提交。
是否還記得show slave status中的Exec_master_log_pos代表的意義?它表示SQL線程最近執行的事件對應的是master binlog中的哪個位置。問題由此而來。通過show slave status,我們看到已經執行事件對應的坐標,它前面可能還有事務沒有執行。而在relay log中,事務B記錄的位置是在事務A之后的(和master一樣),於是事務A和事務B之間可能就存在一個孔洞(gap),這個孔洞是事務A剩余要執行的操作。
正常情況下,多線程協調器記錄了一切和多線程復制相關的內容,它能識別這種孔洞(通過打低水位標記low-watermark),也能正確填充孔洞。即使是在存在孔洞的情況下執行stop slave也不會有任何問題,因為在停止SQL線程之前,它會等待先把孔洞填充完。但危險因素太多,比如突然宕機、突然殺掉mysqld進程等等,這些都會導致孔洞持續下去,甚至可能因為操作不當而永久丟失這部分孔洞。
那么如何避免這種問題,出現這種問題如何解決?
1.如何避免gap。
前面說了,多個SQL線程是通過協調器來調度的。默認情況下,可能會出現gap的情況,這是因為變量slave_preserve_commit_order的默認值為0。該變量指示協調器是否讓每個SQL線程執行的事務按master binlog中的順序提交。因此,將其設置為1,然后重啟SQL線程即可保證SQL線程按序提交,也就不可能會有gap的出現。
當事務B准備先於事務A提交的時候,它將一直等待。此時slave的狀態將顯示:
Waiting for preceding transaction to commit # MySQL 5.7.8之后顯示該狀態
Waiting for its turn to commit # MySQL 5.7.8之前顯示該狀態
盡管不會出現gap,但show slave status的Exec_master_log_pos仍可能顯示在事務A的坐標之后。
由於開啟slave_preserve_commit_order涉及到不少操作,它還要求開啟slave的binlog--log-bin(因此需要重啟mysqld),且開啟重放relay log也記錄binlog的行為--log-slave-updates,此外,還必須設置多線程的並行策略--slave-parallel-type=LOGICAL_CLOCK。
shell> mysqladmin -uroot -p shutdown
shell> cat /etc/my.cnf
log_bin=slave-bin
log-slave-updates
slave_parallel_workers=1
slave_parallel_type=LOGICAL_CLOCK
shell>service mysqld start
2.如何處理已經存在的gap。
方法之一,是從master上重新備份恢復到slave上,這種方法是處理不當的最后解決辦法。
正常的處理方法是,使用START SLAVE [SQL_THREAD] UNTIL SQL_AFTER_MTS_GAPS;,它表示SQL線程只有先填充gaps后才能啟動。實際上,它涉及了兩個操作:
- (1).填充gaps
- (2).自動停止SQL線程(所以之后需要手動啟動SQL線程)
一般來說,在填充完gaps之后,應該先reset slave移除已經執行完的relay log,然后再去啟動sql_thread。
6.4.2 多線程復制切換回單線程復制
多線程的帶來的問題不止gaps一種,所以沒有深入了解多線程的情況下,千萬不能在生產環境中啟用它。如果想將多線程切換回單線程,可以執行如下操作:
START SLAVE UNTIL SQL_AFTER_MTS_GAPS;
SET @@GLOBAL.slave_parallel_workers = 0;
START SLAVE SQL_THREAD;
6.5 slave升級為master的大致操作
當master突然宕機,有時候需要切換到slave,將slave提升為新的master。但對於master突然宕機可能造成的數據丟失,主從切換是無法解決的,它只是盡可能地不間斷提供MySQL服務。
假如現在有主服務器M,從服務器S1、S2,S1作為將來的新的master。
- 在將S1提升為master之前,需要保證S1已經將relay log中的事件已經replay完成。即下面兩個狀態查看語句中SQL線程的狀態顯示為:"Slave has read all relay log; waiting for the slave I/O thread to update it"。
show slave status;
show processlist;
- 停止S1上的IO線程和SQL線程,然后將S1的binlog清空(要求已啟用binlog)。
mysql> stop slave;
mysql> reset master;
- 在S2上停止IO線程和SQL線程,通過
change master to修改master的指向為S1,然后再啟動io線程和SQL線程。
mysql> stop slave;
mysql> change master to master_host=S1,...
mysql> start slave;
- 將應用程序原本指向M的請求修改為指向S1,如修改MySQL代理的目標地址。一般會通過MySQL Router、Amoeba、cobar等數據庫中間件來實現。
- 刪除S1上的master.info、relay-log.info文件,否則下次S1重啟服務器會繼續以slave角色運行。
- 將來M重新上線后,可以將其配置成S1的slave,然后修改應用程序請求的目標列表,添加上新上線的M,如將M加入到MySQL代理的讀目標列表。
注意:reset master很重要,如果不是基於GTID復制且開啟了log-slave-updates選項時,S1在應用relay log的時候會將其寫入到自己的binlog,以后S2會復制這些日志導致重復執行的問題。
其實上面只是提供一種slave升級為Master的解決思路,在實際應用中環境可能比較復雜。例如,上面的S1是S2的master,這時S1如果沒有設置為read-only,當M宕機時,可以不用停止S1,也不需要reset master等操作,受影響的操作僅僅只是S1一直無法連接M而已,但這對業務不會有多大的影響。
相信理解了前面的內容,分析主從切換的思路應該也沒有多大問題。
6.6 指定不復制到slave上的語句
前面說的篩選要復制的庫和表可以用於指定不復制到slave上的庫和表,但卻沒有篩選不復制到slave的語句。
但有些特殊情況下,可能需要這種功能。例如,master上創建專門用於復制的用戶repl,這種語句其實沒有必要復制到slave上,甚至出於安全的考慮不應該復制到slave上。
可以使用sql_log_bin變量對此進行設置,默認該變量的值為1,表示所有語句都寫進binlog,從而被slave復制走。如果設置為0,則之后的語句不會寫入binlog,從而實現"不復制某些語句到slave"上的功能。
例如:屏蔽創建repl用戶的語句。
mysql> set sql_log_bin=0;
mysql> create user repl@'%' identified by 'P@ssword1!';
mysql> grant replication slave on *.* to repl@'%';
mysql> set sql_log_bin=1;
在使用該變量時,默認是會話范圍內的變量,一定不能設置它的全局變量值,否則所有語句都將不寫binlog。
6.7 主從高延遲的解決思路
slave通過IO線程獲取master的binlog,並通過SQL線程來應用獲取到的日志。因為各個方面的原因,經常會出現slave的延遲(即Seconds_Behind_Master的值)非常高(動輒幾天的延遲是常見的,幾個小時的延遲已經算短的),使得主從狀態不一致。
一個很容易理解的延遲示例是:假如master串行執行一個大事務需要30分鍾,那么slave應用這個事務也大約要30分鍾,從master提交的那一刻開始,slave的延遲就是30分鍾,更極端一點,由於binlog的記錄時間點是在事務提交時,如果這個大事務的日志量很大,比如要傳輸10多分鍾,那么很可能延遲要達到40分鍾左右。而且更嚴重的是,這種延遲具有滾雪球的特性,從延遲開始,很容易導致后續加劇延遲。
所以,第一個優化方式是不要在mysql中使用大事務,這是mysql主從優化的第一口訣。
在回歸正題,要解決slave的高延遲問題,先要知道Second_Behind_Master是如何計算延遲的:SQL線程比IO線程慢多少(其本質是NOW()減去Exec_Master_Log_Pos處設置的TIMESTAMP)。在主從網絡狀態良好的情況下,IO線程和master的binlog大多數時候都能保持一致(也即是IO線程沒有多少延遲,除非事務非常大,導致二進制日志傳輸時間久,但mysql優化的一個最基本口訣就是大事務切成小事務),所以在這種理想狀態下,可以認為主從延遲說的是slave上的數據狀態比master要延遲多少。它的計數單位是秒。
1.從產生Binlog的master上考慮,可以在master上應用group commit的功能,並設置參數binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count,前者表示延遲多少秒才提交事務,后者表示要堆積多少個事務之后再提交。這樣一來,事務的產生速度降低,slave的SQL線程相對就得到緩解。
2.再者從slave上考慮,可以在slave上開啟多線程復制(MTS)功能,讓多個SQL線程同時從一個IO線程中取事務進行應用,這對於多核CPU來說是非常有效的手段。但是前面介紹多線程復制的時候說過,沒有掌握多線程復制的方方面面之前,千萬不要在生產環境中使用多線程復制,要是出現gap問題,很讓人崩潰。
3.最后從架構上考慮。主從延遲是因為slave跟不上master的速度,那么可以考慮對master進行節流控制,讓master的性能下降,從而變相提高slave的能力。這種方法肯定是沒人用的,但確實是一種方法,提供了一種思路,比如slave使用性能比master更好的硬件。另一種比較可取的方式是加多個中間slave層(也就是master->slaves->slaves),讓多個中間slave層專注於復制(也可作為非業務的他用,比如用於備份)。
4.使用組復制或者Galera/PXC的多寫節點,此外還可以設置相關參數,讓它們對延遲自行調整。但一般都不需要調整,因為有默認設置。
還有比較細致的方面可以降低延遲,比如設置為row格式的Binlog要比statement要好,因為不需要額外執行語句,直接修改數據即可。比如master設置保證數據一致性的日志刷盤規則(sync_binlog/innodb_flush_log_at_trx_commit設置為1),而slave關閉binlog或者設置性能優先於數據一致性的binlog刷盤規則。再比如設置slave的隔離級別使得slave的鎖粒度放大,不會輕易鎖表(多線程復制時避免使用此方法)。還有很多方面,選擇好的磁盤,設計好分庫分表的結構等等,這些都是直接全局的,實在沒什么必要在這里多做解釋。
