數據庫系統與文件系統最大的區別在於數據庫能保證操做的原子性,一個操做要麼不作要麼都作,即便在數據庫宕機的狀況下,也不會出現操做一半的狀況,這個就須要數據庫的日誌和一套完善的崩潰恢復機制來保證。本文仔細剖析了InnoDB的崩潰恢復流程,代碼基於5.6分支。mysql
lsn: 能夠理解爲數據庫從建立以來產生的redo日誌量,這個值越大,說明數據庫的更新越多,也能夠理解爲更新的時刻。此外,每一個數據頁上也有一個lsn,表示最後被修改時的lsn,值越大表示越晚被修改。好比,數據頁A的lsn爲100,數據頁B的lsn爲200,checkpoint lsn爲150,系統lsn爲300,表示當前系統已經更新到300,小於150的數據頁已經被刷到磁盤上,所以數據頁A的最新數據必定在磁盤上,而數據頁B則不必定,有可能還在內存中。
redo日誌: 現代數據庫都須要寫redo日誌,例如修改一條數據,首先寫redo日誌,而後再寫數據。在寫完redo日誌後,就直接給客戶端返回成功。這樣雖然看過去多寫了一次盤,可是因爲把對磁盤的隨機寫入(寫數據)轉換成了順序的寫入(寫redo日誌),性能有很大幅度的提升。當數據庫掛了以後,經過掃描redo日誌,就能找出那些沒有刷盤的數據頁(在崩潰以前可能數據頁僅僅在內存中修改了,可是還沒來得及寫盤),保證數據不丟。
undo日誌: 數據庫還提供相似撤銷的功能,當你發現修改錯一些數據時,可使用rollback指令回滾以前的操做。這個功能須要undo日誌來支持。此外,現代的關係型數據庫爲了提升併發(同一條記錄,不一樣線程的讀取不衝突,讀寫和寫讀不衝突,只有同時寫才衝突),都實現了相似MVCC的機制,在InnoDB中,這個也依賴undo日誌。爲了實現統一的管理,與redo日誌不一樣,undo日誌在Buffer Pool中有對應的數據頁,與普通的數據頁一塊兒管理,依據LRU規則也會被淘汰出內存,後續再從磁盤讀取。與普通的數據頁同樣,對undo頁的修改,也須要先寫redo日誌。
檢查點: 英文名爲checkpoint。數據庫爲了提升性能,數據頁在內存修改後並非每次都會刷到磁盤上。checkpoint以前的數據頁保證必定落盤了,這樣以前的日誌就沒有用了(因爲InnoDB redolog日誌循環使用,這時這部分日誌就能夠被覆蓋),checkpoint以後的數據頁有可能落盤,也有可能沒有落盤,因此checkpoint以後的日誌在崩潰恢復的時候仍是須要被使用的。InnoDB會依據髒頁的刷新狀況,按期推動checkpoint,從而減小數據庫崩潰恢復的時間。檢查點的信息在第一個日誌文件的頭部。
崩潰恢復: 用戶修改了數據,而且收到了成功的消息,然而對數據庫來講,可能這個時候修改後的數據尚未落盤,若是這時候數據庫掛了,重啓後,數據庫須要從日誌中把這些修改後的數據給撈出來,從新寫入磁盤,保證用戶的數據不丟。這個從日誌中撈數據的過程就是崩潰恢復的主要任務,也能夠成爲數據庫前滾。固然,在崩潰恢復中還須要回滾沒有提交的事務,提交沒有提交成功的事務。因爲回滾操做須要undo日誌的支持,undo日誌的完整性和可靠性須要redo日誌來保證,因此崩潰恢復先作redo前滾,而後作undo回滾。sql
咱們從源碼角度仔細剖析一下數據庫崩潰恢復過程。整個過程都在引擎初始化階段完成(innobase_init
),其中最主要的函數是innobase_start_or_create_for_mysql
,innodb經過這個函數完成建立和初始化,包括崩潰恢復。首先來介紹一下數據庫的前滾。數據庫
前滾數據庫,主要分爲兩階段,首先是日誌掃描階段,掃描階段按照數據頁的space_id和page_no分發redo日誌到hash_table中,保證同一個數據頁的日誌被分發到同一個哈希桶中,且按照lsn大小從小到大排序。掃描完後,再遍歷整個哈希表,依次應用每一個數據頁的日誌,應用完後,在數據頁的狀態上至少恢復到了崩潰以前的狀態。咱們來詳細分析一下代碼。
首先,打開全部的ibdata文件(open_or_create_data_files
)(ibdata能夠有多個),每一個ibdata文件有個flush_lsn在頭部,計算出這些文件中的max_flush_lsn和min_flush_lsn,由於ibdata也有可能有數據沒寫完整,須要恢復,後續(recv_recovery_from_checkpoint_start_func
)經過比較checkpont_lsn和這兩個值來肯定是否須要對ibdata前滾。
接着,打開系統表空間和日誌表空間的全部文件(fil_open_log_and_system_tablespace_files
),防止出現文件句柄不足,清空buffer pool(buf_pool_invalidate
)。接下來就進入最最核心的函數:recv_recovery_from_checkpoint_start_func,注意,即便數據庫是正常關閉的,也會進入。
雖然recv_recovery_from_checkpoint_start_func
看過去很冗長,可是不少代碼都是爲了LOG_ARCHIVE特性而編寫的,真正數據崩潰恢復的代碼其實很少。
首先,初始化一些變量,查看srv_force_recovery
這個變量,若是用戶設置跳過前滾階段,函數直接返回。
接着,初始化recv_sys
結構,分配hash_table的大小,同時初始化flush list rbtree。recv_sys
結構主要在崩潰恢復前滾階段使用。hash_table就是以前說的用來存不一樣數據頁日誌的哈希表,哈希表的大小被初始化爲buffer_size_in_bytes/512, 這個是哈希表最大的長度,超過就存不下了,幸運的是,須要恢復的數據頁的個數不會超過這個值,由於buffer poll最多(數據庫崩潰以前髒頁的上線)只能存放buffer_size_in_bytes/16KB個數據頁,即便考慮壓縮頁,最多也只有buffer_size_in_bytes/1KB個,此外關於這個哈希表內存分配的大小,能夠參考bug#53122。flush list rbtree這個主要是爲了加入插入髒頁列表,InnoDB的flush list必須按照數據頁的最老修改lsn(oldest_modifcation)從小到大排序,在數據庫正常運行時,能夠經過log_sys->mutex和log_sys->log_flush_order_mutex保證順序,在崩潰恢復則沒有這種保證,應用數據的時候,是從第一個元素開始遍歷哈希表,不能保證數據頁按照最老修改lsn(oldest_modifcation)從小到大排序,這樣就須要線性遍歷flush_list來尋找插入位置,效率過低,所以引入紅黑樹,加快查找插入的位置。
接着,從ib_logfile0的頭中讀取checkpoint信息,主要包括checkpoint_lsn和checkpoint_no。因爲InnoDB日誌是循環使用的,且最少要有2個,因此ib_logfile0必定存在,把checkpoint信息存在裏面很安全,不用擔憂被刪除。checkpoint信息其實會寫在文件頭的兩個地方,兩個checkpoint域輪流寫。爲何要兩個地方輪流寫呢?假設只有一個checkpoint域,一直更新這個域,而checkpoint域有512字節(OS_FILE_LOG_BLOCK_SIZE
),若是恰好在寫這個512字節的時候,數據庫掛了,服務器也掛了(先不考慮硬件的原子寫特性,早期的硬件沒有這個特性),這個512字節可能只寫了一半,致使整個checkpoint域不可用。這樣數據庫將沒法作崩潰恢復,從而沒法啓動。若是有兩個checkpoint域,那麼即便一個寫壞了,還能夠用另一個嘗試恢復,雖然有可能這個時候日誌已經被覆蓋,可是至少提升了恢復成功的機率。兩個checkpoint域輪流寫,也能減小磁盤扇區故障帶來的影響。checkpoint_lsn以前的數據頁都已經落盤,不須要前滾,以後的數據頁可能還沒落盤,須要從新恢復出來,即便已經落盤也不要緊,由於redo日誌時冪等的,應用一次和應用兩次都同樣(底層實現: 若是數據頁上的lsn大於等於當前redo日誌的lsn,就不該用,不然應用。checkpoint_no能夠理解爲checkpoint域寫盤的次數,每次刷盤遞增1,同時這個值取模2能夠用來實現checkpoint_no域的輪流寫。正常邏輯下,選取checkpoint_no值大的做爲最終的checkpoint信息,用來作後續崩潰恢復掃描的起始點。
接着,使用checkpoint域的信息初始化recv_sys結構體的一些信息後,就進入日誌解析的核心函數recv_group_scan_log_recs
,這個函數後續咱們再分析,主要做用就是解析redo日誌,若是內存不夠了,就直接調用應用(recv_apply_hashed_log_recs
)日誌,而後再接着解析。若是須要應用的日誌不多,就僅僅解析分發日誌,到recv_recovery_from_checkpoint_finish
函數中在應用日誌。
接着,依據當前刷盤的數據頁狀態作一次checkpoint,由於在recv_group_scan_log_recs
裏可能已經應用部分日誌了。至此recv_recovery_from_checkpoint_start_func
函數結束。
在recv_recovery_from_checkpoint_finish
函數中,若是srv_force_recovery設置正確,就開始調用函數recv_apply_hashed_log_recs
應用日誌,而後等待刷髒的線程退出(線程是崩潰恢復時臨時啓動的),最後釋放recv_sys的相關資源以及hash_table佔用的內存。
至此,數據庫前滾結束。接下來,咱們詳細分析一下redo日誌解析函數以及redo日誌應用函數的實現細節。安全
解析函數的最上層是recv_group_scan_log_recs
,這個函數調用底層函數(log_group_read_log_seg
),按照RECV_SCAN_SIZE(64KB)大小分批讀取。讀取出來後,首先經過block_no和lsn之間的關係以及日誌checksum判斷是否讀到了日誌最後(因此能夠看出,並沒一個標記在日誌頭標記日誌的有效位置,徹底是按照上述兩個條件判斷是否到達了日誌尾部),若是讀到最後則返回(以前說了,即便數據庫是正常關閉的,也要走崩潰恢復邏輯,那麼在這裏就返回了,由於正常關閉的checkpoint值必定是指向日誌最後),不然則把日誌去頭掐尾放到一個recv_sys->buf中,日誌頭裏面存了一些控制信息和checksum值,只是用來校驗和定位,在真正的應用中沒有用。在放到recv_sys->buf以前,須要檢驗一下recv_sys->buf有沒有滿(RECV_PARSING_BUF_SIZE
,2M),滿了就報錯(若是上一批解析有不完整的日誌,日誌解析函數不會分發,而是把這些不完整的日誌留在recv_sys->buf中,直到解析到完整的日誌)。接下的事情就是從recv_sys->buf中解析日誌(recv_parse_log_recs
)。日誌分兩種:single_rec和multi_rec,前者表示只對一個數據頁進行一種操做,後者表示對一個或者多個數據頁進行多種操做。日誌中還包括對應數據頁的space_id,page_no,操做的type以及操做的內容(recv_parse_log_rec
)。解析出相應的日誌後,按照space_id和page_no進行哈希(若是對應的表空間在內存中不存在,則表示表已經被刪除了),放到hash_table裏面(日誌真正存放的位置依然在buffer pool)便可,等待後續應用。這裏有幾個點值得注意:服務器
recv_init_crash_recovery
),好比在錯誤日誌中打印咱們常見的「Database was not shutdown normally!」和「Starting crash recovery.」,還要從double write buffer中檢查是否發生了數據頁半寫,若是有須要恢復(buf_dblwr_process
),還須要啓動一個線程用來刷新應用日誌產生的髒頁(由於這個時候buf_flush_page_cleaner_thread尚未啓動)。最後還須要打開全部的表空間。。注意是全部的表。。。咱們在阿里雲RDS MySQL的運維中,經常發現數據庫hang在了崩潰恢復階段,在錯誤日誌中有相似「Reading tablespace information from the .ibd files...」字樣,這就表示數據庫正在打開全部的表,而後一看錶的數量,發現有幾十甚至上百萬張表。。。數據庫之因此要打開全部的表,是由於在分發日誌的時候,須要肯定space_id對應哪一個ibd文件,經過打開全部的表,讀取space_id信息來肯定,另一個緣由是方便double write buffer檢查半寫數據頁。針對這個表數量過多致使恢復過慢的問題,MySQL 5.7作了優化,WL#7142, 主要思想就是在每次checkpoint後,在第一次修改某個表時,先寫一個新日誌mlog_file_name(包括space_id和filename的映射),來表示對這個表進行了操做,後續對這個表的操做就不用寫這個新日誌了,當須要崩潰恢復時候,多一次掃描,經過蒐集mlog_file_name來肯定哪些表被修改過,這樣就不須要打開全部的表來肯定space_id了。應用日誌的上層函數爲recv_apply_hashed_log_recs
(應用日誌也可能在io_helper函數中進行),主要做用就是遍歷hash_table,從磁盤讀取對每一個數據頁,依次應用哈希桶中的日誌。應用完全部的日誌後,若是須要則把buffer_pool的頁面都刷盤,畢竟空間有限。有如下幾點值得注意:併發
recv_read_in_area
)。因爲這個是異步讀取,因此最終應用日誌的活兒是由io_helper線程來作的(buf_page_io_complete
),此外,爲了防止短期發起太多的io,在代碼中加了流量控制的邏輯(buf_read_recv_pages
)。若是發現某個數據頁在內存中,則直接調用recv_recover_page
應用日誌。由此咱們能夠看出,InnoDB應用日誌其實並非單線程的來應用日誌的,除了崩潰恢復的主線程外,io_helper線程也會參與恢復。併發線程數取決於io_helper中讀取線程的個數。執行完了redo前滾數據庫,數據庫的全部數據頁已經處於一致的狀態,undo回滾數據庫就能夠安全的執行了。數據庫崩潰的時候可能有一些沒有提交的事務或者已經提交的事務,這個時候就須要決定是否提交。主要分爲三步,首先是掃描undo日誌,從新創建起undo日誌鏈表,接着是,依據上一步創建起的鏈表,重建崩潰前的事務,即恢復當時事務的狀態。最後,就是依據事務的不一樣狀態,進行回滾或者提交。app
在recv_recovery_from_checkpoint_start_func
以後,recv_recovery_from_checkpoint_finish
以前,調用了trx_sys_init_at_db_start
,這個函數作了上述三步中的前兩步。
第一步在函數trx_rseg_array_init
中處理,遍歷整個undo日誌空間(最多TRX_SYS_N_RSEGS(128)個segment),若是發現某個undo segment非空,就進行初始化(trx_rseg_create_instance
)。整個每一個undo segment,若是發現undo slot非空(最多TRX_RSEG_N_SLOTS(1024)個slot),也就行初始化(trx_undo_lists_init
)。在初始化undo slot後,就把不一樣類型的undo日誌放到不一樣鏈表中(trx_undo_mem_create_at_db_start
)。undo日誌主要分爲兩種:TRX_UNDO_INSERT和TRX_UNDO_UPDATE。前者主要是提供給insert操做用的,後者是給update和delete操做使用。以前說過,undo日誌有兩種做用,事務回滾時候用和MVCC快照讀取時候用。因爲insert的數據不須要提供給其餘線程用,因此只要事務提交,就能夠刪除TRX_UNDO_INSERT類型的undo日誌。TRX_UNDO_UPDATE在事務提交後還不能刪除,須要保證沒有快照使用它的時候,才能經過後臺的purge線程清理。
第二步在函數trx_lists_init_at_db_start
中進行,因爲第一步中,已經在內存中創建起了undo_insert_list和undo_update_list(鏈表每一個undo segment獨立),因此這一步只須要遍歷全部鏈表,重建起事務的狀態(trx_resurrect_insert
和trx_resurrect_update
)。簡單的說,若是undo日誌的狀態是TRX_UNDO_ACTIVE,則事務的狀態爲TRX_ACTIVE,若是undo日誌的狀態是TRX_UNDO_PREPARED,則事務的狀態爲TRX_PREPARED。這裏還要考慮變量srv_force_recovery的設置,若是這個變量值爲非0,全部的事務都會回滾(即事務被設置爲TRX_ACTIVE),即便事務的狀態應該爲TRX_STATE_PREPARED。重建起事務後,按照事務id加入到trx_sys->trx_list鏈表中。最後,在函數trx_sys_init_at_db_start
中,會統計全部須要回滾的事務(事務狀態爲TRX_ACTIVE)一共須要回滾多少行數據,輸出到錯誤日誌中,相似:5 transaction(s) which must be rolled back or cleaned up。InnoDB: in total 342232 row operations to undo的字樣。
第三步的操做在兩個地方被調用。一個是在recv_recovery_from_checkpoint_finish
的最後,另一個是在recv_recovery_rollback_active
中。前者主要是回滾對數據字典的操做,也就是回滾DDL語句的操做,後者是回滾DML語句。前者是在數據庫可提供服務以前必須完成,後者則能夠在數據庫提供服務(也便是崩潰恢復結束)以後繼續進行(經過新開一個後臺線程trx_rollback_or_clean_all_recovered
來處理)。由於InnoDB認爲數據字典是最重要的,必需要回滾到一致的狀態才行,而用戶表的數據能夠稍微慢一點,對外提供服務後,慢慢恢復便可。所以咱們經常在會發現數據庫已經啓動起來了,而後錯誤日誌中還在不斷的打印回滾事務的信息。事務回滾的核心函數是trx_rollback_or_clean_recovered
,邏輯很簡單,只須要遍歷trx_sys->trx_list,按照事務不一樣的狀態回滾或者提交便可(trx_rollback_resurrected
)。這裏要注意的是,若是事務是TRX_STATE_PREPARED狀態,那麼在InnoDB層,不作處理,須要在Server層依據binlog的狀況來決定是否回滾事務,若是binlog已經寫了,事務就提交,由於binlog寫了就可能被傳到備庫,若是主庫回滾會致使主備數據不一致,若是binlog沒有寫,就回滾事務。運維
innodb_fast_shutdown:
innodb_fast_shutdown = 0。這個表示在MySQL關閉的時候,執行slow shutdown,不但包括日誌的刷盤,數據頁的刷盤,還包括數據的清理(purge),ibuf的合併,buffer pool dump以及lazy table drop操做(若是表上有未完成的操做,即便執行了drop table且返回成功了,表也不必定馬上被刪除)。
innodb_fast_shutdown = 1。這個是默認值,表示在MySQL關閉的時候,僅僅把日誌和數據刷盤。
innodb_fast_shutdown = 2。這個表示關閉的時候,僅僅日誌刷盤,其餘什麼都不作,就好像MySQL crash了同樣。
這個參數值越大,MySQL關閉的速度越快,可是啓動速度越慢,至關於把關閉時候須要作的工做挪到了崩潰恢復上。另外,若是MySQL要升級,建議使用第一種方式進行一次乾淨的shutdown。異步
innodb_force_recovery:
這個參數主要用來控制InnoDB啓動時候作哪些工做,數值越大,作的工做越少,啓動也更加容易,可是數據不一致的風險也越大。當MySQL由於某些不可控的緣由不能啓動時,能夠設置這個參數,從1開始逐步遞增,知道MySQL啓動,而後使用SELECT INTO OUTFILE把數據導出,盡最大的努力減小數據丟失。
innodb_force_recovery = 0。這個是默認的參數,啓動的時候會作全部的事情,包括redo日誌應用,undo日誌回滾,啓動後臺master和purge線程,ibuf合併。檢測到了數據頁損壞了,若是是系統表空間的,則會crash,用戶表空間的,則打錯誤日誌。
innodb_force_recovery = 1。若是檢測到數據頁損壞了,不會crash也不會報錯(buf_page_io_complete
),啓動的時候也不會校驗表空間第一個數據頁的正確性(fil_check_first_page
),表空間沒法訪問也繼續作崩潰恢復(fil_open_single_table_tablespace
、fil_load_single_table_tablespace
),ddl操做不能進行(check_if_supported_inplace_alter
),同時數據庫也被不能進行寫入操做(row_insert_for_mysql
、row_update_for_mysql
等),全部的prepare事務也會被回滾(trx_resurrect_insert
、trx_resurrect_update_in_prepared_state
)。這個選項仍是很經常使用的,數據頁多是由於磁盤壞了而損壞了,設置爲1,能保證數據庫正常啓動。
innodb_force_recovery = 2。除了設置1以後的操做不會運行,後臺的master和purge線程就不會啓動了(srv_master_thread
、srv_purge_coordinator_thread
等),當你發現數據庫由於這兩個線程的緣由而沒法啓動時,能夠設置。
innodb_force_recovery = 3。除了設置2以後的操做不會運行,undo回滾數據庫也不會進行,可是回滾段依然會被掃描,undo鏈表也依然會被建立(trx_sys_init_at_db_start
)。srv_read_only_mode會被打開。
innodb_force_recovery = 4。除了設置3以後的操做不會運行,ibuf的操做也不會運行(ibuf_merge_or_delete_for_page
),表信息統計的線程也不會運行(由於一個壞的索引頁會致使數據庫崩潰)(info_low
、dict_stats_update
等)。從這個選項開始,以後的全部選項,都會損壞數據,慎重使用。
innodb_force_recovery = 5。除了設置4以後的操做不會運行,回滾段也不會被掃描(recv_recovery_rollback_active
),undo鏈表也不會被建立,這個主要用在undo日誌被寫壞的狀況下。
innodb_force_recovery = 6。除了設置5以後的操做不會運行,數據庫前滾操做也不會進行,包括解析和應用(recv_recovery_from_checkpoint_start_func
)。函數
InnoDB實現了一套完善的崩潰恢復機制,保證在任何狀態下(包括在崩潰恢復狀態下)數據庫掛了,都能正常恢復,這個是與文件系統最大的差異。此外,崩潰恢復經過redo日誌這種物理日誌來應用數據頁的方法,給MySQL Replication帶來了新的思路,備庫是否能夠經過相似應用redo日誌的方式來同步數據呢?阿里雲RDS MySQL團隊在後續的產品中,給你們帶來了相似的特性,敬請期待。