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主要包括:
初始化mtr的各個狀態變數
預設模式為MTR_LOG_ALL,表示記錄所有的資料變更
mtr狀態設定為ACTIVE狀態(MTR_STATE_ACTIVE)
為鎖管理對象和日誌管理對象初始化記憶體(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
中。