<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>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                在前面兩期月報中,我們詳細介紹了 InnoDB redo log 和 undo log 的相關知識,本文將介紹 InnoDB 在崩潰恢復時的主要流程。 本文代碼分析基于 MySQL 5.7.7-RC 版本,函數入口為?`innobase_start_or_create_for_mysql`,這是一個非常冗長的函數,本文只涉及和崩潰恢復相關的代碼。 在閱讀本文前,強烈建議翻閱我們之前的兩期月報: 1.?[MySQL · 引擎特性 · InnoDB undo log 漫游](http://mysql.taobao.org/monthly/2015/04/01/) 2.?[MySQL · 引擎特性 · InnoDB redo log漫游](http://mysql.taobao.org/monthly/2015/05/01/) ## 初始化崩潰恢復 首先初始化崩潰恢復所需要的內存對象: ~~~ recv_sys_create(); recv_sys_init(buf_pool_get_curr_size()); ~~~ 當InnoDB正常shutdown,在flush redo log 和臟頁后,會做一次完全同步的checkpoint,并將checkpoint的LSN寫到ibdata的第一個page中(`fil_write_flushed_lsn`)。 在重啟實例時,會打開系統表空間ibdata,并讀取存儲在其中的LSN: ~~~ err = srv_sys_space.open_or_create( false, &sum_of_new_sizes, &flushed_lsn); ~~~ 上述調用將從ibdata中讀取的LSN存儲到變量flushed_lsn中,表示上次shutdown時的checkpoint點,在后面做崩潰恢復時會用到。另外這里也會將double write buffer內存儲的page載入到內存中(`buf_dblwr_init_or_load_pages`),如果ibdata的第一個page損壞了,就從dblwr中恢復出來。 > Tips:注意在MySQL 5.6.16之前的版本中,如果InnoDB的表空間第一個page損壞了,就認為無法確定這個表空間的space id,也就無法決定使用dblwr中的哪個page來進行恢復,InnoDB將崩潰恢復失敗(bug#70087), > 由于每個數據頁上都存儲著表空間id,因此后面將這里的邏輯修改成往后多讀幾個page,并嘗試不同的page size,直到找到一個完好的數據頁, (參考函數`Datafile::find_space_id()`)。因此為了能安全的使用double write buffer保護數據,建議使用5.6.16及之后的MySQL版本。 ## 恢復truncate操作 為了保證對 undo log 獨立表空間和用戶獨立表空間進行 truncate 操作的原子性,InnoDB 采用文件日志的方式為每個 truncate 操作創建一個獨特的文件,如果在重啟時這個文件存在,說明上次 truncate 操作還沒完成實例就崩潰了,在重啟時,我們需要繼續完成truncate操作。 這一塊的崩潰恢復是獨立于redo log系統之外的。 對于 undo log 表空間恢復,在初始化 undo 子系統時完成: ~~~ err = srv_undo_tablespaces_init( create_new_db, srv_undo_tablespaces, &srv_undo_tablespaces_open); ~~~ 對于用戶表空間,掃描數據目錄,找到 truncate 日志文件:如果文件中沒有任何數據,表示truncate還沒開始;如果文件中已經寫了一個MAGIC NUM,表示truncate操作已經完成了;這兩種情況都不需要處理。 ~~~ err = TruncateLogParser::scan_and_parse(srv_log_group_home_dir); ~~~ 但對用戶表空間truncate操作的恢復是redo log apply完成后才進行的,這主要是因為恢復truncate可能涉及到系統表的更新操作(例如重建索引),需要在redo apply完成后才能進行。 ## 進入redo崩潰恢復開始邏輯 入口函數: `c err = recv_recovery_from_checkpoint_start(flushed_lsn);` 傳遞的參數flushed_lsn即為從ibdata第一個page讀取的LSN,主要包含以下幾步: Step 1: 為每個buffer pool instance創建一棵紅黑樹,指向`buffer_pool_t::flush_rbt`,主要用于加速插入flush list (`buf_flush_init_flush_rbt`); Step 2: 讀取存儲在第一個redo log文件頭的CHECKPOINT LSN,并根據該LSN定位到redo日志文件中對應的位置,從該checkpoint點開始掃描。 在這里會調用三次`recv_group_scan_log_recs`掃描redo log文件: 1\. 第一次的目的是找到MLOG_CHECKPOINT日志 MLOG_CHECKPOINT 日志中記錄了CHECKPOINT LSN,當該日志中記錄的LSN和日志頭中記錄的CHECKPOINT LSN相同時,表示找到了符合的MLOG_CHECKPOINT LSN,將掃描到的LSN號記錄到?`recv_sys->mlog_checkpoint_lsn`?中。(在5.6版本里沒有這一次掃描) MLOG_CHECKPOINT在[WL#7142](http://dev.mysql.com/worklog/task/?id=7142)中被引入,其目的是為了簡化 InnoDB 崩潰恢復的邏輯,根據WL#7142的描述,包含幾點改進: 1. 避免崩潰恢復時讀取每個ibd的第一個page來確認其space id; 2. 無需檢查$datadir/*.isl,新的日志類型記錄了文件全路徑,并消除了isl文件和實際ibd目錄的不一致可能帶來的問題; 3. 自動忽略那些還沒有導入到InnoDB的ibd文件(例如在執行IMPORT TABLESPACE時crash); 4. 引入了新的日志類型MLOG_FILE_DELETE來跟蹤ibd文件的刪除操作。 這里可能會產生的問題是,如果MLOG_CHECKPOINT日志和文件頭記錄的CHECKPOINT LSN差距太遠的話,在第一次掃描時可能花費大量的時間做無謂的解析,感覺這里還有優化的空間。 在我的測試實例中,由于崩潰時施加的負載比較大,MLOG_CHECKPOINT和CHECKPOINT點的LSN相差約1G的redo log。 2\. 第二次掃描,再次從checkpoint點開始重復掃描,存儲日志對象 日志解析后的對象類型為`recv_t`,包含日志類型、長度、數據、開始和結束LSN。日志對象的存儲使用hash結構,根據 space id 和 page no 計算hash值,相同頁上的變更作為鏈表節點鏈在一起,大概結構可以表示為: ![recv hash 結構](https://box.kancloud.cn/2015-09-24_560396dc0fe39.png) 掃描的過程中,會基于MLOG_FILE_NAME 和MLOG_FILE_DELETE 這樣的redo日志記錄來構建`recv_spaces`,存儲space id到文件信息的映射(`fil_name_parse`?–>?`fil_name_process`),這些文件可能需要進行崩潰恢復。(實際上第一次掃描時,也會向`recv_spaces`中插入數據,但只到MLOG_CHECKPOINT日志記錄為止) > Tips:在一次checkpoint后第一次修改某個表的數據時,總是先寫一條MLOG_FILE_NAME 日志記錄;通過該類型的日志可以跟蹤一次CHECKPOINT后修改過的表空間,避免打開全部表。 > 在第二次掃描時,總會判斷將要修改的表空間是否在`recv_spaces`中,如果不存在,則認為產生列嚴重的錯誤,拒絕啟動(`recv_parse_or_apply_log_rec_body`) 默認情況下,Redo log以一批64KB(RECV_SCAN_SIZE)為單位讀入到`log_sys->buf`中,然后調用函數`recv_scan_log_recs`處理日志塊。這里會判斷到日志塊的有效性:是否是完整寫入的、日志塊checksum是否正確, 另外也會根據一些標記位來做判斷: * 在每次寫入redo log時,總會將寫入的起始block頭的flush bit設置為true,表示一次寫入的起始位置,因此在重啟掃描日志時,也會根據flush bit來推進掃描的LSN點; * 每次寫redo時,還會在每個block上記錄下一個checkpoint no(每次做checkpoint都會遞增),由于日志文件是循環使用的,因此需要根據checkpoint no判斷是否讀到了老舊的redo日志。 對于合法的日志,會拷貝到緩沖區`recv_sys->buf`中,調用函數`recv_parse_log_recs`解析日志記錄。 這里會根據不同的日志類型分別進行處理,并嘗試進行apply,堆棧為: ~~~ recv_parse_log_recs --> recv_parse_log_rec --> recv_parse_or_apply_log_rec_body ~~~ 如果想理解InnoDB如何基于不同的日志類型進行崩潰恢復的,非常有必要細讀函數`recv_parse_or_apply_log_rec_body`,這里是redo日志apply的入口。 例如如果解析到的日志類型為MLOG_UNDO_HDR_CREATE,就會從日志中解析出事務ID,為其重建undo log頭(`trx_undo_parse_page_header`);如果是一條插入操作標識(MLOG_REC_INSERT 或者 MLOG_COMP_REC_INSERT),就需要從中解析出索引信息(`mlog_parse_index`)和記錄信息(`page_cur_parse_insert_rec`);或者解析一條IN-PLACE UPDATE (MLOG_REC_UPDATE_IN_PLACE)日志,則調用函數`btr_cur_parse_update_in_place`。 第二次掃描只會應用MLOG_FILE_*類型的日志,記錄到`recv_spaces`中,對于其他類型的日志在解析后存儲到哈希對象里。然后調用函數`recv_init_crash_recovery_spaces`對涉及的表空間進行初始化處理: * 首先會打印兩條我們非常熟悉的日志信息: ~~~ [Note] InnoDB: Database was not shutdown normally! [Note] InnoDB: Starting crash recovery. ~~~ * 如果`recv_spaces`中的表空間未被刪除,且ibd文件存在時,則表明這是個普通的文件操作,將該table space加入到`fil_system->named_spaces`鏈表上(`fil_names_dirty`),后續可能會對這些表做redo apply操作; * 對于已經被刪除的表空間,我們可以忽略日志apply,將對應表的space id在`recv_sys->addr_hash`上的記錄項設置為RECV_DISCARDED; * 調用函數`buf_dblwr_process()`,該函數會檢查所有記錄在double write buffer中的page,其對應的數據文件頁是否完好,如果損壞了,則直接從dblwr中恢復; * 最后創建一個臨時的后臺線程,線程函數為`recv_writer_thread`,這個線程和page cleaner線程配合使用,它會去通知page cleaner線程去flush崩潰恢復產生的臟頁,直到`recv_sys`中存儲的redo記錄都被應用完成并徹底釋放掉(`recv_sys->heap == NULL`) 3\. 如果第二次掃描hash表空間不足,無法全部存儲到hash表中,則發起第三次掃描,清空hash,重新從checkpoint點開始掃描。 hash對象的空間最大一般為buffer pool size - 512個page大小。 第三次掃描不會嘗試一起全部存儲到hash里,而是一旦發現hash不夠了,就立刻apply redo日志。但是…如果總的日志需要存儲的hash空間略大于可用的最大空間,那么一次額外的掃描開銷還是非常明顯的。 簡而言之,第一次掃描找到正確的MLOG_CHECKPOINT位置;第二次掃描解析 redo 日志并存儲到hash中;如果hash空間不夠用,則再來一輪重新開始,解析一批,應用一批。 三次掃描后,hash中通常還有redo日志沒有被應用掉。這個留在后面來做,隨后將`recv_sys->apply_log_recs`?設置為true,并從函數`recv_recovery_from_checkpoint_start`返回。 對于正常shutdown的場景,一次checkpoint完成后是不記錄MLOG_CHECKPOINT日志的,如果掃描過程中沒有找到對應的日志,那就認為上次是正常shutdown的,不用考慮崩潰恢復了。 > Tips:偶爾我們會看到日志中報類似這樣的信息: > “The log sequence number xxx in the system tablespace does not match the log sequence number xxxx in the ib_logfiles!” > 從內部邏輯來看是因為ibdata中記錄的lsn和iblogfile中記錄的checkpoint lsn不一致,但系統又判定無需崩潰恢復時會報這樣的錯。單純從InnoDB實例來看是可能的,因為做checkpint 和更新ibdata不是原子的操作,這樣的日志信息一般我們也是可以忽略的。 ## 初始化事務子系統(trx_sys_init_at_db_start) 這里會涉及到讀入undo相關的系統頁數據,在崩潰恢復狀態下,所有的page都要先進行日志apply后,才能被調用者使用,例如如下堆棧: ~~~ trx_sys_init_at_db_start --> trx_sysf_get --> ....->buf_page_io_complete --> recv_recover_page ~~~ 因此在初始化回滾段的時候,我們通過讀入回滾段頁并進行redo log apply,就可以將回滾段信息恢復到一致的狀態,從而能夠 “復活”在系統崩潰時活躍的事務,維護到讀寫事務鏈表中。對于處于prepare狀態的事務,我們后續需要做額外處理。 關于事務如何從崩潰恢復中復活,參閱4月份的月報 “[MySQL · 引擎特性 · InnoDB undo log 漫游](http://mysql.taobao.org/monthly/2015/05/01/)“最后一節。 ## 應用redo日志(`recv_apply_hashed_log_recs`) 根據之前搜集到`recv_sys->addr_hash`中的日志記錄,依次將page讀入內存,并對每個page進行崩潰恢復操作(`recv_recover_page_func`): * 已經被刪除的表空間,直接跳過其對應的日志記錄; * 在讀入需要恢復的文件頁時,會主動嘗試采用預讀的方式多讀點page (`recv_read_in_area`),搜集最多連續32個(RECV_READ_AHEAD_AREA)需要做恢復的page no,然后發送異步讀請求。 page 讀入buffer pool時,會主動做崩潰恢復邏輯; * 只有LSN大于數據頁上LSN的日志才會被apply; 忽略被truncate的表的redo日志; * 在恢復數據頁的過程中不產生新的redo 日志; * 在完成修復page后,需要將臟頁加入到buffer pool的flush list上;由于innodb需要保證flush list的有序性,而崩潰恢復過程中修改page的LSN是基于redo 的LSN而不是全局的LSN,無法保證有序性;InnoDB另外維護了一顆紅黑樹來維持有序性,每次插入到flush list前,查找紅黑樹找到合適的插入位置,然后加入到flush list上。(`buf_flush_recv_note_modification`) ## 完成崩潰恢復(`recv_recovery_from_checkpoint_finish`) 在完成所有redo日志apply后,基本的崩潰恢復也完成了,此時可以釋放資源,等待recv writer線程退出 (崩潰恢復產生的臟頁已經被清理掉),釋放紅黑樹,回滾所有數據詞典操作產生的非prepare狀態的事務 (`trx_rollback_or_clean_recovered`) ### 無效數據清理及事務回滾: 調用函數`recv_recovery_rollback_active`完成下述工作: * 刪除臨時創建的索引,例如在DDL創建索引時crash時的殘留臨時索引(`row_merge_drop_temp_indexes()`); * 清理InnoDB臨時表 (`row_mysql_drop_temp_tables`); * 清理全文索引的無效的輔助表(`fts_drop_orphaned_tables()`); * 創建后臺線程,線程函數為`trx_rollback_or_clean_all_recovered`,和在`recv_recovery_from_checkpoint_finish`中的調用不同,該后臺線程會回滾所有不處于prepare狀態的事務。 至此InnoDB層的崩潰恢復算是告一段落,只剩下處于prepare狀態的事務還有待處理,而這一部分需要和Server層的binlog聯合來進行崩潰恢復。 ## Binlog/InnoDB XA Recover 回到Server層,在初始化完了各個存儲引擎后,如果binlog打開了,我們就可以通過binlog來進行XA恢復: * 首先掃描最后一個binlog文件,找到其中所有的XID事件,并將其中的XID記錄到一個hash結構中(`MYSQL_BIN_LOG::recover`); * 然后對每個引擎調用接口函數`xarecover_handlerton`, 拿到每個事務引擎中處于prepare狀態的事務xid,如果這個xid存在于binlog中,則提交;否則回滾事務。 很顯然,如果我們弱化配置的持久性(`innodb_flush_log_at_trx_commit != 1`?或者?`sync_binlog != 1`), 宕機可能導致兩種丟數據的場景: 1. 引擎層提交了,但binlog沒寫入,備庫丟事務; 2. 引擎層沒有prepare,但binlog寫入了,主庫丟事務。 即使我們將參數設置成`innodb_flush_log_at_trx_commit =1`?和?`sync_binlog = 1`,也還會面臨這樣一種情況:主庫crash時還有binlog沒傳遞到備庫,如果我們直接提升備庫為主庫,同樣會導致主備不一致,老主庫必須根據新主庫重做,才能恢復到一致的狀態。針對這種場景,我們可以通過開啟semisync的方式來解決,一種可行的方案描述如下: 1. 設置雙1強持久化配置; 2. 我們將semisync的超時時間設到極大值,同時使用semisync AFTER_SYNC模式,即用戶線程在寫入binlog后,引擎層提交前等待備庫ACK; 3. 基于步驟1的配置,我們可以保證在主庫crash時,所有老主庫比備庫多出來的事務都處于prepare狀態; 4. 備庫完全apply日志后,記下其執行到的relay log對應的位點,然后將備庫提升為新主庫; 5. 將老主庫的最后一個binlog進行截斷,截斷的位點即為步驟3記錄的位點; 6. 啟動老主庫,那些已經傳遞到備庫的事務都會提交掉,未傳遞到備庫的binlog都會回滾掉。
                  <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>

                              哎呀哎呀视频在线观看