寫在前面:作者水平有限,歡迎不吝賜教,一切以最新源碼為准。
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函數
|
接下來介紹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 |
在獲取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值小的,認為是已經寫入了,不需要再恢復
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的結構圖放在這,再畫圖也不會比登博這張圖畫的更清晰了,版權屬於登博。
