MySQL · 引擎特性 · InnoDB redo log漫遊

來源:互聯網
上載者:User

MySQL · 引擎特性 · InnoDB redo log漫遊
前言

InnoDB 有兩塊非常重要的日誌,一個是undo log,另外一個是redo log,前者用來保證事務的原子性以及InnoDB的MVCC,後者用來保證事務的持久性。

和大多數關係型資料庫一樣,InnoDB記錄了對資料檔案的物理更改,並保證總是日誌先行,也就是所謂的WAL,即在持久化資料檔案前,保證之前的redo日誌已經寫到磁碟。

LSN(log sequence number) 用於記錄日誌序號,它是一個不斷遞增的 unsigned long long 類型整數。在 InnoDB 的日誌系統中,LSN 無處不在,它既用於表示修改髒頁時的日誌序號,也用於記錄checkpoint,通過LSN,可以具體的定位到其在redo log檔案中的位置。

為了管理髒頁,在 Buffer Pool 的每個instance上都維持了一個flush list,flush list 上的 page 按照修改這些 page 的LSN號進行排序。因此定期做redo checkpoint點時,選擇的 LSN 總是所有 bp instance 的 flush list 上最老的那個page(擁有最小的LSN)。由於採用WAL的策略,每次事務提交時需要持久化 redo log 才能保證事務不丟。而延遲刷髒頁則起到了合并多次修改的效果,避免頻繁寫資料檔案造成的效能問題。

由於 InnoDB 日誌組的特性已經被廢棄(redo日誌寫多份),歸檔日誌(InnoDB archive log)特性也在5.7被徹底移除,本文在描述相關邏輯時會忽略這些邏輯。另外限於篇幅,InnoDB崩潰恢複的邏輯將在下期講述,本文重點闡述redo log 產生的生命週期以及MySQL 5.7的一些改進點。

本文的分析基於最新的MySQL 5.7.7-RC版本。

MySQL InnoDB表--BTree基本資料結構

在MySQL的InnoDB儲存引擎中count(*)函數的最佳化

MySQL InnoDB儲存引擎鎖機制實驗

InnoDB儲存引擎的啟動、關閉與恢複

MySQL InnoDB獨立資料表空間的配置

MySQL Server 層和 InnoDB 引擎層 體繫結構圖

InnoDB 死結案例解析

MySQL Innodb獨立資料表空間的配置

InnoDB 記錄檔

InnoDB的redo log可以通過參數innodb_log_files_in_group配置成多個檔案,另外一個參數innodb_log_file_size表示每個檔案的大小。因此總的redo log大小為innodb_log_files_in_group * innodb_log_file_size

Redo log檔案以ib_logfile[number]命名,日誌目錄可以通過參數innodb_log_group_home_dir控制。Redo log 以順序的方式寫入檔案檔案,寫滿時則回溯到第一個檔案,進行覆蓋寫。(但在做redo checkpoint時,也會更新第一個記錄檔的頭部checkpoint標記,所以嚴格來講也不算順序寫)。

在InnoDB內部,邏輯上ib_logfile被當成了一個檔案,對應同一個space id。由於是使用512位元組block對齊寫入檔案,可以很方便的根據全域維護的LSN號計算出要寫入到哪一個檔案以及對應的位移量。

Redo log檔案是迴圈寫入的,在覆蓋寫之前,總是要保證對應的髒頁已經刷到了磁碟。在非常大的負載下,Redo log可能產生的速度非常快,導致頻繁的刷髒操作,進而導致效能下降,通常在未做checkpoint的日誌超過檔案總大小的76%之後,InnoDB 認為這可能是個不安全的點,會強制的preflush髒頁,導致大量使用者線程stall住。如果可預期會有這樣的情境,我們建議調大redo log檔案的大小。可以做一次乾淨的shutdown,然後修改Redo log配置,重啟執行個體。

除了redo log檔案外,InnoDB還有其他的記錄檔,例如為了保證truncate操作而產生的中間記錄檔,包括 truncate innodb 表以及truncate undo log tablespace,都會產生一個中間檔案,來標識這些操作是成功還是失敗,如果truncate沒有完成,則需要在 crash recovery 時進行重做。有意思的是,根據官方worklog的描述,最初實現truncate操作的原子化時是通過增加新的redo log類型來實現的,但後來不知道為什麼又改成了採用記錄檔的方式,也許是考慮到低版本相容的問題吧。

關鍵結構體log_sys對象

log_sys是InnoDB日誌系統的中樞及核心對象,控制著日誌的拷貝、寫入、checkpoint等核心功能。它同時也是大寫入負載情境下的熱點模組,是串連InnoDB記錄檔及log buffer的樞紐,對應結構體為log_t

其中與 redo log 檔案相關的成員變數包括:

變數名 描述
log_groups 日誌組,目前的版本僅支援一組日誌,對應類型為 log_group_t ,包含了當前日誌組的檔案個數、每個檔案的大小、space id等資訊
lsn_t log_group_capacity 表示當前記錄檔的總容量,值為:(Redo log檔案總大小 - redo 檔案個數 * LOG_FILE_HDR_SIZE) * 0.9,LOG_FILE_HDR_SIZE 為 4*512 位元組
lsn_t max_modified_age_async 非同步 preflush dirty page 點
lsn_t max_modified_age_sync 同步 preflush dirty page 點
lsn_t max_checkpoint_age_async 非同步 checkpoint 點
lsn_t max_checkpoint_age 同步 checkpoint 點

上述幾個sync/async點的計算方式可以參閱函數log_calc_max_ages,以如下執行個體配置為例:

innodb_log_files_in_group=4innodb_log_file_size=4G總檔案大小: 17179869184

各個成員變數值及佔總檔案大小的比例:

log_sys->log_group_capacity = 15461874893 (90%)log_sys->max_modified_age_async = 12175607164 (71%)log_sys->max_modified_age_sync = 13045293390 (76%)log_sys->max_checkpoint_age_async = 13480136503 (78%)log_sys->max_checkpoint_age = 13914979615 (81%)

通常的:

噹噹前未刷髒的最老lsn和當前lsn的距離超過max_modified_age_async(71%)時,且開啟了選項innodb_adaptive_flushing時,page cleaner線程會去嘗試做更多的dirty page flush工作,避免髒頁堆積。
噹噹前未刷髒的最老lsn和當前Lsn的距離超過max_modified_age_sync(76%)時,使用者線程需要去做同步刷髒,這是一個效能下降的臨界點,會極大的影響整體輸送量和回應時間。
當上次checkpoint的lsn和當前lsn超過max_checkpoint_age(81%),使用者線程需要同步地做一次checkpoint,需要等待checkpoint寫入完成。
當上次checkpoint的lsn和當前lsn的距離超過max_checkpoint_age_async(78%)但小於max_checkpoint_age(81%)時,使用者線程做一次非同步checkpoint(後台非同步線程執行CHECKPOINT資訊寫入操作),無需等待checkpoint完成。

log_group_t結構體主要成員如下表所示:

變數名 描述
ulint n_files Ib_logfile的檔案個數
lsn_t file_size 檔案大小
ulint space_id Redo log 的space id, 固定大小,值為SRV_LOG_SPACE_FIRST_ID
ulint state LOG_GROUP_OK 或者 LOG_GROUP_CORRUPTED
lsn_t lsn 該group內寫到的lsn
lsn_t lsn_offset 上述lsn對應的檔案位移量
byte** file_header_bufs Buffer地區,用於設定記錄檔頭資訊,並寫入ib logfile。當切換到新的ib_logfile時,更新該檔案的起始lsn,寫入頭部。 頭部資訊還包含: LOG_GROUP_ID, LOG_FILE_START_LSN(當前檔案起始lsn)、LOG_FILE_WAS_CREATED_BY_HOT_BACKUP(函數log_group_file_header_flush)
lsn_t scanned_lsn 用於崩潰恢複時輔助記錄掃描到的lsn號
byte* checkpoint_buf Checkpoint緩衝區,用於向記錄檔寫入checkpoint資訊(下文詳細描述)

與redo log 記憶體緩衝區相關的成員變數包括:

變數名 描述
ulint buf_free Log buffer中當前空閑可寫的位置
byte* buf Log buffer起始位置指標
ulint buf_size Log buffer 大小,受參數innodb_log_buffer_size控制,但可能會自動extend
ulint max_buf_free 值為log_sys->buf_size / LOG_BUF_FLUSH_RATIO - LOG_BUF_FLUSH_MARGIN, 其中: LOG_BUF_FLUSH_RATIO=2, LOG_BUF_FLUSH_MARGIN=(4 * 512 + 4* page_size) ,page_size預設為16k,當buf_free超過該值時,可能觸發使用者線程去寫redo;在事務拷redo 到buffer後,也會判斷該值,如果超過buf_free,設定log_sys->check_flush_or_checkpoint為true
ulint buf_next_to_write Log buffer位移量,下次寫入redo檔案的起始位置,即本次寫入的結束位置
volatile bool is_extending Log buffer是否進行中擴充 (防止過大的redo log entry無法寫入buffer), 實際上,當寫入的redo log長度超過buf_size/2時,就會去調用函數log_buffer_extend,一旦擴充Buffer,就不會在縮減回去了!
ulint write_end_offset 本次寫入的結束位置位移量(從邏輯來看有點多餘,直接用log_sys->buf_free就行了)

和Checkpoint檢查點相關的成員變數:

變數名 描述
ib_uint64_t next_checkpoint_no 每完成一次checkpoint遞增該值
lsn_t last_checkpoint_lsn 最近一次checkpoint時的lsn,每完成一次checkpoint,將next_checkpoint_lsn的值賦給last_checkpoint_lsn
lsn_t next_checkpoint_lsn 下次checkpoint的lsn(本次發起的checkpoint的lsn)
mtr_buf_t* append_on_checkpoint 5.7新增,在做DDL時(例如增刪列),會先將包含MLOG_FILE_RENAME2日誌記錄的buf掛到這個變數上。 在DDL完成後,再清理掉。(log_append_on_checkpoint),主要是防止DDL期間crash產生的資料詞典不一致。 該變數在如下commit加上: a5ecc38f44abb66aa2024c70e37d1f4aa4c8ace9
ulint n_pending_checkpoint_writes 大於0時,表示有一個checkpoint寫入操作進行中。使用者發起checkpoint時,遞增該值。後台線程完成checkpoint寫入後,遞減該值(log_io_complete)
rw_lock_t checkpoint_lock checkpoint鎖,每次寫checkpoint資訊時需要加x鎖。由非同步io線程釋放該x鎖
byte* checkpoint_buf Checkpoint資訊緩衝區,每次checkpoint前,先寫該buf,再將buf刷到磁碟

其他狀態變數

變數名 描述
bool check_flush_or_checkpoint 當該變數被設定時,使用者線程可能需要去檢查釋放要刷log buffer、或是做preflush、checkpoint等以防止Redo 空間不足
lsn_t write_lsn 最近一次完成寫入到檔案的LSN
lsn_t current_flush_lsn 當前正在fsync到的LSN
lsn_t flushed_to_disk_lsn 最近一次完成fsync到檔案的LSN
ulint n_pending_flushes 表示pending的redo fsync,這個值最大為1
os_event_t flush_event 若當前有進行中的fsync,並且本次請求也是fsync操作,則需要等待上次fsync操作完成

log_sys與記錄檔和日誌緩衝區的關係可用來表示:

Mini transaction

Mini transaction(簡稱mtr)是InnoDB對物理資料檔案操作的最小事務單元,用於管理對Page加鎖、修改、釋放、以及日誌提交到公用buffer等工作。一個mtr操作必須是原子的,一個事務可以包含多個mtr。每個mtr完成後需要將本地產生的日誌拷貝到公用緩衝區,將修改的髒頁放到flush list上。

mtr事務對應的類為mtr_t, mtr_t::Impl中儲存了當前mtr的相關資訊,包括:

變數名 描述
mtr_buf_t m_memo 用於儲存該mtr持有的鎖類型
mtr_buf_t m_log 儲存redo log記錄
bool m_made_dirty 是否產生了至少一個髒頁
bool m_inside_ibuf 是否在操作change buffer
bool m_modifications 是否修改了buffer pool page
ib_uint32_t m_n_log_recs 該mtr log記錄個數
mtr_log_t m_log_mode Mtr的工作模式,包括四種: MTR_LOG_ALL:預設模式,記錄所有會修改磁碟資料的操作;MTR_LOG_NONE:不記錄redo,髒頁也不放到flush list上;MTR_LOG_NO_REDO:不記錄redo,但髒頁放到flush list上;MTR_LOG_SHORT_INSERTS:插入記錄操作REDO,在將記錄從一個page拷貝到另外一個建立的page時用到,此時忽略寫索引資訊到redo log中。(參閱函數page_cur_insert_rec_write_log)
fil_space_t* m_user_space 當前mtr修改的使用者資料表空間
fil_space_t* m_undo_space 當前mtr修改的undo資料表空間
fil_space_t* m_sys_space 當前mtr修改的系統資料表空間
mtr_state_t m_state 包含四種狀態: MTR_STATE_INIT、MTR_STATE_COMMITTING、 MTR_STATE_COMMITTED

在修改或讀一個資料檔案中的資料時,一般是通過mtr來控制對對應page或者索引樹的加鎖,在5.7中,有以下幾種鎖類型(mtr_memo_type_t):

變數名 描述
MTR_MEMO_PAGE_S_FIX 用於PAGE上的S鎖
MTR_MEMO_PAGE_X_FIX 用於PAGE上的X鎖
MTR_MEMO_PAGE_SX_FIX 用於PAGE上的SX鎖,以上鎖通過mtr_memo_push 儲存到mtr中
MTR_MEMO_BUF_FIX PAGE上未加讀寫鎖,僅做buf fix
MTR_MEMO_S_LOCK S鎖,通常用於索引鎖
MTR_MEMO_X_LOCK X鎖,通常用於索引鎖
MTR_MEMO_SX_LOCK SX鎖,通常用於索引鎖,以上3個鎖,通過mtr_s/x/sx_lock加鎖,通過mtr_memo_release釋放鎖
mtr log產生

InnoDB的redo log都是通過mtr產生的,先寫到mtr的cache中,然後再提交到公用buffer中,本小節以INSERT一條記錄對page產生的修改為例,闡述一個mtr的典型生命週期。

入口函數:row_ins_clust_index_entry_low

開啟mtr

執行如下代碼塊

mtr_start(&mtr);mtr.set_named_space(index->space);

mtr_start主要包括:

  1. 初始化mtr的各個狀態變數

  2. 預設模式為MTR_LOG_ALL,表示記錄所有的資料變更

  3. mtr狀態設定為ACTIVE狀態(MTR_STATE_ACTIVE)

  4. 為鎖管理對象和日誌管理對象初始化記憶體(mtr_buf_t),初始化對象鏈表

mtr.set_named_space 是5.7新增的邏輯,將當前修改的資料表空間對象fil_space_t儲存下來:如果是系統資料表空間,則賦值給m_impl.m_sys_space, 否則賦值給m_impl.m_user_space

Tips: 在5.7裡針對暫存資料表做了最佳化,直接關閉redo記錄:
mtr.set_log_mode(MTR_LOG_NO_REDO)

定位記錄插入的位置

主要入口函數: btr_cur_search_to_nth_level

不管插入還是更新操作,都是先以樂觀方式進行,因此先加索引S鎖
mtr_s_lock(dict_index_get_lock(index),&mtr),對應mtr_t::s_lock函數
如果以悲觀方式插入記錄,意味著可能產生索引分裂,在5.7之前會加索引X鎖,而5.7版本則會加SX鎖(但某些情況下也會退化成X鎖)
加X鎖: mtr_x_lock(dict_index_get_lock(index), mtr),對應mtr_t::x_lock函數
加SX鎖:mtr_sx_lock(dict_index_get_lock(index),mtr),對應mtr_t::sx_lock函數

對應到內部實現,實際上就是加上對應的鎖對象,然後將該鎖的指標和類型構建的mtr_memo_slot_t對象插入到mtr.m_impl.m_memo中。

當找到預插入page對應的block,還需要加block鎖,並把對應的鎖類型加入到mtr:mtr_memo_push(mtr, block, fix_type)

如果對page加的是MTR_MEMO_PAGE_X_FIX或者MTR_MEMO_PAGE_SX_FIX鎖,並且當前block是clean的,則將m_impl.m_made_dirty設定成true,表示即將修改一個乾淨的page。

如果加鎖類型為MTR_MEMO_BUF_FIX,實際上是不加鎖對象的,但需要判斷暫存資料表的情境,暫存資料表page的修改不加latch,但需要將m_impl.m_made_dirty設定為true(根據block的成員m_impl.m_made_dirty來判斷),這也是5.7對InnoDB暫存資料表情境的一種最佳化。

同樣的,根據鎖類型和鎖對象構建mtr_memo_slot_t加入到m_impl.m_memo中。

  • 1
  • 2
  • 下一頁

相關文章

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.