MySQL主備複製原理、實現及異常處理
複製概述
MySQL支援三種複製方式:基於行(Row)的複製、基於語句(Statement)的複製和混合型別(Mixed)的複製。
基於語句的複製早在3.23版本中就存在,而基於行的複製方式在5.1版本中才被加進來。這兩種方式都是通過在主庫上記錄二進位制日誌、在備庫重放日誌的方式來實現非同步的資料複製。
混合型別的複製:預設採用基於語句的複製,一旦發現基於語句的無法精確的複製時,就會採用基於行的複製。
複製通常不會增加主庫的開銷,主要是啟用二進位制日誌帶來的開銷,但出於備份或及時從崩潰中恢復的目的,這點開銷也是必要的。除此之外,每個備庫也會對主庫增加一些負載(例如網路I/O開銷),尤其當備庫請求從主庫讀取舊的二進位制日誌檔案時,可能會造成更高的I/O開銷。另外鎖競爭也可能阻礙事務的提交。最後,如果是從一個高吞吐量的主庫上覆制到多個備庫,喚醒多個複製執行緒傳送事件的開銷將會累加。
工作原理
mysql主備複製實現分成三個步驟:
- master將改變記錄到二進位制日誌(binary log)中(這些記錄叫做二進位制日誌事件,binary log events,可以通過show binlog events進行檢視);
- slave將master的binary log events拷貝到它的中繼日誌(relay log);
- slave重做中繼日誌中的事件,將改變反映它自己的資料。
以上只是概述,實際上每一步都很複雜:
- 第一步是在主庫上記錄二進位制日誌。在每次準備提交事務完成資料更新前,主庫將資料更新的事件記錄到二進位制日誌中。MySQL會按事務提交的順序而非每條語句的執行順序來記錄二進位制日誌。在記錄二進位制日誌後,主庫會告訴儲存引擎可以提交事務了。
- 下一步,備庫將主庫的二進位制日誌複製到其本地的中繼日誌中。首先,備庫會啟動一個工作執行緒。稱為I/O執行緒,I/O執行緒跟主庫建立一個普通的客戶端連線,然後在主庫上啟動一個特殊的二進位制轉儲(binlog dump)執行緒,這個二進位制轉儲執行緒會讀取主庫上二進位制日誌中的事件。它不會對事件進行輪詢。如果該執行緒追趕上了主庫,它將進入睡眠狀態,直到主庫傳送訊號量通知其有新的事件產生時才會被喚醒,備庫I/O執行緒會將接收到的事件記錄到中繼日誌中。
- 備庫的SQL執行緒執行最後一步,該執行緒從中繼日誌中讀取事件並在備庫執行,從而實現備庫資料的更新。當SQL執行緒趕上I/O執行緒時,中繼日誌通常已經在系統快取中,所以中繼日誌的開銷很低。SQL執行緒執行的事件也可以通過配置選項來決定是否寫入其自己的二進位制日誌中,它對我們稍後提到的場景非常有用。
主備配置
許可權配置
mysql>GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'root@'%' IDENTIFIED BY 'root';
複製賬戶事實上只需要有主庫上的REPLICATION SLAVE許可權,並不一定需要每一端伺服器都有REPLICATION CLIENT許可權,那麼為什麼我們要把這兩種許可權給主/備庫都賦予呢?這有兩個原因:
1. 用來監控和管理複製的賬號需要REPLICATION CLIENT許可權,並且針對這兩種目的使用同一個賬號更加容易。
2. 如果在主庫上建立了賬號,然後從主庫將資料克隆到備庫上時,備庫也就設定好了——變成主庫所需要的配置。這樣後續有需要可以方便地交換主備庫的角色。
如果無腦式配置可以:
主備庫配置
關停Master伺服器,將Master中的資料拷貝到B伺服器中,使得Master和slave中的資料同步,並且確保在全部設定操作結束前,禁止在Master和slave伺服器中進行寫操作,使得兩資料庫中的資料一定要相同!
備註:文中採用的案例中主備庫都有5個schema:
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| canal_test |
| mysql |
| performance_schema |
| test |
+--------------------+
主庫的/etc/my.cnf配置(主機host:xx.xx.xx.73)
[mysqld]
log-bin=mysql-bin
server-id=1
備庫上也需要在/ect/my.cnf進行配置(備機host:xx.xx.xx.60)
[mysqld]
log-bin=mysql-bin
server-id=2
relay_log=mysql-relay-bin
log_slave_updates=1
read_only=1
server_id 是必須的,而且唯一。slave沒有必要開啟二進位制日誌,但是在一些情況下,必須設定,例如,如果slave為其它slave的master,必須設定 bin_log。在這裡,我們開啟了二進位制日誌,而且顯示的命名(預設名稱為hostname,但是,如果hostname改變則會出現問題)。
relay_log配置中繼日誌,log_slave_updates表示slave將複製事件寫進自己的二進位制日誌(後面會看到它的用處)。
有 些人開啟了slave的二進位制日誌,卻沒有設定log_slave_updates,然後檢視slave的資料是否改變,這是一種錯誤的配置。所以,儘量 使用read_only,它防止改變資料(除了特殊的執行緒)。但是,read_only並是很實用,特別是那些需要在slave上建立表的應用。
啟動slave
接 下來就是讓slave連線master,並開始重做master二進位制日誌中的事件。你不應該用配置檔案進行該操作,而應該使用CHANGE MASTER TO語句,該語句可以完全取代對配置檔案的修改,而且它可以為slave指定不同的master,而不需要停止伺服器。如下:
mysql> CHANGE MASTER TO
-> MASTER_HOST='xx.xx.xx.73',
-> MASTER_USER='root',
-> MASTER_PASSWORD='xxxx',
-> MASTER_LOG_FILE='mysql-bin.000004',
-> MASTER_LOG_POS=0;
MASTER_LOG_POS的值為0,因為它是日誌的開始位置。
你可以用SHOW SLAVE STATUS語句檢視slave的設定是否正確:
mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State:
Master_Host: xx.xx.xx.73
Master_User: root
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000004
Read_Master_Log_Pos: 4
Relay_Log_File: mysql-relay-bin.000001
Relay_Log_Pos: 4
Relay_Master_Log_File: mysql-bin.000004
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)
Slave_IO_State, Slave_IO_Running, 和Slave_SQL_Running是No表明slave還沒有開始複製過程。日誌的位置為4而不是0,這是因為0只是日誌檔案的開始位置,並不是日誌位置。實際上,MySQL知道的第一個事件的位置是4。
為了開始複製,你可以執行:
mysql> start slave;
執行show slave status檢視輸出結果:
mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: xx.xx.xx.73
Master_User: root
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000004
Read_Master_Log_Pos: 2395
Relay_Log_File: mysql-relay-bin.000002
Relay_Log_Pos: 253
Relay_Master_Log_File: mysql-bin.000004
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: 2395
Relay_Log_Space: 409
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
在這裡主要是看:
Slave_IO_Running=Yes
Slave_SQL_Running=Yes
slave的I/O和SQL執行緒都已經開始執行,而且Seconds_Behind_Master不再是NULL。日誌的位置增加了,意味著一些事件被獲取並執行了。如果你在master上進行修改,你可以在slave上看到各種日誌檔案的位置的變化,同樣,你也可以看到資料庫中資料的變化。
(如果此時Slave_SQL_Running=No,可以參考下一節“異常情況處理”進行解決)
你可檢視master和slave上執行緒的狀態。在master上,你可以看到slave的I/O執行緒建立的連線(Binlog Dump):
在master上輸入show processlist\G;
mysql> show processlist\G
*************************** 1. row ***************************
Id: 30
User: root
Host: localhost
db: canal_test
Command: Query
Time: 0
State: NULL
Info: show processlist
*************************** 2. row ***************************
Id: 33
User: root
Host: xx.xx.xx.xx:49005
db: NULL
Command: Binlog Dump
Time: 33
State: Master has sent all binlog to slave; waiting for binlog to be updated
Info: NULL
同樣,在備庫也可以看到兩個執行緒,一個是I/O執行緒,一個是SQL執行緒(Connect):
mysql> show processlist\G
*************************** 1. row ***************************
Id: 3
User: root
Host: xx.xx.xx.60:62159
db: NULL
Command: Binlog Dump
Time: 67811
State: Master has sent all binlog to slave; waiting for binlog to be updated
Info: NULL
*************************** 2. row ***************************
Id: 14
User: root
Host: localhost
db: canal_test
Command: Query
Time: 0
State: NULL
Info: show processlist
*************************** 3. row ***************************
Id: 19
User: root
Host: xx.xx.xx.60:62390
db: NULL
Command: Sleep
Time: 187
State:
Info: NULL
*************************** 4. row ***************************
Id: 20
User: system user
Host:
db: NULL
Command: Connect
Time: 64
State: Waiting for master to send event
Info: NULL
*************************** 5. row ***************************
Id: 21
User: system user
Host:
db: NULL
Command: Connect
Time: 64
State: Slave has read all relay log; waiting for the slave I/O thread to update it
Info: NULL
異常情況處理
在上一小節中在start slave之後進行show slave status就出現了想要的結果——“Slave_SQL_Running=Yes”.但是有些時候,卻不是這樣的:
mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: xx.xx.xx.73
Master_User: root
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000004
Read_Master_Log_Pos: 2172
Relay_Log_File: mysql-relay-bin.000002
Relay_Log_Pos: 253
Relay_Master_Log_File: mysql-bin.000004
Slave_IO_Running: Yes
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: 1007
Last_Error: Error 'Can't create database 'canal_test'; database exists' on query. Default database: 'canal_test'. Query: 'create database canal_test'
Skip_Counter: 0
Exec_Master_Log_Pos: 107
Relay_Log_Space: 2474
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: 1007
Last_SQL_Error: Error 'Can't create database 'canal_test'; database exists' on query. Default database: 'canal_test'. Query: 'create database canal_test'
Replicate_Ignore_Server_Ids:
Master_Server_Id: 1
可以看到Slave_SQL_Running=No,那麼該怎麼解決呢?
解決方案1
程式可能在slave上進行了寫操作,也可能是slave機器重啟後事務回滾造成的。
如果是事務回滾造成的,可以:
mysql> slave stop;
Query OK, 0 rows affected (0.00 sec)
mysql> set GLOBAL SQL_SLAVE_SKIP_COUNTER=1;
Query OK, 0 rows affected (0.00 sec)
mysql> slave start;
Query OK, 0 rows affected (0.00 sec)
最後通過show slave status進行檢視。
解決方案2
首先停掉slave服務:
mysql> slave stop;
到master上檢視主機狀態:
mysql> show master status;
+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000004 | 2395 | | |
+------------------+----------+--------------+------------------+
1 row in set (0.00 sec)
然後到slave伺服器上執行手動同步:
mysql> change master to
-> master_host='xx.xx.xx.73',
-> master_user='root',
-> master_password='xxxx',
-> master_port=3306,
-> master_log_file='mysql-bin.000004',
-> master_log_pos=2395;
mysql> slave start;
案例測試
在master上的Schema Name: canal_test中有一個perosn的表,表結構如下:
mysql> describe person;
+-------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| name | varchar(100) | YES | | NULL | |
| age | int(11) | YES | | NULL | |
| sex | char(1) | YES | | NULL | |
+-------+--------------+------+-----+---------+-------+
表中有一條記錄:
mysql> select * from person;
+----+------+------+------+
| id | name | age | sex |
+----+------+------+------+
| 2 | zzh2 | 21 | m |
+----+------+------+------+
(注意此時slave中的資料是一樣的)
往master上插入一條資料,之後檢視:
mysql> insert into person values(1,'zzh',22,'m');
mysql> select * from person;
+----+------+------+------+
| id | name | age | sex |
+----+------+------+------+
| 1 | zzh | 22 | m |
| 2 | zzh2 | 21 | m |
+----+------+------+------+
可以看到master中成功插入了一條資料,之後可以同樣在slave中輸入select * from person來檢視,如果結果master和slave相同,那麼恭喜你主備複製已經成功了。
參考資料
- 《Optimization, Backups and Replication High Performance MySQL》Baron schwartz, Peter Zaitsev, Vadim Tkachenko.
歡迎支援《RabbitMQ實戰指南》以及關注微信公眾號:朱小廝的部落格。