MySQL redo log及recover過程淺析

寫在前面:做者水平有限,歡迎不吝賜教,一切以最新源碼爲準。html

InnoDB redo logmysql

首先介紹下Innodb redo log是什麼,爲何須要記錄redo log,以及redo log的做用都有哪些。這些做爲常識,只是爲了本文完整。
 
InnoDB有buffer pool(簡稱bp)。bp是數據庫頁面的緩存,對InnoDB的任何修改操做都會首先在bp的page上進行,而後這樣的頁面將被標記爲dirty並被放到專門的flush list上,後續將由master thread或專門的刷髒線程階段性的將這些頁面寫入磁盤(disk or ssd)。這樣的好處是避免每次寫操做都操做磁盤致使大量的隨機IO,階段性的刷髒能夠將屢次對頁面的修改merge成一次IO操做,同時異步寫入也下降了訪問的時延。然而,若是在dirty page還未刷入磁盤時,server非正常關閉,這些修改操做將會丟失,若是寫入操做正在進行,甚至會因爲損壞數據文件致使數據庫不可用。爲了不上述問題的發生,Innodb將全部對頁面的修改操做寫入一個專門的文件,並在數據庫啓動時今後文件進行恢復操做,這個文件就是redo log file。這樣的技術推遲了bp頁面的刷新,從而提高了數據庫的吞吐,有效的下降了訪問時延。帶來的問題是額外的寫redo log操做的開銷(順序IO,固然很快),以及數據庫啓動時恢復操做所需的時間。
 
接下來將結合MySQL 5.6的代碼看下Log文件的結構、生成過程以及數據庫啓動時的恢復流程。
 
Log文件結構
Redo log文件包含一組log files,其會被循環使用。Redo log文件的大小和數目能夠經過特定的參數設置,詳見: innodb_log_file_size 和  innodb_log_files_in_group 。每一個log文件有一個文件頭,其代碼在"storage/innobase/include/log0log.h"中,咱們看下log文件頭都記錄了哪些信息:
669 /* Offsets of a log file header */
670 #define LOG_GROUP_ID    0 /* log group number */
671 #define LOG_FILE_START_LSN  4 /* lsn of the start of data in this
672           log file */
673 #define LOG_FILE_NO   12  /* 4-byte archived log file number;
674           this field is only defined in an
675           archived log file */
676 #define LOG_FILE_WAS_CREATED_BY_HOT_BACKUP 16
677           /* a 32-byte field which contains
678           the string 'ibbackup' and the
679           creation time if the log file was
680           created by ibbackup --restore;
681           when mysqld is first time started
682           on the restored database, it can
683           print helpful info for the user */
684 #define LOG_FILE_ARCH_COMPLETED OS_FILE_LOG_BLOCK_SIZE
685           /* this 4-byte field is TRUE when
686           the writing of an archived log file
687           has been completed; this field is
688           only defined in an archived log file */
689 #define LOG_FILE_END_LSN  (OS_FILE_LOG_BLOCK_SIZE + 4)
690           /* lsn where the archived log file
691           at least extends: actually the
692           archived log file may extend to a
693           later lsn, as long as it is within the
694           same log block as this lsn; this field
695           is defined only when an archived log
696           file has been completely written */
697 #define LOG_CHECKPOINT_1  OS_FILE_LOG_BLOCK_SIZE
698           /* first checkpoint field in the log
699           header; we write alternately to the
700           checkpoint fields when we make new
701           checkpoints; this field is only defined
702           in the first log file of a log group */
703 #define LOG_CHECKPOINT_2  (3 * OS_FILE_LOG_BLOCK_SIZE)
704           /* second checkpoint field in the log
705           header */
706 #define LOG_FILE_HDR_SIZE (4 * OS_FILE_LOG_BLOCK_SIZE)
日誌文件頭共佔用4個OS_FILE_LOG_BLOCK_SIZE的大小,這裏對部分字段作簡要介紹:
1.    LOG_GROUP_ID               這個log文件所屬的日誌組,佔用4個字節,當前都是0;
2.    LOG_FILE_START_LSN     這個log文件記錄的初始數據的lsn,佔用8個字節;
3.    LOG_FILE_WAS_CRATED_BY_HOT_BACKUP   備份程序所佔用的字節數,共佔用32字節,如xtrabackup在備份時會在xtrabackup_logfile文件中記錄"xtrabackup backup_time";
4.    LOG_CHECKPOINT_1/LOG_CHECKPOINT_2   兩個記錄InnoDB checkpoint信息的字段,分別從文件頭的第二個和第四個block開始記錄,只使用日誌文件組的第一個日誌文件。
       這裏多說兩句,每次checkpoint後InnoDB都須要更新這兩個字段的值,所以redo log的寫入並不是嚴格的順序寫;
    
每一個log文件包含許多log records。log records將以OS_FILE_LOG_BLOCK_SIZE(默認值爲512字節)爲單位順序寫入log文件。每一條記錄都有本身的LSN(log sequence number,表示從日誌記錄建立開始到特定的日誌記錄已經寫入的字節數)。每一個Log Block包含一個header段、一個tailer段,以及一組log records。
 
首先看下Log Block header。block header的開始4個字節是log block number,表示這是第幾個block塊。其是經過LSN計算得來,計算的函數是log_block_convert_lsn_to_no();接下來兩個字節表示該block中已經有多少個字節被使用;再後邊兩個字節表示該block中做爲一個新的MTR開始log record的偏移量,因爲一個block中能夠包含多個MTR記錄的log,因此須要有記錄表示此偏移量。再而後四個字節表示該block的checkpoint number。block trailer佔用四個字節,表示此log block計算出的checksum值,用於正確性校驗,MySQL5.6提供了若干種計算checksum的算法,這裏再也不贅述。咱們能夠結合代碼中給出的註釋,再瞭解下header和trailer的各個字段的含義。
580 /* Offsets of a log block header */
581 #define LOG_BLOCK_HDR_NO  0 /* block number which must be > 0 and
582           is allowed to wrap around at 2G; the
583           highest bit is set to 1 if this is the
584           first log block in a log flush write
585           segment */
586 #define LOG_BLOCK_FLUSH_BIT_MASK 0x80000000UL
587           /* mask used to get the highest bit in
588           the preceding field */
589 #define LOG_BLOCK_HDR_DATA_LEN  4 /* number of bytes of log written to
590           this block */
591 #define LOG_BLOCK_FIRST_REC_GROUP 6 /* offset of the first start of an
592           mtr log record group in this log block,
593           0 if none; if the value is the same
594           as LOG_BLOCK_HDR_DATA_LEN, it means
595           that the first rec group has not yet
596           been catenated to this log block, but
597           if it will, it will start at this
598           offset; an archive recovery can
599           start parsing the log records starting
600           from this offset in this log block,
601           if value not 0 */
602 #define LOG_BLOCK_CHECKPOINT_NO 8 /* 4 lower bytes of the value of
603           log_sys->next_checkpoint_no when the
604           log block was last written to: if the
605           block has not yet been written full,
606           this value is only updated before a
607           log buffer flush */
608 #define LOG_BLOCK_HDR_SIZE  12  /* size of the log block header in
609           bytes */
610
611 /* Offsets of a log block trailer from the end of the block */
612 #define LOG_BLOCK_CHECKSUM  4 /* 4 byte checksum of the log block
613           contents; in InnoDB versions
614           < 3.23.52 this did not contain the
615           checksum but the same value as
616           .._HDR_NO */
617 #define LOG_BLOCK_TRL_SIZE  4 /* trailer size in bytes */

 

Log 記錄生成
在介紹了log file和log block的結構後,接下來描述log record在InnoDB內部是如何生成的,其「生命週期」是如何在內存中一步步流轉並最終寫入磁盤中的。這裏涉及到兩塊內存緩衝,涉及到mtr/log_sys等內部結構,後續會一一介紹。
 
首先介紹下log_sys。log_sys是InnoDB在內存中保存的一個全局的結構體(struct名爲log_t,global object名爲log_sys),其維護了一塊全局內存區域叫作log buffer(log_sys->buf),同時維護有若干lsn值等信息表示logging進行的狀態。其在log_init函數中對全部的內部區域進行分配並對各個變量進行初始化。
 
log_t的結構體很大,這裏再也不粘出來,能夠自行看"storage/innobase/include/log0log.h: struct log_t"。下邊會對其中比較重要的字段值加以說明:
log_sys->lsn 接下來將要生成的log record使用此lsn的值
log_sys->flushed_do_disk_lsn
redo log file已經被刷新到此lsn。比該lsn值小的日誌記錄已經被安全的記錄在磁盤上
log_sys->write_lsn
當前正在執行的寫操做使用的臨界lsn值;
log_sys->current_flush_lsn
當前正在執行的write + flush操做使用的臨界lsn值,通常和log_sys->write_lsn相等;
log_sys->buf
內存中全局的log buffer,和每一個mtr本身的buffer有所區別;
log_sys->buf_size
log_sys->buf的size
log_sys->buf_free
寫入buffer的起始偏移量
log_sys->buf_next_to_write
buffer中還未寫到log file的起始偏移量。下次執行write+flush操做時,將會今後偏移量開始
log_sys->max_buf_free
肯定flush操做執行的時間點,當log_sys->buf_free比此值大時須要執行flush操做,具體看log_check_margins函數
lsn是聯繫dirty page,redo log record和redo log file的紐帶。在每一個redo log record被拷貝到內存的log buffer時會產生一個相關聯的lsn,而每一個頁面修改時會產生一個log record,從而每一個數據庫的page也會有一個相關聯的lsn,這個lsn記錄在每一個page的header字段中。爲了保證WAL(Write-Ahead-Logging)要求的邏輯,dirty page要求其關聯lsn的log record已經被寫入log file才容許執行flush操做。
 
接下來介紹mtr。mtr是mini-transactions的縮寫。其在代碼中對應的結構體是mtr_t,內部有一個局部buffer,會將一組log record集中起來,批量寫入log buffer。mtr_t的結構體以下所示:
376 /* Mini-transaction handle and buffer */
377 struct mtr_t{
378 #ifdef UNIV_DEBUG
379   ulint   state;  /*!< MTR_ACTIVE, MTR_COMMITTING, MTR_COMMITTED */
380 #endif
381   dyn_array_t memo; /*!< memo stack for locks etc. */
382   dyn_array_t log;  /*!< mini-transaction log */
383   unsigned  inside_ibuf:1;
384         /*!< TRUE if inside ibuf changes */
385   unsigned  modifications:1;
386         /*!< TRUE if the mini-transaction
387         modified buffer pool pages */
388   unsigned  made_dirty:1;
389         /*!< TRUE if mtr has made at least
390         one buffer pool page dirty */
391   ulint   n_log_recs;
392         /* count of how many page initial log records
393         have been written to the mtr log */
394   ulint   n_freed_pages;
395         /* number of pages that have been freed in
396         this mini-transaction */
397   ulint   log_mode; /* specifies which operations should be
398         logged; default value MTR_LOG_ALL */
399   lsn_t   start_lsn;/* start lsn of the possible log entry for
400         this mtr */
401   lsn_t   end_lsn;/* end lsn of the possible log entry for
402         this mtr */
403 #ifdef UNIV_DEBUG
404   ulint   magic_n;
405 #endif /* UNIV_DEBUG */
406 };
mtr_t::log        --做爲mtr的局部緩存,記錄log record;
mtr_t::memo    --包含了一組由此mtr涉及的操做形成的髒頁列表,其會在mtr_commit執行後添加到flush list(參見mtr_memo_pop_all()函數);
 
mtr的一個典型應用場景以下:
1.    建立一個mtr_t類型的對象;
2.    執行mtr_start函數,此函數將會初始化mtr_t的字段,包括local buffer;
3.    在對內存bp中的page進行修改的同時,調用mlog_write_ulint相似的函數,生成redo log record,保存在local buffer中;
4.    執行mtr_commit函數,此函數將會將local buffer中的redo log拷貝到全局的log_sys->buffer,同時將髒頁添加到flush list,供後續執行flush操做時使用;
 
mtr_commit函數調用mtr_log_reserve_and_write,進而調用log_write_low執行上述的拷貝操做。若是須要,此函數將會在log_sys->buf上建立一個新的log block,填充header、tailer以及計算checksum。
 
咱們知道,爲了保證數據庫ACID特性中的原子性和持久性,理論上,在事務提交時,redo log應已經安全原子的寫到磁盤文件之中。回到MySQL,文件內存中的log_sys->buffer什麼時候以及如何寫入磁盤中的redo log file與innodb_flush_log_at_trx_commit的設置密切相關。不管對於DBA仍是MySQL的使用者對這個參數都已經至關熟悉,這裏直接舉例不一樣取值時log子系統是如何操做的。
 
innodb_flush_log_at_trx_commit=1/2。此時每次事務提交時都會寫redo log,不一樣的是1對應write+flush,2只write,而由指定線程週期性的執行flush操做(週期多爲1s)。執行write操做的函數是log_group_write_buf,其由log_write_up_to函數調用。一個典型的調用棧以下:
(trx_commit_in_memory()  /
trx_commit_complete_for_mysql()  /
trx_prepare() e.t.c)->
trx_flush_log_if_needed()->
trx_flush_log_if_needed_low()->
log_write_up_to()->
log_group_write_buf().
log_group_write_buf會再調用innodb封裝的底層IO系統,其實現很複雜,這裏再也不展開。
 
innodb_flush_log_at_trx_commit=0時,每次事務commit不會再調用寫redo log的函數,其寫入邏輯都由master_thread完成,典型的調用棧以下:
srv_master_thread()->
(srv_master_do_active_tasks() / srv_master_do_idle_tasks() / srv_master_do_shutdown_tasks())->
srv_sync_log_buffer_in_background()->
log_buffer_sync_in_background()->log_write_up_to()->... .
除此參數的影響以外,還有一些場景下要求刷新redo log文件。這裏舉幾個例子:
1)爲了保證write ahead logging(WAL),在刷新髒頁前要求其對應的redo log已經寫到磁盤,所以須要調用log_write_up_to函數;
2)爲了循環利用log file,在log file空間不足時須要執行checkpoint(同步或異步),此時會經過調用log_checkpoint執行日誌刷新操做。checkpoint會極大的影響數據庫的性能,這也是log file不能設置的過小的主要緣由;
3)在執行一些管理命令時要求刷新redo log文件,好比關閉數據庫;
 
這裏再簡要總結一下一個log record的「生命週期」:
1.    redo log record首先由mtr生成並保存在mtr的local buffer中。這裏保存的redo log record須要記錄數據庫恢復階段所需的全部信息,而且要求恢復操做是冪等的;
2.    當mtr_commit被調用後,redo log record被記錄在全局內存的log buffer之中;
3.    根據須要(須要額外的空間?事務commit?),redo log buffer將會write(+flush)到磁盤上的redo log文件中,此時redo log已經被安全的保存起來;
4.    mtr_commit執行時會給每一個log record生成一個lsn,此lsn肯定了其在log file中的位置;
5.    lsn同時是聯繫redo log和dirty page的紐帶,WAL要求redo log在刷髒前寫入磁盤,同時,若是lsn相關聯的頁面都已經寫入了磁盤,那麼磁盤上redo log file中對應的log record空間能夠被循環利用;
6.    數據庫恢復階段,使用被持久化的redo log來恢復數據庫;
 
接下來介紹redo log在數據庫恢復階段所起的重要做用。
 
Log Recovery
InnoDB的recovery的函數入口是innobase_start_or_create_for_mysql,其在mysql啓動時由innobase_init函數調用。咱們接下來看下源碼,在此函數內能夠看到以下兩個函數調用:
1.    recv_recovery_from_checkpoint_start
2.    recv_recovery_from_checkpoint_finish
代碼註釋中特地強調,在任何狀況下,數據庫啓動時都會嘗試執行recovery操做,這是做爲函數啓動時正常代碼路徑的一部分。
主要恢復工做在第一個函數內完成,第二個函數作掃尾清理工做。這裏,直接看函數的註釋能夠清楚函數的具體工做是什麼。
146 /** Wrapper for recv_recovery_from_checkpoint_start_func().
147 Recovers from a checkpoint. When this function returns, the database is able
148 to start processing of new user transactions, but the function
149 recv_recovery_from_checkpoint_finish should be called later to complete
150 the recovery and free the resources used in it.
151 @param type in: LOG_CHECKPOINT or LOG_ARCHIVE
152 @param lim  in: recover up to this log sequence number if possible
153 @param min  in: minimum flushed log sequence number from data files
154 @param max  in: maximum flushed log sequence number from data files
155 @return error code or DB_SUCCESS */
156 # define recv_recovery_from_checkpoint_start(type,lim,min,max)    \
157   recv_recovery_from_checkpoint_start_func(type,lim,min,max)
與log_t結構體相對應,恢復階段也有一個結構體,叫作recv_sys_t,這個結構體在recv_recovery_from_checkpoint_start函數中經過recv_sys_create和recv_sys_init兩個函數初始化。recv_sys_t中一樣有幾個和lsn相關的字段,這裏作下介紹。
recv_sys->limit_lsn
恢復應該執行到的最大的LSN值,這裏賦值爲LSN_MAX(uint64_t的最大值)
recv_sys->parse_start_lsn
恢復解析日誌階段所使用的最起始的LSN值,這裏等於最後一次執行checkpoint對應的LSN值
recv_sys->scanned_lsn
當前掃描到的LSN值
recv_sys->recovered_lsn
當前恢復到的LSN值,此值小於等於recv_sys->scanned_lsn
parse_start_lsn值是recovery的起點,其經過recv_find_max_checkpoint函數獲取,讀取的就是log文件LOG_CHECKPOINT_1/LOG_CHECKPOINT_2字段的值。
 
在獲取start_lsn後,recv_recovery_from_checkpoint_start函數調用recv_group_scan_log_recs函數讀取及解析log records。
咱們重點看下recv_group_scan_log_recs函數:
2908 /*******************************************************//**
2909 Scans log from a buffer and stores new log data to the parsing buffer. Parses
2910 and hashes the log records if new data found. */
2911 static
2912 void
2913 recv_group_scan_log_recs(
2914 /*=====================*/
2915   log_group_t*  group,    /*!< in: log group */
2916   lsn_t*    contiguous_lsn, /*!< in/out: it is known that all log
2917           groups contain contiguous log data up
2918           to this lsn */
2919   lsn_t*    group_scanned_lsn)/*!< out: scanning succeeded up to
2920           this lsn */


2930   while (!finished) {
2931     end_lsn = start_lsn + RECV_SCAN_SIZE;
2932
2933     log_group_read_log_seg(LOG_RECOVER, log_sys->buf,
2934                group, start_lsn, end_lsn);
2935
2936     finished = recv_scan_log_recs(
2937       (buf_pool_get_n_pages()
2938       - (recv_n_pool_free_frames * srv_buf_pool_instances))
2939       * UNIV_PAGE_SIZE,
2940       TRUE, log_sys->buf, RECV_SCAN_SIZE,
2941       start_lsn, contiguous_lsn, group_scanned_lsn);
2942     start_lsn = end_lsn;
2943   }
此函數內部是一個while循環。log_group_read_log_seg函數首先將log record讀取到一個內存緩衝區中(這裏是log_sys->buf),接着調用recv_scan_log_recs函數用來解析這些log record。解析過程會計算log block的checksum以及block no和lsn是否對應。解析過程完成後,解析結果會存入recv_sys->addr_hash維護的hash表中。這個hash表的key是經過space id和page number計算獲得,value是一組應用到指定頁面的通過解析後的log record,這裏再也不展開。
 
上述步驟完成後,recv_apply_hashed_log_recs函數可能會在recv_group_scan_log_recs或recv_recovery_from_checkpoint_start函數中調用,此函數將addr_hash中的log應用到特定的page上。此函數會調用recv_recover_page函數作真正的page recovery操做,此時會判斷頁面的lsn要比log record的lsn小。
105 /** Wrapper for recv_recover_page_func().
106 Applies the hashed log records to the page, if the page lsn is less than the
107 lsn of a log record. This can be called when a buffer page has just been
108 read in, or also for a page already in the buffer pool.
109 @param jri  in: TRUE if just read in (the i/o handler calls this for
110 a freshly read page)
111 @param block  in/out: the buffer block
112 */
113 # define recv_recover_page(jri, block)  recv_recover_page_func(jri, block)
如上就是整個頁面的恢復流程。
 
附一個問題環節,後續會將redo log相關的問題記錄在這裏。
1. Q: Log_file, Log_block, Log_record的關係?
    A: Log_file由一組log block組成,每一個log block都是固定大小的。log block中除了header\tailer之外的字節都是記錄的log record
2. Q: 是否是每一次的Commit,產生的應該是一個Log_block ?
    A: 這個不必定的。寫入log_block由mtr_commit肯定,而不是事務提交肯定。看log record大小,若是大小不須要跨log block,就會繼續在當前的log block中寫 。
3. Q: Log_record的結構又是怎麼樣的呢?
    A: 這個結構不少,也沒有細研究,具體看後邊登博圖中的簡要介紹吧;
4. Q: 每一個Block應該有下一個Block的偏移嗎,仍是順序便可,仍是記錄下一個的Block_number
    A: block都是固定大小的,順序寫的 
5. Q: 那如何知道這個Block是否是完整的,是否是依賴下一個Block呢?
    A: block開始有2個字節記錄 此block中第一個mtr開始的位置,若是這個值是0,證實仍是上一個block的同一個mtr。
6. Q: 一個事務是否是須要多個mtr_commit
    A: 是的。mtr的m == mini;
7. Q: 這些Log_block是否是在Commit的時候一塊兒刷到當中?
    A: mtr_commit時會寫入log buffer,具體何時寫到log file就不必定了
8. Q: 那LSN是如何寫的呢?
    A: lsn就是至關於在log file中的位置,那麼在寫入log buffer時就會肯定這個lsn的大小了 。當前只有一個log buffer,在log buffer中的位置和在log file中的位置是一致的
9. Q: 那我Commit的時候作什麼事情呢? 
    A: 可能寫log 、也可能不寫,由innodb_flush_log_at_trx_commit這個參數決定啊 
10. Q: 這兩個值是幹嗎用的: LOG_CHECKPOINT_1/LOG_CHECKPOINT_2   
      A: 這兩個能夠理解爲log file頭信息的一部分(佔用文件頭第二和第四個block),每次執行checkpoint都須要更新這兩個字段,後續恢復時,每一個頁面對應lsn中比這個checkpoint值小的,認爲是已經寫入了,不須要再恢復 
 
文章最後,將網易杭研院何登成博士-登博博客上的一個log block的結構圖放在這,再畫圖也不會比登博這張圖畫的更清晰了,版權屬於登博。
相關文章
相關標籤/搜索