天天看點

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

MySQL redo log及recover過程淺析