MySQL系列:innodb原始碼分析之重做日誌結構
在innodb的引擎實現中,為了實現事務的永續性,構建了重做日誌系統。重做日誌由兩部分組成:記憶體日誌緩衝區(redo log buffer)和重做日誌檔案。這樣設計的目的顯而易見,日誌緩衝區是為了加快寫日誌的速度,而重做日誌檔案為日誌資料提供持久化的作用。在innodb的重做日誌系統中,為了更好實現日誌的易恢復性、安全性和持久化性,引入了以下幾個概念:LSN、log block、日誌檔案組、checkpoint和歸檔日誌。以下我們分別一一來進行分析。
1.LSN
在innodb中的重做日誌系統中,定義一個LSN序號,其代表的意思是日誌序號。LSN在引擎中定義的是一個dulint_t型別值,相當於uint64_t,typedef struct dulint_struct
{
ulint high; /* most significant 32 bits */
ulint low; /* least significant 32 bits */
}dulint_t;
LSN真正的含義是儲存引擎向重做日誌系統寫入的日誌量(位元組數),這個日誌量包括寫入的日誌位元組 + block_header_size + block_tailer_size。LSN的初始化值是:LOG_START_LSN(相當於8192),在呼叫日誌寫入函式LSN就一直隨著寫入的日誌長度增加,具體看:void log_write_low(byte* str, ulint str_len) { log_t* log = log_sys; . . . part_loop: /*計算part length*/ data_len = log->buf_free % OS_FILE_LOG_BLOCK_SIZE + str_len; . . . /*將日誌內容拷貝到log buffer*/ ut_memcpy(log->buf + log->buf_free, str, len); str_len -= len; str = str + len; . . . if(data_len = OS_FILE_LOG_BLOCK_SIZE - LOG_BLOCK_TRL_SIZE){ /*完成一個block的寫入*/ . . . len += LOG_BLOCK_HDR_SIZE + LOG_BLOCK_TRL_SIZE; log->lsn = ut_dulint_add(log->lsn, len); . . . } else /*更改lsn*/ log->lsn = ut_dulint_add(log->lsn, len); . . . }
LSN是不會減小的,它是日誌位置的唯一標記。在重做日誌寫入、checkpoint構建和PAGE頭裡面都有LSN。
關於日誌寫入:
例如當前重做日誌的LSN = 2048,這時候innodb呼叫log_write_low寫入一個長度為700的日誌,2048剛好是4個block長度,那麼需要儲存700長度的日誌,需要量個BLOCK(單個block只能存496個位元組)。那麼很容易得出新的LSN = 2048 + 700 + 2 * LOG_BLOCK_HDR_SIZE(12) + LOG_BLOCK_TRL_SIZE(4) = 2776。
關於checkpoint和日誌恢復:
在page的fil_header中的LSN是表示最後重新整理是的LSN, 假如資料庫中存在PAGE1 LSN = 1024,PAGE2 LSN = 2048, 系統重啟時,檢測到最後的checkpoint LSN = 1024,那麼系統在檢測到PAGE1不會對PAGE1進行恢復重做,當系統檢測到PAGE2的時候,會將PAGE2進行重做。一次類推,小於checkpoint LSN的頁不用重做,大於LSN checkpoint的PAGE就要進行重做。
2.Log Block
innodb在日誌系統裡面定義了log block的概念,其實log block就是一個512位元組的資料塊,這個資料塊包括塊頭、日誌資訊和塊的checksum.其結構如下:
Block no的最高位是描述block是否flush磁碟的標識位.通過lsn可以blockno,具體的計算過程是lsn是多少個512的整數倍,也就是no = lsn / 512 + 1;為什麼要加1呢,因為所處no的塊算成clac_lsn一定會小於傳入的lsn.所以要+1。其實就是block的陣列索引值。checksum是通過從塊頭開始到塊的末尾前4個位元組為止,做了一次數字疊加,程式碼如下:
sum = 1;
sh = 0;
for(i = 0; i < OS_FILE_LOG_BLOCK_SIZE - LOG_BLOCK_TRL_SIZE, i ++){
sum = sum & 0x7FFFFFFF;
sum += (((ulint)(*(block + i))) << sh) + (ulint)(*(block + i));
sh ++;
if(sh > 24)
sh = 0;
}
在日誌恢復的時候,innodb會對載入的block進行checksum校驗,以免在恢復過程中資料產生錯誤。事務的日誌寫入是基於塊的,如果事務的日誌大小小於496位元組,那麼會合其他的事務日誌合併在一個塊中,如果事務日誌的大小大於496位元組,那麼會以496為長度進行分離儲存。例如:T1 = 700位元組大小,T2 = 100位元組大小儲存結構如下:
3.重做日誌結構和關係圖
innodb在重做日誌實現當中,設計了3個層模組,即redo log buffer、group files和archive files。這三個層模組的描述如下:redo log buffer 重做日誌的日誌記憶體緩衝區,新寫入的日誌都是先寫入到這個地方.redo log buffer中資料同步到磁碟上,必須進行刷盤操作。
group files 重做日誌檔案組,一般由3個同樣大小的檔案組成。3個檔案的寫入是依次迴圈的,每個日誌檔案寫滿後,即寫下一個,日誌檔案如果都寫滿時,會覆蓋第一次重新寫。重做日誌組在innodb的設計上支援多個。
archive files 歸檔日誌檔案,是對重做日誌檔案做增量備份,它是不會覆蓋以前的日誌資訊。
以下是它們關係示意圖:
3.1重做日誌組
重做日誌組可以支援多個,這樣做的目的應該是為了防止一個日誌組損壞後,可以從其他並行的日誌組裡面進行資料恢復。在MySQL-5.6的將日誌組的個數設定為1,不允許多個group存在。網易姜承堯的解釋是innodb的作者認為通過外層儲存硬體來保證日誌組的完整性比較好,例如raid磁碟。重做日誌組的主要功能是實現對組內檔案的寫入管理、組內的checkpoint建立和checkpiont資訊的儲存、歸檔日誌狀態管理(只有第一個group才做archive操作).以下是對日誌組的定義:
typedef struct log_group_struct
{
ulint id; /*log group id*/
ulint n_files; /*group包含的日誌檔案個數*/
ulint file_size; /*日誌檔案大小,包括檔案頭*/
ulint space_id; /*group對應的fil_space的id*/
ulint state; /*log group狀態,LOG_GROUP_OK、LOG_GROUP_CORRUPTED*/
dulint lsn; /*log group的lsn*/
dulint lsn_offset; /*當前lsn相對組內檔案起始位置的偏移量 */
ulint n_pending_writes; /*本group 正在執行fil_flush的個數*/
byte** file_header_bufs; /*檔案頭緩衝區*/
byte** archive_file_header_bufs;/*歸檔檔案頭資訊的緩衝區*/
ulint archive_space_id; /*歸檔重做日誌ID*/
ulint archived_file_no; /*歸檔的日誌檔案編號*/
ulint archived_offset; /*已經完成歸檔的偏移量*/
ulint next_archived_file_no; /*下一個歸檔的檔案編號*/
ulint next_archived_offset; /*下一個歸檔的偏移量*/
dulint scanned_lsn;
byte* checkpoint_buf; /*本log group儲存checkpoint資訊的緩衝區*/
UT_LIST_NODE_T(log_group_t) log_groups;
}log_group_t;
上面結構定義中的spaceid是對應fil0fil中的fil_space_t結構,一個fil_space_t結構可以管理多個檔案fil_node_t,關於fil_node_t參見這裡。
3.1.1LSN與組內偏移
在log_goup_t組內日誌模組當中,其中比較重要的是關於LSN與組內偏移之間的換算關係。在組建立時,會對lsn和對應lsn_offset做設定,假如 初始化為 group lsn = 1024, group lsn_offset = 2048,group由3個10240大小的檔案組成,LOG_FILE_HDR_SIZE = 2048, 我們需要知道buf lsn = 11240對應的組內offset的偏移是多少,根據log_group_calc_lsn_offset函式可以得出如下公式:group_size = 3 * 11240;
相對組起始位置的LSN偏移 = (buf_ls - group_ls) + log_group_calc_size_offset(lsn_offset ) = (11240 - 1024) - 0 = 10216;
lsn_offset = log_group_calc_lsn_offset(相對組起始位置的LSN偏移 % group_size) = 10216 + 2 * LOG_FILE_HDR_SIZE = 14312;
這個偏移一定是加上了檔案頭長度的。
3.1.2 file_header_bufs
file_header_bufs是一個buffer緩衝區陣列,陣列長度和組內檔案數是一致的,每個buf長度是2048。其資訊結構如下:
log_group_id 對應log_group_t結構中的id
file_start_lsn 當前檔案其實位置資料對應的LSN值
File_no 當前的檔案編號,一般在archive file頭中體現
Hot backup str 一個空字串,如果是hot_backup,會填上檔案字尾ibackup。
File_end_ls 檔案結尾資料對應的LSN值,一般在archive file檔案中體現。
3.2 checkpoint
checkpoint是日誌的檢查點,其作用就是在資料庫異常後,redo log是從這個點的資訊獲取到LSN,並對檢查點以後的日誌和PAGE做重做恢復。那麼檢查點是怎麼生成的呢?當日志緩衝區寫入的日誌LSN距離上一次生成檢查點的LSN達到一定差距的時候,就會開始建立檢查點,建立檢查點首先會將記憶體中的表的髒資料寫入到硬碟,讓後再將redo log buffer中小於本次檢查點的LSN的日誌也寫入硬碟。在log_group_t中的checkpoint_buf,以下是它對應欄位的解釋:
LOG_CHECKPOINT_NO checkpoint序號,
LOG_CHECKPOINT_LSN 本次checkpoint起始的LSN
LOG_CHECKPOINT_OFFSET 本次checkpoint相對group file的起始偏移量
LOG_CHECKPOINT_LOG_BUF_SIZE redo log buffer的大小,預設2M
LOG_CHECKPOINT_ARCHIVED_LSN 當前日誌歸檔的LSN
LOG_CHECKPOINT_GROUP_ARRAY 每個log group歸檔時的檔案序號和偏移量,是一個數組
3.3 log_t
重做日誌的寫入、資料刷盤、建立checkpoint和歸檔操作都是通過全域性唯一的,log_sys進行控制的,這是個非常龐大而又複雜的結構,定義如下:
typedef struct log_struct
{
byte pad[64]; /*使得log_struct物件可以放在通用的cache line中的資料,這個和CPU L1 Cache和資料競爭有和
直接關係*/
dulint lsn; /*log的序列號,實際上是一個日誌檔案偏移量*/
ulint buf_free; /*buf可以寫的位置*/
mutex_t mutex; /*log保護的mutex*/
byte* buf; /*log緩衝區*/
ulint buf_size; /*log緩衝區長度*/
ulint max_buf_free; /*在log buffer刷盤後,推薦buf_free的最大值,超過這個值會被強制刷盤*/
ulint old_buf_free; /*上次寫時buf_free的值,用於除錯*/
dulint old_lsn; /*上次寫時的lsn,用於除錯*/
ibool check_flush_or_checkpoint; /*需要日誌寫盤或者是需要重新整理一個log checkpoint的標識*/
ulint buf_next_to_write; /*下一次開始寫入磁碟的buf偏移位置*/
dulint written_to_some_lsn; /*第一個group刷完成是的lsn*/
dulint written_to_all_lsn; /*已經記錄在日誌檔案中的lsn*/
dulint flush_lsn; /*flush的lsn*/
ulint flush_end_offset; /*最後一次log file刷盤時的buf_free,也就是最後一次flush的末尾偏移量*/
ulint n_pending_writes; /*正在呼叫fil_flush的個數*/
os_event_t no_flush_event; /*所有fil_flush完成後才會觸發這個訊號,等待所有的goups刷盤完成*/
ibool one_flushed; /*一個log group被刷盤後這個值會設定成TRUE*/
os_event_t one_flushed_event; /*只要有一個group flush完成就會觸發這個訊號*/
ulint n_log_ios; /*log系統的io操作次數*/
ulint n_log_ios_old; /*上一次統計時的io操作次數*/
time_t last_printout_time;
ulint max_modified_age_async; /*非同步日誌檔案刷盤的閾值*/
ulint max_modified_age_sync; /*同步日誌檔案刷盤的閾值*/
ulint adm_checkpoint_interval;
ulint max_checkpoint_age_async; /*非同步建立checkpoint的閾值*/
ulint max_checkpoint_age; /*強制建立checkpoint的閾值*/
dulint next_checkpoint_no;
dulint last_checkpoint_lsn;
dulint next_checkpoint_lsn;
ulint n_pending_checkpoint_writes;
rw_lock_t checkpoint_lock; /*checkpoint的rw_lock_t,在checkpoint的時候,是獨佔這個latch*/
byte* checkpoint_buf; /*checkpoint資訊儲存的buf*/
ulint archiving_state;
dulint archived_lsn;
dulint max_archived_lsn_age_async;
dulint max_archived_lsn_age;
dulint next_archived_lsn;
ulint archiving_phase;
ulint n_pending_archive_ios;
rw_lock_t archive_lock;
ulint archive_buf_size;
byte* archive_buf;
os_event_t archiving_on;
ibool online_backup_state; /*是否在backup*/
dulint online_backup_lsn; /*backup時的lsn*/
UT_LIST_BASE_NODE_T(log_group_t) log_groups;
}log_t;
3.3.1各種LSN之間的關係和分析
從上面的結構定義可以看出有很多LSN相關的定義,那麼這些LSN直接的關係是怎麼樣的呢?理解這些LSN之間的關係對理解整個重做日誌系統的運作機理會有極大的信心。以下各種LSN的解釋:lsn 當前log系統最後寫入日誌時的LSN
flush_lsn redolog buffer最後一次資料刷盤資料末尾的LSN,作為下次刷盤的起始LSN
written_to_some_lsn 單個日誌組最後一次日誌刷盤時的起始LSN
written_to_all_lsn 所有日誌組最後一次日誌刷盤是的起始LSN
last_checkpoint_lsn 最後一次建立checkpoint日誌資料起始的LSN
next_checkpoint_lsn 下一次建立checkpoint的日誌 資料起始的LSN,用log_buf_pool_get_oldest_modification獲得的
archived_lsn 最後一次歸檔日誌資料起始的LSN
next_archived_lsn 下一次歸檔日誌資料的其實LSN關係圖如下:
3.3.2偏移量的分析
log_t有各種偏移量,例如:max_buf_free、buf_free、flush_end_offset、buf_next_to_write等。偏移和LSN不一樣,偏移量是相對redo log buf其實位置的絕對偏移量,LSN是整個日誌系統的序號。
max_buf_free 寫入日誌是不能超過的偏移位置,如果超過,將強制redo log buf寫入磁碟
buf_free 當前日誌可以寫的偏移位置
buf_next_to_write 下一次redo log buf資料寫盤的資料起始偏移,在所有刷盤IO完成後,其值和 flush_end_offset是一致的。
flush_end_offset 本次刷盤的資料末尾的偏移量,相當於刷盤時的buf_free,當flush_end_offset 超過max_buf_free的一半時會將未寫入的資料移到 redobuffer的最前面,這時buf_free和buf_next_to_write都將做調整
大小關係圖如下:
3.4記憶體結構關係圖
4.日誌寫入和日誌保護機制
innodb有四種日誌刷盤行為,分別是非同步redo log buffer刷盤、同步redo log buffer刷盤、非同步建立checkpoint刷盤和同步建立checkpoint刷盤。在innodb中,刷盤行為是非常耗磁碟IO的,innodb對刷盤做了一套非常完善的策略。
4.1重做日誌刷盤選項
在innodb引擎中有個全域性變數srv_flush_log_at_trx_commit,這個全域性變數是控制flushdisk的策略,也就是確定調不呼叫fsync這個函式,什麼時候掉這個函式。這個變數有3個值。這三個值的解釋如下:
0 每隔1秒由MasterThread控制重做日誌模組呼叫log_flush_to_disk來刷盤,好處是提高了效率,壞處是1秒內如果資料庫崩潰,日誌和資料會丟失。
1 每次寫入重做日誌後,都呼叫fsync來進行日誌寫入磁碟。好處是每次日誌都寫入了磁碟,資料可靠性大大提高,壞處是每次呼叫fsync會產生大量的磁碟IO,影響資料庫效能。
2 每次寫入重做日誌後,都將日誌寫入日誌檔案的page cache。這種情況如果物理機崩潰後,所有的日誌都將丟失。
4.2日誌刷盤保護
由於重做日誌是一個組內多檔案重複寫的一個過程,那麼意味日誌如果不及時寫盤和建立checkpoint,就有可能會產生日誌覆蓋,這是一個我們不願意看到的。在innodb定義了一個日誌保護機制,在儲存引擎會定時呼叫log_check_margins日誌函式來檢查保護機制。簡單介紹如下:
引入三個變數 buf_age、checkpoint_age和日誌空間大小.
buf_age = lsn -oldest_lsn;
checkpoint_age =lsn - last_checkpoint_lsn;
日誌空間大小 = 重做日誌組能儲存日誌的位元組數(通過log_group_get_capacity獲得);
當buf_age >=日誌空間大小的7/8時,重做日誌系統會將red log buffer進行非同步資料刷盤,這個時候因為是非同步的,不會造成資料操作阻塞。
當buf_age >=日誌空間大小的15/16時,重做日誌系統會將redlog buffer進行同步資料刷盤,這個時候會呼叫fsync函式,資料庫的操作會進行阻塞。
當 checkpoint_age >=日誌空間大小的31/32時,日誌系統將進行非同步建立checkpoint,資料庫的操作不會阻塞。
當 checkpoint_age == 日誌空間大小時,日誌系統將進行同步建立checkpoint,大量的表空間髒頁和log檔案髒頁同步刷入磁碟,會產生大量的磁碟IO操作。資料庫操作會堵塞。整個資料庫事務會掛起。
5.總結
Innodb的重做日誌系統是相當完備的,它為資料的持久化做了很多細微的考慮,它效率直接影響MySQL的寫效率,所以我們深入理解了它便以我們去優化它,尤其是在大量資料刷盤的時候。假設資料庫的受理的事務速度大於磁碟IO的刷入速度,一定會出現同步建立checkpoint操作,這樣資料庫是堵塞的,整個資料庫都在都在進行髒頁刷盤。避免這樣的問題發生就是增加IO能力,用多磁碟分散IO壓力。也可以考慮SSD這讀寫速度很高的儲存介質來做優化。