1. 程式人生 > >InnoDB日誌管理機制(五) – 運維派

InnoDB日誌管理機制(五) – 運維派

前面幾篇連載講述了Buffer Pool以及日誌的基礎內容,接下來,講述的是基於日誌的想象,理解,以及它的儲存格式。

日誌的意義在哪裡?

上面已經講述過,日誌是在邏輯事務對資料庫做DML操作時,其所包含的物理事務MTR所記錄的針對所有涉及到的Buffer Pool頁面的修改記錄。

為了更好的講述日誌的意義,這裡通過下面幾個方面來更好的說明。

假如沒有寫日誌

假如沒有寫日誌,那資料庫在做了任何修改之後,必須要直接將Buffer Page刷磁碟,不然如果此時資料庫掛了,即使事務被提交了,這些修改還是沒法恢復。這將帶來的災難是,IO大量增加,這個時候的資料庫,相當於是一個簡單的檔案系統,無論寫什麼資料,都必須馬上刷入磁碟,Buffer Pool的作用可能只是一個用來修改檔案頁面的臨時快取而已。

假如沒有寫日誌,在資料庫做了DML操作之後,資料庫可能在事務沒有提交時就將Buffer Page刷到磁碟了,但此時需要回滾,而我們知道,回滾段的內容也是通過Buffer Pool管理的,它的每個頁和B樹頁面是一樣的,只是作用不一樣而已,由此可知,回滾段資料也是通過REDO日誌來保證完整性的。那麼如果沒有了日誌,Buffer Page中的回滾段頁面也需要直接寫入,沒有了任何快取,效能會非常低。

假如沒有寫日誌,資料庫在關閉(掛掉)後再啟動時,就不需要做REDO操作了(因為沒有寫日誌),但需要做UNDO操作,因為UNDO不是通過REDO來恢復的,而是自己寫入了(假設每次寫Buffer Page之後都直接刷盤了),所以回滾段是有效的,還可以讓沒有提交的事務回滾掉(因為如果一個事務修改的頁面很多的話,肯定會有一部分頁面先刷掉的,所以有可能需要回滾),勉強還可以保證資料庫的完整性。

綜合上面的假設,現在已經明白,日誌的作用就是用來保證Buffer Pool頁面的資料寫入不丟失,反過來說,如果每個Buffer Pool中的Page每次都刷入到磁碟了,這樣就不需要REDO日誌了,此時這個資料庫就是一個檔案系統了,因為Buffer Pool每次都刷,相當於每次寫完直接寫檔案。所以說,日誌,是資料庫管理系統與檔案系統的最核心的區別。

所以如果沒有日誌,資料庫的效能就低到完全沒法用了,因為IO太大了,同時,這種IO操作都是隨機寫入,很容易導致IO到達瓶頸,所以為了提高資料庫效能,就必須要使用到REDO日誌機制。

使用日誌能提高效能的關鍵原因,有以下三方面:

1. 因為日誌是用來記錄Buffer Pool中Page的修改記錄的,把對Page的寫入轉化為對日誌的寫入,那此時Page就不需要每次都刷盤,寫Page頁面只需要在記憶體中寫入即可,效能會非常好。

2. 通常,一個頁面是16K,如果不寫日誌的話,每次的寫入單位還是16K,即使修改很少量的資料,也是如此,這樣會導致無效IO非常嚴重,反過來說,也只有通過日誌機制,才能真正體現出真實寫入的資料量,不會存在對IO的浪費,Page的刷盤數量會大大減少。

3. 如果沒有日誌就會每次都刷Page,而這些Page的相對位置是亂的,並不是順序的,刷盤大多都是隨機IO,這對於機械硬碟,效能是非常差的,而有了日誌,巧妙的將隨機IO轉化成了日誌的順序IO,這將大大的提高IOPS,效能會非常好。

日誌檔案大小區別

使用日誌對資料庫的效能有很大的影響,那日誌還有什麼其它的因素會影響資料庫的效能呢?那就是日誌檔案空間容量。

現在已經知道,日誌在設定好後其容量是固定的,它是迴圈使用的,如果不夠用了,引發的事件是做一個檢查點,讓最小有效的LSN向前推,讓出一部分空間給新產生的日誌來使用,也就是說,只要這個日誌空間未用完,那麼Buffer Pool中的Page將會一直不刷盤(因為還有其它的刷盤時機,所以這裡單指因為日誌不夠用導致檢查點的刷盤),任何修改都是在記憶體中發生的,那麼下面做一個計算。

假如當前日誌容量設定為128M,某一個DML操作只針對某一行記錄一直做修改操作。每次操作產生日誌量為1KB(包括Buffer中資料頁面的修改及UNDO記錄的產生),這樣算下來,128M的日誌量可以容納對這條記錄的131072(128M/1K)次修改,也就是說,在這麼多次修改之後,這個頁面才需要刷盤,才會產生一次隨機刷盤操作。而如果把日誌檔案設定為1280M,很容易知道,這將容納對這條記錄的1310720次修改,這麼多次修改只產生一次隨機刷盤操作,而如果還是128M的話,需要10次隨機刷盤。很明顯,日誌容量對資料庫的效能還是有很大影響的。

日誌

但也不是設定的越大越好,這裡有兩點需要注意:

1. 如果設定的非常大,固然效能可能會很好,但如果某一天(真的有可能到來),資料庫異常掛了,此時可能有很多的日誌都沒有刷盤,也就是Log flushed up to與Last checkpoint at兩個值之間相差太多,恢復起來,可能需要比較長的時間。但這個一般問題不大,本身掛的機率不大,同時REDO日誌的恢復是順序的,都是根據頁面號的大小排序恢復的,所以比較快。同時在以後的MySQL版本中,會有多執行緒REDO恢復(聽說的),這樣就更快了,所以這一點不需要太多擔心。

2. 日誌容量大小的設定,最好要與Buffer Pool的總大小匹配,假如日誌容量太小,Buffer Pool太大,那麼這樣的一個後果是導致Buffer Pool頻繁做檢查點,大的Buffer Pool不能被好好的利用。如果是日誌容量很大,而Buffer Pool很小,此時Buffer Page經常會被淘汰出去,增加了IO頻次,同時如果資料庫意外掛掉,Buffer Pool小的話恢復起來也會比較慢。一般情況下,Buffer Pool的總大小與日誌容量的大小比例最好保持在10~5:1的範圍內。

日誌的格式

前面已經講述了太多的日誌相關的內容了,這一節將要講一下,具體到一個日誌記錄時,它是如何組織的,一條日誌記錄,究竟儲存了什麼?在這裡都會說清楚。

前面已經講到,InnoDB的日誌是具有邏輯意義的物理日誌,所以日誌記錄的格式就不完全是物理資訊,而是有一定的邏輯意義,首先看一個基本的格式,如下圖所示:

格式

圖中各個列代表的意義如下:

  1. Type:日誌型別,是一個日誌記錄的最高位,只佔一個位元組的空間。
  2. Space:表空間ID值,如果是系統頁面(UNDO頁面,或者是字典表頁面),則是0,如果是索引頁面,則是這個索引所在的表空間ID值。
  3. Offset:在上面Space所指定的檔案中的頁面號,以頁面大小為單位,它是第幾個頁面(從0開始計數),則這個Offset就是幾。
  4. Data:表示這條日誌記錄對應的資料,這個資料是不確定的,根據不同的Type值而不同,分別具有自己的格式。

Type型別有很多,下面列舉一些在InnoDB中比較常用的型別,並簡單做一些解釋,以便可以更好的理解。InnoDB中的REDO,究竟是在做什麼?究竟是儲存了什麼內容?功能是什麼?知道每個型別之後,這些問題也就清楚了。

注:下面講到的資料記錄,都是以Compact格式的記錄為物件的,其它型別這裡不考慮。

  1. MLOG_(1,2,4,8)BYTE: 這四個型別,表示要在某一個位置,寫入一個(兩個、四個、八個)位元組的內容,在日誌記錄中,Type分別是MLOG_1BYTE(MLOG_2BYTES、MLOG_4BYTES、MLOG_8BYTES),Space就是對應的表空間ID,Offset對應的是頁面號,在Data中,還需要儲存三個(四個、六個、十個)位元組,前兩個為要寫入的資料在頁面內的偏移值,因為頁面大小為16K,所以需要用兩個位元組來儲存,而後面才是真正需要寫入的資料,佔一個(兩個、四個、八個)位元組,這就是關於這個型別的日誌的完整內容。
  2. MLOG_WRITE_STRING:這種型別的日誌,其實和MLOG_1BYTE是類似的,只是MLOG_1BYTE是要寫一個固定長度的資料,而MLOG_WRITE_STRING是要寫一段變長的資料,Data部分的格式,首先用2個位元組儲存在頁面中的寫入位置,然後是2個位元組的寫入資料長度,最後是儲存指定長度的字串。
  3. MLOG_COMP_REC_MIN_MARK:這個型別的日誌,是在將一條記錄設定為頁面中的最小記錄(這個涉及到頁面管理的內容,在一個頁面中只有一個最小記錄,它指向的是B樹下一層的最左邊的節點)時產生的,因為只是打個標記,儲存內容比較簡單,除了基本的日誌頭外,在Data內容中只儲存了這條最小記錄在頁面內的偏移位置。
  4. MLOG_UNDO_INSERT:這個型別的日誌,是用來保證一個插入操作可以在事務沒有提交的情況下回滾時用的,在插入一條記錄時,不止要寫一個插入操作的日誌(型別為MLOG_REC_INSERT,後面會著重介紹),還要寫一個針對這個操作的回滾記錄。我們已經知道,回滾記錄的寫入,其實也是向ibdata檔案中寫入資料,同樣也是寫在Buffer Pool中的,這個操作對應的REDO日誌,就是當前介紹的MLOG_UNDO_INSERT型別的日誌,在資料庫恢復時,只有這個REDO日誌做完了,相應的UNDO記錄才有效(存在),如果對應的事務沒有提交,會通過這個回滾記錄將這個插入操作回滾掉,這也正是為什麼REDO必須要在UNDO之前執行的原因,這是後話。至於這種型別的日誌格式是什麼樣子的,與前面所說型別的區別還是在Data上面。前面兩個位元組儲存的是回滾記錄的長度,接著就是回滾記錄的完整資料,不包括回滾記錄前後各兩個位元組的指標資訊,具體到回滾記錄的格式,後面會講述。
  5. MLOG_INIT_FILE_PAGE:這個型別的日誌比較簡單,只有前面的基本頭資訊,沒有Data部分,因為在InnoDB中,初始化一個頁面,所有的資訊都是固定的,沒有額外的處理,只要表明初始化哪一個位置的頁面就好了,所以沒有Data部分。這裡初始化頁面所做的操作,只涉及到對頁面中檔案管理方面的資訊,比如這個頁面的頁面號,檔案號(表空間ID)等資訊,這個與後面將要介紹的MLOG_COMP_PAGE_CREATE是不同的,這個屬於頁面管理的檔案資訊部分的初始化,而MLOG_COMP_PAGE_CREATE屬於頁面的索引、資料儲存方面的管理資訊的初始化。後者是在前者的基礎上做的。
  6. MLOG_COMP_PAGE_CREATE:這個型別的日誌,其實和上面已經說過的MLOG_INIT_FILE_PAGE是一樣的道理,因為在Buffer中建立一個新的可以使用的頁面是固定的,只需要儲存一個型別及要建立的頁面的位置即可。建立一個頁面所做的操作,包括初始化頁面頭資訊,建立頁面中最小記錄與最大記錄,初始化頁面中記錄數、HEAP大小、HEAP首地址及槽資訊,初始化之後,這個頁面就可以在B樹中使用了,它是一個頁面在沒有插入任何資料時的狀態。
  7. MLOG_MULTI_REC_END:這個型別的日誌是非常特殊的,它只起一個標記的作用,其儲存的內容只有佔一個位元組的型別值。在前面介紹MTR時說到,一個MTR所寫的日誌,要麼全部寫入,要麼全部不寫入,如何保證這個原子性就是通過這個型別的日誌來實現的,每次MTR提交時,都會在後面加上這個日誌記錄,用來表示這個MTR已經結束了。只有在恢復的時候才會使用到它,在分析MTR時,只有找到這個日誌,前面的日誌才會去做REDO,做完之後,再向後掃描找到這個日誌,然後再去REDO,如此反覆,如果有一次找不到了,則說明日誌檔案是不完整的,已經掃描到的REDO日誌就不會去執行了,從而保證了已經執行的MTR每個都是完整的。
  8. MLOG_COMP_REC_CLUST_DELETE_MARK: 這個型別的日誌是表示,需要將聚集索引中的某一個記錄打上刪除標誌。因為,眾所周知,在InnoDB資料庫中聚簇索引的刪除在沒有提交之前,只是打了一個刪除標誌而已。這個型別的日誌記錄內容,除了基本的內容之外,其Data資料的組成主要包括,兩個位元組的索引列的個數n,兩個位元組的唯一索引的個數u。接下來儲存的是所有索引列的長度資訊,每個列用2個位元組儲存,佔用空間2*n個位元組,然後,再儲存索引中兩個系統列資訊,分別是TRXID在索引中的列位置資訊、ROLLPTR的值及TRXID的值,最後再儲存當前要刪除記錄在所在頁面中的偏移值,也就是那條記錄的頭指標資訊。這個型別的日誌,儲存的內容比較複雜,其Data部分使用下圖來簡明表示一下:

Data

  1. 這裡有一個奇怪的地方是,在給一個記錄打刪除標誌時,為什麼不使用這條記錄的主鍵值來直接定位,而是使用了一些在定位記錄時被認為是沒有用的東西呢?因為如果需要資料恢復了,只需要找到這行記錄的主鍵資訊,就可以重新給這個記錄打刪除標誌,那為什麼儲存的都是一些索引的定義資訊,比如,索引列個數,唯一鍵列個數,每個列的長度等,關於這個問題,是因為這裡InnoDB還需要考慮其自身的問題,那就是它的REDO日誌是半邏輯半物理的,在恢復時,不能保證對應的資料字典是可用的(因為資料字典的正確性還是需要REDO來保證),所以這個日誌就會記錄一些索引的資訊,在恢復時使用這些資訊來構造一個LOG_DUMMY表及LOG_DUMMY索引,然後再用這個表和索引來輔助這個REDO日誌的執行,這樣真正的表及索引可以不正確(暫時的),因為此時是不需要它們的。綜合上面所述的日誌Data部分,就可以知道這條記錄的確切資訊了,也就可以對它加刪除標誌了。
  2. MLOG_COMP_REC_UPDATE_IN_PLACE:這個型別的日誌,和上面的MLOG_COMP_REC_CLUST_DELETE_MARK基本是差不多的,只是這個會在最後儲存原地更新後的記錄資訊,包括所有被更新的列的資訊,儲存方式是:前面用1個位元組或2個位元組來儲存長度,後面跟著的是更新後的資料,直到記錄所有的列為止。
  3. MLOG_COMP_REC_DELETE:在InnoDB中,刪除資料是通過打刪除標誌來實現的,但是,在事務提交後做Purge操作時,這條記錄始終是要被刪除的,所以還存在一個真正的將資料記錄刪除的操作,那這個型別的日誌就是用來記錄這個動作的。不過這個日誌需要記錄的內容也比較少,除了基本的日誌頭資訊之外,在Data中只需要儲存這條記錄在頁面內的偏移即可。那麼在恢復資料時,這種型別的日誌的作用就是會將這個頁面中的對應的記錄直接刪除掉,而不再是打刪除標記那麼簡單了。
  4. MLOG_COMP_PAGE_REORGANIZE:這個型別的日誌,表示的是要重組指定的頁面,其記錄的內容也是很簡單的,只需要儲存要重組哪一個頁面即可,沒有Data部分。在恢復的時候,找到這個頁面,對其中的資料碎片做整理,將頁面內部的記錄一條條向前移,將原來記錄之間不能再被使用的空間收回合在一起變成一塊連續的空間,這樣原來貌似已經滿了的頁面,又可以插入新的資料了,這個就是表的碎片整理過程。
  5. MLOG_COMP_REC_INSERT:這個型別是在插入一條記錄時產生的,它的產生過程可能存在一點爭議,這裡重點說一下。首先當然還是基本的日誌頭資訊,然後儲存的是被插入記錄在頁面內的偏移資訊,然後就是關於索引的資訊,這些都與上面MLOG_COMP_REC_CLUST_DELETE_MARK型別日誌的內容相同,然而,再後面,所儲存的資訊就比較複雜了。首先會計算出,當前要插入的記錄與前一條記錄第一個不相同的位元組的位置,然後在日誌中記錄的是,從這個位置開始到當前記錄結束位置之間的資料,當然還有一些其它的資訊,比如第一個不相同的位元組的位置資訊等。這裡主要想說的是它這個設計方式,簡單一點說,就是當前記錄中儲存的只是記錄的後半部分資料,前半部分資料依賴的是前一條記錄,這樣儲存會比儲存整個記錄省多少空間呢?最主要的是,這需要依賴插入資料之間的相關性,如果非常像,則可能會省一些,否則效果可能不明顯。

日誌

上面講述的是,InnoDB儲存引擎中,REDO日誌的一部分型別,並對不同型別做了解釋。從解釋中可以看到,基本上每一個型別其實都是具有邏輯意義的,與DML相關的型別中,不是儲存了列資料,就是儲存了記錄在頁面內的偏移等資訊。

這樣做的優點有:

  1. 可以寫REDO解析工具,去做一個第三方的同步工具,或者瞭解資料庫做了什麼操作,類似Binlog內容,但側重點不同。
  2. 日誌佔用空間比全物理日誌少。

最大的缺點就是系統首先要保證日誌對應的頁面的正確性,否則會造成邏輯日誌執行不成功,或者造成資料的不一致等問題,這個問題在InnoDB中的解決方式,就是後面介紹的Double Write機制。

本文就此結束,接下來繼續連載,講述有關日誌的其他內容。

文章來自微信公眾號:DBAce