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

前言

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

和大多數關係型數據庫同樣,InnoDB記錄了對數據文件的物理更改,並保證老是日誌先行,也就是所謂的WAL,即在持久化數據文件前,保證以前的redo日誌已經寫到磁盤。mysql

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

爲了管理髒頁,在 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的一些改進點。sql

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

MySQL InnoDB表--BTree基本數據結構 http://www.linuxidc.com/Linux/2015-12/126151.htm安全

在MySQL的InnoDB存儲引擎中count(*)函數的優化 http://www.linuxidc.com/Linux/2015-09/123494.htm數據結構

MySQL InnoDB存儲引擎鎖機制實驗 http://www.linuxidc.com/Linux/2013-04/82240.htm架構

InnoDB存儲引擎的啓動、關閉與恢復 http://www.linuxidc.com/Linux/2013-06/86415.htm併發

MySQL InnoDB獨立表空間的配置 http://www.linuxidc.com/Linux/2013-06/85760.htm

MySQL Server 層和 InnoDB 引擎層 體系結構圖 http://www.linuxidc.com/Linux/2013-05/84406.htm

InnoDB 死鎖案例解析 http://www.linuxidc.com/Linux/2013-10/91713.htm

MySQL Innodb獨立表空間的配置 http://www.linuxidc.com/Linux/2013-06/85760.htm

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日誌文件

在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=4
innodb_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與日誌文件和日誌緩衝區的關係可用下圖來表示:

InnoDB日誌文件和緩衝區對應關係

Mini transaction

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

mtr事務對應的類爲mtr_tmtr_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中。

插入數據

在插入數據過程當中,包含大量的redo寫cache邏輯,例如更新二級索引頁的max trx id、寫undo log產生的redo(嵌套另一個mtr)、修改數據頁產生的日誌。這裏咱們只討論修改數據頁產生的日誌,進入函數page_cur_insert_rec_write_log:

Step 1: 調用函數mlog_open_and_write_index記錄索引相關信息

  1. 調用mlog_open,分配足夠日誌寫入的內存地址,並返回內存指針
  2. 初始化日誌記錄:mlog_write_initial_log_record_fast
    寫入 |類型=MLOG\_COMP\_REC\_INSERT,1字節|space id | page no| space id 和page no採用一種壓縮寫入的方式(mach_write_compressed),根據數字的具體大小,選擇從1到4個字節記錄整數,節約redo空間,對應的解壓函數爲mach_read_compressed
  3. 寫入當前索引列個數,佔兩個字節
  4. 寫入行記錄上決定惟一性的列的個數,佔兩個字節(dict_index_get_n_unique_in_tree)
    對於彙集索引,就是PK上的列數;對於二級索引,就是二級索引列+PK列個數
  5. 寫入每一個列的長度信息,每一個列佔兩個字節
    若是這是 varchar 列且最大長度超過255字節, len = 0x7fff;若是該列非空,len |= 0x8000;其餘狀況直接寫入列長度。

Step 2: 寫入記錄在page上的偏移量,佔兩個字節

mach_write_to_2(log_ptr, page_offset(cursor_rec));

Step 3: 寫入記錄其它相關信息 (rec size, extra size, info bit,關於InnoDB的數據文件物理描述,咱們之後再介紹,本文不展開)

Step 4: 將插入的記錄拷貝到redo文件,同時關閉mlog

memcpy(log_ptr, ins_ptr, rec_size);
mlog_close(mtr, log_ptr + rec_size);

經過上述流程,咱們寫入了一個類型爲MLOG_COMP_REC_INSERT的日誌記錄。因爲特定類型的記錄都基於約定的格式,在崩潰恢復時也能夠基於這樣的約定解析出日誌。

這裏只舉了一個很是簡單的例子,該mtr中只包含一條redo記錄。實際上mtr遍及整個InnoDB的邏輯,但只要遵循相同的寫入和讀取約定,並對寫入的單元(page)加上互斥鎖,就能從崩潰恢復。

更多的redo log記錄類型參見enum mlog_id_t

在這個過程當中產生的redo log都記錄在mtr.m_impl.m_log中,只有顯式提交mtr時,纔會寫到公共buffer中。

提交mtr log

當提交一個mini transaction時,須要將對數據的更改記錄提交到公共buffer中,並將對應的髒頁加到flush list上。

入口函數爲mtr_t::commit(),當修改產生髒頁或者日誌記錄時,調用mtr_t::Command::execute,執行過程以下:

Step 1: mtr_t::Command::prepare_write()

  1. 若當前mtr的模式爲MTR_LOG_NO_REDO 或者MTR_LOG_NONE,則獲取log_sys->mutex,從函數返回
  2. 若當前要寫入的redo log記錄的大小超過log buffer的二分之一,則去擴大log buffer,大小約爲原來的兩倍。
  3. 持有log_sys->mutex
  4. 調用函數log_margin_checkpoint_age檢查本次寫入:
    若是本次產生的redo log size的兩倍超過redo log文件capacity,則打印一條錯誤信息;若本次寫入可能覆蓋檢查點,還須要去強制作一次同步chekpoint
  5. 檢查本次修改的表空間是不是上次checkpoint後第一次修改,調用函數(fil_names_write_if_was_clean
    若是space->max_lsn = 0,表示自上次checkpoint後第一次修改該表空間:
    a. 修改space->max_lsn爲當前log_sys->lsn
    b. 調用fil_names_dirty_and_write將該tablespace加入到fil_system->named_spaces鏈表上;
    c. 調用fil_names_write寫入一條類型爲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

  6. 若是不是從上一次checkpoint後第一次修改該表,則根據mtr中log的個數,或標識日誌頭最高位爲MLOG_SINGLE_REC_FLAG,或附加一個1字節的MLOG_MULTI_REC_END日誌。

注意從prepare_write函數返回時是持有log_sys->mutex鎖的。

至此一條插入操做產生的mtr日誌格式有可能以下圖所示:

mtr日誌格式

Step 2: mtr_t::Command::finish_write

將日誌從mtr中拷貝到公共log buffer。這裏有兩種方式

  1. 若是mtr中的日誌較小,則調用函數log_reserve_and_write_fast,嘗試將日誌拷貝到log buffer最近的一個block。若是空間不足,走邏輯b),不然直接拷貝
  2. 檢查是否有足夠的空閒空間後,返回當前的lsn賦值給m_start_lsnlog_reserve_and_open(len)),隨後將日誌記錄寫入到log buffer中。

    m_start_lsn = log_reserve_and_open(len);
      mtr_write_log_t write_log;
      m_impl->m_log.for_each_block(write_log);
  3. 在完成將redo 拷貝到log buffer後,須要調用log_close, 若是最後一個block未寫滿,則設置該block頭部的LOG_BLOCK_FIRST_REC_GROUP信息;
    知足以下狀況時,設置log_sys->check_flush_or_checkpoint爲true:

    • 當前寫入buffer的位置超過log buffer的一半
    • bp中最老lsn和當前lsn的距離超過log_sys->max_modified_age_sync
    • 當前未checkpoint的lsn age超過log_sys->max_checkpoint_age_async
    • 當前bp中最老lsn爲0 (沒有髒頁)

    check_flush_or_checkpoint被設置時,用戶線程在每次修改數據前調用log_free_check時,會根據該標記決定是否刷redo日誌或者髒頁。

注意log buffer遵循必定的格式,它以512字節對齊,和redo log文件的block size必須徹底匹配。因爲以固定block size組織結構,所以一個block中可能包含多個mtr提交的記錄,也可能一個mtr的日誌佔用多個block。以下圖所示:

redo log buffer

Step 3:若是本次修改產生了髒頁,獲取log_sys->log_flush_order_mutex,隨後釋放log_sys->mutex

Step 4. 將當前Mtr修改的髒頁加入到flush list上,髒頁上記錄的lsn爲當前mtr寫入的結束點lsn。基於上述加鎖邏輯,可以保證flush list上的髒頁老是以LSN排序。

Step 5. 釋放log_sys->log_flush_order_mutex

Step 6. 釋放當前mtr持有的鎖(主要是page latch)及分配的內存,mtr完成提交。

Redo 寫盤操做

有幾種場景可能會觸發redo log寫文件:

  1. Redo log buffer空間不足時
  2. 事務提交
  3. 後臺線程
  4. 作checkpoint
  5. 實例shutdown時
  6. binlog切換時

咱們所熟悉的參數innodb_flush_log_at_trx_commit 做用於事務提交時,這也是最多見的場景:

  • 當設置該值爲1時,每次事務提交都要作一次fsync,這是最安全的配置,即便宕機也不會丟失事務;
  • 當設置爲2時,則在事務提交時只作write操做,只保證寫到系統的page cache,所以實例crash不會丟失事務,但宕機則可能丟失事務;
  • 當設置爲0時,事務提交不會觸發redo寫操做,而是留給後臺線程每秒一次的刷盤操做,所以實例crash將最多丟失1秒鐘內的事務。

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

redo持久化程度

顯然對性能的影響是隨着持久化程度的增長而增長的。一般咱們建議在平常場景將該值設置爲1,但在系統高峯期臨時修改爲2以應對大負載。

因爲各個事務能夠交叉的將事務日誌拷貝到log buffer中,於是一次事務提交觸發的寫redo到文件,可能隱式的幫別的線程「順便」也寫了redo log,從而達到group commit的效果。

寫redo log的入口函數爲log_write_up_to,該函數的邏輯比較簡單,這裏不詳細描述,但有幾點說明下。

log_write_up_to邏輯重構

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

  • 早期版本的innodb支持將redo寫到多個group中,但如今只支持一個group,所以移除相關的變量,消除log_write_up_to的第二個傳參;
  • write redo操做一直持有log_sys->mutex, 全部隨後的write請求,再也不進入condition wait, 而是經過log_sys->mutex序列化;
  • 以前的邏輯中,在write一次redo後,須要釋放log_sys->mutex,再從新獲取,更新相關變量,新的邏輯消除了第二次獲取 log_sys->mutex
  • write請求的寫redo無需等待fsync,這意味着寫redo log文件和fsync文件能夠同時進行。

理論上該改動能夠幫助優化innodb_flush_log_at_trx_commit=2時的性能。

log write ahead

上面已經介紹過,InnoDB以512字節一個block的方式對齊寫入ib_logfile文件,但現代文件系統通常以4096字節爲一個block單位。若是即將寫入的日誌文件塊不在OS Cache時,就須要將對應的4096字節的block讀入內存,修改其中的512字節,而後再把該block寫回磁盤。

爲了解決這個問題,MySQL 5.7引入了一個新參數:innodb_log_write_ahead_size。噹噹前寫入文件的偏移量不能整除該值時,則補0,多寫一部分數據。這樣當寫入的數據是以磁盤block size對齊時,就能夠直接write磁盤,而無需read-modify-write這三步了。

注意innodb_log_write_ahead_size的默認值爲8196,你可能須要根據你的系統配置來修改該值,以得到更好的效果。

Innodb redo log checksum

在寫入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測試中,使用update_non_index,128個併發下TPS能夠從55000上升到60000(非雙1),效果仍是很是明顯的。

Redo checkpoint

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的入口函數爲log_checkpoint,其執行流程以下:

Step1. 持有log_sys->mutex鎖,並獲取buffer pool的flush list鏈表尾的block上的lsn,這個lsn是buffer pool中未寫入數據文件的最老lsn,在該lsn以前的數據都保證已經寫入了磁盤。

Step 2. 調用函數fil_names_clear

  1. 若是log_sys->append_on_checkpoint被設置,表示當前有會話正處於DDL的commit階段,但尚未完成,向redo log buffer中追加一個新的redo log記錄
    該邏輯由commita5ecc38f44abb66aa2024c70e37d1f4aa4c8ace9引入,用於解決DDL過程當中crash的問題

  2. 掃描fil_system->named_spaces上的fil_space_t對象,若是表空間fil_space_t->max_lsn小於當前準備作checkpoint的Lsn,則從鏈表上移除並將max_lsn重置爲0。同時爲每一個被修改的表空間構建MLOG_FILE_NAME類型的redo記錄。(這一步將來可能會移除,只要跟蹤第一次修改該表空間的min_lsn,而且min_lsn大於當前checkpoint的lsn,就能夠忽略調用fil_names_write

  3. 寫入一個MLOG_CHECKPOINT類型的CHECKPOINT REDO記錄,並記入當前的checkpoint LSN

Step3 . fsync redo log到當前的lsn

Step4. 寫入checkpoint信息

函數:log_write_checkpoint_info --> log_group_checkpoint

checkpoint信息被寫入到了第一個iblogfile的頭部,但寫入的文件偏移位置比較有意思,當log_sys->next_checkpoint_no爲奇數時,寫入到LOG_CHECKPOINT_2(3 *512字節)位置,爲偶數時,寫入到LOG_CHECKPOINT_1(512字節)位置。

大體結構以下圖所示:

checkpoint

在crash recover重啓時,會讀取記錄在checkpoint中的lsn信息,而後從該lsn開始掃描redo日誌。

Checkpoint操做由異步IO線程執行寫入操做,當完成寫入後,會調用函數log_io_complete執行以下操做:

  1. fsync 被修改的redo log文件
  2. 更新相關變量:

    log_sys->next_checkpoint_no++
     log_sys->last_checkpoint_lsn = log_sys->next_checkpoint_lsn
  3. 釋放log_sys->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的大小超過 (innodb_log_file_size *innodb_log_files_in_group)的十分之一時,就會給應用報錯,然而這可能會帶來不兼容問題,用戶會發現,早期版本用的好好的SQL,在最新版本的5.6裏竟然跑不動了。

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

文件日誌

除了普通的redo log日誌外,InnoDB還增長了一種文件日誌類型,即經過建立特定文件,賦予特定的文件名來標示某種操做。目前有兩種類型:undo table space truncate操做及用戶表空間truncate操做。經過文件日誌能夠保證這些操做的原子性。

Undo tablespace 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操做以前,會先建立一個命名爲undo_space_id_trunc.log的文件,而後將undo tablespace truncate 到10M大小,在完成truncate後刪除日誌文件。

若是在truncate過程當中實例崩潰重啓,若發現該文件存在,則認爲truncate操做沒有完成,須要重作一遍。注意這種文件操做是沒法回滾的。

User tablespace truncate

相似的,在5.7版本里,也是經過日誌文件來保證用戶表空間truncate操做的原子性。在作實際的文件操做前,建立一個命名爲ib_space-id_table-id_trunc.log的文件。在完成操做後刪除。

一樣的,在崩潰重啓時,若是檢查到該文件存在,須要確認是否重作。

InnoDB shutdown

實例關閉分爲兩種,一種是正常shutdown(非fast shutdown),實例重啓時無需apply日誌,另一種是異常shutdown,包括實例crash以及fast shutdown。

當正常shutdown實例時,會將全部的髒頁都刷到磁盤,並作一次徹底同步的checkpoint;同時將最後的lsn寫到系統表ibdata的第一個page中(函數fil_write_flushed_lsn)。在重啓時,能夠根據該lsn來判斷這是否是一次正常的shutdown,若是不是就須要去作崩潰恢復邏輯。

參閱函數logs_empty_and_mark_files_at_shutdown

關於異常重啓的邏輯,因爲崩潰恢復涉及到的模塊衆多,邏輯複雜,咱們將在下期月報單獨進行描述。

MySQL InnoDB表--BTree基本數據結構 http://www.linuxidc.com/Linux/2015-12/126151.htm

在MySQL的InnoDB存儲引擎中count(*)函數的優化 http://www.linuxidc.com/Linux/2015-09/123494.htm

MySQL InnoDB存儲引擎鎖機制實驗 http://www.linuxidc.com/Linux/2013-04/82240.htm

InnoDB存儲引擎的啓動、關閉與恢復 http://www.linuxidc.com/Linux/2013-06/86415.htm

MySQL InnoDB獨立表空間的配置 http://www.linuxidc.com/Linux/2013-06/85760.htm

MySQL Server 層和 InnoDB 引擎層 體系結構圖 http://www.linuxidc.com/Linux/2013-05/84406.htm

InnoDB 死鎖案例解析 http://www.linuxidc.com/Linux/2013-10/91713.htm

MySQL Innodb獨立表空間的配置 http://www.linuxidc.com/Linux/2013-06/85760.htm

本文永久更新連接地址http://www.linuxidc.com/Linux/2016-03/128830.htm

相關文章
相關標籤/搜索