Mysql—Innodb Buffer Pool

一、Innodb Buffer Pool簡介

我們知道Mysql是基於磁盤的永久性存儲的一個數據庫,但是磁盤的讀寫速度遠遠趕不上內存的速度,當數據庫訪問量級比較大時,頻繁的磁盤IO不僅速度慢,還有可能造成數據庫的崩潰。爲了緩解這一問題,通過使用內存來彌補磁盤緩慢讀寫的性能,儘量減少磁盤的IO,因而產生了Innodb緩衝池,提高數據庫的速度以及穩定性。

緩衝池緩存全部數據?緩衝池只是緩存了部分數據以及索引等信息,遵循着部分原則,使得緩衝池中緩存着相對較“熱”的數據,如果緩存了全部數據,不僅非常耗費內存,並且維護緩存以及磁盤數據的一致性也是很艱難的。

緩衝池緩存數據單位?數據在緩衝池中以什麼樣的形式存儲呢?上文(Mysql—Innodb引擎邏輯結構)中提到數據在Innodb中是以頁的形式存儲的,頁默認是16k大小,扇區是512b,每次獲取數據都是以頁的形式來返回,在數據庫中有一個概念叫做“局部性原理”,符合條件的某條記錄,其周圍的記錄被讀取的概率更大,因此一般會將目標記錄的周圍數據一起返回,也就是所謂的預讀。緩衝池中也是以頁的形式來緩衝數據以及索引,與數據存儲最小單位一致,便於管理,效率最也比較高。

Innodb buffer pool中主要緩存了數據頁,也就是實體數據,提高查詢的效率,而不是每次都去磁盤io,另外緩衝池中還包含了插入緩衝、自適應哈希、鎖信息等其它緩衝的數據,包含信息如下圖:

在Mysql啓動期間,分配一段連續的內存,平均劃分爲相同的物理塊,物理塊分爲控制體和緩存頁,每個緩存頁會跟隨一個控制體,控制體中包含了表空間編號、頁號以及頁的地址、鎖信息等位置記錄信息,如下圖所示:

二、緩衝池管理

緩衝池內部是以控制體-緩存頁的結構來組織的。管理這些緩存頁的關係,則依託於Innodb中的三個鏈表,FREE鏈表、LRU鏈表以及FLU鏈表。

2.1 FREE鏈表

當我們進行緩存初始化的時候,磁盤中的信息是沒有緩存到緩衝池中的,之後隨着程序的運行,會不斷有磁盤中的頁數據緩存至緩衝池中。

即將要緩存的數據是放在那一個位置上呢?哪些緩存頁是空的?哪些緩存頁是在用的呢?FREE鏈表便是要解決這幾個問題,FREE鏈表記錄了緩衝池中沒有使用到的緩存頁,以鏈表的形式來記錄頁的使用情況,每個節點有指針指向控制塊,控制塊指向緩存頁,以此來指示哪些緩存頁是空閒的,也就是可以被存儲使用的。

當innodb buffer pool初始化分配物理塊之後,會將所有節點放到FREE鏈表中,也就是所有緩存頁都是空閒可用的。我們可以看到在FREE鏈表存儲控制信息,每個節點通過pre和next指針分別指向前後節點,通過ctl指針指向控制體,在FREE鏈表內還會有一個控制頭信息,裏邊指針指向了鏈表的頭和尾,且記錄了總的節點數量(空閒可用緩存頁數量),如下圖所示:

2.2 LRU鏈表

提高緩衝池命中機率?緩衝池的大小是有限的,當我們不斷的從磁盤讀取數據時,有限的緩衝池終究會耗盡,因此要有一個機制保障緩衝池中緩存的是使用頻率比較高的數據,這樣使得70%的數據訪問量走內存查詢,那麼不僅速度提升,數據庫的壓力也會降低,使得有限的緩存發揮了它最大的作用。

當想到如何保障有限的緩存總是緩存最熱的數據時候,我們會想到LRU,LRU全稱爲Latest Recently Used最近最少使用,也是一種算法,當然可以使用這種算法,以此保障使用頻率較高的數據存儲於緩存中。每當FREE鏈表中分配一個節點緩存數據頁時,還需要在該鏈表維護一下已經使用的緩存頁節點。假設有如下LRU鏈表,長度爲10,首尾節點如圖:

當有數據頁需要緩存到緩衝池中時,先去FREE鏈表找到一個空的控制體,然後在LRU鏈表記錄一下使用的控制體節點,假設當前要插入控制體節點13,則頭部插入,尾部需要淘汰,則操作過程如下:

預讀失效?上文提到了預讀功能,那麼相反預讀也會有失效的情況,也就是預讀進來的頁後來並沒有被實際查詢,這樣就造成了在緩衝池中的數據非熱點數據,就會影響性能,爲了避免這種情況,對LRU鏈表進行了優化,宗旨就是減少預讀失效的頁在LRU列表停留時間,讓真正讀取的頁加入頭部。

對於預讀失效解決方式如下,一方面當節點樹量大於512時候,將鏈表分成了新生代(new)和老年代(old),分別佔列表的70%、30%(佔比可可配置),總是從年老代列表尾部進行淘汰,另一方面鏈表首尾相連,中間連接的地方叫做Mid point,結構如下:

 加入當前要插入節點13,則會把13放到old list的頭部,並且把old list尾部節點淘汰,只有當被查詢命中時且滿足一定規則,纔會被放到new list的頭結點,操作過程如下:

緩衝池污染?通過分割鏈表解決了預讀失效的問題,但是若此時有全表掃面該如何處理,按照當前的處理方式,假如有如下的sql:select * form table where name like ‘%ldy%’,我們知道這種方式是會掃描全表的,因此對於LRU鏈表會經歷如下的過程:

1.全表記錄分批返回,加入到old list頭部;

2.遍歷查找頁中記錄匹配條件,此時會把頁放於new list頭部;

3.如此循環往復,直至掃面完畢;

此時全表掃描已經導致緩存數據不是熱點的數據了,因此緩存變成了一個髒緩存,也就是所謂的“緩衝池污染”。爲了解決該問題,加了一個間隔時間innodb_old_blocks_time,第一次訪問不會把這個頁放在new list頭部,當第二次訪問該緩存頁時(該緩存頁沒有被淘汰掉),如果距離上一次訪問的時間小於這個時間,就不會把這個緩存頁放到new list區域,這樣就可以降低在短時間內有大量全表掃描對的緩存命中率的影響,操作過程如下:

現在由於掃面,要插入51,52,53,54的頁到緩存中,

 剛剛進入到old list的頁第一次訪問不會立即放入到new list頭部,並且由於間隔時間innodb_old_blocks_time限制,只有大於該時間纔會放到new list頭部,過程如下:

若沒有上述兩個限制條件,新插入的條件需要查詢遍歷記錄,則可能導致的列表如下:

爲了判斷需要查詢的數據是否在緩存中,在LRU鏈表之上建立一個hash的映射,用以判斷頁是否存在於緩衝池之中。

2.3 FLU鏈表

Innodb引擎的機制是修改數據的時候會先修改緩存中的數據(若數據存在於緩衝池),當我們修改了某個緩存頁的數據,那它就和磁盤上的頁不一致了,這樣的緩存頁也被稱爲髒頁。

解決“髒頁”問題,最簡單的做法就是每發生一次修改就立即同步到磁盤上對應的頁上,但是頻繁的往磁盤中寫數據會嚴重的影響程序的性能。所以每次修改緩存頁後,我們並不着急立即把修改同步到磁盤上,而是在未來的某個時間點進行同步,但是如果不立即同步到磁盤的話,那之後再同步的時候我們怎麼知道中哪些頁是髒頁,哪些頁從來沒被修改過呢?總不能把所有的緩存頁都同步到磁盤上。因此,我們創建一個存儲髒頁的鏈表,凡是修改過的緩存頁都會被包裝成一個節點加入到這個鏈表中,因爲這個鏈表中的頁都是需要被刷新到磁盤上的,維護該鏈表可以分辨哪些緩存頁是需要回寫到磁盤中的。

三、多實例緩衝池

上文可知,Innodb的buffer pool以鏈表的方式來組織分配管理,每個頁是唯一存在的,因此操作緩存頁的時候需要鎖來保證一致性,也就是說free、lru、flu鏈表的使用操作是需要鎖開銷來保證安全的,因此在高併發或者訪問的量比較大時,是比較影響性能的,因此設計出了多buffer pool實例的方式,併發情況下多實例減少鎖開銷,實例和實例之間是隔離的關係,獨立的鏈表,獨立的控制體等。

有利必有弊,多實例雖然增加了緩衝池在高併發、高訪問量下的性能,也同樣增加了引擎管理的開銷,需要引擎去監控、操作各個緩衝池的實例,以保證緩衝池每個都是可用且安全的。

在實際經驗下,普遍認爲,當緩衝內存小於1G的時候,無需使用多實例,當可用緩衝內存大於1G的時候,根據實際併發以及訪問量來評估設定該參數,建議每個buffer pool不小於1G。

四、參數設定

Innodb buffer param
name explain

remark

innodb_buffer_pool_size 緩衝池大小(字節單位,默認128M)  例如:134217728字節
innodb_buffer_pool_instances 緩衝池實例數量(1-64) 設置緩衝池的數量,建議buffer pool大於1G時,在設置此參數
innodb_buffer_pool_chunk_size buffer pool動態調整大小 修改buffer pool時,會以chunk大小來動態申請(參數在服務啓動後不可修改)
innodb_old_blocks_pct LRU old佔比(默認37) LRU鏈表old與new長度約爲3:7
innodb_old_blocks_time LRU鏈表old節點訪問更新間隔時間(默認1000ms) 新節點插入old head,第二次和第一次訪問時間間隔大於該參數,纔將其移動至new head
innodb_read_ahead_threshold 線性預讀異步請求順序頁訪問數(0-64,1extend=64page)

線性預讀:順序訪問頁超過該數值,將下一區(extend)加入緩存;

隨機預讀:順序訪問頁超過該數值,將該區剩餘的頁加入緩存;(默認關閉) 

innodb_random_read_ahead 隨機預讀開關(默認OFF) 控制隨機預讀功能的開關

如何配置緩衝池?緩衝池的大小調整支持脫機和聯機兩種情況。在不關閉數據庫服務的情況下,我們可以通過調整innodb_buffer_pool的大小調節緩衝池大小,調整是有規則的,調整的大小必須是innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances的倍數大小,如果調整的值不滿足此規則,則innodb會調整爲等於或不小於innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances的倍數。innodb_buffer_pool_chunk_size參數以1M的單位進行設置調整,並且只能在啓動之前設置,無法在運行中進行調整。

如何配置多緩衝池?理論上緩衝池越大越好,因爲緩存的數據越多,則磁盤讀取越少,提高性能和效率,在不影響服務器其他服務的前提下,設置大小爲總內存的70%~80%。時下內存資源越來越豐富,當內存足夠大時,可以劃分多個buffer pool實例,在減少鎖開銷的前提下提高性能。mysql5.7下若分配的緩衝內存(innodb_buffer_pool_size)大於1G時,會默認分成8個實例。最好的情況是多實例下,每個實例的大小都大於1G。 

如何設置預讀策略?上文提到過預讀的概念,是局部性原理而引出來的一個數據額外讀取方式,異步的把目前頁的下頁或者下區放在在緩衝池中。預讀分爲線性預讀和隨機預讀。

1.線性預讀(liner read ahead):通過順序訪問緩衝池中的頁面,來預測很快將會訪問到的頁,從而提前放入緩存,提高速度。當順序訪問的頁的數量超過某一個值時,會將下一個區(extend)中的所有頁都加入到緩存中,一個區由64個頁組成(見文章:Mysql—Innodb引擎邏輯結構),innodb中的innodb_read_ahead_threshold參數控制了順序訪問的頁數,默認是56,也就是說,在一個區中順序訪問56個頁時,就會異步的將下一個區(extend)中的所有頁放進緩存中。

2.隨機預讀(random read ahead):當一個extend中的部分頁被隨機訪問後,會把該extend中剩餘的頁異步加入到緩衝池中。因爲這種方式需要innodb維護以及操作方面存在複雜性,是會消耗性能,性價比不高,因此在5.5已經默認是關閉的狀態,如果打開,可以通過參數innodb_random_read_ahead設置爲ON,打開此功能。

五、實時緩衝池運行狀態

若我們調整buffer pool的各種參數時,需要知道當前buffer pool的運行情況,以此來判定是否需要調整部分參數,使得緩衝池更好更高效的運行,避免緩衝池過大導致的內存浪費,或者過小導致頻繁更新而增加引擎負擔,可以通過下面sql查看當前buffer pool的使用情況:show engine innodb status \G,

 部分參數的意義如下表格所示:

buffer pool run
name explain remark
Total large memory 緩衝池總的大小  
Dictionary memory 數據字典的大小  
Buffer pool size 緩衝池頁的總數量  
Free buffers 可使用的頁數量 FREE鏈表節點數量
Database pages 數據頁的數量 LRU鏈表節點數量
Old database pages 優先淘汰列表的數量 LRU鏈表old list的節點數量
Modified db pages 緩衝池中已經修改的頁的數量 髒頁的數量;FLU鏈表數量

六、加入緩存的歷程

當我們一次查詢,查詢到一個頁時,該頁可能會經歷磁盤到緩衝池的過程,過程如下圖:

 1.查詢到一個頁時,首先需要判斷該頁是否在緩存中,依據LRU鏈表的hash映射判斷,該哈希表是通過space id(表空間)和page no(頁編號)一起映射的一個page hash,頁可以根據其space id和page no定位到對應的buffer pool instance以及控制體、緩存頁,若在,則直接返回,若不在,則需要去磁盤查詢。

2.磁盤查詢到數據之後,需要將其放於緩衝池中,首先需要到free list獲取一個空閒的緩存頁,如果緩存還有空間的空間頁,則移除該空閒頁對應的控制體,並且在lru list的old list的head部加入該控制體,然後將緩存頁緩存至該空閒緩存頁中;

3.若緩存頁已經全部用完,此時沒有空閒的空閒頁,則需要額外的操作進行回收空閒頁,以供新的頁進行緩存;

4.先查看lru list是否有可以淘汰的緩存頁,如果有,則將該緩存頁從lru list移除,並加入到free list中,然後在去free list中尋找空閒頁;循環lru list第一次時,最多會找到100個頁,第二次會遍歷整個lru list。

5.如果lru list沒有可以淘汰的緩存頁,接下來會進行單頁刷髒,因爲刷髒是磁盤進行IO,比較耗費性能,所以採用單頁,當單頁刷髒釋放一個緩存頁時,就會到free list進行查找空閒頁,但是我們知道數據庫多線程情況下,可能剛剛釋放的空閒頁可能被其它線程使用,因此如果當前線程依然沒有獲取到空心頁,會重複上面的流程繼續查找。

最糟糕的情況就是用戶線程進來,發現沒有可用的空閒頁,則會走上邊的遍歷列表、刷髒的過程,是不理想的,是需要儘量避免上述這種情況,應該儘量保持free list具有可用性,或者lru list的可替換性。

如何保持緩存頁可用量?爲了爆保持緩衝池中總是有可用的空間頁,避免上述耗性能的情況發生,因此在innodb中會做一些操作來保證空閒頁的可用量。刷髒操作便是很重要的一項工作,也可以叫做checkpoint,目前innodb中刷髒機制可以分爲兩種:

1.Sharp Checkpoint:當數據庫服務關閉時,會將所有的髒頁刷回磁盤,通過參數innodb_fast_shutdown=1來設置;

2.Fuzzy CheckPoint:在運行期間,經由各個參數以及狀態會進行刷髒操作;

其中Fuzzy CheckPoint又分了四種情況:

  • Master Thread Checkpoint
  • FLUSH_LRU_LIST Checkpoint
  • Async/Sync Flush Checkpoint
  • Dirty Page too much Checkpoint​​

Master Thread Checkpoint

Mysql主線程會以十秒的時間間隔從flu list中對一定比例的髒頁進行刷髒的操作,不會阻塞用戶線程;

FLUSH_LRU_LIST Checkpoint

緩衝池中默認要保持free list有100左右的空閒緩存頁可以使用,因此當free list中的空閒頁少於100時,會觸發這種機制。在innodb1.1.x版本之前,需要去檢查空閒頁的數量是否能滿足用戶查詢線程的需求,如果不夠,則會從lru list進行末尾淘汰釋放空閒頁,如果淘汰的頁中有髒頁的話,還需要執行checkpoint刷髒操作,這種操作是會阻塞用戶線程的。在innodb1.2.x版本之後,這個一系列的檢查操作放在了單獨的線程裏Page Cleaner Thread,因此不會阻塞用戶線程,還添加了控制參數innodb_lru_scan_depth,可以設置free list空閒頁的數量。

Async/Sync Flush Checkpoint

當重做日誌文件(redo log)不可用時,需要刷新一定的髒頁到磁盤,使得redo log可以循環使用。在innodb1.2.x之前,async flush checkpoint會阻塞當前用戶線程,sync flush checkpoint會阻塞所有用戶線程,在innodb1.2.x版本之後則使用單獨的線程Page Cleaner Thread處理。

Dirty Page too much Checkpoint​​

Innodb1.2.x有專門的線程進行髒頁的刷新工作,這樣可以減輕對用戶線程的影響,innodb_max_dirty_pages_pct_lwm參數表示髒頁的佔比(flu列表的長度)超過該比例時,會有後臺線程進行刷髒的操作,當髒頁的佔比超過了innodb_max_dirty_pages_pct_lwm,並且還超過了innodb_max_dirty_pages_pct時,會採用勤快刷髒的機制,也就是加快刷髒的速度,

六、資源地址

官網:https://www.mysql.com

文檔:《Mysql技術內幕-innodb存儲引擎》《高性能Mysql》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章