天天看點

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版本。

innodb的redo log可以通過參數<code>innodb_log_files_in_group</code>配置成多個檔案,另外一個參數<code>innodb_log_file_size</code>表示每個檔案的大小。是以總的redo log大小為<code>innodb_log_files_in_group * innodb_log_file_size</code>。

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

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

在innodb内部,邏輯上<code>ib_logfile</code>被當成了一個檔案,對應同一個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類型來實作的,但後來不知道為什麼又改成了采用日志檔案的方式,也許是考慮到低版本相容的問題吧。

<code>log_sys</code>是innodb日志系統的中樞及核心對象,控制着日志的拷貝、寫入、checkpoint等核心功能。它同時也是大寫入負載場景下的熱點子產品,是連接配接innodb日志檔案及log buffer的樞紐,對應結構體為<code>log_t</code>。

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

變量名

描述

log_groups

日志組,目前版本僅支援一組日志,對應類型為 <code>log_group_t</code> ,包含了目前日志組的檔案個數、每個檔案的大小、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點的計算方式可以參閱函數<code>log_calc_max_ages</code>,以如下執行個體配置為例:

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

通常的:

當目前未刷髒的最老lsn和目前lsn的距離超過<code>max_modified_age_async</code>(71%)時,且開啟了選項<code>innodb_adaptive_flushing</code>時,page cleaner線程會去嘗試做更多的dirty page flush工作,避免髒頁堆積。

當目前未刷髒的最老lsn和目前lsn的距離超過<code>max_modified_age_sync</code>(76%)時,使用者線程需要去做同步刷髒,這是一個性能下降的臨界點,會極大的影響整體吞吐量和響應時間。

當上次checkpoint的lsn和目前lsn超過<code>max_checkpoint_age</code>(81%),使用者線程需要同步地做一次checkpoint,需要等待checkpoint寫入完成。

當上次checkpoint的lsn和目前lsn的距離超過<code>max_checkpoint_age_async</code>(78%)但小于<code>max_checkpoint_age</code>(81%)時,使用者線程做一次異步checkpoint(背景異步線程執行checkpoint資訊寫入操作),無需等待checkpoint完成。

<code>log_group_t</code>結構體主要成員如下表所示:

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-&gt;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-&gt;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-&gt;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鎖

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與日志檔案和日志緩沖區的關系可用下圖來表示:

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

mini transaction(簡稱mtr)是innodb對實體資料檔案操作的最小事務單元,用于管理對page加鎖、修改、釋放、以及日志送出到公共buffer等工作。一個mtr操作必須是原子的,一個事務可以包含多個mtr。每個mtr完成後需要将本地産生的日志拷貝到公共緩沖區,将修改的髒頁放到flush list上。

mtr事務對應的類為<code>mtr_t</code>, <code>mtr_t::impl</code>中儲存了目前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_state_active

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

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釋放鎖

innodb的redo log都是通過mtr産生的,先寫到mtr的cache中,然後再送出到公共buffer中,本小節以insert一條記錄對page産生的修改為例,闡述一個mtr的典型生命周期。

入口函數:<code>row_ins_clust_index_entry_low</code>

執行如下代碼塊

mtr_start主要包括:

初始化mtr的各個狀态變量

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

mtr狀态設定為active狀态(mtr_state_active)

為鎖管理對象和日志管理對象初始化記憶體(mtr_buf_t),初始化對象連結清單

<code>mtr.set_named_space</code> 是5.7新增的邏輯,将目前修改的表空間對象<code>fil_space_t</code>儲存下來:如果是系統表空間,則指派給<code>m_impl.m_sys_space</code>, 否則指派給<code>m_impl.m_user_space</code>。

tips: 在5.7裡針對臨時表做了優化,直接關閉redo記錄:

mtr.set_log_mode(mtr_log_no_redo)

主要入口函數: <code>btr_cur_search_to_nth_level</code>

不管插入還是更新操作,都是先以樂觀方式進行,是以先加索引s鎖

<code>mtr_s_lock(dict_index_get_lock(index),&amp;mtr)</code>,對應<code>mtr_t::s_lock</code>函數

如果以悲觀方式插入記錄,意味着可能産生索引分裂,在5.7之前會加索引x鎖,而5.7版本則會加sx鎖(但某些情況下也會退化成x鎖)

加x鎖: <code>mtr_x_lock(dict_index_get_lock(index), mtr)</code>,對應<code>mtr_t::x_lock</code>函數

加sx鎖:<code>mtr_sx_lock(dict_index_get_lock(index),mtr)</code>,對應<code>mtr_t::sx_lock</code>函數

對應到内部實作,實際上就是加上對應的鎖對象,然後将該鎖的指針和類型建構的<code>mtr_memo_slot_t</code>對象插入到<code>mtr.m_impl.m_memo</code>中。

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

如果對page加的是mtr_memo_page_x_fix或者mtr_memo_page_sx_fix鎖,并且目前block是clean的,則将<code>m_impl.m_made_dirty</code>設定成true,表示即将修改一個幹淨的page。

如果加鎖類型為mtr_memo_buf_fix,實際上是不加鎖對象的,但需要判斷臨時表的場景,臨時表page的修改不加latch,但需要将<code>m_impl.m_made_dirty</code>設定為true(根據block的成員<code>m_impl.m_made_dirty</code>來判斷),這也是5.7對innodb臨時表場景的一種優化。

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

在插入資料過程中,包含大量的redo寫cache邏輯,例如更新二級索引頁的max trx id、寫undo log産生的redo(嵌套另外一個mtr)、修改資料頁産生的日志。這裡我們隻讨論修改資料頁産生的日志,進入函數<code>page_cur_insert_rec_write_log</code>:

step 1: 調用函數<code>mlog_open_and_write_index</code>記錄索引相關資訊

調用<code>mlog_open</code>,配置設定足夠日志寫入的記憶體位址,并傳回記憶體指針

初始化日志記錄:<code>mlog_write_initial_log_record_fast</code>

寫入 <code>|類型=mlog\_comp\_rec\_insert,1位元組|space id | page no|</code> space id 和page no采用一種壓縮寫入的方式(<code>mach_write_compressed</code>),根據數字的具體大小,選擇從1到4個位元組記錄整數,節約redo空間,對應的解壓函數為<code>mach_read_compressed</code>

寫入目前索引列個數,占兩個位元組

寫入行記錄上決定唯一性的列的個數,占兩個位元組(<code>dict_index_get_n_unique_in_tree</code>)

對于聚集索引,就是pk上的列數;對于二級索引,就是二級索引列+pk列個數

寫入每個列的長度資訊,每個列占兩個位元組

如果這是 varchar 列且最大長度超過255位元組, len = 0x7fff;如果該列非空,len |= 0x8000;其他情況直接寫入列長度。

step 2: 寫入記錄在page上的偏移量,占兩個位元組

<code>mach_write_to_2(log_ptr, page_offset(cursor_rec));</code>

step 3: 寫入記錄其它相關資訊 (rec size, extra size, info bit,關于innodb的資料檔案實體描述,我們以後再介紹,本文不展開)

step 4: 将插入的記錄拷貝到redo檔案,同時關閉mlog

通過上述流程,我們寫入了一個類型為mlog_comp_rec_insert的日志記錄。由于特定類型的記錄都基于約定的格式,在崩潰恢複時也可以基于這樣的約定解析出日志。

這裡隻舉了一個非常簡單的例子,該mtr中隻包含一條redo記錄。實際上mtr遍布整個innodb的邏輯,但隻要遵循相同的寫入和讀取約定,并對寫入的單元(page)加上互斥鎖,就能從崩潰恢複。

更多的redo log記錄類型參見<code>enum mlog_id_t</code>。

在這個過程中産生的redo log都記錄在<code>mtr.m_impl.m_log</code>中,隻有顯式送出mtr時,才會寫到公共buffer中。

當送出一個mini transaction時,需要将對資料的更改記錄送出到公共buffer中,并将對應的髒頁加到flush list上。

入口函數為<code>mtr_t::commit()</code>,當修改産生髒頁或者日志記錄時,調用<code>mtr_t::command::execute</code>,執行過程如下:

step 1: <code>mtr_t::command::prepare_write()</code>

若目前mtr的模式為mtr_log_no_redo 或者mtr_log_none,則擷取<code>log_sys-&gt;mutex</code>,從函數傳回

若目前要寫入的redo log記錄的大小超過log buffer的二分之一,則去擴大log buffer,大小約為原來的兩倍。

持有<code>log_sys-&gt;mutex</code>

調用函數<code>log_margin_checkpoint_age</code>檢查本次寫入:

如果本次産生的redo log size的兩倍超過redo log檔案capacity,則列印一條錯誤資訊;若本次寫入可能覆寫檢查點,還需要去強制做一次同步chekpoint

檢查本次修改的表空間是否是上次checkpoint後第一次修改,調用函數(<code>fil_names_write_if_was_clean</code>)

如果space-&gt;max_lsn = 0,表示自上次checkpoint後第一次修改該表空間:

修改<code>space-&gt;max_lsn</code>為目前<code>log_sys-&gt;lsn</code>;

調用<code>fil_names_dirty_and_write</code>将該tablespace加入到<code>fil_system-&gt;named_spaces</code>連結清單上;

調用<code>fil_names_write</code>寫入一條類型為mlog_file_name的日志,寫入類型、spaceid, page no(0)、檔案路徑長度、以及檔案路徑名。

在mtr日志末尾追加一個位元組的mlog_multi_rec_end類型的标記,表示這是多個日志類型的mtr。

tips:在5.6及之前的版本中,每次crash recovery時都需要打開所有的ibd檔案,如果表的數量非常多時,會非常影響崩潰恢複性能,是以從5.7版本開始,每次checkpoint後,第一次修改的檔案名被記錄到redo log中,這樣在重新開機從檢查點恢複時,就隻打開那些需要打開的檔案即可(wl#7142)

如果不是從上一次checkpoint後第一次修改該表,則根據mtr中log的個數,或辨別日志頭最高位為mlog_single_rec_flag,或附加一個1位元組的mlog_multi_rec_end日志。

注意從<code>prepare_write</code>函數傳回時是持有<code>log_sys-&gt;mutex</code>鎖的。

至此一條插入操作産生的mtr日志格式有可能如下圖所示:

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

step 2: mtr_t::command::finish_write

将日志從mtr中拷貝到公共log buffer。這裡有兩種方式

如果mtr中的日志較小,則調用函數<code>log_reserve_and_write_fast</code>,嘗試将日志拷貝到log buffer最近的一個block。如果空間不足,走邏輯b),否則直接拷貝

檢查是否有足夠的空閑空間後,傳回目前的lsn指派給<code>m_start_lsn</code>(<code>log_reserve_and_open(len)</code>),随後将日志記錄寫入到log buffer中。

在完成将redo 拷貝到log buffer後,需要調用<code>log_close</code>, 如果最後一個block未寫滿,則設定該block頭部的log_block_first_rec_group資訊;

滿足如下情況時,設定<code>log_sys-&gt;check_flush_or_checkpoint</code>為true:

當<code>check_flush_or_checkpoint</code>被設定時,使用者線程在每次修改資料前調用<code>log_free_check</code>時,會根據該标記決定是否刷redo日志或者髒頁。

注意log buffer遵循一定的格式,它以512位元組對齊,和redo log檔案的block size必須完全比對。由于以固定block size組織結構,是以一個block中可能包含多個mtr送出的記錄,也可能一個mtr的日志占用多個block。如下圖所示:

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

step 3:如果本次修改産生了髒頁,擷取<code>log_sys-&gt;log_flush_order_mutex</code>,随後釋放<code>log_sys-&gt;mutex</code>。

step 4. 将目前mtr修改的髒頁加入到flush list上,髒頁上記錄的lsn為目前mtr寫入的結束點lsn。基于上述加鎖邏輯,能夠保證flush list上的髒頁總是以lsn排序。

step 5. 釋放<code>log_sys-&gt;log_flush_order_mutex</code>鎖

step 6. 釋放目前mtr持有的鎖(主要是page latch)及配置設定的記憶體,mtr完成送出。

有幾種場景可能會觸發redo log寫檔案:

redo log buffer空間不足時

事務送出

背景線程

做checkpoint

執行個體shutdown時

binlog切換時

我們所熟悉的參數<code>innodb_flush_log_at_trx_commit</code> 作用于事務送出時,這也是最常見的場景:

當設定該值為1時,每次事務送出都要做一次fsync,這是最安全的配置,即使當機也不會丢失事務;

當設定為2時,則在事務送出時隻做write操作,隻保證寫到系統的page cache,是以執行個體crash不會丢失事務,但當機則可能丢失事務;

當設定為0時,事務送出不會觸發redo寫操作,而是留給背景線程每秒一次的刷盤操作,是以執行個體crash将最多丢失1秒鐘内的事務。

下圖表示了不同配置值的持久化程度:

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

顯然對性能的影響是随着持久化程度的增加而增加的。通常我們建議在日常場景将該值設定為1,但在系統高峰期臨時修改成2以應對大負載。

由于各個事務可以交叉的将事務日志拷貝到log buffer中,因而一次事務送出觸發的寫redo到檔案,可能隐式的幫别的線程“順便”也寫了redo log,進而達到group commit的效果。

寫redo log的入口函數為<code>log_write_up_to</code>,該函數的邏輯比較簡單,這裡不較長的描述,但有幾點說明下。

首先是在該代碼邏輯上,相比5.6及之前的版本,5.7在沒有更改日志寫主要架構的基礎上重寫了<code>log_write_up_to</code>,讓其代碼更加可讀,同時消除一次多餘的擷取<code>log_sys-&gt;mutex</code>,具體的(wl#7050):

早期版本的innodb支援将redo寫到多個group中,但現在隻支援一個group,是以移除相關的變量,消除<code>log_write_up_to</code>的第二個傳參;

write redo操作一直持有<code>log_sys-&gt;mutex</code>, 所有随後的write請求,不再進入condition wait, 而是通過log_sys-&gt;mutex序列化;

之前的邏輯中,在write一次redo後,需要釋放<code>log_sys-&gt;mutex</code>,再重新擷取,更新相關變量,新的邏輯消除了第二次擷取 <code>log_sys-&gt;mutex</code>;

write請求的寫redo無需等待fsync,這意味着寫redo log檔案和fsync檔案可以同時進行。

理論上該改動可以幫助優化<code>innodb_flush_log_at_trx_commit=2</code>時的性能。

上面已經介紹過,innodb以512位元組一個block的方式對齊寫入ib_logfile檔案,但現代檔案系統一般以4096位元組為一個block機關。如果即将寫入的日志檔案塊不在os cache時,就需要将對應的4096位元組的block讀入記憶體,修改其中的512位元組,然後再把該block寫回磁盤。

為了解決這個問題,mysql 5.7引入了一個新參數:<code>innodb_log_write_ahead_size</code>。當目前寫入檔案的偏移量不能整除該值時,則補0,多寫一部分資料。這樣當寫入的資料是以磁盤block size對齊時,就可以直接write磁盤,而無需read-modify-write這三步了。

注意<code>innodb_log_write_ahead_size</code>的預設值為8196,你可能需要根據你的系統配置來修改該值,以獲得更好的效果。

在寫入redo log到檔案之前,redo log的每一個block都需要加上checksum校驗位,以防止apply了損壞的redo log。

然而在5.7.7版本之前版本,都是使用的innodb的預設checksum算法(稱為innodb checksum),這種算法的效率較低。是以在mysql5.7.8以及percona server 5.6版本都支援使用crc32的checksum算法,該算法可以引用硬體特性,因而具有非常高的效率。

在我的sysbench測試中,使用<code>update_non_index</code>,128個并發下tps可以從55000上升到60000(非雙1),效果還是非常明顯的。

innodb的redo log采用覆寫循環寫的方式,而不是擁有無限的redo空間;即使擁有理論上極大的redo log空間,為了從崩潰中快速恢複,及時做checkpoint也是非常有必要的。

innodb的master線程大約每隔10秒會做一次redo checkpoint,但不會去preflush髒頁來推進checkpoint點。

通常普通的低壓力負載下,page cleaner線程的刷髒速度足以保證可作為檢查點的lsn被及時的推進。但如果系統負載很高時,redo log推進速度過快,而page cleaner來不及刷髒,這時候就會出現使用者線程陷入同步刷髒并做同checkpoint的境地,這種政策的目的是為了保證redo log能夠安全的寫入檔案而不會覆寫最近的檢查點。

redo checkpoint的入口函數為<code>log_checkpoint</code>,其執行流程如下:

step1. 持有log_sys-&gt;mutex鎖,并擷取buffer pool的flush list連結清單尾的block上的lsn,這個lsn是buffer pool中未寫入資料檔案的最老lsn,在該lsn之前的資料都保證已經寫入了磁盤。

step 2. 調用函數fil_names_clear

如果<code>log_sys-&gt;append_on_checkpoint</code>被設定,表示目前有會話正處于ddl的commit階段,但還沒有完成,向redo log buffer中追加一個新的redo log記錄

該邏輯由commita5ecc38f44abb66aa2024c70e37d1f4aa4c8ace9引入,用于解決ddl過程中crash的問題

掃描<code>fil_system-&gt;named_spaces</code>上的<code>fil_space_t</code>對象,如果表空間<code>fil_space_t-&gt;max_lsn</code>小于目前準備做checkpoint的lsn,則從連結清單上移除并将max_lsn重置為0。同時為每個被修改的表空間建構mlog_file_name類型的redo記錄。(這一步未來可能會移除,隻要跟蹤第一次修改該表空間的min_lsn,并且min_lsn大于目前checkpoint的lsn,就可以忽略調用<code>fil_names_write</code>)

寫入一個mlog_checkpoint類型的checkpoint redo記錄,并記入目前的checkpoint lsn

step3 . fsync redo log到目前的lsn

step4. 寫入checkpoint資訊

函數:<code>log_write_checkpoint_info --&gt; log_group_checkpoint</code>

checkpoint資訊被寫入到了第一個iblogfile的頭部,但寫入的檔案偏移位置比較有意思,當<code>log_sys-&gt;next_checkpoint_no</code>為奇數時,寫入到log_checkpoint_2(3 *512位元組)位置,為偶數時,寫入到log_checkpoint_1(512位元組)位置。

大緻結構如下圖所示:

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

在crash recover重新開機時,會讀取記錄在checkpoint中的lsn資訊,然後從該lsn開始掃描redo日志。

checkpoint操作由異步io線程執行寫入操作,當完成寫入後,會調用函數<code>log_io_complete</code>執行如下操作:

fsync 被修改的redo log檔案

更新相關變量:

釋放log_sys-&gt;checkpoint_lock鎖

然而在5.7之前的版本中,我們并沒有根據即将寫入的資料大小來預測目前是否需要做checkpoint,而是在寫之前檢測,保證redo log檔案中有"足夠安全"的空間(而非絕對安全)。假定我們的ib_logfile檔案很小,如果我們更新一個非常大的blob字段,就有可能覆寫掉未checkpoint的redo log, 大神jeremy cole 在buglist上提了一個bug#69477。

為了解決該問題,在mysql 5.6.22版本開始,對blob列做了限制: 當redo log的大小超過 (<code>innodb_log_file_size *innodb_log_files_in_group</code>)的十分之一時,就會給應用報錯,然而這可能會帶來不相容問題,使用者會發現,早期版本用的好好的sql,在最新版本的5.6裡居然跑不動了。

在5.7.5及之後版本,則沒有5.6的限制,其核心思路是每操作4個外部存儲頁,就檢查一次redo log是否足夠用,如果不夠,就會推進checkpoint的lsn。當然具體的實作比較複雜,感興趣的參考如下comit:f88a5151b18d24303746138a199db910fbb3d071

除了普通的redo log日志外,innodb還增加了一種檔案日志類型,即通過建立特定檔案,賦予特定的檔案名來标示某種操作。目前有兩種類型:undo table space truncate操作及使用者表空間truncate操作。通過檔案日志可以保證這些操作的原子性。

我們知道undo log是mvcc多版本控制的核心子產品,一直以來undo log都存儲在ibdata系統表空間中,而從5.6開始,使用者可以把undo log存儲到獨立的tablespace中,并拆分成多個undo log檔案,但無法縮小檔案的大小。而長時間未送出事務導緻大量undo空間的浪費的例子,在我們的生産場景也不是一次兩次了。

5.7版本的undo log的truncate操作是基于獨立undo 表空間來實作的。在purge線程標明需要清理的undo tablespace後,開始做truncate操作之前,會先建立一個命名為<code>undo_space_id_trunc.log</code>的檔案,然後将undo tablespace truncate 到10m大小,在完成truncate後删除日志檔案。

如果在truncate過程中執行個體崩潰重新開機,若發現該檔案存在,則認為truncate操作沒有完成,需要重做一遍。注意這種檔案操作是無法復原的。

類似的,在5.7版本裡,也是通過日志檔案來保證使用者表空間truncate操作的原子性。在做實際的檔案操作前,建立一個命名為<code>ib_space-id_table-id_trunc.log</code>的檔案。在完成操作後删除。

同樣的,在崩潰重新開機時,如果檢查到該檔案存在,需要确認是否重做。

執行個體關閉分為兩種,一種是正常shutdown(非fast shutdown),執行個體重新開機時無需apply日志,另外一種是異常shutdown,包括執行個體crash以及fast shutdown。

當正常shutdown執行個體時,會将所有的髒頁都刷到磁盤,并做一次完全同步的checkpoint;同時将最後的lsn寫到系統表ibdata的第一個page中(函數<code>fil_write_flushed_lsn</code>)。在重新開機時,可以根據該lsn來判斷這是不是一次正常的shutdown,如果不是就需要去做崩潰恢複邏輯。

參閱函數<code>logs_empty_and_mark_files_at_shutdown</code>。