在《redo Log 格式淺析》文章中,我們介紹了redo log的基本格式和結構以及寫入步驟。資料庫系統與檔案系統的最大的區別就是要最大限度的保證操作的原子性,在InnoDB儲存引擎中就是依靠redo log來保證的。當資料庫異常崩潰後,資料庫重新啟動時會根據redo log進行資料恢復,保證資料庫恢復到崩潰前的狀態。那麼這個過程在InnoDB裡面是如何進行的呢,本文將結合MySQL 8.0.12的原始碼進行簡要的解析。
1. InnoDB崩潰恢復相關引數
Innodb_fast_shutdown: 在mysql關閉時,引數innodb_fast_shutdown 影響著儲存引擎innodb的行為。引數為0,1,2三個值。
0,代表當MYSQL關閉時,Innodb需要完成所有full purge和merge insert buffer操作,這需要花費時間來完成。
1,是引數的預設值,不需要完成full purge和merge insert buffer操作,但是在緩衝池的一些資料髒頁還是會重新整理到磁碟。
2,表示不需要完成full purge和merge insert buffer操作 ,也不將緩衝池中的資料髒頁寫回磁碟,而是將日誌都寫入日誌檔案。這樣不會有任何事務丟失,但是MySQL在下次啟動時,會執行恢復操作(recovery)。
innodb_force_recovery: 影響了整個Innodb儲存引擎的恢復狀況。該值預設為0,表示當需要恢復時執行所有的恢復操作。當不能進行有效恢復時(如資料頁發生了corruption,InnoDB引擎可能會無法啟動)把錯誤寫入錯誤日誌中。
Innodb_force_recovery可以設定6個非零值:
(SRV_FORCE_IGNORE_CORRUPT):忽略檢查到的corrupt頁。
(SRV_FORCE_NO_BACKGROUND):阻止主執行緒的執行,如主執行緒需要執行full purge操作,會導致crash。
(SRV_FORCE_NO_TRX_UNDO):不執行事務回滾操作.
(SRV_FORCE_NO_IBUF_MERGE):不執行插入緩衝的合併操作.
(SRV_FORCE_NO_UNDO_LOG_SCAN):不檢視重做日誌,InnoDB儲存引擎會將未提交的事務視為已提交。
(SRV_FORCE_NO_LOG_REDO):不執行前滾的操作。
2. 與崩潰恢復相關的重要資料結構
log_t : 該結構體的定義在storage/innobase/include/log0types.h中。InnoDB繫系執行時刻只會有一個該資料結構的例項log_sys,log_sys的定義在strorage/innobase/log0log.cc中(log_t *log_sys) 。 log_sys中儲存了innodb redo log系統執行時刻的各種狀態。
recv_sys_t : 該結構體的定義在/storage/innobase/include/log0recv.h中。這個結構體變數用來描述恢復系統執行時刻的狀態。InnoDB執行時刻有一個該資料結構的例項recv_sys,recv_sys的定義在storage/innobase/log/log0recv.cc中(recv_sys_t * recv_sys =nullptr)。
在這裡我們列出一個該結構體內部的部分定義
1. struct recv_sys_t {
2. ……...
3. lsn_t parse_start_lsn;
4. lsn_t checkpoint_lsn;
5. lsn_t scanned_lsn;
6. lsn_t recovered_lsn;
7. Spaces *spaces;
8. …………..
9. }
其中Spaces *spaces 是以space_id做hash的hash表,表裡面存放的元素是以page_no做hash的hash表(pages), pages表裡存放的是按照lsn大小排序的需要在該頁上進行恢復的日誌記錄。
parse_start_lsn:本次日誌重做恢復起始的lsn,如果是從checkpoint處開始恢復,等於checkpoint_lsn。
scanned_lsn: 在恢復過程,將恢復日誌從log_sys->buf解析塊後存入recv_sys->buf的日誌lsn.
recovered_lsn:已經將資料恢復到page中或者已經將日誌操作儲存addr_hash當中的日誌lsn;
在日誌開始恢復時:
parse_start_lsn = scanned_lsn = recovered_lsn = checkpoint_lsn。
在日誌完成恢復時:
parse_start_lsn = checkpoint_lsn
scanned_lsn = recovered_lsn = log_sys->lsn。
另外還有2個重要的資料結構,結構體定義非常簡單,大家看英文就能明白什麼意思。
1. /** Hashed page file address struct */
2. struct recv_addr_t {
3. using List = UT_LIST_BASE_NODE_T(recv_t);
4.
5. /** recovery state of the page */
6. recv_addr_state state;
7. /** Space ID */
8. space_id_t space;
9. /** Page number */
10. page_no_t page_no;
11. /** List of log records for this page */
12. List rec_list;
13. };
1. /** Stored log record struct */
2. struct recv_t {
3. using Node = UT_LIST_NODE_T(recv_t)
4. /** Log record type */
5. mlog_id_t type;
6. /** Log record body length in bytes */
7. ulint len;
8. /** Chain of blocks containing the log record body */
9. recv_data_t *data;
10. /** Start lsn of the log segment written by the mtr which generated
11. this log record: NOTE that this is not necessarily the start lsn of
12. this log record */
13. lsn_t start_lsn;
14. /** End lsn of the log segment written by the mtr which generated
15. this log record: NOTE that this is not necessarily the end LSN of
16. this log record */
17. lsn_t end_lsn;
18. /** List node, list anchored in recv_addr_t */
19. Node rec_list;
20. };
這幾個資料結構執行時刻的記憶體關係如下:
3. InnoDB崩潰恢復階段基本流程
3.1. 基本總體流程
InnoDB的recovery的函式入口是srv_start(storage/innobase/srv/srv0start.cc)。
srv_start是MySQL啟動的時候由innodb初始化函式innobase_init_files呼叫,
在srv_start中與崩潰恢復有關的程式碼流程如下:
首先獲取當前已經寫入redo log的日誌量(flush_lsn),這個已經寫入的日誌量存放在系統表空間的第一頁中。
srv_sys_space.open_or_create(false, create_new_db, &sum_of_new_sizes, &flushed_lsn);
然後將flush_lsn作為引數呼叫recv_recovery_from_checkpoint_start函式,初始化recv_sys_t結構,讀取checkpoint, 然後從checkpoint開始讀取日誌,解析日誌,應用日誌。
err = recv_recovery_from_checkpoint_start(*log_sys, flushed_lsn);
最後呼叫recv_recovery_from_checkpoint_finish函式進行一些崩潰恢復的清理工作,釋放建立的recv_sys_t記憶體空間。
srv_dict_metadata = recv_recovery_from_checkpoint_finish(*log_sys, false);
3.2. recv_recovery_from_checkpoint_start
其中崩潰恢復絕大部分的邏輯都集中在recv_recovery_from_checkpoint_start中,下面我們對它進行進一步的介紹。這其中的程式碼邏輯主要分為2個階段,首先是日誌掃描階段,掃描階段按照資料頁的space_id和page_no分發redo日誌到hash_table中,保證同一個資料頁的日誌被分發到同一個雜湊桶中,且按照lsn大小從小到大排序。掃描完後,再遍歷整個雜湊表,依次應用每個資料頁的日誌。下面我們按照流程順序做個基本的簡介。
首先從ib_logfile0中找到最大的checkpoint
err = recv_find_max_checkpoint(log, &max_cp_field);
在日誌頭中有2個checkpoint block域。InnoDB是採用2個checkpoint了輪流寫的方式來保證checkpoint寫操作的安全(並不是一次寫2份checkpoint, 而是輪流寫)。 由於redo log是冪等的,應用一次和與應用兩次都是一樣的(在實際的應用redo log時,如果當前這一條log記錄的lsn大於當前page的lsn,說明這一條log還沒有被應用到當前的page中去)。所以,即使某次checkpoint block寫失敗了,那麼崩潰恢復的時候從上一次記錄的checkpoint點開始恢復也能正確的恢復資料庫事務
接下來用讀取到的checkpoint資料作為引數呼叫recv_recovery_begin。崩潰恢復的主要程式碼流程來到了recv_recovery_begin。
在recv_recovery_begin中,存在一個迴圈,該迴圈以checkpoint為起點,呼叫底層函式(recv_read_log_seg),按照RECV_SCAN_SIZE(64KB)大小分批讀取日誌資料到log_sys->buf中,然後呼叫呼叫recv_scan_log_recs對讀取到的資料進行掃描解析和應用,直到所有的日誌都處理完畢。
在recv_scan_log_recs中,首先透過block_no和lsn之間的關係以及日誌checksum判斷是否讀到了日誌最後,如果讀到最後則返回(即使資料庫是正常關閉的,也要走崩潰恢復邏輯,那麼在這裡就返回了,因為正常關閉的checkpoint值一定是指向日誌最後),否則呼叫recv_sys_add_to_parsing_buf函式把日誌去頭去尾放到一個recv_sys->buf中,日誌頭裡面存了一些控制資訊和checksum值,只是用來校驗和定位,在真正的應用時沒有用。接下來就開始呼叫recv_parse_log_recs對recv_sys->buf中的日誌資料進行解析然後放到前面提到的hash表中。當hash表中存放的資料recv_addr_t達到一定的大小之後,就呼叫recv_apply_hashed_log_recs進行日誌應用。
在recv_parse_log_recs時,解析到的日誌分兩種:single_rec和multi_rec,前者表示只對一個資料頁進行一種操作,後者表示對一個或者多個資料頁進行多種操作。日誌中還包括對應資料頁的space_id,page_no,操作的type以及操作的內容(具體單條日誌記錄的解析邏輯在recv_parse_log_rec函式中)。解析出相應的日誌後,按照space_id和page_no進行雜湊並放到hash_table裡面即可,等待後續應用。
在recv_single_rec和recv_multi_rec中都會呼叫到recv_parse_log_rec進行單條記錄的解析(記錄解析部分的邏輯與mtr/MLOG有關,以後會另外單獨寫一篇文章進行介紹),然後呼叫recv_add_to_hash_table放到hash表中去。
在recv_apply_hashed_log_recs中,就是遍歷hash_table,針對hash表中的每一個有日誌的資料頁,呼叫recv_apply_log_rec應用與其有關的redo日誌。應用完所有的日誌後,如果需要則把buffer_pool的頁面都刷盤(buf_pool_invalidate)。
在recv_apply_log_rec首先把需要應用日誌的頁讀取到buffer pool中buf_page_get,然後呼叫recv_recover_page將日誌的修改應用到該頁中去。(具體的應用邏輯與mtr/MLOG後會另外單獨寫一篇文章進行介紹)。
下面我們用一個流程示意圖歸納一下上面的流程
3.3. 回滾未完成的無效事務
嚴格來說上面的流程只是做到了資料庫的前滾,也就是說到此為止恢復到了資料庫崩潰前的狀態,但是資料庫崩潰前存在的一些未完成提交的事務需要在這個階段做一些清理。(這裡指的未完成的事務不是XA事務,處於Prepare階段的XA事務的清理需要用到Binlog,這是MySQL服務層的概念和職責,與本地儲存引擎無關,不在本文的闡述範圍之內)。
在InnoDB初始化innobase_init_files函式執行完成之後,MySQL服務層會呼叫InnoDB引擎層的innobase_dict_recover函式,執行InnoDB更高層次的恢復過程。該函式會啟動一個執行緒trx_recovery_rollback_thread,這個執行緒主要執行的函式為trx_rollback_or_clean_recovered,這裡面會檢測前滾階段產生的事務是否已經提交,如果已經提交,那麼清除這個事務可能存在的insert undo log(trx_undo_insert_cleanup)。如果這個事務未提交,那麼就對其進行回滾(trx_rollback_active)。這裡面的清除和回滾需要用到undo log。(關於undo log的介紹,我們會在另寫一篇文章進行介紹)。