寫在前面:作者水準有限,歡迎不吝賜教,一切以最新源碼為準。
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的結構圖放在這,再畫圖也不會比登博這張圖畫的更清晰了,版權屬于登博。