MySQL redo log及recover過程淺析


寫在前面:作者水平有限,歡迎不吝賜教,一切以最新源碼為准。

InnoDB redo log

首先介紹下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的結構圖放在這,再畫圖也不會比登博這張圖畫的更清晰了,版權屬於登博。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM