一、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。
四、參數設定
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,
部分參數的意義如下表格所示:
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時,會採用勤快刷髒的機制,也就是加快刷髒的速度,
六、資源地址
文檔:《Mysql技術內幕-innodb存儲引擎》《高性能Mysql》