天天看點

MySQL · TokuDB · 日志子系統和崩潰恢複過程

mysql重新開機後自動加載innodb和其他的動态plugin,包括tokudb。每一plugin在注冊的時候指定init和deinit回調函數。tokudb的init/deinit函數分别是<code>tokudb_init_func</code>和<code>tokudb_done_func</code>。

mysql重新開機過程中調用<code>tokudb_init_func</code>進行必要的初始化。在<code>tokudb_init_func</code>裡面,調用<code>db_env_create</code>建立一個env執行個體,進行參數設定和callback設定。<code>db_env_create</code>是一個簡單的封裝,最終會調用<code>toku_env_create</code>來進行參數設定,callback設定和初始化的。<code>toku_env_create</code>初始化工作中一個很重要的事情就是調用<code>toku_logger_create</code>初始化tokudb的日志子系統。

在tokudb中,日志子系統是由tokulogger資料結構管理的。下面僅列出了主要的資料成員。

logger子系統在env-&gt;create階段由<code>toku_logger_create</code>進行初步的初始化工作。代碼片段如下:

logger子系統在env-&gt;open階段,調用<code>toku_logger_open</code>函數進行進一步的初始化。函數<code>toku_logger_open</code>是<code>toku_logger_open_with_last_xid</code>的簡單封裝。env-&gt;open最終調用<code>toku_logger_open_with_last_xid</code>解析redo log file擷取下一個可用的lsn,下一個可用的redo log file的序列号index并打開相應redo log file。在env-&gt;open時,調用<code>toku_logger_open_with_last_xid</code>的最後一個參數last_xid為txnid_none,表示由<code>toku_logger_open_with_last_xid</code>指定事務子系統初始化時最新的txnid。

解析redo log file的過程在函數<code>toku_logfilemgr_init</code>實作,依次解析redo log目錄下的每一個檔案名符合特定格式的redo log file,從中讀取最後一個log entry的lsn儲存下來。redo log檔案名遵循”log$index.tokulog$version”格式,$index是64位無符号整數表示的redo log file的序列号index,$version是32位無符号整數表示版本資訊。

如果最新的redo log file最後一個log entry是lt_shutdown(表示正常關閉不需要進行recovery),那麼把對應的txnid記錄在last_xid_if_clean_shutdown變量,作為tokudb事務子系統初始化時最新的txnid。在解析redo log file的時候,還會用最新的redo log file的最後一個log entry的lsn更新logger的lsn,written_lsn,fsynced_lsn。接着,<code>toku_logger_find_next_unused_log_file</code>找到下一個可用的redo log檔案的序列号,并建立新的redo log file。每個redo log file最開始的12個位元組是固定的,首先是8個位元組的magic字元串“tokulogg“,緊接着4個位元組是log的版本資訊。代碼片段如下:

下面我們一起看一下往redo log新加一條insert的過程。函數<code>toku_log_enq_insert</code>的第2,第5,第6,第7,第8參數表示描述一條insert的五元組(lsn, ft, xid, key, value)。代碼片段如下:

tokudb的logger有兩個buffer:inbuf和outbuf。inbuf表示接收log entry的buffer,而outbuf表示寫到redo log檔案的buffer。這兩個buffer是如何切換的呢?當inbuf滿或者inbuf裡的free space無法滿足新來的log entry的存儲需求時,需要觸發redo buffer flush過程,即将inbuf日志flush到redo log檔案裡。這個過程比較耗時,而且很可能inbuf裡面還有free space,隻是由于目前這個log entry比較大而無法滿足存儲需求,tokudb實作了output permission機制,使得需要free space的請求等待在output permission的條件變量上,其他client thread上下文的redo log請求可以繼續使用inbuf寫日志。等待上一個flush完成後(即條件變量被signaled),檢查目前inbuf的free space,如果可以滿足這條redo log entry就直接傳回,說明别的線程幫我們flush好了。如果free space不夠,需要在目前線程的上下文去做flush,實際上是把inbuf和outbuf互換,然後把outbuf寫到redo log檔案中。寫完之後适當調整inbuf的大小使之滿足目前redo log entry請求。最後喚醒等待inbuf提供足夠空間的線程(阻塞在output permission上的線程)。簡而言之,把redo log buffer拆分成inbuf和outbuf,最重要的作用是在redo log flush的時候不會阻塞新的log entry寫入,感興趣的朋友可以看一下函數<code>toku_logger_maybe_fsync</code>的實作,這裡就不一一展開了。函數<code>toku_logger_make_space_in_inbuf</code>的代碼片段如下:

前面提到mysql重新開機過程中會調用<code>db_env_create</code>建立env執行個體,進行參數設定和callback設定,然後調用env-&gt;open來做進一步初始化。同樣env-&gt;open也是一個回調函數,它是在<code>db_env_create</code>設定的,指向env_open函數。

在env_open裡調用validate_env判斷是否需要進行recovery。validate_env函數傳回時表明這個env是否是emptyenv (env目錄為空,且不存在rollback檔案,不存在資料檔案),是否是newnev (env目錄不存在),是否是emptyrollback (env目錄存在,rollback檔案為空)。

如果滿足條件 !emptyenv &amp;&amp; !new_env &amp;&amp; is_set(db_recovery) 就嘗試進行recovery。簡單地說recovery的條件就是env存在,log_dir存在,redo log存在。

判斷是否真正做recovery的函數是<code>tokuft_needs_recovery</code>。代碼如下:

<code>tokuft_needs_recovery</code>嘗試讀取最後一條redo log entry,如果不是lt_shutdown,就需要真正做recovery。讀取最後一條redo log entry的代碼片段如下:

在讀最後一個log entry的過程中,在讀log entry出錯的情況下(crash的時候把redo log寫壞了)會調用<code>lc_fix_bad_logfile</code>嘗試修複redo log檔案。修複的過程很簡單:從目前redo log頭部開始向後scan直到找到第一非法log entry的位置,并把redo log檔案truncate到那個位置。此時,檔案指針也指向檔案末尾。極端的情況是,修複完redo log,發現目前redo log中的所有entry都是壞的,那樣需要切換到前面一個redo log檔案。

如果需要做recovery,tokudb會調用do_recovery進行恢複,恢複的時候先做redo log apply,然後進行undo rollback。代碼片段如下:

scan log entry分别兩個階段:backward階段和forward階段。這兩個階段是由scan_state狀态機控制的。在scan開始之前在<code>scan_state_init</code>函數中把狀态機ss的初始狀态設定為backward_newer_checkpoint_end。

MySQL · TokuDB · 日志子系統和崩潰恢複過程

backward階段:從最後一個log entry開始向前讀,直到讀到checkpoint end。對在這個過程中讀到的每一個log entry調用<code>logtype_dispatch_assign(le, toku_recover_backward_, r, renv)</code>。在這個階段對于checkpoint以外的操作,toku_recover_backward_字首的處理函數都是noop。當讀到checkpoint end的log entry時,會把ss狀态設定為backward_between_checkpoint_begin_end,并記錄這個checkpoint的begin_lsn和lsn。然後繼續向前scan直到讀到checkpoint begin的log entry,確定ss中記錄的checkpoint_begin_lsn和log entry的lsn是相等的,然後 把ss的狀态設定為forward_between_checkpoint_begin_end,并設定renv-&gt;goforward為true。

forward階段:對目前的log entry調用<code>logtype_dispatch_assign(le, toku_recover_, r, renv)</code>重放redo log。然後向後scan直到讀到checkpoint end,確定ss中記錄的<code>checkpoint_begin_lsn</code>和<code>checkpoint_end_lsn</code>與log entry裡面記錄的<code>lsn_begin_checkpoint</code>和lsn是相等的,然後把ss的狀态設定為forward_newer_checkpoint_end。這樣,崩潰之前的最後一個checkpoint就回放完成了。下面要做的事情就是,回放committed txn的redo log。代碼片段如下:

上面我們是tokudb recovery的過程。對讀redo log一筆帶過。現在一起看看讀log entry的過程:

向後讀:從目前位置讀4個位元組的長度len1,然後讀1個位元組cmd。然後按照不同cmd的定義來讀log entry。

向前讀:從目前位置讀nocrc的長度len2,把檔案指針向前移動len2個位元組。從那個位置向後讀。

verify:讀的過程需要計算crc校驗碼。len1是參與crc計算的,而len2不參與crc計算。計算得到的crc應該與log entry裡面記錄的crc相等。而且len1應該等于len2。