《《MySQL運維內參》節選》要點:
本文介紹了《MySQL運維內參》節選,希望對您有用。如果有疑問,可以聯系我們。
前面幾篇連載講述了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的日志是具有邏輯意義的物理日志,所以日志記錄的格式就不完全是物理信息,而是有一定的邏輯意義,首先看一個基本的格式,如下圖所示:

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

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

上面講述的是,InnoDB存儲引擎中,REDO日志的一部分類型,并對不同類型做了解釋.從解釋中可以看到,基本上每一個類型其實都是具有邏輯意義的,與DML相關的類型中,不是存儲了列數據,就是存儲了記錄在頁面內的偏移等信息.
這樣做的優點有:
- 可以寫REDO解析工具,去做一個第三方的同步工具,或者了解數據庫做了什么操作,類似Binlog內容,但側重點不同.
- 日志占用空間比全物理日志少.
最大的缺點就是系統首先要保證日志對應的頁面的正確性,否則會造成邏輯日志執行不成功,或者造成數據的不一致等問題,這個問題在InnoDB中的解決方式,就是后面介紹的Double Write機制.
本文就此結束,接下來繼續連載,講述有關日志的其他內容.
文章來自微信公眾號:DBAce
轉載請注明本頁網址:
http://www.snjht.com/jiaocheng/4184.html