<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                ## 前言 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 日志文件 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日志文件](https://box.kancloud.cn/2015-09-24_5603946e65a8d.png "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日志文件和緩沖區對應關系](https://box.kancloud.cn/2015-09-24_5603946e914ab.png "InnoDB 日志文件和緩沖區對應關系") ## 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`中。 ### 插入數據 在插入數據過程中,包含大量的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](http://dev.mysql.com/worklog/task/?id=7142)) 6. 如果不是從上一次checkpoint后第一次修改該表,則根據mtr中log的個數,或標識日志頭最高位為MLOG_SINGLE_REC_FLAG,或附加一個1字節的MLOG_MULTI_REC_END日志。 注意從`prepare_write`函數返回時是持有`log_sys->mutex`鎖的。 至此一條插入操作產生的mtr日志格式有可能如下圖所示: ![mtr日志格式](https://box.kancloud.cn/2015-09-24_56039473c354e.png "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_lsn`(`log_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](https://box.kancloud.cn/2015-09-24_56039474415f0.png "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持久化程度](https://box.kancloud.cn/2015-09-24_56039474848b5.png "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](http://dev.mysql.com/worklog/task/?id=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](https://box.kancloud.cn/2015-09-24_56039474c13c1.png) 在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](https://bugs.mysql.com/bug.php?id=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`。 關于異常重啟的邏輯,由于崩潰恢復涉及到的模塊眾多,邏輯復雜,我們將在下期月報單獨進行描述。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看