Mysql的Innodb儲存引擎緩衝池個人理解
綜合借鑑了網上其他同行的博文,增加了一些自己的理解。
在資料庫資料處理中, 緩衝池在改善效能方面扮演著很重要的角色, 為了保證效能, innodb 維護了自己的緩衝池。 文章大體介紹一下innodb緩衝區實現和管理策略。
在innodb中,需要用到資料頁(需要儲存到磁碟的資料)均是從這個緩衝池裡分配出來的, 因此,可以說,緩衝池在對innodb的效能很大的影響。
幾個基本的概念
AWE(Address Windowing Extensions):地址視窗化擴充套件,允許在 32 位版本的 Windows 作業系統上使用 4 GB 以上的實體記憶體。最多可支援 64 GB 的實體記憶體。更多資訊請看
Frame;幀,16K的虛擬地址空間, 在緩衝池的管理上,整個緩衝區是是以大小為16k的frame(可以理解為資料塊)為單位來進行的,frame是innodb中頁的大小。
Page: 頁,16K的實體記憶體, page上存的是需要儲存到磁碟上的資料, 這些資料可能是資料記錄資訊, 也可以是索引資訊或其他的元資料等;
Control Block:控制塊,對於每個frame, 對應一個block, block上的資訊是專門用於進行frame控制的管理資訊, 但是這些資訊不需要記錄到磁碟,而是根據讀入資料塊在記憶體中的狀態動態生成的, 主要包括: 1. 頁面管理的普通訊息,互斥鎖, 頁面的狀態, awe(windows平臺上awe機制的管理資訊)等 2. 髒回寫(flush)管理資訊3. lru控制資訊 4. 快速查詢的管理資訊, 為了便於快速的超找某一個block或frame, 緩衝區裡面的block被組織到一些hash表中; 緩衝區中的block的數量是一定得, innodb緩衝區對所管理的block用lru
互斥訪問
緩衝池的整個緩衝區一個數據結構buf_pool進行管理和控制, 一個專門的mutex保護著, 這個mutex是用來保護buf_pool這個控制結構中的資料域的, 並不保護緩衝區中的資料frame以及用於管理的block, 緩衝區裡block或者frame中的訪問是由專門的讀寫鎖來保護的, 每個block/frame一個。在5.1以前, 每個block是沒專門的mutex保護的,如果需要進行互斥保護,直接使用緩衝區的mutex, 結果導致很高的爭用; 5.1以後,每個block一個mutex對其進行保護, 從而在很大程度上解緩了對buf_pool的mutex的爭用。
記憶體緩衝池:
首先將從磁碟讀到的頁放在緩衝池中,這個過程稱為將頁“FIX”在緩衝池。下次訪問時,若在緩衝池中,則該頁被命中;若不在緩衝池中,讀取磁碟上的頁。修改操作,首先修改緩衝池中的頁,然後再以一定的頻率重新整理到磁碟。通過show variables like ‘innodb_buffer_pool_size’\G;檢視緩衝池大小。
具體的說緩衝池中快取的資料頁型別有:索引頁、資料頁、undo頁、插入緩衝(insert buffer、自適應雜湊索引(adaptive hash index、InnoDB儲存的鎖資訊(lock info、資料字典資訊(data dictionary等。注意區別與MyISAM儲存引擎。
緩衝池管理用到的幾個重要的列表
在緩衝區的管理中, 幾個重要的block列表(雙向連結串列):
關鍵:
IO載入一個page頁到緩衝池中生成一個frame, 同時生成了控制資訊block,他們都是唯一的。
LRU列表引用此block作為一個節點,當frame修改了資料,相應block也發生了改變,flush列表引用此block作為一個髒頁節點。
Mysql插入資料行是實時的,沒經過緩衝(記憶體中只含有部分資料頁,若插入記憶體中的資料頁時,可能與磁碟中的記錄產生主鍵或唯一鍵衝突,
當修改記錄行的主鍵欄位或者唯一鍵欄位時,會實時同步主鍵或唯一鍵索引到磁碟中,檢驗是否發生衝突)。
當主鍵自增時,後一條資料總是插入前一條資料之下,此時磁頭就不需要經過尋道和旋轉,效率很高
插入緩衝,並不是快取的一部分,而是物理頁對於非聚集索引的插入或更新操作,不是每一次直接插入索引頁.而是先判斷插入的非聚集索引頁是否在緩衝池中.如果在,則直接插入,如果不再,則先放入一個插入緩衝區中.然後再以一定的頻率執行插入緩衝和非聚集索引頁子節點的合併操作.使用條件:非聚集索引,非唯一
LRU列表: 用來進行lru管理的列表, 列表裡的每個block所控制的資料都是當前效的資料;列表中的block基本是照訪問的順序排列的;最近被訪問的放在最前面, 最先被訪問的放在最後;lru列表中維護中維護了一個LRU_old, 大概指向整個列表的倒數3/8左右, 當增加一個新的block(主語與最近被訪問的block的區別)進來的時候, 把新的塊剛好放到這個點附近, 具體是前還是後取決於這個LRU_old目前的位置, 這麼做的目的是使新增加進來的block放到一個合適的位置, 不至於放到最先(最近被使用過)或最後(最老)
MySQL的InnoDB引擎設定索引及資料快取池,其中用到的LRU演算法來維持快取的命中率
這裡用到了雙向連結串列來作為緩衝池,每個資料節點稱為block
該演算法採用“中點插入法”:當插入一個新block時,移除表尾最近最少使用的block,在中點插入新block。
這個中點將連結串列分為兩部分:倒數3/8
1.靠近表頭的一部分,為young區,這裡的block是最近使用的節點
2.靠近表尾的一部分,為old區,這裡的block是最近少使用的
該演算法通過連結串列中的block的使用熱度來維持各block的位置,其中old區的block為連結串列滿的時候移除的候選區
具體演算法如下:(類似LinkedHashMap的lru演算法)
1.連結串列的3/8被設定為old區
2.中點不是連結串列的中間點,而是old區的表頭節點,即old區與young區的相鄰的那個節點
3.當讀取的資料不在緩衝池裡的時候,讀取到的block需要插入到連結串列中,插入點為中點,但是插入的新節點為old區的節點,如果此時old區滿了得話,移除表尾的block(LRU節點
4.當讀取old區的block時,該節點將變成“young”節點:此節點移動到young區的表頭(young區的頭部那裡
5.在資料庫操作中,被訪問的節點將移動到young的表頭,這樣一來,在young區中的未被訪問的節點將逐漸往表尾移動,當移動過中點,將變為old區的節點。而old區的節點若被訪問到將變為young節點移動到表頭,而old區中的為被訪問的節點依舊往表尾移動,當表滿時,表尾那個block將會被淘汰掉
LRU List——Latest Recent Used(最近最少使用)
預設大小頁的大小16KB,通過show engine innodb status;可以檢視當前緩衝池的頁數。InnoDB對傳統的LRU演算法進行了優化。在InnoDB中加入了midpoint。傳統的LRU演算法當訪問到的頁不在緩衝區是直接將磁碟頁資料調到緩衝區佇列列頭;而InnoDB並不是直接插入到緩衝區佇列的隊頭,而是插入LRU列表的midpoint位置。這個演算法稱之為midpoint insertion stategy。預設配置插入到列表長度的5/8處。midpoint由引數innodb_old_blocks_pct控制。
midpoint之前的列表稱之為new列表,之後的列表稱之為old列表。可以簡單的將new列表中的頁理解為最為活躍的熱點資料。
好處:不使用樸素的LRU演算法。出於效率考慮,因為可能存在類似於“掃表”等偶然操作,這樣做可以避免將熱點資料替換掉,可能新增到緩衝區的頁是偶然操作用到的頁。
然而mid位置的頁不是永久的。為了解決這個問題,InnoDB儲存引擎引入了innodb_old_blocks_time來表示頁讀取到mid位置之後需要等待多久才會被加入到LRU列表的熱端。可以通過設定該引數保證熱點資料不輕易被刷出。
flush列表: 列表block是那些所管理的資料被修改但是還沒更新到磁碟的髒frame,(即緩衝池中的頁和磁碟上的頁資料產生了不一致),根據修改的先後順序排列的block列表, 最老的放最後。innodb起來後, 主執行緒會定期去檢查緩衝區中存在的髒頁block所佔block總數的比例, 一旦佔比大於srv_max_buf_pool_modified_pct(70%), 就會試圖把一些髒頁flush到磁碟(通過checkpoint機制);除了主執行緒會定期做這個事情外,工作執行緒在進行資料操作時 ,如果發現沒可用的free block, 也會通過flush一些髒頁來騰出空間。
Checkpoint:
1、innodb會批量的把buffer pool中的髒頁以及redo log 重新整理到磁碟,稱之為檢查點.
2、並不是在一次重新整理中重新整理所有的內容,因為這樣會降低mysql的效能,甚至無法提供服務
3、在恢復的過程中,innodb會向前掃描實務日誌,把這些髒資料重新整理到磁碟中
4、innodb迴圈使用它的事務日誌,所以舊的日誌必然在未來某一時刻被覆蓋,innodb必須保證,在舊日誌被覆蓋之前,與這些舊日誌條目相關的髒資料都被重新整理到了磁碟
5、如果這一點不能保證,萬一伺服器crash,buffer pool中的髒頁就永遠也無法恢復了.
6、所以在切換日誌的時候,innodb必然會做檢查點,把所的髒頁都重新整理到磁碟
7、從這個意義上,innodb的事物日誌越大,節省的磁碟IO越多,對系統性能越好.但是crash後恢復的時間肯定會變長
8、innodb的檢查點每隔幾秒鐘就會做一次
9、只是經過日誌切換後,在日誌被衝用前,該日誌的內容必須被全部重新整理到磁碟,否則系統就會hung住
10、嘗試用大一點的事務日誌,可以減少檢查點過程中寫磁碟的次數(之所以節省,是因為IO的合併)
Checkpoint觸發條件:
1、每1秒,若buffer pool中的髒頁比率超過了srv_max_buf_pool_modified_pct = 70%,則進行checkpoint,刷髒頁, flush PCT_IO(100)的dirty pages = 200(引數:innodb_io_capacity 能夠對其定義);若採用adaptive flushing,則計算flush rate,進行必要的flush。
2、每10秒,若buffer pool中的髒頁比率超過了70%,flush PCT_IO(100)的dirty pages,若buffer pool中的髒頁比率未超過70%,flush PCT_IO(10%)的dirty pages = 20;每10s,必定呼叫一次log_checkpoint,做一次checkpoint
髒頁比率 = 需要被flush的頁面數/(使用中的頁面數+空閒頁面數+1)
innodb_adaptive_flushing_lwm —設定redo log flush低水位線,當需要flush的redo log超過這個低水位時,立即強制啟用adaptive flushing,即便沒設定使用adaptive flush 機制
innodb_io_capacity = N —-設定Innodb後臺程序最大的IO效能指標,列如:從buffer pool中重新整理資料頁,從insert buffer中合併資料等.預設值200,在繁忙的OLTP模式下,需要適當提高.
innodb_io_capacity_max = N —設定Innodb_io_capacity_在緊急情況下的上限值
innodb_flushing_avg_loops = N —-設定Innodb統計前N個page flush 速率,避免太快flush
free list : 所空閒block的列表, 當需要分配一個block時, 從中取出一個block。
Free List
資料庫剛啟動的時候,LRU 列表為空,此時需要用到的時候直接將Free列表中的頁刪除,在LRU列表中增加相應的頁,維持頁數守恆。
awe_LRU_free_mapped: 用於方便awe的block列表, 這些block所管理的page已經對映到了frame(實體記憶體對應的虛擬記憶體空間),其中的元素必定也處於free列表或lru列表中。 這個列表會在當分配一個awe頁面時用到。
adaptive hash search: 緩衝區中的頁面是通過雙向列表的方式組織起來的, 如果需要查詢根據頁號查詢某個頁面block的話, 速度不會快,尤其是資料塊多的時候; 為了加速查詢過程,在用雙向列表組織block的時候,也採用了adaptive hash的資料組織, hash的鍵值(key)是頁號(資料頁在所在表空間的編號), 這個在一定程度上能加速block的查詢, 但是當以awe方式管理記憶體的話,這種
hash查詢方式不會啟用
頁面讀入機制
當讀入頁面的時候, 首先需要找到一個可用的block, 這個block或者來自於free list或者lru-list; 在讀入資料塊的時候, block會加上排他鎖以防止其他執行緒再次使用這個block,
會在block中標記出這個塊正處於io狀態, 然後把io請求加入到io呼叫請求對列; 完成讀入操作時, 再釋放block上的排它鎖更新io狀態。
資料頁面的讀取寫入
緩衝池中的資料基本是採用同步aio的方式,這裡的同步aio的意思是: 工作執行緒發出讀或寫請求後,請求會被放到一個讀寫請求佇列中,由專門的io執行緒負責寫盤和讀盤,而工作執行緒則等待io的完成,
注意, 這裡是等待完成,而不會放棄執行,因而稱作為同步aio.
提前讀(預讀)機制
為了優化資料讀入效能, 緩衝區讀入採取了提前讀的機制(當然,這個機制可以配置成不啟用,當然, 提前讀的機制是基於資料區域性性的原理來的,
這就是為什麼採用取值過於隨機的欄位作為主鍵會導致效能降低的原因之一:過於隨機的主鍵會導致提前讀不起作用, 而且會導致更多的換頁行為; 這個預讀取對於上層的功能如索引管理是透明的,
對於上層的功能來說, 需要提供的資訊是需要讀取的頁是否有後繼頁和前置頁。 兩種預讀機制: 線性預讀和隨機預讀
線性預讀: 當第一讀緩衝區中某一個已經存在(注意,這裡必須是已經存在於緩衝區額資料塊)的資料塊時, 會檢查這個資料塊是不是處於所謂的線性預讀區域(比如, 區域大小是64, 當前讀入的也是100,那麼所在的預讀區域是65 ~ 128),如果是, 則統計一下這個區域中目前沒有被訪問過的頁面,如果數量多於預讀機制設定的預讀數量,則放棄本次預讀, 這個其實是檢查目前的區域是否還大量的頁面沒被訪問過, 如果是的話, 自然沒必要去做預讀了; 否則, 取得讀取頁面的後繼頁和前置頁(照資料頁的自然順序?,然後檢查後繼頁或者前置頁是否是一個新區域的邊界,如果是, 則發出讀取
該區域裡面的資料頁面的非同步請求。
隨機讀: 當讀取一個頁面時, 根據所讀取頁的位置計算出該頁面所在的隨機預讀區域, 區域的計算也基於設定的區域頁面數量來計算的(如前面關於線性預讀的例子), 然後根據lru對列的資訊計算該區域中多少塊最近被訪問到了, 如果被訪問的數量達到一定的額度(這個額度是根據預讀區域的大小計算出來的, 5 + 預讀區域大小 / 8),則預讀取該區域中目前還沒讀取到緩衝區的塊.
show innodb status 中關於buffer pool的輸出
Buffer pool size 262144 整個緩衝池中的頁的數量, 包括flush列表中的和lru列表中的,以及被分配出去的頁的數量
Free buffers 0 free列表中的頁的數量
Database pages 258053 分配出去, 正在被使用頁的數量
Modified db pages 37491 flush列表中的數量
Pending reads 0 發出了請求但沒完成的io讀個數
Pending writes: LRU 0, flush list 0, single page 0 發出了請求但沒完成的io讀個數在各個列表上的體現
Pages read 57973114, created 251137, written 10761167, 從磁碟上讀取出來的頁數, 在記憶體中建立了頁面但是沒從磁碟上讀取出來的頁面數以及寫入了的頁面數
9.79 reads/s, 0.31 creates/s, 6.00 在剛過去的時間間隔裡, 平均每秒的讀取數和新建數
Buffer pool hit rate 999 / 1000 命中率, 緩衝區中讀到的頁 / 總共發出的讀頁數
與緩衝池相關的狀態變數及含義
| Innodb_buffer_pool_pages_data 分配出去, 正在被使用頁的數量
| Innodb_buffer_pool_pages_dirty 髒頁但沒被flush除去的頁面數
| Innodb_buffer_pool_pages_flushed 已經flush的頁面數
| Innodb_buffer_pool_pages_free 當前空閒頁面數
| Innodb_buffer_pool_pages_latched 當前被鎖住的頁面數
| Innodb_buffer_pool_pages_misc 用於管理功能的頁面數, 如adaptive hash等
| Innodb_buffer_pool_pages_total 緩衝區總共的頁面數
| Innodb_buffer_pool_read_ahead_rnd 隨機預讀的次數
| Innodb_buffer_pool_read_ahead_seq 線性預讀的次數
| Innodb_buffer_pool_read_requests 總共從緩衝池中快取的頁面中讀取出的頁數
| Innodb_buffer_pool_reads 從磁碟上一頁一頁的讀取的頁數,從緩衝池中讀取頁面, 但緩衝池裡面沒, 就會從磁碟讀取
| Innodb_buffer_pool_wait_free 緩衝池等待空閒頁的次數, 當需要空閒塊而系統中沒有時, 就會等待空閒頁面
| Innodb_buffer_pool_write_requests 緩衝池總共發出的寫請求次數
| Innodb_data_fsyncs 總共完成的fsync次數
| Innodb_data_pending_fsyncs innodb當前等待的fsync次數
| Innodb_data_pending_reads innodb當前等待的讀的次數
| Innodb_data_pending_writes innodb當前等待的寫的次數
| Innodb_data_read | 總共讀入的位元組數
| Innodb_data_reads innodb完成的讀的次數
| Innodb_data_writes innodb完成的寫的次數
| Innodb_data_written 總共寫出的位元組數
關於緩衝池的關鍵的配置變數
innodb_buffer_pool_size: 緩衝池的大小