MySQL系列:innodb源碼分析之重做日誌結構,mysqlinnodb

來源:互聯網
上載者:User

MySQL系列:innodb源碼分析之重做日誌結構,mysqlinnodb

在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_size。LSN的初始化值是: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剛好是4個block長度,那麼需要儲存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 Blockinnodb在日誌系統裡面定義了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    一個Null 字元串,如果是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這讀寫速度很高的儲存介質來做最佳化。

 

 




相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.