日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

MySQL · 引擎特性 · InnoDB 崩溃恢复过程

發布時間:2025/5/22 数据库 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 MySQL · 引擎特性 · InnoDB 崩溃恢复过程 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

在前面兩期月報中,我們詳細介紹了 InnoDB redo log 和 undo log 的相關知識,本文將介紹 InnoDB 在崩潰恢復時的主要流程。

本文代碼分析基于 MySQL 5.7.7-RC 版本,函數入口為?innobase_start_or_create_for_mysql,這是一個非常冗長的函數,本文只涉及和崩潰恢復相關的代碼。

在閱讀本文前,強烈建議翻閱我們之前的兩期月報:
1.?MySQL · 引擎特性 · InnoDB undo log 漫游
2.?MySQL · 引擎特性 · InnoDB redo log漫游

初始化崩潰恢復

首先初始化崩潰恢復所需要的內存對象:

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中被引入,其目的是為了簡化 InnoDB 崩潰恢復的邏輯,根據WL#7142的描述,包含幾點改進:

  • 避免崩潰恢復時讀取每個ibd的第一個page來確認其space id;
  • 無需檢查$datadir/*.isl,新的日志類型記錄了文件全路徑,并消除了isl文件和實際ibd目錄的不一致可能帶來的問題;
  • 自動忽略那些還沒有導入到InnoDB的ibd文件(例如在執行IMPORT TABLESPACE時crash);
  • 引入了新的日志類型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值,相同頁上的變更作為鏈表節點鏈在一起,大概結構可以表示為:

    掃描的過程中,會基于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 漫游“最后一節。

    應用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), 宕機可能導致兩種丟數據的場景:

  • 引擎層提交了,但binlog沒寫入,備庫丟事務;
  • 引擎層沒有prepare,但binlog寫入了,主庫丟事務。
  • 即使我們將參數設置成innodb_flush_log_at_trx_commit =1?和?sync_binlog = 1,也還會面臨這樣一種情況:主庫crash時還有binlog沒傳遞到備庫,如果我們直接提升備庫為主庫,同樣會導致主備不一致,老主庫必須根據新主庫重做,才能恢復到一致的狀態。針對這種場景,我們可以通過開啟semisync的方式來解決,一種可行的方案描述如下:

  • 設置雙1強持久化配置;
  • 我們將semisync的超時時間設到極大值,同時使用semisync AFTER_SYNC模式,即用戶線程在寫入binlog后,引擎層提交前等待備庫ACK;
  • 基于步驟1的配置,我們可以保證在主庫crash時,所有老主庫比備庫多出來的事務都處于prepare狀態;
  • 備庫完全apply日志后,記下其執行到的relay log對應的位點,然后將備庫提升為新主庫;
  • 將老主庫的最后一個binlog進行截斷,截斷的位點即為步驟3記錄的位點;
  • 啟動老主庫,那些已經傳遞到備庫的事務都會提交掉,未傳遞到備庫的binlog都會回滾掉。
  • 總結

    以上是生活随笔為你收集整理的MySQL · 引擎特性 · InnoDB 崩溃恢复过程的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。