6 mysql底層解析——緩存,Innodb_buffer_pool,包括連接、解析、緩存、引擎、存儲等

前面幾篇主要學習存儲,在磁盤上的存儲結構,內部格式等。

這一篇就來看內存,對於Innodb來說,也就是最關鍵的Innodb_buffer_pool。

我們都知道,內存讀寫和磁盤讀寫的速度不在一個數量級,在數據庫中,數據都是最終落到磁盤上的,想要達成快速的讀寫,必然要依靠緩存技術。

Innodb的這個緩存區就是Innodb_buffer_pool,當讀取數據時,就會先從緩存中查看是否數據的頁(page)存在,不存在的話纔去磁盤上檢索,查到後緩存到這個pool裏。同理,插入、修改、刪除也是先操作緩存裏數據,之後再以一定頻率更新到磁盤上。控制刷盤的機制,叫做Checkpoint。

Innodb_buffer_pool內部結構

注意,左邊那兩個不在Innodb_buffer_pool裏,是另外一塊內存。只不過大部分的內存都屬於Innodb_buffer_pool的。

mysql安裝後,默認pool的大小是128M,可以通過show variables like 'innodb_buffer_pool%';命令查看。

可以通過show global status like '%innodb_buffer_pool_pages%';  查看已經被佔用的和空閒的。共計8000多個page。

所以如果你的數據很多,而pool很小,那麼性能就好不了。

理論上來說,如果你給pool的內存足夠大,夠裝下所有數據,要訪問的所有數據都在pool裏,那麼你的所有請求都是走內存,性能將是最好的,和redis類似。

官方建議pool的空間設置爲物理內存的50%-75%。

在mysql5.7.5之後,可以在mysql不重啓的情況下動態修改pool的size,如果你設置的pool的size超過了1G的話,應該再修改一下Innodb_buffer_pool_instances=N,將pool分成N個pool實例,將來操作的數據會按照page的hash來映射到不同的pool實例。

這樣可以大幅優化多線程情況下,併發讀取同一個pool造成的鎖的競爭。

緩衝區LRU淘汰算法

當pool的大小不夠用了,滿了,就會根據LRU算法(最近最少使用)來淘汰老的頁面。最頻繁使用的頁在LRU列表的前端,最少使用的頁在LRU列表的尾端。淘汰的話,就首先釋放尾端的頁。

InnoDB的LRU和普通的不太一樣,Innodb的加入了midpoint位置的概念。最新讀取到的頁,並不是直接放到LRU列表的頭部的,而是放到midpoint位置。這個位置大概是LRU列表的5/8處,該參數由innodb_old_blocks_pct控制。

如37是默認值,表示新讀取的頁插入到LRU尾端37%的位置。在midpoint之後的列表都是old列表,之前的是new列表,可以簡單理解爲new列表的頁都是最活躍的數據。

爲什麼不直接放頭部?因爲某些數據掃描操作需要訪問的頁很多,有時候這些頁僅僅在本次查詢有效,以後就不怎麼用了,並不算是活躍的熱點數據。那麼真正活躍的還是希望放到頭部去,這些新的暫不確定是否真正未來要活躍。所以,這可以理解爲預熱。還引入了一個參數innodb_old_blocks_time用來表示頁讀取到mid位置後需要等待多久纔會被加入到LRU列表的熱端。

還有一個重要的查詢命令可以看到這些信息,show engine innodb status;

Database pages表示LRU列表中頁的數量,pages made young顯示了LRU列表中頁移動到前端的次數,Buffer pool hit rate表示緩衝池的命中率,100%表示良好,該值小於95%時,需要考慮是否因爲全表掃描引起了LRU列表被污染。裏面還有其他的參數,可以自行查閱一下代表什麼意思。

Pool的主要空間

上面主要是講了insert buffer。對應的是增刪改時的緩存處理。

從最上面的圖能看到,其實更多的、對性能影響更大的是讀緩存。畢竟多數數據庫是讀多寫少。

讀緩存主要數據是索引頁和數據頁,這個前面也說過,如果要讀取的數據在pool裏沒有,那就去磁盤讀,讀到後的新頁放到pool的3/8位置,後續根據情況再決定是否放到LRU列表的頭部。注意,最小單位是頁,哪怕只讀一條數據,也會加載整個頁進去。如果是順序讀的話,剛好又在同一個頁裏,譬如讀了id=1的,那麼再讀id=2的時,大概率直接從緩存裏讀。 

插入緩衝insert buffer

從名字就能看出來是幹什麼的,它是buffer_pool的一部分,用來做insert操作時的緩存的。

我們之前學習過b+ tree,也知道數據的存放格式,那麼當新插入數據時,倘若直接就插入到b+ tree裏,那麼可想會多麼緩慢,需要讀取、找到要插入的地方,還要做樹的擴容、校驗、尋址、落盤等等一大堆操作,等你插進去,姑娘都等老了。

在Innodb中,主鍵是行唯一標識,如果你的插入順序是按照主鍵遞增進行插入的,那麼還好,它不需要磁盤的隨機讀取,找到了頁,就能插,這樣速度還是可以的。

然而,如果你的表上有多個別的索引(二級索引),那麼當插入時,對於那個二級索引樹,就不是順序的了,它需要根據自己的索引列進行排序,這就需要隨機讀取了。二級索引越多,那麼插入就會越慢,因爲要尋找的樹更多了。還有,如果你頻繁地更新同一條數據,倘若也頻繁地讀寫磁盤,那就不合適了,最好是將多個對同一page的操作,合併起來,統一操作。

所以,Innodb設計了Insert Buffer,對於非聚簇索引的插入、更新操作,不是每次都插入到索引頁中,而是先判斷該二級索引頁是否在緩衝池中,若在,就直接插入,若不在,則先插入一個insert buffer裏,再以一定的頻率進行真正的插入到二級索引的操作,這時就可以聚合多個操作,一起去插入,就能提高性能。

然而,insert buffer需要同時滿足兩個條件時,纔會被使用:

    1 索引是二級索引

    2 索引不是unique

注意,索引不能是unique,因爲在插入緩衝時,數據庫並不去查詢索引頁來判斷插入的記錄的唯一性,如果查找了,就又會產生隨機讀取。

insert buffer的問題是,在寫密集的情況下,內存會佔有很大,默認最大可以佔用1/2的Innodb_buffer_pool的空間。很明顯,如果佔用過大,就會對其他的操作有影響,譬如能緩存的查詢頁就變少了。可以通過IBUF_POOL_SIZE_PER_MAX_SIZE來進行控制。

變更緩衝change buffer

新版的Innodb引入了Change buffer,其實就是insert buffer類似的東西,只是把insert、update、delete都進行緩衝。

也就是所有DML操作,都會先進緩衝區,進行邏輯操作,後面纔會真正落地。

通過參數Innodb_change_buffering開始查看修改各種buffer的選項。可選值有inserts\deletes\purges\changes\all\none。

默認是所有操作都入buffer,右圖的參數是控制內存大小的,25代表最多使用1/4的緩衝池空間。

Insert Buffer原理

insert buffer的數據結構是一棵B+ tree,全局就一棵B+樹,負責對所有的表的二級索引進行插入緩存。在磁盤上,該tree存放在共享表空間(希望還記得是什麼),默認ibdata1中。有時,通過獨立表空間的ibd文件試圖恢復表中數據時,可能會有CHECK TABLE錯誤,就是因爲該表的二級索引中的數據可能還在insert buffer裏,沒有刷新到自己的表空間。這時,可以通過repair table來重建表上的所有二級索引。

我們下面來看看這棵B+ tree裏是什麼樣的。

首先,這裏存的值將來是要刷到二級索引的,我們至少要知道的信息有:哪個表、表的哪個頁面(page)。

所以,insert buffer的b+ tree的內節點(非葉子節點)存放的是查詢的search key

space存的是哪個表,offset是所在頁的偏移量,可以理解爲pageNo。

當發起了一次插入、更新時,首先判斷要操作的數據的頁(是二級索引的頁)是否已經在Innodb_buffer_pool裏了,如果在,說明之前可能是查詢過該頁的數據,既然在緩存了,那就不需要insert buffer了,直接去修改pool裏該頁的數據即可。

如果不在,那就需要構造search key了,構造好,再加上被插入、修改的數據,插入到insert buffer的葉子節點裏去。

何時Merge insert buffer

insert buffer是一棵b+ tree,如果插入、修改的記錄的二級索引沒在pool裏,就需要將記錄插到insert buffer。那麼什麼時候,insert buffer裏的數據會被merge到真正的二級索引裏去呢?

1  二級索引被讀取到pool時

2 insert buffer已無可用空間

3 master thread主線程後臺刷入

第一種情況好理解,因爲寫到insert buffer就是因爲該記錄的二級索引頁不在pool裏,現在如果被select到pool裏去了,那麼自然就會直接merge過去。

第二種情況,那就是沒可用空間了,迫不得已,就得去刷磁盤了。

第三種,之前的文章還沒提到過,那就是有個master線程每秒或每10秒回進行一次merge insert buffer的操作,不同之處是每次merge的數量不同。

CheckPoint技術(redo log)

上面主要說了insert buffer,它是針對二級索引的插入、修改、刪除的緩存,並且是數據頁不在pool裏才用的。

那如果數據頁在pool裏,發生了增刪改操作後,系統又是何時將數據落地刷入到磁盤呢?

你執行了一條DML語句,pool的頁就變成了髒頁,因爲pool裏的比磁盤裏的新,兩者並不一致。數據庫就需要按一定規則將髒頁刷入到磁盤。

倘若每次一個頁發生變化就刷入磁盤,那開銷是非常大的,必須要攢夠一定數量或一段時間,再去刷入。還有,pool是內存,倘若還沒來得及刷入到磁盤,發生了宕機,那麼這些髒頁數據就會丟失。

爲了解決上面的問題,當前事務數據庫系統普通採用了write ahead log策略,即當事務提交時,先寫重做日誌(redo log),再修改頁。當發生故障時,通過redo log來進行數據的恢復。

到這時,基本Innodb的增刪改查的流程,基本清晰了。

增刪改時,首先順序寫入redo log(順序寫磁盤,類似於kafka),然後修改pool頁(pool裏沒有的,插入insert buffer),之後各種線程,會按照規則從緩存裏將數據刷入到磁盤,進行持久化,發生故障了,就從redo log恢復。

checkpoint(檢查點)做的事情就是:

1 緩衝池不夠用時,將髒頁刷新到磁盤

2 redo log不夠用時,刷新髒頁。

redo log也是磁盤文件,並不能無限增長,而且要循環利用。

Checkpoint所做的事情就是將緩衝池髒頁寫回磁盤,那麼主要就是每次刷多少,每次從哪裏去髒頁,什麼時間去刷的問題了。

目前有兩種Checkpoint:

1 數據庫關閉時,將所有髒頁刷新到磁盤,這是默認的方式。

2 Master Thread操作,這個主線程會每秒、每10秒從髒頁列表刷新一定比例的頁到磁盤,這是個異步的操作,不會阻塞查詢。

3 LRU 列表空閒頁不足時,需要刷新一部分來自LRU列表的髒頁。

4 redo log文件不可用時,需要強制刷新一部分,爲了保證redo log的循環利用。

5 pool空間不足時,髒頁太多,需要刷新。

 

兩次寫

這是Innodb的一個很獨特的功能,是用來保證數據頁的可靠性。

我仔細看過後,感覺設計很巧妙,但好像離我們比較遠了,所以就不寫了,想深入瞭解的可以自己去查查。

 

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