Reference: https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.htmlhtml
爲了最大程度避免數據寫入時 IO 瓶頸帶來的性能問題,MySQL 採用了這樣一種緩存機制:mysql
當修改數據庫內數據時,InnoDB 先將該數據從磁盤讀物到內存中,修改內存中的數據拷貝,並將該修改行爲持久化到磁盤上的事務日誌(先寫 redo log buffer,在按期批量寫入),而不是每次都直接將修改過的數據記錄到磁盤內,等事務日誌持久化完成以後,內存中的髒數據能夠慢慢刷會磁盤,稱之爲 Write-Ahead Logging(預寫式日誌)。sql
事務日誌採用的是追加寫入,順序 IO 會帶來更好的性能優點數據庫
爲了不髒數據刷回到磁盤過程當中,掉電或系統故障帶來的數據丟失問題,InnoDB 採用事務日誌來解決這些問題。緩存
innodb 事務日誌包括 redo log 和 undo log。redo log是重作日誌,提供前滾操做,undo log 是回滾日誌,提供回滾操做。安全
undo Log 不是 redo log的逆向過程,其實它們都算是用來恢復的日誌:網絡
redo log 一般是物理日誌,記錄的是數據頁的物理修改,而不是某一行或某幾行修改爲怎樣怎樣,它用來恢復提交後的物理數據頁(恢復數據頁,且只能恢復到最後一次提交的位置)app
undo 用來回滾行記錄到某個版本。undo log 通常是邏輯日誌,根據每行記錄進行記錄異步
redo log包括兩部分:一是內存中的日誌緩衝(redo log buffer),該部分日誌是易失性的;二是磁盤上的重作日誌文件(redo log file),該部分日誌是持久的。async
在概念上,innodb 經過 force log at commit 機制實現事務的持久性,即在事務提交的時候,必須先將該事務的全部事務日誌寫入到磁盤上的 redo log file 和 undo log file 中進行持久化。
爲了確保每第二天志都能寫入到事務日誌文件中,在每次將 log buffer 中的日誌寫入日誌文件的過程當中都會調用一次操做系統的fsync操做(即fsync()系統調用)。由於 MariaDB/MySQL 是工做在用戶空間的,MariaDB/MySQL 的 log buffer 處於用戶空間的內存中。要寫入到磁盤上的 log file 中(redo:ib_logfileN文件,undo:share tablespace或.ibd文件),中間還要通過操做系統內核空間的 OS buffer,調用fsync()的做用就是將 OS buffer 中的日誌刷到磁盤上的 log file 中。
也就是說,從redo log buffer寫日誌到磁盤的redo log file中,過程以下:
圖片來源網絡 侵權聯繫刪除
在此處須要注意一點,通常所說的 log file 並非磁盤上的物理日誌文件,而是操做系統緩存中的log file,官方手冊上的意思也是如此(例如:With a value of 2, the contents of the InnoDB log buffer are written to the log file after each transaction commit and the log file is flushed to disk approximately once per second)。但說實話,這不太好理解,既然都稱爲 file 了,應該已經屬於物理文件了。因此在本文後續內容中都以 os buffer 或者 file system buffer 來表示官方手冊中所說的 Log file,而後 log file 則表示磁盤上的物理日誌文件,即 log file on disk。
另外,之因此要通過一層os buffer,是由於 open 日誌文件的時候, open 沒有使用 O_DIRECT 標誌位,該標誌位意味着繞過操做系統層的 os buffer,IO直寫到底層存儲設備。不使用該標誌位意味着將日誌進行緩衝,緩衝到了必定容量,或者顯式fsync()
纔會將緩衝中的刷到存儲設備。使用該標誌位意味着每次都要發起系統調用。好比寫 abcde,不使用 o_direct 將只發起 1 次系統調用,使用 o_object 將發起 5 次系統調用。
MySQL 支持用戶自定義在 commit 時如何將 log buffer 中的日誌刷 log file 中。這種控制經過變量 innodb_flush_log_at_trx_commit
的值來決定。該變量有3種值:0、一、2,默認爲 1。但注意,這個變量只是控制 commit 動做是否刷新 log buffer 到磁盤。
當設置爲 1 的時候,事務每次提交都會將 log buffer 中的日誌寫入 os buffer 並調用 fsync()
刷到 log file on disk中。這種方式即便系統崩潰也不會丟失任何數據,可是由於每次提交都寫入磁盤,IO 的性能較差。
當設置爲 0 的時候,事務提交時不會將 log buffer 中日誌寫入到 os buffer,而是每秒寫入 os buffer 並調用fsync()
寫入到 log file on disk 中。也就是說設置爲 0 時是(大約)每秒刷新寫入到磁盤中的,當系統崩潰,會丟失 1 秒鐘的數據。
當設置爲 2 的時候,每次提交都僅寫入到 os buffer,而後是每秒調用 fsync() 將 os buffer 中的日誌寫入到 log file on disk。
圖片來源網絡 侵權聯繫刪除
注意,有一個變量innodb_flush_log_at_timeout
的值爲1秒,該變量表示的是刷日誌的頻率,不少人誤覺得是控制innodb_flush_log_at_trx_commit
值爲 0 和 2 時的 1 秒頻率,實際上並不是如此。測試時將頻率設置爲 5 和設置爲 1,當 innodb_flush_log_at_trx_commit
設置爲 0 和 2 的時候性能基本都是不變的。關於這個頻率是控制什麼的,在後面的"刷日誌到磁盤的規則"中會說。
在主從複製結構中,要保證事務的持久性和一致性,須要對日誌相關變量設置爲以下:
若是啓用了二進制日誌,則設置sync_binlog=1,即每提交一次事務同步寫到磁盤中。
老是設置innodb_flush_log_at_trx_commit=1,即每提交一次事務都寫到磁盤中。
上述兩項變量的設置保證了:每次提交事務都寫入二進制日誌和事務日誌,並在提交時將它們刷新到磁盤中。
選擇刷日誌的時間會嚴重影響數據修改時的性能,特別是刷到磁盤的過程。下例就測試了 innodb_flush_log_at_trx_commit
分別爲 0、一、2 時的差距。
--建立測試表
drop table if exists test_flush_log;
create table test_flush_log(id int,name char(50))engine=innodb;
--建立插入指定行數的記錄到測試表中的存儲過程
drop procedure if exists proc_batch_insert;
delimiter $$
create procedure proc_batch_insert(i int)
begin
declare s int default 1;
declare c char(50) default repeat('a',50);
while s <= i do
start transaction;
insert into test_flush_log values(NULL,c);
commit;
set s = s + 1;
end while;
end$$
delimiter ;
當前環境下,
-- 調用存儲過程 100000 表示生成 100w 條記錄
mysql> call proc_batch_insert(100000);
Query OK, 0 rows affected (15.48 sec)
結果是 15.48 秒。
再測試值爲 2 的時候,即每次提交都刷新到 os buffer,但每秒才刷入磁盤中。
mysql> set @@global.innodb_flush_log_at_trx_commit = 2;
mysql> truncate test_flush_log;
mysql> call proc(100000);
Query OK, 0 rows affected (3.41 sec)
結果插入時間大減,只需 3.41 秒。
最後測試值爲0的時候,即每秒才刷到os buffer和磁盤。
mysql> set @@global.innodb_flush_log_at_trx_commit=0;
mysql> truncate test_flush_log;
mysql> call proc(100000);
Query OK, 0 rows affected (2.10 sec)
結果只有 2.10 秒。
最後能夠發現,其實值爲 2 和 0 的時候,它們的差距並不太大,但 2 卻比 0 要安全的多。它們都是每秒從 os buffer 刷到磁盤,它們之間的時間差體如今 log buffer 刷到 os buffer 上。由於將 log buffer 中的日誌刷新到 os buffer 只是內存數據的轉移,並無太大的開銷,因此每次提交和每秒刷入差距並不大。能夠測試插入更多的數據來比較,如下是插入 100W 行數據的狀況。從結果可見,值爲 2 和 0 的時候差距並不大,但值爲 1 的性能卻差太多。
img
儘管設置爲 0 和 2 能夠大幅度提高插入性能,可是在故障的時候可能會丟失1秒鐘數據,這1秒鐘極可能有大量的數據,從上面的測試結果看,100W 條記錄也只消耗了 20 多秒,1秒鐘大約有 4W-5W 條數據,儘管上述插入的數據簡單,但卻說明了數據丟失的大量性。更好的插入數據的作法是將值設置爲1,而後修改存儲過程,將每次循環都提交修改成只提交一次,這樣既能保證數據的一致性,也能提高性能,修改以下:
drop procedure if exists proc;
delimiter $$
create procedure proc(i int)
begin
declare s int default 1;
declare c char(50) default repeat('a',50);
start transaction;
while s<=i DO
insert into test_flush_log values(null,c);
set s=s+1;
end while;
commit;
end$$
delimiter ;
測試值爲1時的狀況。
mysql> set @@global.innodb_flush_log_at_trx_commit=1;
mysql> truncate test_flush_log;
mysql> call proc(1000000);
Query OK, 0 rows affected (11.26 sec)
innodb 存儲引擎中,redo log 以塊爲單位進行存儲的,每一個塊佔 512 字節,這稱爲 redo log block。因此不論是 log buffer 中仍是 os buffer 中以及 redo log file on disk 中,都是這樣以 512字節的塊存儲的。
每一個redo log block 由3部分組成:日誌塊頭、日誌塊尾和日誌主體。其中日誌塊頭佔用 12 字節,日誌塊尾佔用 8 字節,因此每一個 redo log block 的日誌主體部分只有 512-12-8=492 字節。
img
由於 redo log 記錄的是數據頁的變化,當一個數據頁產生的變化須要使用超過 492 字節()的 redo log 來記錄,那麼就會使用多個 redo log block 來記錄該數據頁的變化。
日誌塊頭包含 4 部分:
log_block_hdr_no:(4字節)該日誌塊在redo log buffer中的位置ID。
log_block_hdr_data_len:(2字節)該log block中已記錄的log大小。寫滿該log block時爲0x200,表示512字節。
log_block_first_rec_group:(2字節)該log block中第一個log的開始偏移位置。
lock_block_checkpoint_no:(4字節)寫入檢查點信息的位置。
關於log block塊頭的第三部分 log_block_first_rec_group ,由於有時候一個數據頁產生的日誌量超出了一個日誌塊,這是須要用多個日誌塊來記錄該頁的相關日誌。例如,某一數據頁產生了552字節的日誌量,那麼須要佔用兩個日誌塊,第一個日誌塊佔用492字節,第二個日誌塊須要佔用60個字節,那麼對於第二個日誌塊來講,它的第一個log的開始位置就是73字節(60+12)。若是該部分的值和 log_block_hdr_data_len 相等,則說明該log block中沒有新開始的日誌塊,即表示該日誌塊用來延續前一個日誌塊。
日誌尾只有一個部分: log_block_trl_no ,該值和塊頭的 log_block_hdr_no 相等。
上面所說的是一個日誌塊的內容,在redo log buffer或者redo log file on disk中,由不少log block組成。以下圖:
img
log group 表示的是 redo log group,一個組內由多個大小徹底相同的 redo log file 組成。組內redo log file的數量由變量innodb_log_files_group
決定,默認值爲 2,即兩個 redo log file。這個組是一個邏輯的概念,並無真正的文件來表示這是一個組,可是能夠經過變量 innodb_log_group_home_dir
來定義組的目錄,redo log file 都放在這個目錄下,默認是在datadir 下。
mysql> show global variables like "innodb_log%";
+-----------------------------+----------+
| Variable_name | Value |
+-----------------------------+----------+
| innodb_log_buffer_size | 8388608 |
| innodb_log_compressed_pages | ON |
| innodb_log_file_size | 50331648 |
| innodb_log_files_in_group | 2 |
| innodb_log_group_home_dir | ./ |
+-----------------------------+----------+
[root@xuexi data]# ll /mydata/data/ib*
-rw-rw---- 1 mysql mysql 79691776 Mar 30 23:12 /mydata/data/ibdata1
-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile0
-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile1
能夠看到在默認的數據目錄下,有兩個 ib_logfile 開頭的文件,它們就是 log group 中的 redo log file,並且它們的大小徹底一致且等於變量innodb_log_file_size
定義的值。第一個文件ibdata1是在沒有開啓 innodb_file_per_table 時的共享表空間文件,對應於開啓 innodb_file_per_table 時的.ibd文件。
在 innodb 將 log buffer 中的 redo log block 刷到這些 log file 中時,會以追加寫入的方式循環輪訓寫入。即先在第一個 log file(即 ib_logfile0)的尾部追加寫,直到滿了以後向第二個 log file(即ib_logfile1)寫。當第二個 log file 滿了會清空一部分第一個 log file繼續寫入。
因爲是將log buffer中的日誌刷到log file,因此在log file中記錄日誌的方式也是log block的方式。
在每一個組的第一個redo log file中,前2KB記錄4個特定的部分,從2KB以後纔開始記錄log block。除了第一個redo log file中會記錄,log group中的其餘log file不會記錄這2KB,可是卻會騰出這2KB的空間。以下:
img
redo log file 的大小對 innodb 的性能影響很是大,設置的太大,恢復的時候就會時間較長,設置的過小,就會致使在寫 redo log 的時候循環切換 redo log file。
由於 innodb 存儲引擎存儲數據的單元是頁,因此 redo log 也是基於頁的格式來記錄的。默認狀況下,innodb 的頁大小是16KB(由 innodb_page_size 變量控制),一個頁內能夠存放很是多的 log block (每一個512字節),而 log block 中記錄的又是數據頁的變化。
其中 log block 中492字節的部分是 log body,該 log body 的格式分爲4部分:
redo_log_type:佔用1個字節,表示redo log的日誌類型。
space:表示表空間的ID,採用壓縮的方式後,佔用的空間可能小於4字節。
page_no:表示頁的偏移量,一樣是壓縮過的。
redo_log_body表示每一個重作日誌的數據部分,恢復時會調用相應的函數進行解析。例如insert語句和delete語句寫入redo log的內容是不同的。
以下圖,分別是insert和delete大體的記錄方式。
img
log buffer中未刷到磁盤的日誌稱爲髒日誌(dirty log)。
在上面的說過,默認狀況下事務每次提交的時候都會刷事務日誌到磁盤中,這是由於變量 innodb_flush_log_at_trx_commit
的值爲1。可是innodb不只僅只會在有commit動做後纔會刷日誌到磁盤,這只是innodb存儲引擎刷日誌的規則之一。
刷日誌到磁盤有如下幾種規則:
1.發出commit動做時。已經說明過,commit發出後是否刷日誌由變量 innodb_flush_log_at_trx_commit 控制。
2.每秒刷一次。這個刷日誌的頻率由變量 innodb_flush_log_at_timeout 值決定,默認是1秒。要注意,這個刷日誌頻率和commit動做無關。
3.當log buffer中已經使用的內存超過一半時。
4.當有checkpoint時,checkpoint在必定程度上表明瞭刷到磁盤時日誌所處的LSN位置。
內存中(buffer pool)未刷到磁盤的數據稱爲髒數據(dirty data)。因爲數據和日誌都以頁的形式存在,因此髒頁表示髒數據和髒日誌。
上一節介紹了日誌是什麼時候刷到磁盤的,不只僅是日誌須要刷盤,髒數據頁也同樣須要刷盤。
在innodb中,數據刷盤的規則只有一個:checkpoint。可是觸發checkpoint的狀況卻有幾種。
無論怎樣,checkpoint 觸發後,會將 buffer 中髒數據頁和髒日誌頁都刷到磁盤
innodb存儲引擎中checkpoint分爲兩種:
sharp checkpoint:在重用redo log文件(例如切換日誌文件)的時候,將全部已記錄到redo log中對應的髒數據刷到磁盤。
fuzzy checkpoint:一次只刷一小部分的日誌到磁盤,而非將全部髒日誌刷盤。有如下幾種狀況會觸發該檢查點:
master thread checkpoint:由master線程控制,每秒或每10秒刷入必定比例的髒頁到磁盤。
flush_lru_list checkpoint:從MySQL5.6開始可經過 innodb_page_cleaners 變量指定專門負責髒頁刷盤的page cleaner線程的個數,該線程的目的是爲了保證lru列表有可用的空閒頁。
async/sync flush checkpoint:同步刷盤仍是異步刷盤。例如還有很是多的髒頁沒刷到磁盤(很是可能是多少,有比例控制),這時候會選擇同步刷到磁盤,但這不多出現;若是髒頁不是不少,能夠選擇異步刷到磁盤,若是髒頁不多,能夠暫時不刷髒頁到磁盤
dirty page too much checkpoint:髒頁太多時強制觸發檢查點,目的是爲了保證緩存有足夠的空閒空間。too much的比例由變量 innodb_max_dirty_pages_pct 控制,MySQL 5.6默認的值爲75,即當髒頁佔緩衝池的百分之75後,就強制刷一部分髒頁到磁盤。
因爲刷髒頁須要必定的時間來完成,因此記錄檢查點的位置是在每次刷盤結束以後纔在redo log中標記的。
MySQL中止時是否將髒數據和髒日誌刷入磁盤,由變量innodb_fast_shutdown={ 0|1|2 }控制,默認值爲1,即中止時只作一部分purge,忽略大多數flush操做(但至少會刷日誌),在下次啓動的時候再flush剩餘的內容,實現fast shutdown。
在啓動innodb的時候,無論上次是正常關閉仍是異常關閉,老是會進行恢復操做。
由於redo log記錄的是數據頁的物理變化,所以恢復的時候速度比邏輯日誌(如二進制日誌)要快不少。並且,innodb自身也作了必定程度的優化,讓恢復速度變得更快。
重啓innodb時,checkpoint表示已經完整刷到磁盤上data page上的LSN,所以恢復時僅須要恢復從checkpoint開始的日誌部分。例如,當數據庫在上一次checkpoint的LSN爲10000時宕機,且事務是已經提交過的狀態。啓動數據庫時會檢查磁盤中數據頁的LSN,若是數據頁的LSN小於日誌中的LSN,則會從檢查點開始恢復。
還有一種狀況,在宕機前正處於checkpoint的刷盤過程,且數據頁的刷盤進度超過了日誌頁的刷盤進度。這時候一宕機,數據頁中記錄的LSN就會大於日誌頁中的LSN,在重啓的恢復過程當中會檢查到這一狀況,這時超出日誌進度的部分將不會重作,由於這自己就表示已經作過的事情,無需再重作。
另外,事務日誌具備冪等性,因此屢次操做獲得同一結果的行爲在日誌中只記錄一次。而二進制日誌不具備冪等性,屢次操做會所有記錄下來,在恢復的時候會屢次執行二進制日誌中的記錄,速度就慢得多。例如,某記錄中id初始值爲2,經過update將值設置爲了3,後來又設置成了2,在事務日誌中記錄的將是無變化的頁,根本無需恢復;而二進制會記錄下兩次update操做,恢復時也將執行這兩次update操做,速度比事務日誌恢復更慢。
innodb_flush_log_at_trx_commit={0|1|2} # 指定什麼時候將事務日誌刷到磁盤,默認爲1。
0表示每秒將"log buffer"同步到"os buffer"且從"os buffer"刷到磁盤日誌文件中。
1表示每事務提交都將"log buffer"同步到"os buffer"且從"os buffer"刷到磁盤日誌文件中。
2表示每事務提交都將"log buffer"同步到"os buffer"但每秒才從"os buffer"刷到磁盤日誌文件中。
innodb_log_buffer_size:# log buffer的大小,默認8M
innodb_log_file_size:#事務日誌的大小,默認5M
innodb_log_files_group =2:# 事務日誌組中的事務日誌文件個數,默認2個
innodb_log_group_home_dir =./:# 事務日誌組路徑,當前目錄表示數據目錄
innodb_mirrored_log_groups =1:# 指定事務日誌組的鏡像組個數,但鏡像功能好像是強制關閉的,因此只有一個log group。在MySQL5.7中該變量已經移除。