1. 程式人生 > >MySQL系列:innodb原始碼分析之重做日誌結構

MySQL系列:innodb原始碼分析之重做日誌結構

在innodb的引擎實現中,為了實現事務的永續性,構建了重做日誌系統。重做日誌由兩部分組成:記憶體日誌緩衝區(redo log buffer)和重做日誌檔案。這樣設計的目的顯而易見,日誌緩衝區是為了加快寫日誌的速度,而重做日誌檔案為日誌資料提供持久化的作用。在innodb的重做日誌系統中,為了更好實現日誌的易恢復性、安全性和持久化性,引入了以下幾個概念:LSN、log block、日誌檔案組、checkpoint和歸檔日誌。以下我們分別一一來進行分析。

1.LSN

innodb中的重做日誌系統中,定義一個LSN序號,其代表的意思是日誌序號。LSN在引擎中定義的是一個dulint_t型別值,相當於uint64_t,
關於dulint_t的定義如下:

typedef struct dulint_struct
{
     ulint high;     /* most significant 32 bits */
     ulint low;       /* least significant 32 bits */
}dulint_t;
LSN真正的含義是儲存引擎向重做日誌系統寫入的日誌量(位元組數),這個日誌量包括寫入的日誌位元組 + block_header_size + block_tailer_sizeLSN的初始化值是:LOG_START_LSN(相當於8192),在呼叫日誌寫入函式LSN就一直隨著寫入的日誌長度增加,具體看:

void log_write_low(byte* str, ulint str_len)
{
 log_t* log = log_sys;
. . .
part_loop:
 /*計算part length*/
 data_len = log->buf_free % OS_FILE_LOG_BLOCK_SIZE + str_len;
.  .  . 
 /*將日誌內容拷貝到log buffer*/
 ut_memcpy(log->buf + log->buf_free, str, len);
 str_len -= len;
 str = str + len;
 . . .
 if(data_len = OS_FILE_LOG_BLOCK_SIZE - LOG_BLOCK_TRL_SIZE){ /*完成一個block的寫入*/
. . .
      len += LOG_BLOCK_HDR_SIZE + LOG_BLOCK_TRL_SIZE;
      log->lsn = ut_dulint_add(log->lsn, len);
 . . .
 }
 else /*更改lsn*/
  log->lsn = ut_dulint_add(log->lsn, len);
 . . .
}

LSN是不會減小的,它是日誌位置的唯一標記。在重做日誌寫入、checkpoint構建和PAGE頭裡面都有LSN。

關於日誌寫入:

例如當前重做日誌的LSN = 2048,這時候innodb呼叫log_write_low寫入一個長度為700的日誌,2048剛好是4block長度,那麼需要儲存700長度的日誌,需要量個BLOCK(單個block只能存496個位元組)。那麼很容易得出新的LSN = 2048 + 700 + 2 * LOG_BLOCK_HDR_SIZE(12) + LOG_BLOCK_TRL_SIZE(4) = 2776

關於checkpoint和日誌恢復:

page的fil_header中的LSN是表示最後重新整理是的LSN, 假如資料庫中存在PAGE1 LSN  = 1024,PAGE2 LSN = 2048, 系統重啟時,檢測到最後的checkpoint LSN = 1024,那麼系統在檢測到PAGE1不會對PAGE1進行恢復重做,當系統檢測到PAGE2的時候,會將PAGE2進行重做。一次類推,小於checkpoint LSN的頁不用重做,大於LSN checkpoint的PAGE就要進行重做。

2.Log Block

innodb在日誌系統裡面定義了log block的概念,其實log block就是一個512位元組的資料塊,這個資料塊包括塊頭、日誌資訊和塊的checksum.其結構如下:

Block no的最高位是描述block是否flush磁碟的標識位.通過lsn可以blockno,具體的計算過程是lsn是多少個512的整數倍,也就是no = lsn / 512 + 1;為什麼要加1呢,因為所處no的塊算成clac_lsn一定會小於傳入的lsn.所以要+1。其實就是block的陣列索引值。checksum是通過從塊頭開始到塊的末尾前4個位元組為止,做了一次數字疊加,程式碼如下:

sum = 1;
 sh = 0;
 for(i = 0; i < OS_FILE_LOG_BLOCK_SIZE - LOG_BLOCK_TRL_SIZE, i ++){
      sum = sum & 0x7FFFFFFF;
      sum += (((ulint)(*(block + i))) << sh) + (ulint)(*(block + i));
      sh ++;
      if(sh > 24) 
        sh = 0;
 }
在日誌恢復的時候,innodb會對載入的block進行checksum校驗,以免在恢復過程中資料產生錯誤。事務的日誌寫入是基於塊的,如果事務的日誌大小小於496位元組,那麼會合其他的事務日誌合併在一個塊中,如果事務日誌的大小大於496位元組,那麼會以496為長度進行分離儲存。例如:T1 = 700位元組大小,T2 = 100位元組大小儲存結構如下:


3.重做日誌結構和關係圖

 innodb在重做日誌實現當中,設計了3個層模組,即redo log buffer、group files和archive files。這三個層模組的描述如下:

redo log buffer       重做日誌的日誌記憶體緩衝區,新寫入的日誌都是先寫入到這個地方.redo log buffer中資料同步到磁碟上,必須進行刷盤操作。

group files      重做日誌檔案組,一般由3個同樣大小的檔案組成。3個檔案的寫入是依次迴圈的,每個日誌檔案寫滿後,即寫下一個,日誌檔案如果都寫滿時,會覆蓋第一次重新寫。重做日誌組在innodb的設計上支援多個。

archive files         歸檔日誌檔案,是對重做日誌檔案做增量備份,它是不會覆蓋以前的日誌資訊。

 以下是它們關係示意圖:

3.1重做日誌組

重做日誌組可以支援多個,這樣做的目的應該是為了防止一個日誌組損壞後,可以從其他並行的日誌組裡面進行資料恢復。在MySQL-5.6的將日誌組的個數設定為1,不允許多個group存在。網易姜承堯的解釋是innodb的作者認為通過外層儲存硬體來保證日誌組的完整性比較好,例如raid磁碟。重做日誌組的主要功能是實現對組內檔案的寫入管理、組內的checkpoint建立和checkpiont資訊的儲存、歸檔日誌狀態管理(只有第一個group才做archive操作).以下是對日誌組的定義:

typedef struct log_group_struct
{
 ulint id;                             /*log group id*/
 ulint n_files;                     	/*group包含的日誌檔案個數*/
 ulint file_size;                  	/*日誌檔案大小,包括檔案頭*/
 ulint space_id;                 	/*group對應的fil_space的id*/
 ulint state;                        /*log group狀態,LOG_GROUP_OK、LOG_GROUP_CORRUPTED*/
 dulint lsn;                         /*log group的lsn*/
 dulint lsn_offset;             	/*當前lsn相對組內檔案起始位置的偏移量 */
 ulint n_pending_writes; 		/*本group 正在執行fil_flush的個數*/
 byte** file_header_bufs; 	/*檔案頭緩衝區*/
 byte** archive_file_header_bufs;/*歸檔檔案頭資訊的緩衝區*/
 ulint archive_space_id;     	/*歸檔重做日誌ID*/
 ulint archived_file_no;     	/*歸檔的日誌檔案編號*/
 ulint archived_offset;     	/*已經完成歸檔的偏移量*/
 ulint next_archived_file_no;	/*下一個歸檔的檔案編號*/
 ulint next_archived_offset;	/*下一個歸檔的偏移量*/
 dulint scanned_lsn;
 byte* checkpoint_buf; 		/*本log group儲存checkpoint資訊的緩衝區*/
 UT_LIST_NODE_T(log_group_t) log_groups;
}log_group_t;

上面結構定義中的spaceid是對應fil0fil中的fil_space_t結構,一個fil_space_t結構可以管理多個檔案fil_node_t,關於fil_node_t參見這裡

3.1.1LSN與組內偏移

   在log_goup_t組內日誌模組當中,其中比較重要的是關於LSN與組內偏移之間的換算關係。在組建立時,會對lsn和對應lsn_offset做設定,假如 初始化為 group lsn = 1024,  group lsn_offset = 2048,group由3個10240大小的檔案組成,LOG_FILE_HDR_SIZE = 2048, 我們需要知道buf lsn = 11240對應的組內offset的偏移是多少,根據log_group_calc_lsn_offset函式可以得出如下公式:
    group_size = 3 * 11240;
   相對組起始位置的LSN偏移 = (buf_ls - group_ls)  + log_group_calc_size_offset(lsn_offset ) = (11240 - 1024) - 0 = 10216;
   lsn_offset = log_group_calc_lsn_offset(相對組起始位置的LSN偏移 % group_size) = 10216 + 2 * LOG_FILE_HDR_SIZE = 14312;
這個偏移一定是加上了檔案頭長度的。

3.1.2 file_header_bufs

file_header_bufs是一個buffer緩衝區陣列,陣列長度和組內檔案數是一致的,每個buf長度是2048。其資訊結構如下:


  log_group_id      對應log_group_t結構中的id

  file_start_lsn    當前檔案其實位置資料對應的LSN值

  File_no           當前的檔案編號,一般在archive file頭中體現

  Hot backup str    一個空字串,如果是hot_backup,會填上檔案字尾ibackup。

  File_end_ls       檔案結尾資料對應的LSN值,一般在archive file檔案中體現。

3.2 checkpoint

checkpoint是日誌的檢查點,其作用就是在資料庫異常後,redo log是從這個點的資訊獲取到LSN,並對檢查點以後的日誌和PAGE做重做恢復。那麼檢查點是怎麼生成的呢?當日志緩衝區寫入的日誌LSN距離上一次生成檢查點的LSN達到一定差距的時候,就會開始建立檢查點,建立檢查點首先會將記憶體中的表的髒資料寫入到硬碟,讓後再將redo log buffer中小於本次檢查點的LSN的日誌也寫入硬碟。在log_group_t中的checkpoint_buf,以下是它對應欄位的解釋:

 LOG_CHECKPOINT_NO            checkpoint序號,

 LOG_CHECKPOINT_LSN           本次checkpoint起始的LSN

 LOG_CHECKPOINT_OFFSET        本次checkpoint相對group file的起始偏移量

 LOG_CHECKPOINT_LOG_BUF_SIZE  redo log buffer的大小,預設2M

 LOG_CHECKPOINT_ARCHIVED_LSN  當前日誌歸檔的LSN

 LOG_CHECKPOINT_GROUP_ARRAY   每個log group歸檔時的檔案序號和偏移量,是一個數組

3.3 log_t

重做日誌的寫入、資料刷盤、建立checkpoint和歸檔操作都是通過全域性唯一的,log_sys進行控制的,這是個非常龐大而又複雜的結構,定義如下:

typedef struct log_struct
{
 byte pad[64];                     /*使得log_struct物件可以放在通用的cache line中的資料,這個和CPU L1 Cache和資料競爭有和
直接關係*/
 dulint lsn;                    	/*log的序列號,實際上是一個日誌檔案偏移量*/
 ulint buf_free;             	/*buf可以寫的位置*/
 
 mutex_t mutex;         		/*log保護的mutex*/
 byte* buf;                   	/*log緩衝區*/
 ulint buf_size;             	/*log緩衝區長度*/
 ulint max_buf_free;     		/*在log buffer刷盤後,推薦buf_free的最大值,超過這個值會被強制刷盤*/
 
 ulint old_buf_free;       		/*上次寫時buf_free的值,用於除錯*/
 dulint old_lsn;             	/*上次寫時的lsn,用於除錯*/
 ibool check_flush_or_checkpoint; /*需要日誌寫盤或者是需要重新整理一個log checkpoint的標識*/
 ulint buf_next_to_write;             /*下一次開始寫入磁碟的buf偏移位置*/
 dulint written_to_some_lsn;         /*第一個group刷完成是的lsn*/
 dulint written_to_all_lsn;             /*已經記錄在日誌檔案中的lsn*/
 dulint flush_lsn;                       /*flush的lsn*/
 ulint flush_end_offset;               /*最後一次log file刷盤時的buf_free,也就是最後一次flush的末尾偏移量*/
 ulint n_pending_writes;              /*正在呼叫fil_flush的個數*/
 os_event_t no_flush_event;          /*所有fil_flush完成後才會觸發這個訊號,等待所有的goups刷盤完成*/ 
 ibool one_flushed;                   /*一個log group被刷盤後這個值會設定成TRUE*/
 os_event_t one_flushed_event;     /*只要有一個group flush完成就會觸發這個訊號*/
 ulint n_log_ios;                        /*log系統的io操作次數*/
 ulint n_log_ios_old;                   /*上一次統計時的io操作次數*/
 time_t last_printout_time;
 ulint max_modified_age_async;     /*非同步日誌檔案刷盤的閾值*/
 ulint max_modified_age_sync;       /*同步日誌檔案刷盤的閾值*/
 ulint adm_checkpoint_interval;
 ulint max_checkpoint_age_async;    /*非同步建立checkpoint的閾值*/
 ulint max_checkpoint_age;            /*強制建立checkpoint的閾值*/
 dulint next_checkpoint_no;
 dulint last_checkpoint_lsn;
 dulint next_checkpoint_lsn;
 ulint n_pending_checkpoint_writes;
 rw_lock_t checkpoint_lock;            /*checkpoint的rw_lock_t,在checkpoint的時候,是獨佔這個latch*/
 byte* checkpoint_buf;                 /*checkpoint資訊儲存的buf*/
 ulint archiving_state;
 dulint archived_lsn;
 dulint max_archived_lsn_age_async;
 dulint max_archived_lsn_age;
 dulint next_archived_lsn;
 ulint archiving_phase;
 ulint n_pending_archive_ios;
 rw_lock_t archive_lock;
 ulint archive_buf_size;
 byte* archive_buf;
 os_event_t archiving_on;
 ibool online_backup_state;             /*是否在backup*/
 dulint online_backup_lsn;                /*backup時的lsn*/
 UT_LIST_BASE_NODE_T(log_group_t) log_groups;
}log_t;

3.3.1各種LSN之間的關係和分析

從上面的結構定義可以看出有很多LSN相關的定義,那麼這些LSN直接的關係是怎麼樣的呢?理解這些LSN之間的關係對理解整個重做日誌系統的運作機理會有極大的信心。以下各種LSN的解釋:

 lsn                        當前log系統最後寫入日誌時的LSN

 flush_lsn                  redolog buffer最後一次資料刷盤資料末尾的LSN,作為下次刷盤的起始LSN

 written_to_some_lsn       單個日誌組最後一次日誌刷盤時的起始LSN

 written_to_all_lsn         所有日誌組最後一次日誌刷盤是的起始LSN

 last_checkpoint_lsn        最後一次建立checkpoint日誌資料起始的LSN

 next_checkpoint_lsn        下一次建立checkpoint的日誌    資料起始的LSN,用log_buf_pool_get_oldest_modification獲得的

 archived_lsn               最後一次歸檔日誌資料起始的LSN

 next_archived_lsn          下一次歸檔日誌資料的其實LSN  

關係圖如下:


3.3.2偏移量的分析

log_t有各種偏移量,例如:max_buf_free、buf_free、flush_end_offset、buf_next_to_write等。偏移和LSN不一樣,偏移量是相對redo log buf其實位置的絕對偏移量,LSN是整個日誌系統的序號。

  max_buf_free        寫入日誌是不能超過的偏移位置,如果超過,將強制redo log buf寫入磁碟

  buf_free            當前日誌可以寫的偏移位置

  buf_next_to_write   下一次redo log buf資料寫盤的資料起始偏移,在所有刷盤IO完成後,其值和 flush_end_offset是一致的。

  flush_end_offset    本次刷盤的資料末尾的偏移量,相當於刷盤時的buf_free,當flush_end_offset 超過max_buf_free的一半時會將未寫入的資料移到                       redobuffer的最前面,這時buf_free和buf_next_to_write都將做調整

大小關係圖如下:


3.4記憶體結構關係圖


4.日誌寫入和日誌保護機制

 innodb有四種日誌刷盤行為,分別是非同步redo log buffer刷盤、同步redo log buffer刷盤、非同步建立checkpoint刷盤和同步建立checkpoint刷盤。在innodb中,刷盤行為是非常耗磁碟IO的,innodb對刷盤做了一套非常完善的策略。

4.1重做日誌刷盤選項

在innodb引擎中有個全域性變數srv_flush_log_at_trx_commit,這個全域性變數是控制flushdisk的策略,也就是確定調不呼叫fsync這個函式,什麼時候掉這個函式。這個變數有3個值。這三個值的解釋如下:

0     每隔1秒由MasterThread控制重做日誌模組呼叫log_flush_to_disk來刷盤,好處是提高了效率,壞處是1秒內如果資料庫崩潰,日誌和資料會丟失。

1     每次寫入重做日誌後,都呼叫fsync來進行日誌寫入磁碟。好處是每次日誌都寫入了磁碟,資料可靠性大大提高,壞處是每次呼叫fsync會產生大量的磁碟IO,影響資料庫效能。

2     每次寫入重做日誌後,都將日誌寫入日誌檔案的page cache。這種情況如果物理機崩潰後,所有的日誌都將丟失。

4.2日誌刷盤保護

 由於重做日誌是一個組內多檔案重複寫的一個過程,那麼意味日誌如果不及時寫盤和建立checkpoint,就有可能會產生日誌覆蓋,這是一個我們不願意看到的。在innodb定義了一個日誌保護機制,在儲存引擎會定時呼叫log_check_margins日誌函式來檢查保護機制。簡單介紹如下:

 引入三個變數 buf_age、checkpoint_age和日誌空間大小.       

           buf_age = lsn -oldest_lsn;

           checkpoint_age =lsn - last_checkpoint_lsn;

          日誌空間大小 = 重做日誌組能儲存日誌的位元組數(通過log_group_get_capacity獲得);

當buf_age >=日誌空間大小的7/8時,重做日誌系統會將red log buffer進行非同步資料刷盤,這個時候因為是非同步的,不會造成資料操作阻塞。

當buf_age >=日誌空間大小的15/16時,重做日誌系統會將redlog buffer進行同步資料刷盤,這個時候會呼叫fsync函式,資料庫的操作會進行阻塞。

當 checkpoint_age >=日誌空間大小的31/32時,日誌系統將進行非同步建立checkpoint,資料庫的操作不會阻塞。

當 checkpoint_age == 日誌空間大小時,日誌系統將進行同步建立checkpoint,大量的表空間髒頁和log檔案髒頁同步刷入磁碟,會產生大量的磁碟IO操作。資料庫操作會堵塞。整個資料庫事務會掛起。

5.總結

       Innodb的重做日誌系統是相當完備的,它為資料的持久化做了很多細微的考慮,它效率直接影響MySQL的寫效率,所以我們深入理解了它便以我們去優化它,尤其是在大量資料刷盤的時候。假設資料庫的受理的事務速度大於磁碟IO的刷入速度,一定會出現同步建立checkpoint操作,這樣資料庫是堵塞的,整個資料庫都在都在進行髒頁刷盤。避免這樣的問題發生就是增加IO能力,用多磁碟分散IO壓力。也可以考慮SSD這讀寫速度很高的儲存介質來做優化。