由之前的文章可以了解到,二進制日志在復制中起到舉足輕重的作用,所以這一篇文章著重了解一下Mysql復制背後核心組件:二進制日志的廬山真面目。
從概念上講,二進制日志是一系列二進制日志事件。它包括一系列的binlog文件和一個binlog索引文件,當前服務器正在寫入的binlog文件稱之為active binlog。其文件名是通過配置文件中的log-bin和log-bin-index來定義的。
每個binlog文件是由若干binlog事件組成,以Format_description事件開始,以Rotate事件作為文件尾。
Format_description事件包含寫binlog文件的服務器信息,以及關於文件狀態的關鍵信息。如果服務器關閉或者重新啟動,會創建一個新的binlog文件,同時寫入新的Format_description事件,這個事件是必須的,因為服務器關閉和重啟都會產生更新。服務器寫完binlog文件後,在文件結尾添加一個Rotate事件,該事件包含下一個binlog文件的文件名及其開始讀取的位置。除了Format_description和Rotate事件之外,binlog文件的其他事件都被分成 group進行管理。在事務存儲引擎中,每個組大致對應一個事務,對於非事務存儲引擎,每個語句本身就是一個組。通常情況下,每個組要麼全部執行,要麼去不執行。如果由於某種原因Slave在組執行的過程中停機,那麼將從該組的起點而不是剛剛執行的語句開始復制。
二進制日志版本4(binlog format 4)是在MySQL 5.0中引入,是專門為擴展而設計的。這裡主要討論二進制日志版本4。(MySQL 3.23 4.0 4.1版本都是使用二進制日志版本3)
每個binlog事件由三個部分組成:
具體看一下Format_description事件:
首先,由於二進制日志是公共資源,所有線程都向它寫入語句,為了避免兩個線程同時更新二進制日志,在寫之前需要獲得一個互斥鎖Lock_log,寫完之後再釋放。
所有涉及到數據庫更新的語句都會以Query事件的形式寫入二進制日志中,除了實際執行的語句外,Query事件還包含執行語句必需的上下文附加信息。下面給出了如何記錄這些上下文信息
* 對於SYSDATE函數,它返回的是函數執行時的時間,這一點不同於NOW函數,NOW返回的是語句執行的時間。所以SYSDATE對於復制來說是不安全的,盡量少用。
LOAD DATA INFILE比較特殊,它的上下文是文件系統的文件。要正確地傳遞和執行LOAD DATA INFILE語句,需要引入新的事件類型:
對Master上執行的每個LOAD DATA INFILE語句而言,被讀取的文件被映射到一個支持內部文件的緩沖區,並在接下來的處理流程中使用。此外,一個唯一的文件ID被分配給該執行語句,並用於指向該語句讀取的文件。
當語句在執行的時,該文件的內容被寫入二進制日志,作為以Begin_load_query事件開頭的事件序列,Begin_load_query事件表示新文件的開始,且這個事件序列後面緊跟著零個或多個Append_block事件。每個寫入二進制的事件都不會超過包大小所允許的最大值,這個最大值由max-allowed_packet選項指定。
當整個文件讀取到表中後,通過寫Execute_load_query事件到二進制日志來終止語句的執行。這個事件包含了執行語句和分配給該執行語句的文件ID。請注意,這並非是用戶寫的原始語句,而是重新創建的。
* Mysql 5.0.3之前的版本使用的事件名有點不一樣,依次為Load_log_event,Execute_log_event,Create_file_log_event
my.cnf中有兩個選項可用於過濾日志:binlog-do-db和binlog-ignore-db。這兩個選項可以使用多次。
MySQL過濾事件的方式對於不熟悉的人來說可能有點奇怪。Mysql過濾是在語句級完成的,binlog-*-db使用當前數據庫來決定是否應該過濾該語句,而不是由語句所影響的表所在的數據庫決定的。對於下面的例子,使用binlog-ignore-db=bad篩選bad數據庫,下例中一個都不會寫入日志。
USE bad; INSERT INTO t1 VALUES (1),(2); USE bad; INSERT INTO good.t2 VALUES (1),(2); USE bad; UPDATE good.t1, ugly.t2 SET a = b;
至於為什麼不是以語句所影響的表所在的數據庫來決定,可以嘗試分析一下。如果以這種邏輯,使用binlog-ignore-db=ugly篩選時,第三條語句到底要不要寫入日志呢?
為了避免在執行可能被過濾的語句時發生錯誤,請不要編寫那種表名,函數名或存儲過程名前面加數據庫名的語句,而是通過使用use來改變當前數據庫。
還有一個需要說明的是,只要設置了binlog-do-db,過濾器會無視binlog-ignore-db的設置。
當然對於MySQL復制來說,本身不建議使用過濾器,因為日志是不完整的。
一般來說,一個有REPLICATION SLAVE權限的用戶擁有讀取Master上發生的所有事件的權限,因此為了安全應該保護該賬戶不被損害。具體預防的措施有:
# 第二種做法不會把明文密碼寫入到日志中,更安全些 UPDATE employee SET pass = PASSWORD('foobar') SET @pass = PASSWORD('foobar'); UPDATE employee SET pass = @pass
為了在服務器上重放二進制日志,毫無問題的處理各種表的權限,有必要用SUPER權限的用戶執行所有語句。但觸發器沒有被定義使用SUPER權限,所以重要的是以正確的用戶作為觸發器的定義者去重新創建觸發器。CREATE TRIGGER提供了一個DEFINER子句,如果沒有給語句指定DEFINER,該語句添加DEFINER子句後被寫到二進制日志中,且使用當前用戶作為其定義者。
master>SHOW BINLOG EVENTS FROM 92236 LIMIT 1\G ******************** 1. row ******************** Log_name: master-bin.000038 Pos: 92236 Event_type: Query Server_id: 1 End_log_pos: 92491 Info: use `test`; CREATE DEFINER=`root`@`localhost` TRIGGER ...
調用觸發器的語句被記錄到二進制日志,但它沒有連接到特定的觸發器。相反,當Slave執行該語句時,它會自動執行受該語句影響的表相關聯的所有觸發器,這意味著可以在Master和Slave上有不同的觸發器。
存儲過程的定義語句的處理和觸發器是類似的,CREATE PROCETURE語句也有可選的子語句DEFINER,寫入二進制日志的時候,會強制加上該子句的。調用過程和觸發器不一樣。
# 定義存儲過程
delimiter $$
CREATE PROCEDURE employee_add(p_name CHAR(64), p_email CHAR(64), p_password CHAR(64))
MODIFIES SQL DATA
BEGIN
DECLARE pass CHAR(64);
set pass = PASSWORD(p_pass)
INSERT INTO employee(name, email, password) VALUES (p_name, p_email, pass);
END $$
delimiter ;
# 調用存儲過程
master> CALL employee_add('chunk', '[email protected]', 'abrakadabra');
master> SHOW BINLOG EVENTS FROM 104033\G
******************** 1. row ********************
Log_name: master-bin.000038
Pos: 104033
Event_type: Intvar
Server_id: 1
End_log_pos: 104061
Info: INSERT_ID=1
******************** 2. row ********************
Log_name: master-bin.000038
Pos: 104061
Event_type: Query
Server_id: 1
End_log_pos: 104416
Info: use `test`; INSERT INTO employee(name, email, password) VALUES(
NAME_CONST('p_name',_latin1'chuck' COLLATE 'latin1_swedish_ci'),
NAME_CONST('p_email',_latin1'[email protected]' COLLATE 'latin1_swedish_ci'),
NAME_CONST('pass',_latin1'*FEB778934FDSFQOPL7...' COLLATE 'latin1_swedish_ci'))
有四點需要注意:
CALL語句沒有被寫入二進制日志。取而代之的是,執行語句作為調用的結果被寫入二進制日志。
該語句改寫為不包含任何對存儲過程的參數的引用。取而代之的是,使用NAME_CONST函數為每個參數創建一個單值的結果集
局部聲明的變量pass也被換成了NAME_CONST表達式
調用語句寫入二進制日志之前,上下文信息已經寫入日志,這裡指Intvar事件
存儲過程的定義語句的處理和觸發器是類似的,CREATE FUNCTION語句也有可選的子語句DEFINER,寫入二進制日志的時候,會強制加上該子句的。調用的時候,存儲函數以與觸發器相同的方式被復制。有一點需要注意的就是,SELECT語句不會被寫入二進制日志,但是一個含有存儲函數的SELECT語句是個例外。
對於存儲函數還有一個需要提到的是權限問題。CREATE ROUTINE權限是定義一個存儲過程或存儲函數所必需的。嚴格說創建一個存儲程序不需要其他權限,但它通常根據定義者的權限執行。在Slave上的復制線程在不進行權限檢查的情況下執行,這留下了嚴重的安全漏洞。MySQL 5.0之前的版本沒有存儲程序,這樣不會有問題,因為在Master上違規的語句不會寫到二進制日志中。由於存儲過程被展開了,只有在Master上成功執行的語句才會寫進二進制日志,所以也不會有問題。而存儲函數有點不同,它並沒有被展開,也就是說有可能在Master和Slave上執行不同的程序分支,帶來潛在安全漏洞。在存儲函數定義時使用SQL SECURITY DEFINER而不是SQL SECURITY INVOKER可以防止這一點。因為這一點的考慮,MySQL默認要求SUPER權限來定義存儲函數。
定義跟其他存儲程序一樣,也會有DEFINER子句。由於事件由事件調度器調用,因此它們總是以定義者執行從而不會存在存儲函數的安全漏洞。當事件被執行時,該語句被直接寫入二進制日志。由於事件是在Master上執行的,他們在Slave上是自動禁止的。但有時候如果需要升級Slave,就需要允許在Slave上執行這些事件。
UPDATE mysql.events SET status = ENABLED WHERE status = SLAVESIDE_DISABLED;
盡管基於語句的復制通常是簡單的,但一些特殊結構必須小心處理,才能很好的來保證Slave執行語句時的上下文跟Master上執行時是一樣的。
LOAD_FILE函數讓你可以獲取一個文件,由於在復制過程中,它不會被傳輸,所以需要改寫。
INSERT INTO document(author, body) VALUES ('Fox', LOAD_FILR('index.html')); # 可以用LOAD DATA FILE改寫 LOAD DATA INFILE 'index.html' INTO TABLE document FIELDS TERMINATED BY '@*@' LINES TERMINATED BY '&%&' (author, body) SET author = 'FOX'; # 還可以用用戶定義變量改寫 SET @document = LOAD_FILE('index.html'); INSERT INTO document(author, body) VALUES ('Fox', @document);
如果有一個employee表是支持事務的InnoDB存儲引擎(主鍵是mail),而跟蹤employee修改的log表是不支持事務的MyISAM存儲引擎。在其上定義兩個觸發器,一個在INSERT之前觸發tr_insert_before,插入一條記錄到log表,插入紀錄的狀態為FAIL;一個在INSERT之後觸發tr_insert_after,更改剛才插入紀錄的狀態為OK。連續插入兩條完全相同記錄時,tr_insert_before被觸發,tr_insert_after則不會被觸發。雖然employee失敗回滾了,但是log裡面插入的數據卻沒辦法回滾,這是個問題。執行後二進制日志文件內容如下。
master> SET @pass = PASSWORD('xyz'); master> INSERT INTO employee (name, mail, password) VALUES ('hu', '[email protected]', @pass); master> INSERT INTO employee (name, mail, password) VALUES ('hu', '[email protected]', @pass); master> SHOW BINLOG EVENTS IN 'local-bin.000023' ******************** 1. row ******************** Log_name: master-bin.000023 Pos: 1252 Event_type: Query Server_id: 1 End_log_pos: 1320 Info: use 'test'; BEGIN ******************** 2. row ******************** Log_name: master-bin.000023 Pos: 1320 Event_type: Intvar Server_id: 1 End_log_pos: 1348 Info: LAST_INSERT_ID=1 ******************** 3. row ******************** Log_name: master-bin.000023 Pos: 1348 Event_type: User var Server_id: 1 End_log_pos: 1426 Info: @'pass'=_utf 0x432423jklfslagklr... COLLATE utf8_general_ci ******************** 4. row ******************** Log_name: master-bin.000023 Pos: 1426 Event_type: Query Server_id: 1 End_log_pos: 1567 Info: use 'test'; INSERT INTO employee ... ******************** 5. row ******************** Log_name: master-bin.000023 Pos: 1567 Event_type: Xid Server_id: 1 End_log_pos: 1594 Info: COMMIT /* xid=60 */ ******************** 6. row ******************** Log_name: master-bin.000023 Pos: 1594 Event_type: Query Server_id: 1 End_log_pos: 1662 Info: use 'test'; BEGIN ******************** 7. row ******************** Log_name: master-bin.000023 Pos: 1662 Event_type: Intvar Server_id: 1 End_log_pos: 1690 Info: LAST_INSERT_ID=1 ******************** 8. row ******************** Log_name: master-bin.000023 Pos: 1690 Event_type: User var Server_id: 1 End_log_pos: 1768 Info: @'pass'=_utf 0x432423jklfslagklr... COLLATE utf8_general_ci ******************** 9. row ******************** Log_name: master-bin.000023 Pos: 1768 Event_type: Query Server_id: 1 End_log_pos: 1909 Info: use 'test'; INSERT INTO employee ... ******************** 10. row ******************** Log_name: master-bin.000023 Pos: 1909 Event_type: Query Server_id: 1 End_log_pos: 1980 Info: use 'test'; ROLLBACK
由上面的二進制日志內容可以看到,執行事務的時候需要額外的處理。對於事務來說,為了使得每個事務的所有語句在一起,不是按照事務的開始順序而是提交順序記入二進制日志。為了確保每個事務都作為一個單元被寫入二進制日志,服務器需要將在不同線程中執行的語句分開,保存在一個事務緩存中,在事務提交的時候緩存被清空,同時事務緩存的內容被復制到二進制日志中。
那如何記錄非事務性的語句呢?有這麼三條規則可以使用:
到目前為止,所提到的事件都是Master上的數據的改動。有一些事件雖然不是代表在Master上修改數據,但它們卻會影響復制。比如在服務器停止的期間修改了數據文件之類,為了應對這些問題,也需要額外類型的事件。
在數據庫崩潰的時候,保持數據庫和二進制日志相互一致性非常重要。換句話說,如果沒有寫入二進制日志,那麼就應該沒有更改被提交到存儲引擎,反之亦然。
但對於非事務性引擎則有問題。例如,不可能保證二進制日志和MyISAM表之間的一致性,因為MyISAM是非事務性的,且MyISAM在試圖記錄語句之前就完成了修改。對於事務性存儲引擎則不一樣。正如前面所講,事件被寫入二進制日志是在釋放所有表鎖之前,所有改變傳輸到各個存儲引擎之後的。如果在存儲引擎釋放鎖之前系統宕機了,服務器在允許事務提交之前一定要確認寫進二進制日志的改變已經寫進實際表中,而這是需要和標准文件系統同步進行協調。
回憶一下XA,為了能安全應對宕機,當第一階段完成的時候,所有的數據都應該已經寫到了磁盤。這就意味著每次一個事務完成,系統頁緩存(page cache)就必須寫到磁盤,這種想法的代價很高,而且很多應用並不必須這樣。可以通過sync-binlog選項來控制數據寫磁盤的頻率,默認為0,也就是不寫磁盤的調度完全交給操作系統;設置n,表示每n次事務提交就寫一次磁盤。
MySQL隔一段時間就會啟用一個新文件來保存二進制日志事件。把文件切換稱之為binlog file rotate。
主要有四種操作會導致文件輪換:
所謂incident事件是指那些在服務器上沒有產生數據改變但卻必須要寫進二進制日志的事件,因為它們有可能影響到復制。大多數這種事件並不需要DBA干預,比如數據庫的重啟等。
有幾種方式可以刪除二進制文件:
1:設置my.cnf的expire-logs-days參數 2:PURGE BINARY LOGS BEFORE datetime; 3:PURGE BINARY LOGS TO 'filename';
刪除二進制文件的機制:
開始刪除文件之前,服務器會把要刪除的文件列表寫到一個臨時文件(purge index file),然後才開始刪除文件,最後刪除該臨時文件。這樣即使在刪除日志文件過程中系統宕機也能在服務器再啟動時,繼續刪除未刪除的文件。在前面講到,purge index file也用於文件rotate的時候。
mysqlbinlog是一個可以查看binlog日志文件和relay日志文件內容的小程序。用mysqlbinlog工具來查看二進制日志內容的輸出是可以直接在服務器上執行的。該命令是分析日志的一個利器,可以查看所有日志的語句內容和事件內容,因此用來查錯。該命令的具體使用方法參照官方文檔。注意可以用使用--hexdump選項來查看二進制日志,不過需要了解一下日志的數據格式。比如二進制日志的整數字段是以little-Endian順序打印出來的,所以你必須從右往左讀。32位的block 03 01 00 00表示16進制的103。
---待續