InnoDB日誌管理機制(七) – 運維派
引子:
書接上文,在之前六篇講述了寫日誌,其實正常情況下,這都是無用功,因為根本用不到。上一節講到了,在什麼情況下會用到日誌,以及在什麼時候會用到,如何用到等等內容,我們這一節繼續講述,在掃描完成日誌之後,如何做資料庫恢復工作,裡面有什麼邏輯,有什麼可以改進的地方等等,這都是我們讀者要去深思的地方。
(本書作者在“白家大院”齊聚首)
從這些程式碼段中可以看到,快取到HASH表之後,應該是可以找合適的時機去APPLY了,那什麼時候呢?我們可以返回去看看函式recv_scan_log_recs的最後,呼叫了函式recv_apply_hashed_log_recs,那這個就是我們要找的真正做APPLY的函數了。我們詳細看一下它的實現。
繼續:
從這些程式碼段中可以看到,快取到HASH表之後,應該是可以找合適的時機去APPLY了,那什麼時候呢?我們可以返回去看看函式recv_scan_log_recs的最後,呼叫了函式recv_apply_hashed_log_recs,那這個就是我們要找的真正做APPLY的函數了。我們詳細看一下它的實現。
UNIV_INTERN void recv_apply_hashed_log_recs(
ibool allow_ibuf
)
{
/* local vaiables … */
loop:
recv_sys->apply_log_recs = TRUE;
recv_sys->apply_batch_on = TRUE;
/* 遍歷HASH表?是的,把HASH表中的每一個桶中的每一個頁面,連續處理 */
for (i = 0; i < hash_get_n_cells(recv_sys->addr_hash); i++) {
/* 遍歷HASH表一個桶中的多個地址 */
for (recv_addr = static_cast<recv_addr_t*>(
HASH_GET_FIRST(recv_sys->addr_hash, i));
recv_addr != 0;
recv_addr = static_cast<recv_addr_t*>(
HASH_GET_NEXT(addr_hash, recv_addr))) {
/* 針對每一個頁面,做這個頁面上所有的REDO操作 */
ulint space = recv_addr->space;
ulint zip_size = fil_space_get_zip_size(space);
ulint page_no = recv_addr->page_no;
if (recv_addr->state == RECV_NOT_PROCESSED) {
mutex_exit(&(recv_sys->mutex));
if (buf_page_peek(space, page_no)) {
buf_block_t* block;
mtr_start(&mtr);
block = buf_page_get(
space, zip_size, page_no,
RW_X_LATCH, &mtr);
buf_block_dbg_add_level(
block, SYNC_NO_ORDER_CHECK);
/* 恢復一個頁面的資料,APPLY recv_addr中儲存的所有REDO記錄,
這裡使用了一個MTR來恢復。需要注意的是,這個MTR只是用來
獲取頁面時,給這個頁面加鎖使用的,而不會涉及REDO操作,因為
REDO是不需要再寫日誌的,所以不用擔心這個MTR涉及到的日誌量
太大的問題 */
recv_recover_page(FALSE, block);
mtr_commit(&mtr);
} else {
/* 這裡的操作是,如果上面的buf_page_peek沒有在Buffer Pool中
找到這個頁面,那這裡就從檔案中將這個頁面載入到Buffer Pool,
並且預讀32個頁面以提高效能。恢復方法與是一樣的。*/
recv_read_in_area(space, zip_size, page_no);
}
mutex_enter(&(recv_sys->mutex));
}
}
}
/* Wait until all the pages have been processed */
while (recv_sys->n_addrs != 0) {
mutex_exit(&(recv_sys->mutex));
os_thread_sleep(500000);
mutex_enter(&(recv_sys->mutex));
}
/* Wait for any currently run batch to end.
如註釋所述,如果上面的操作做完了,則需要保證這些日誌APPLY之後
要在ibdata及ibd(s)中落地,此時就會將Buffer Pool中全部的髒頁刷一遍
以保證已經處理的這些日誌失效。可能有人會問,如果在恢復的過程中,假設
就是這裡吧,還沒有做刷盤操作,資料庫又掛了,那怎麼辦?
其實沒事兒,整個恢復過程,日誌也沒有寫,只是掃描了一遍,並且有可能在
Buffer Pool中已經寫了很多頁面,有可能這些頁面已經因為LRU已經刷過
了,但這些操作是可重入的,也就是說,資料庫再起來,可以重新做一次REDO
操作,直到做成功為止。*/
success = buf_flush_list(ULINT_MAX, LSN_MAX, NULL);
recv_sys->apply_log_recs = FALSE;
recv_sys->apply_batch_on = FALSE;
/* 將HASH表中快取的所有內容清空 */
recv_sys_empty_hash();
mutex_exit(&(recv_sys->mutex));
}
到這裡,我們應該已經清楚了REDO資料庫恢復的整個過程,並且可以返回到函式recv_recovery_from_checkpoint_start_func中,看一下最後的說明,做完REOD之後,做一次檢查點以說明這次資料庫恢復已經完成。
但這裡我又有話說了,各位同學有沒有發現一個細節,那就是InnoDB在辛辛苦苦的將所有日誌分析並且根據不同頁面通過HASH表儲存之後,我們特別要注意下面兩點特徵:
- 對於同一個頁面的REDO記錄,必然是儲存在同一個HASH桶中的。
- 對於某一個頁面的所有日誌記錄,是按照先後順序來管理的。
這兩個特徵非常重要,因為我們知道,REDO日誌的APPLY,與順序有關係,LSN小的,必定要比LSN大的先做APPLY,不然有可能造成資料的覆蓋。但這有一個前提就是同一個頁面,不同頁面之間是不存在這樣的問題的。
那我們想想,是不是隻需要保證,同一個頁面的日誌順序執行其所有的日誌記錄即可,而不同頁面就沒必要守這個規則了,答案是肯定的。
那目前InnoDB難道不是這樣做的麼?上面程式碼中我們已經看到了,他是用了一個兩層迴圈,掃描了整個HASH表,慢慢的一條條的做REDO恢復。基於上面的分析,其實可以大膽的想象的一下,REDO恢復可以實現並行恢復。按照桶的下標為鍵值分配執行緒,那這樣同一個桶必然會分到同一個執行緒中去做,這樣自然保證了同一個頁面的執行順序,而不同的桶之間的頁面是沒有關係的,自然就可以並行恢復了。
啊?可以這樣?這個想法,可能會讓那些把日誌檔案設定的很大,又經常出現機器宕機問題的同學(上面已經提到了他們)心潮澎湃,這樣效能提升的不只一點點了。
還是那句話,這個是需要把日誌檔案設定很大,並且經常出現宕機時,優化效果才明顯。有需求,就能解決,我們希望這個優化會出現在某個版本中,少一些浪費的時間。
到現在為止,REDO日誌的恢復就做完了,到這個時候,才真正體現了這個“累贅”的價值,感謝有你!
上面所講的,是使用REDO日誌來恢復資料庫的過程,在它做完之後,整個資料庫就是完整的了,已經保證了所有的資料庫表都沒有丟資料的情況,所有的資料庫頁面也已經是完整的了。假設此時對資料庫做DML操作,也已經是可以的了,但還有一個問題沒有處理,那就是此時的資料庫,存在髒資料。因為有些事務沒有提交,但資料已經存在了(舉一個例子,事務在做的過程中,日誌已經寫完並刷盤,就是沒有提交,此時資料庫掛了),那根據事務的ACID特性,這樣的資料就不應該存在,此時InnoDB需要做的就是把這些事務回滾掉,這就用到了我們下面將要講的“資料庫回滾”。
(神形兼備啊,另外那種霸氣也流露出來了)
資料庫回滾
回滾段的管理,也是有一個入口位置用來儲存回滾段的管理資訊的,在InnoDB中,是用第6個頁面(5號)來管理的,這個頁面是專門用來儲存事務相關的資訊的,我們先看看其頁面格式:
/** Transaction system header */
/*————————————————————- @{ */
#define TRX_SYS_TRX_ID_STORE 0 /*!< the maximum trx id or trx
number modulo
TRX_SYS_TRX_ID_UPDATE_MARGIN
written to a file page by any
transaction; the assignment of
transaction ids continues from
this number rounded up by
TRX_SYS_TRX_ID_UPDATE_MARGIN
plus
TRX_SYS_TRX_ID_UPDATE_MARGIN
when the database is
started */
#define TRX_SYS_FSEG_HEADER 8 /*!< segment header for the
tablespace segment the trx
system is created into */
#define TRX_SYS_RSEGS (8 + FSEG_HEADER_SIZE)
/*!< the start of the array of
rollback segment specification
slots */
上面定義的是第6號頁面中儲存的資訊及其對應的位置,每一項的詳細意義如下:
- TRX_SYS_TRX_ID_STORE:用來儲存事務號,在每次新啟動一個事務時,都會去檢查當前最大事務號是不是達到了TRX_SYS_TRX_ID_WRITE_MARGIN(256)的倍數,如果達到了,就會將最大的事務號寫入到這個位置,在下次啟動時,將這個值取出來,再加上一個步長(TRX_SYS_TRX_ID_WRITE_MARGIN),來保證事務號的唯一性,其實就是一個經典的取號器的實現原理。
- TRX_SYS_FSEG_HEADER:用來儲存事務段資訊。
- TRX_SYS_RSEGS:這是一個數組,InnoDB有128個回滾段,那這個陣列的長度就是128,每一個元素佔用8個位元組,對應的一個回滾段儲存的內容包括回滾段首頁面的表空間ID號及頁面號。
而針對每一個回滾段,即上面陣列中的一個元素,也有其自己的儲存格式,程式碼中的巨集定義如下:
#define TRX_RSEG_MAX_SIZE 0 /* Maximum allowed size for rollback
segment in pages */
#define TRX_RSEG_HISTORY_SIZE 4 /* Number of file pages occupied
by the logs in the history list */
#define TRX_RSEG_HISTORY 8 /* The update undo logs for committed
transactions */
#define TRX_RSEG_FSEG_HEADER (8 + FLST_BASE_NODE_SIZE)
/* Header for the file segment where
this page is placed */
#define TRX_RSEG_UNDO_SLOTS (8 + FLST_BASE_NODE_SIZE + FSEG_HEADER_SIZE)
/* Undo log segment slots */
上面這些資訊的儲存,是從頁面偏移38的位置開始的,在這個位置之前,儲存的是檔案管理的資訊(講參考索引管理相關章節),從38開始,儲存了上面5個資訊,它們的意義分別如下:
- TRX_RSEG_MAX_SIZE:回滾段管理頁面的總數量,即所有undo段頁面之和,一般為ULINT_MAX,即無上限。
- TRX_RSEG_HISTORY_SIZE:這個表來表示當前InnoDB中,在History List中有多少頁面,即需要做PURGE的回滾段頁面個數。
- TRX_RSEG_HISTORY:這個用來儲存History List的連結串列首地址,事務提交之後,其對應的回滾段如果還不能PURGE,那都會加入到這個連結串列中。
- TRX_RSEG_FSEG_HEADER:這個用來儲存回滾段的Inode位置資訊,通過這個地址,就可以找到這個段的詳細資訊。
- TRX_RSEG_UNDO_SLOTS:這個位置所儲存的是一個數組,長度為1024,每一個元素是一個頁面號,初始化為FIL_NULL,即空頁面。
這5個資訊,儲存了一個回滾段的資訊,最後一個位置的陣列,就是用來真正儲存回滾段的位置,我們後面會講到這128*1024個槽是如何使用的。
根據上面的講述,我們現在已經知道所有回滾段的儲存架構了,如下圖所示:
現在就可以知道,InnoDB中支援的回滾段總共有128*1024=131072個,TRX_RSEG_UNDO_SLOTS陣列的每個元素指向一個頁面,這個頁面對應一個段,頁面號就是段首頁的頁面號。
在每一個事務開始的時候,都會分配一個rseg,就是從長度為128的陣列中,根據最近使用的情況,找到一個臨近位置的rseg,在這個事務的生命週期內,被分配的rseg就會被這個事務所使用。
在事務執行過程中,會產生兩種回滾日誌,一種是INSERT的UNDO記錄,一種是UPDATE的UNDO記錄,可能有人會問DELETE哪去了?其實是包含在UPDATE的回滾記錄中的,因為InnoDB把UNDO分為兩類,一類就是新增,也就是INSERT,一類就是修改,就是UPDATE,分類的依據就是事務提交後要不要做PURGE操作,因為INSERT是不需要PURGE的,只要事務提交了,那這個回滾記錄就可以丟掉了,而對於更新和刪除操作而言,如果事務提交了,還需要為MVCC服務,那就需要將這些日誌放到History List中去,等待去做PURGE,以及MVCC的多版本查詢等,所以分為兩類。
所以,一個事務被分配了一個rseg之後,通常情況下,如果一個事務中既有插入,又有更新(或刪除),那這個事務就會對應兩個UNDO段,即在一個rseg的1024個槽中,要使用兩個槽來儲存這個事務的回滾段,一個是插入段,一個是更新段。
在事務要儲存回滾記錄的時候,事務就要從1024個槽中,根據相應的更新型別(插入或者更新)找到空閒的槽來作為自己的UNDO段。如果已經申請過相同型別的UNDO段,就直接使用,否則就需要新建立一個段,並將段首頁號寫入到這個rseg的長度為1024的陣列的對應位置(空閒位置)中去,這樣就將具體的回滾段與整個架構聯絡起來了。
如果在1024個槽中找不到空閒的位置,那這個事務就會被回滾掉,報出錯誤為:“Too many active concurrent transactions”,錯誤號為1637的異常。當然這種情況一般不會見到,如果能把這個用完,估計資料庫已經根本動不了了。
上面講述了整個回滾段儲存架構及與事務的相關性,具體到一個事務所使用的某個回滾段的管理,就儲存在了回滾段首頁中,管理資訊包括三部分,分別是Undo page header、Undo segment header及Undo log header。下面分別介紹:
Undo page header:
/** Transaction undo log page header offsets */
#define TRX_UNDO_PAGE_TYPE 0 /*!< TRX_UNDO_INSERT or
TRX_UNDO_UPDATE */
#define TRX_UNDO_PAGE_START 2 /*!< Byte offset where the undo log
records for the LATEST transaction
start on this page (remember that
in an update undo log, the first page
can contain several undo logs) */
#define TRX_UNDO_PAGE_FREE 4 /*!< On each page of the undo log this
field contains the byte offset of the
first free byte on the page */
#define TRX_UNDO_PAGE_NODE 6 /*!< The file list node in the chain
of undo log pages */
- TRX_UNDO_PAGE_TYPE:這個我們在上面已經解釋過了,就包括兩個值,分別是TRX_UNDO_INSERT和TRX_UNDO_UPDATE。
- TRX_UNDO_PAGE_START:用來表示當前頁面中,從什麼位置開始儲存了UNDO日誌。
- TRX_UNDO_PAGE_FREE:與上面的START相對,這個用來表示當前頁面中,UNDO日誌的結束位置,也表示從這個位置開始,可以繼續追加UNDO日誌,直到頁面儲存滿為止。
- TRX_UNDO_PAGE_NODE:一個UNDO段中所有的頁面,通過一個雙向連結串列來管理,這個位置儲存的就是雙向連結串列的指標。
Undo segment header:
/** Undo log segment header */
#define TRX_UNDO_STATE 0 /*!< TRX_UNDO_ACTIVE, … */
#define TRX_UNDO_LAST_LOG 2 /*!< Offset of the last undo log header
on the segment header page, 0 if
none */
#define TRX_UNDO_FSEG_HEADER 4 /*!< Header for the file segment which
the undo log segment occupies */
#define TRX_UNDO_PAGE_LIST (4 + FSEG_HEADER_SIZE)
/*!< Base node for the list of pages in
the undo log segment; defined only on
the undo log segment’s first page */
- TRX_UNDO_STATE:用來儲存當前UNDO段的狀態,狀態包括TRX_UNDO_ACTIVE,TRX_UNDO_CACHED、TRX_UNDO_TO_FREE、TRX_UNDO_TO_PURGE、TRX_UNDO_PREPARED五種。
- TRX_UNDO_LAST_LOG:用來儲存最後一個UNDO日誌的偏移位置,用來在一個UNDO段中,找到最後一個UNDO日誌。
- TRX_UNDO_FSEG_HEADER:這個位置,就是用來儲存當前UNDO段的Inode資訊的,通過這個資訊可以知道本UNDO段的詳細資訊。
- TRX_UNDO_PAGE_LIST:段內所有的頁面都是通過連結串列連線起來的,這個位置是連結串列的首地址,用來管理這個連結串列,上面已經介紹的TRX_UNDO_PAGE_NODE則是每個節點的雙鏈指標。
Undo log header:
/** The undo log header. There can be several undo log headers on the first
page of an update undo log segment. */
#define TRX_UNDO_TRX_ID 0 /*!< Transaction id */
#define TRX_UNDO_TRX_NO 8 /*!< Transaction number of the
transaction; defined only if the log
is in a history list */
#define TRX_UNDO_DEL_MARKS 16 /*!< Defined only in an update undo
log: TRUE if the transaction may have
done delete markings of records, and
thus purge is necessary */
#define TRX_UNDO_LOG_START 18 /*!< Offset of the first undo log record
of this log on the header page; purge
may remove undo log record from the
log start, and therefore this is not
necessarily the same as this log
header end offset */
#define TRX_UNDO_XID_EXISTS 20 /*!< TRUE if undo log header includes
X/Open XA transaction identification
XID */
#define TRX_UNDO_DICT_TRANS 21 /*!< TRUE if the transaction is a table
create, index create, or drop
transaction: in recovery
the transaction cannot be rolled back
in the usual way: a ‘rollback’ rather
means dropping the created or dropped
table, if it still exists */
#define TRX_UNDO_TABLE_ID 22 /*!< Id of the table if the preceding
field is TRUE */
#define TRX_UNDO_NEXT_LOG 30 /*!< Offset of the next undo log header
on this page, 0 if none */
#define TRX_UNDO_PREV_LOG 32 /*!< Offset of the previous undo log
header on this page, 0 if none */
#define TRX_UNDO_HISTORY_NODE 34 /*!< If the log is put to the history
list, the file list node is here */
這是一個針對UNDO日誌的頭資訊,一個事務寫入一次UNDO日誌就會建立一個UNDO日誌單元,都會對應一個這樣的UNDO日誌頭資訊,用來管理這個日誌資訊的狀態,儲存一些相關的資訊以備恢復時使用,多個UNDO日誌之間,通過雙向連結串列連線起來(通過我們即將介紹的TRX_UNDO_NEXT_LOG及TRX_UNDO_PREV_LOG來管理)。
- TRX_UNDO_TRX_ID:用來儲存當前UNDO日誌對應的事務的事務ID號。
- TRX_UNDO_TRX_NO:事務序列號,在恢復時使用,這個序列號就是我們前面講的TRX_SYS_TRX_ID_STORE位置儲存的ID值。這個與上面的ID的區別是,NO用來在回滾時保持順序使用,而ID是在事務執行時使用的。
- TRX_UNDO_DEL_MARKS:用來表示當前UNDO日誌中有沒有通過打標誌刪除過記錄的操作,並決定是不是要做PURGE操作。
- TRX_UNDO_LOG_START:用來儲存當前頁面中,第一個UNDO日誌的開始位置。
- TRX_UNDO_XID_EXISTS:用來標誌當前日誌中有沒有包含Xid事務。
- TRX_UNDO_DICT_TRANS:用來標誌當前日誌對應的事務是不是DDL的,用來在回滾時判斷如何操作。
- TRX_UNDO_TABLE_ID:與上面一個相關,如果上面標誌是真的,則這個標誌的是DDL的表ID。
- TRX_UNDO_NEXT_LOG:用來連結當前UNDO段中所有的UNDO日誌,這個是指向下一個UNDO日誌。
- TRX_UNDO_PREV_LOG:與上一個對應,這個用來指向上一個UNDO日誌,從而構成雙向連結串列。
- TRX_UNDO_HISTORY_NODE:用來儲存在History List中的雙向連結串列指標。而這個連結串列的首地址,是在之前我們所介紹的TRX_RSEG_HISTORY位置,可以回到前面去檢視相關資訊。
到現在為止,關於具體一個UNDO段中每個頁面及頁面內容是如何管理的已經清楚了,當一個事務需要寫入UNDO日誌時,就可以直接從對應的UNDO段中找到一個頁面及對應的追加日誌的偏移位置,然後將對應的UNDO日誌寫入即可。
(還是昨天那個球,線下討論了關於會原始碼的作用,有很多人認為,一提到某人會原始碼,就覺得一般是用來裝一下的,並且實際上沒有幾家公司強大到放心地讓你寫原始碼去,所以覺得這不是一個名副其實的技能。我認為實則不然,會原始碼,99%的機會不是用來寫原始碼的,而是閱讀其實際方法及原理,儘可能的做到知MySQL,或者在出現問題之後,是一個很好的用來解決問題的方法,要知道,我不只一次碰到手冊上面所述內容是錯誤的,並且有很多問題網上也是沒有的,那可想而知,如果你會閱讀原始碼了,這樣的問題可以立刻迎刃而解)
篇外續
日誌這篇,以這樣的角度及思維模式來講述真的是沒有見過的,並且到今天已經是連載第七篇,
可以很確定的告訴大家,還有幾篇明天繼續發。
就在今天,關於這篇內容,我們的韓朱忠老師也說話了:
他也是我們本書的推薦序之一。在這裡謝謝他。
文章來自微信公眾號:DBAce