InnoDB的Buffer Pool簡介
阿新 • • 發佈:2018-05-10
個數 說過 下一個 而且 角度 比例 控制 刷新 變化
這篇非常重要!這篇非常重要!這篇非常重要!重要的事情說三遍,這篇是後續事務和鎖的基礎,一定要看懂這篇,反正我寫的已經夠白話了,你要再看不懂呢,那你告訴我,我改還不行麽~下邊是建議正文:
1. 最好使用電腦觀看。
2. 如果你非要使用手機觀看,那請把字體調整到最小,這樣觀看效果會好一些。
3. 碎片化閱讀並不會得到真正的知識提升,要想有提升還得找張書桌認認真真看一會書,或者我們公眾號的文章。
4. 如果覺得不錯,各位幫著轉發轉發,如果覺得有問題或者寫的哪不清晰,務必私聊我~
5. 本公眾號的文章都是需要被系統性學習的,在閱讀本篇文章前最好已經閱讀過下邊幾篇文章,要不然可能會有閱讀不暢的體驗:
表空間的編號
我們在嘮叨的時候就已經說過,存儲引擎是使用來存儲的,又可以被分為系統表空間和獨立表空間。為了方便管理,每個都會有一個字節的編號,值得註意的一點是,系統表空間的編號始終為,也會根據一定規則給其他獨立表空間也編上號~
所以,當我們查看或修改某個的數據的時候,實際上需要同時知道表空間的編號和該頁的編號,也就是的組合才能定位到某一個具體的。如果你有認真看前邊嘮叨的那篇文章,肯定記得每個的編號也是占用個字節,而在一個內頁的編號是不能重復的,個字節是個二進制位,也就是說:一個表空間最多擁有232個頁,默認情況下一個頁的大小為16KB,也就是說一個表空間最多存儲64TB的數據~
緩存的重要性
所謂的只不過是對文件系統上一個或幾個實際文件的抽象,我們的數據說到底還是存儲在磁盤上的。但是各位也都知道,磁盤的速度慢的跟烏龜一樣,怎麽能配得上“快如風,疾如電”的呢?所以將內存作為緩存也是無奈之舉。MySQL服務器在處理客戶端的請求時,當需要訪問某個頁的數據時,就會把完整的頁的數據全部加載到內存中,也就是說即使我們只需要訪問一個數據頁的一條記錄,那也需要先把整個頁的數據加載到內存中。這是為了不必每次請求都去訪問一下磁盤,那得多慢啊~
對某個的訪問類型分為兩種,一種是只讀訪問,一種是寫入訪問。只讀訪問好辦,就是把磁盤上的加載到內存中讀而已;而如果需要修改該頁的數據就有點尷尬了,首先會把數據寫到內存中的頁中,然後在某個合適的時刻將修改過的頁同步到磁盤上,同步的時候斷電咋辦?數據就丟了麽?那支付寶或者微信支付的童鞋們還不得緊張死,天天祈禱神千萬不要宕機,宕機的話好多人就要家破人亡了?當然不是了,凡是成熟的數據庫系統都會有一套完整的機制來保證寫入過程要麽完整的完成,要麽就把已經寫入的數據恢復到之前沒寫的情況,總之不會家破人亡的~ 我們後邊幾篇文章的內容就是仔細嘮叨這個過程是怎麽實現的,哈哈,是不是有點兒小刺激~ 不過本篇文章作為先導篇,大家先得搞清楚這塊緩存是個神馬玩意兒,是用啥方式來管理這塊兒緩存的~
InnoDB的Buffer Pool
啥是個Buffer Pool?設計的大叔為了緩存磁盤中的,向操作系統申請了一片連續的內存,他們給這片內存起了個名,叫做(中文名是),那它有多大呢?這個其實看我們機器的配置,如果你是土豪,你有內存,你分配個幾百G作為也可以啊,當然你要是沒那麽有錢,設小點也行呀~ 默認情況下只有大小。當然如果你嫌棄這個太大或者太小,可以在啟動服務器的時候配置參數的值,它表示的大小,就像這樣:其中,的單位是字節,也就是我指定的大小為。需要註意的是,也不能太小,最小值為(當小於該值時會自動設置成)。
Buffer Pool內部組成
我們已經知道這個其實是一片連續的內存空間,那現在就面臨這個問題了:怎麽將磁盤上的頁緩存到內存中的中呢?直接把需要緩存的頁向裏一個一個往裏懟麽?不不不,為了更好的管理這些被緩存的,為每一個緩存頁都創建了一些所謂的,這些控制信息包括該頁所屬的表空間編號、頁號、頁在中的地址,一些鎖信息以及信息(鎖和我們之後會具體嘮叨,現在可以先忽略),當然還有一些別的控制信息,我們這就不全嘮叨一遍了,挑重要的說嘛哈哈~
每個緩存頁對應的控制信息占用的內存大小是相同的,我們就把每個頁對應的控制信息占用的一塊內存稱為一個吧,控制塊和緩存頁是一一對應的,它們都被存放到 Buffer Pool 中,其中控制塊被存放到 Buffer Pool 的前邊,緩存頁被存放到 Buffer Pool 後邊,所以整個對應的內存空間看起來就是這樣的:咦?控制塊和緩存頁之間的那個是個什麽玩意兒?你想想啊,每一個控制塊都對應一個緩存頁,那在分配足夠多的控制塊和緩存頁後,可能剩余的那點兒空間不夠一對控制塊和緩存頁的大小,自然就用不到嘍,這個用不到的那點兒內存空間就被稱為了。當然,如果你把的大小設置的剛剛好的話,也可能不會產生~
FREE鏈表的管理
當我們最初啟動服務器的時候,需要完成對的初始化過程,就是分配的內存空間,把它劃分成若幹對控制塊和緩存頁。但是此時並沒有真實的磁盤頁被緩存到中(因為還沒有用到),之後隨著程序的運行,會不斷的有磁盤上的頁被緩存到中,那麽問題來了,從磁盤上讀取一個頁到中的時候該放到哪個緩存頁的位置呢?或者說怎麽區分中哪些緩存頁是空閑的,哪些已經被使用了呢?我們最好在某個地方記錄一下哪些頁是可用的,我們可以把所有空閑的頁包裝成一個節點組成一個鏈表,這個鏈表也可以被稱作(或者說空閑鏈表)。因為剛剛完成初始化的中所有的緩存頁都是空閑的,所以每一個緩存頁都會被加入到中,假設該中可容納的緩存頁數量為,那增加了的效果圖就是這樣的:
從圖中可以看出,我們為了管理好這個,特意為這個鏈表定義了一個,裏邊兒包含著鏈表的頭節點地址,尾節點地址,以及當前鏈表中節點的數量等信息。我們在每個的節點中都記錄了某個緩存頁控制塊的地址,而每個緩存頁控制塊都記錄著對應的緩存頁地址,所以相當於每個Free鏈表節點都對應一個空閑的緩存頁。
有了這個事兒就好辦了,每當需要從磁盤中加載一個頁到中時,就從中取一個空閑的緩存頁,並且把該緩存頁對應的的信息填上,然後把該緩存頁對應的節點從鏈表中移除,表示該緩存頁已經被使用了~
緩存頁的哈希處理
我們前邊說過,當我們需要訪問某個頁中的數據時,就會把該頁加載到中,如果該頁已經在中的話直接使用就可以了。那麽問題也就來了,我們怎麽知道該頁在不在中呢?難不成需要依次遍歷中各個緩存頁麽?一個中的緩存頁這麽多都遍歷完豈不是要累死?
再回頭想想,我們其實是根據來定位一個頁的,也就相當於是一個,就是對應的,怎麽通過一個來快速找著一個呢?哈哈,那肯定是哈希表嘍~
所以我們可以用作為,作為創建一個哈希表,在需要訪問某個頁的數據時,先從哈希表中根據看看有沒有對應的緩存頁,如果有,直接使用該緩存頁就好,如果沒有,那就從中選一個空閑的緩存頁,然後把磁盤中對應的頁加載到該緩存頁的位置。
FLU鏈表的管理
如果我們修改了中某個緩存頁的數據,那它就和磁盤上的頁不一致了,這樣的緩存頁也被稱為(英文名:)。當然,最簡單的做法就是每發生一次修改就立即同步到磁盤上對應的頁上,但是頻繁的往磁盤中寫數據會嚴重的影響程序的性能(畢竟磁盤慢的像烏龜一樣)。所以每次修改緩存頁後,我們並不著急立即把修改同步到磁盤上,而是在未來的某個時間點進行同步,至於這個同步的時間點我們後邊的文章會特別詳細的說明的,現在先不用管哈~
但是如果不立即同步到磁盤的話,那之後再同步的時候我們怎麽知道中哪些頁是,哪些頁從來沒被修改過呢?總不能把所有的緩存頁都同步到磁盤上吧,假如被設置的很大,比方說,那一次性同步這麽多數據豈不是要慢死!所以,我們不得不再創建一個存儲臟頁的鏈表,凡是修改過的緩存頁都會被包裝成一個節點加入到這個鏈表中,因為這個鏈表中的頁都是需要被刷新到磁盤上的,所以也叫,有時候也會被簡寫為。鏈表的構造和差不多,這就不贅述了。
LRU鏈表的管理
緩存不夠的窘境,對應的內存大小畢竟是有限的,如果需要緩存的頁占用的內存大小超過了大小,也就是中已經沒有多余的空閑緩存頁的時候豈不是很尷尬,發生了這樣的事兒該咋辦?當然是把某些舊的緩存頁從中移除,然後再把新的頁放進來嘍~ 那麽問題來了,移除哪些緩存頁呢?
為了回答這個問題,我們還需要回到我們設立的初衷,我們就是想減少和磁盤的交互,最好每次在訪問某個頁的時候它都已經被緩存到中了。假設我們一共訪問了次頁,那麽被訪問的頁已經在緩存中的次數除以就是所謂的,我們的期望就是讓越高越好~ 從這個角度出發,回想一下我們的微信聊天列表,排在前邊的都是最近很頻繁使用的,排在後邊的自然就是最近很少使用的,假如列表能容納下的聯系人有限,你是會把最近很頻繁使用的留下還是最近很少使用的留下呢?廢話,當然是留下最近很頻繁使用的了~
簡單的LRU鏈表
管理的緩存頁其實也是這個道理,當中不再有空閑的緩存頁時,就需要淘汰掉部分最近很少使用的緩存頁。不過,我們怎麽知道哪些緩存頁最近頻繁使用,哪些最近很少使用呢?呵呵,神奇的鏈表再一次派上了用場,我們可以再創建一個鏈表,由於這個鏈表是為了的原則去淘汰緩存頁的,所以這個鏈表可以被稱為(Least Recently Used)。當我們需要訪問某個時,可以這樣處理:
如果該頁不在中,在把該頁從磁盤加載到中的緩存頁時,就把該緩存頁包裝成節點塞到鏈表的頭部。
如果該頁在中,則直接把該頁對應的節點移動到鏈表的頭部。
也就是說:只要我們使用到某個緩存頁,就把該緩存頁調整到的頭部,這樣尾部就是最近最少使用的緩存頁嘍~ 所以當中的空閑緩存頁使用完時,到的尾部找些緩存頁淘汰就OK啦,真簡單,嘖嘖…
劃分區域的LRU鏈表
高興的太早了,上邊的這個簡單的用了沒多長時間就發現問題了,有的小夥伴可能會寫一些需要掃描全表的查詢語句(比如沒有建立合適的索引或者壓根兒沒有WHERE子句的查詢),掃描全表意味著什麽?意味著將訪問到該表所在的所有數據頁!假設這個表中記錄非常多的話,那該表會占用特別多的,當需要訪問這些頁時,會把它們統統都加載到中,這也就意味著吧唧一下,中的所有數據頁都被換了一次血,其他查詢語句在執行時又得執行一次從磁盤加載到的操作。而這種全表掃描的語句執行的頻率也不高,每次執行都要把中的緩存頁換一次血,這嚴重的影響到其他查詢對Buffer Pool的使用,嚴重的降低了緩存命中率!這能忍麽?這肯定不能忍啊!再想想辦法,把這個按照一定比例分成兩截,分別是:
一部分存儲使用頻率非常高的緩存頁,所以也叫做,或者稱。另一部分存儲使用頻率不是很高的緩存頁,所以也叫做,或者稱。為了方便大家理解,我們把示意圖做了簡化,各位領會精神就好:
大家要特別註意一個事兒:我們是按照某個比例將 LRU鏈表 分成兩半的,不是某些節點固定是young區域的,某些節點固定式old區域的,隨著程序的運行,某個節點所屬的區域也可能發生變化。所以把一個完整的分成了和兩個部分之後,修改鏈表的方式也就可以變一變了:
如果某個頁第一次從磁盤加載到中,則放到區域的頭部。
如果該頁已經在中,則將其放到區域的頭部,也就是的頭部。
這樣搞有啥好處呢?在沒有空閑的緩存頁時,我們可以從old區域中淘汰一些頁,而不影響young區域中的緩存頁。這樣全表掃描的頁雖然也會進入中,但是由於首次緩存時只會放到區域,區域不受影響,也就是只會對造成部分換血,而不是全部換血,這在一定程度上降低了全表掃描對的緩存命中率的影響。
那這個劃分成兩截的比例怎麽確定呢?對於存儲引擎來說,我們可以通過查看系統變量的值來確定區域在中所占的比例,比方說這樣:
從結果可以看出來,默認情況下,區域在中所占的比例是,也就是說區域大約占的。這個比例我們是可以設置的,我們可以在啟動時修改參數來控制區域在中所占的比例,比方說這樣修改配置文件:
這樣我們在啟動服務器後,區域占的比例就是。當然,如果在服務器運行期間,我們也可以修改這個系統變量的值,不過需要註意的是,這個系統變量屬於,一經修改,會對所有客戶端生效,所以我們只能這樣修改:
更進一步優化LRU鏈表
這就說完了麽?沒有,早著呢~ 首次從磁盤上加載到的頁會放到區域,第二次訪問該頁的時候便會被放到區域,那如果在很短的時間內進行了兩次全表掃描操作豈不是會把區域的節點都移動到區域了,那相當於又把給破壞掉了,咋辦?
我們可以設置一個間隔時間,當第二次訪問區域的某個緩存頁時(該緩存頁沒有被淘汰掉),如果距離上一次訪問的時間小於這個時間,那就不把這個緩存頁放到區域,這個過程稱之為;而如果距離上一次訪問的時間不小於這個時間,那就把這個緩存頁放到區域,這個過程稱之為。這樣就可以降低在短時間內有大量全表掃描對的緩存命中率的影響。中這個間隔時間是由系統變量控制的,你看:
在我的電腦上的值是,它的單位是毫秒,也就意味著如果在1秒內發生了多次全表掃描,這些在區域的頁也不會被加入到區域的~ 當然,像一樣,我們也可以在服務器啟動或運行時設置的值,這裏就不贅述了,你自己試試吧~
還有一個問題,對於區域的緩存頁來說,我們每次訪問一個緩存頁就要把它移動到的頭部,這樣開銷是不是太大啦,畢竟在區域的緩存頁都是熱點數據,也就是可能被經常訪問的,這樣頻繁的對進行節點移動操作是不是不太好啊?是的,為了解決這個問題其實我們還可以提出一些優化策略,比如只有被訪問的緩存頁其於區域的(這個值可調節)之後,才會被移動到頭部,這樣就可以降低調整的頻率,從而提升性能。
還有木有什麽別的針對的優化措施呢?當然有啊,你要是好好學,寫篇論文,寫本書都不是問題,可是我們畢竟是一個介紹MySQL基礎知識的文章,再說多了篇幅就受不了了,適可而止,想了解更多的優化知識,自己去看源碼或者更多關於鏈表的知識嘍~ 另外,不同的大公司,可能會針對自己的業務對鏈表進行自己的定制,優化是無窮盡的,但是千萬別忘了我們的初心:盡量提高Buffer Pool的緩存命中率。
其他的一些鏈表
為了更好的管理中的緩存頁,除了我們上邊提到的一些措施,設計的大叔們還引進了其他的一些,比如用於管理解壓頁,用於管理存儲沒有被解壓的壓縮頁,用來管理被壓縮的頁等等,反正是為了更好的管理這個引入了各種鏈表,構造和我們介紹的鏈表都差不多,具體的使用方式就不啰嗦了,大家有興趣深究的再去找些更深的書或者直接看源代碼吧~
InnoDB中對各種列表的處理
上邊對各種鏈表的介紹,只是從我們初學者的學習原理的角度去看的,設計的大叔在真正實現這些鏈表上又下了一番苦功夫,都是為了節省內存和挺高性能而做的努力。比方說
實際的鏈表節點並不是獨立於的而存在的,而是被放在了緩存頁控制塊中。
雖然為了不同的目的我們提出了很多的鏈表,但是每個緩存頁控制塊其實只有一個節點和一個通用的鏈表節點。為了節省內存,針對緩存頁所處於的不同狀態,對緩存頁控制塊的通用鏈表節點進行了復用,比方說在該緩存頁空閑時,該節點代表的節點;比方說該緩存頁被修改時,該節點代表的節點,吧啦吧啦~
當然,如果你看不懂我上邊在說啥(本來也沒打算讓你看懂),那就不用看了,這只是設計的大叔在針對具體的場景做的優化方案,待你真正動手去設計一個存儲引擎時,你才會考慮如何更好地實現我們上邊的這些原理,現在可以先跳過,設計的大叔們為了更好地實現,使用了好幾萬行代碼,要說清楚這些,怎麽也得好幾篇文章了,我沒那個時間,等以後牛逼有空了,我再來仔細說清楚針對這些鏈表實現的具體細節~
多個Buffer Pool實例
我們上邊說過,本質是向操作系統申請的一塊連續的內存空間,在多線程環境下,為了保護緩存頁可能會對緩存頁進行加鎖處理啥的(具體怎麽鎖我們後邊的文章會嘮叨),在特別大而且多線程並發訪問特別高的情況下,單一的可能會影響請求的處理速度。所以在特別大的時候,我們可以把它們拆分成若幹個小的,每個都稱為一個,它們都是獨立的,獨立的去申請內存空間,獨立的管理各種鏈表,獨立的吧啦吧啦,所以在多線程並發訪問時並不會相互影響,從而提高並發處理能力。我們可以在服務器啟動的時候通過設置的值來修改的個數,比方說這樣:
這樣就表明我們要創建2個。那每個實際占多少內存空間呢?其實使用這個公式算出來的:
也就是總共的大小除以個數,結果就是每個占用的大小。
不過也不是說實例創建的越多越好,分別管理各個也是需要性能開銷的,設計的大叔們規定:Buffer Pool的大小小於1G的時候設置多個實例是無效的,InnoDB會默認把innodb_buffer_pool_instances 的值修改為1。而我們鼓勵在大小大於1G的時候設置多個Buffer Pool實例。
InnoDB中查看的狀態信息
作為MySQL的管理人員,我們有時候需要查看一下中的情況,設計的大叔們給我們提供了這樣的查詢請求,就像這樣(為了突出重點,我們只把輸出中關於的部分提取了出來):
雖然這裏頭的參數我們並不能全部看懂,但是有一些還是很眼熟的:
代表該可以容納多少緩存,註意,單位是!
代表當前還有多少空閑緩存頁,也就是中還有多少個節點。
代表鏈表中的頁的數量,包含和兩個區域的節點數量。
代表鏈表區域的節點數量。
代表臟頁數量,也就是中節點的數量。
代表從區域移動到區域的節點數量,代表區域沒有移動到區域就被淘汰的節點數量。後邊跟著移動的速率。
代表讀取,創建,寫入了多少頁。後別跟著讀取、創建、寫入的速率。
代表中節點的數量。
代表非大小的頁的數量,這些非大小的頁都是被管理的,我們也沒多嘮叨它,看不懂就忽略它吧~
其他的參數我們目前不需要理解,之後遇到的話會仔細說的~
總結
我們可以通過的組合可以定位到某一個具體的。磁盤太慢,用內存作為緩存很有必要。本質上是向操作系統申請的一段連續的內存空間,可以通過來調整它的大小由控制塊和緩存頁組成,每個控制塊和緩存頁都是一一對應的,在填充足夠多的控制塊和緩存頁的組合後,剩余的空間可能產生不夠填充一組控制塊和緩存頁,這部分空間不能被使用,也被稱為。
使用了許多來管理。
中每一個節點都代表一個空閑的緩存頁,在將磁盤中的頁加載到時,會從中尋找空閑的緩存頁。
為了快速定位某個頁是否被加載到,使用作為,緩存頁作為,建立哈希表。
在中被修改的頁稱為,臟頁並不是立即刷新,而是被加入到中,待之後的某個時刻同步到磁盤上。
分為和兩個區域,可以通過來調節區域所占的比例。首次從磁盤上加載到的頁會被放到區域的頭部,如果在間隔時間後該頁該頁沒有被淘汰掉並且仍在區域時,會把它放到鏈表的頭部,也就是區域的頭部。在沒有可用的空閑緩存頁時,會首先淘汰掉區域的一些頁。
我們可以通過指定來控制的個數,每個中都有各自獨立的鏈表,互不幹擾。
可以用下邊的命令查看的狀態信息:
題外話
寫文章挺累的,有時候你覺得閱讀挺流暢的,那其實是背後無數次修改的結果。如果你覺得不錯請幫忙轉發一下,萬分感謝~
轉載於https://view.inews.qq.com/a/20180424G0YLHQ00?uid=&from=singlemessage
原作者:我們都是小青蛙 2018-04-24
原內容有很多文字錯誤,在原內容上進行了更新和修正……
InnoDB的Buffer Pool簡介