詳細瞭解 InnoDB 記憶體結構及其原理

detectiveHLH發表於2021-04-13

最近發現,文章太長的話,包含的資訊量較大, 並且需要更多的時間去閱讀。而大家看文章,應該都是利用的一些碎片時間。所以我得出一個結論,文章太長不太利於大家的吸收和消化。所以我之後會減少文章的長度,2-3K字就差不多,也能夠快速的閱讀完。

之前寫過一篇文章「簡單瞭解InnoDB原理」,現在回過頭看,其實裡面只是把緩衝池(Buffer Pool),重做日誌緩衝(Redo Log Buffer)、插入緩衝(Insert Buffer)和自適應雜湊索引(Adaptive Hash Index)等概念簡單的介紹了一下。

除此之外還聊了一下MySQL和InnoDB的日誌,和兩次寫,總的來說算是一個入門級別的介紹,這篇文章就來詳細介紹一下InnoDB的記憶體結構

InnoDB記憶體結構

其大致結構如下圖。

InnoDB記憶體的兩個主要區域分別為Buffer PoolLog Buffer,此處的Log Buffer目前是用於快取Redo Log。而Buffer Pool則是MySQL或者說InnoDB中,十分重要、核心的一部分,位於主存。這也是為什麼其訪問資料的效率高,你可以暫時把它理解成Redis那樣的記憶體資料庫,因為我們更新和新增當然它不是,只是這樣會更加方便我們理解。

Buffer Pool

通常來說,宿主機80%的記憶體都應該分配給Buffer Pool,因為Buffer Pool越大,其能快取的資料就更多,更多的操作都會發生在記憶體,從而達到提升效率的目的。

由於其儲存的資料型別和資料量非常多,Buffer Pool儲存的時候一定會按照某些結構去儲存,並且做了某些處理。否則獲取的時候除了遍歷所有資料之外,沒有其他的捷徑,這樣的低效率操作肯定是無法支撐MySQL的高效能的。

因此,Buffer Pool被分成了很多,這在之前的文章中也有講過,這裡不再贅述。每頁可以存放很多資料,剛剛也提到了,InnoDB一定是對資料做了某些操作。

InnoDB使用了連結串列來組織頁和頁中儲存的資料,頁與頁之間形成了雙向連結串列,這樣可以方便的從當前頁跳到下一頁,同時使用LRU(Least Recently Used)演算法去淘汰那些不經常使用的資料。

同時,每頁中的資料也通過單向連結串列進行連結。因為這些資料是分散到Buffer Pool中的,單向連結串列將這些分散的記憶體給連線了起來。

Log Buffer

Log Buffer用來儲存那些即將被刷入到磁碟檔案中的日誌,例如Redo Log,該區域也是InnoDB記憶體的重要組成部分。Log Buffer的預設值為16M,如果我們需要進行調整的話,可以通過配置引數innodb_log_buffer_size來進行調整。

當Log Buffer如果較大,就可以儲存更多的Redo Log,這樣一來在事務提交之前我們就不需要將Redo Log刷入磁碟,只需要丟到Log Buffer中去即可。因此較大的Log Buffer就可以更好的支援較大的事務執行;同理,如果有事務會大量的更新、插入或者刪除行,那麼適當的增大Log Buffer的大小,也可以有效的減少部分磁碟I/O操作。

至於Log Buffer中的資料刷入到磁碟的頻率,則可以通過引數innodb_flush_log_at_trx_commit來決定。

Buffer Pool的LRU演算法

瞭解完了InnoDB的記憶體結構之後,我們來仔細看看Buffer Pool的LRU演算法是如何實現將最近沒有使用過的資料給過期的。

原生LRU

首先明確一點,此處的LRU演算法和我們傳統的LRU演算法有一定的區別。為什麼呢?因為實際生產環境中會存在全表掃描的情況,如果資料量較大,可能會將Buffer Pool中存下來的熱點資料給全部替換出去,而這樣就會導致該段時間MySQL效能斷崖式下跌。

對於這種情況,MySQL有一個專用名詞叫緩衝池汙染。所以MySQL對LRU演算法做了優化。

優化後的LRU

優化之後的連結串列被分成了兩個部分,分別是 New Sublist 和 Old Sublist,其分別佔用了 Buffer Pool 的3/4和1/4。

連結串列的前3/4,也就是 New Sublist 存放的是訪問較為頻繁的頁,而後1/4也就是 Old Sublist 則是反問的不那麼頻繁的頁。Old Sublist中的資料,會在後續Buffer Pool剩餘空間不足、或者有新的頁加入時被移除掉。

瞭解了連結串列的整體構造和組成之後,我們就以新頁被加入到連結串列為起點,把整體流程走一遍。首先,一個新頁被放入到Buffer Pool之後,會被插入到連結串列中 New Sublist 和 Old Sublist 相交的位置,該位置叫MidPoint

該連結串列儲存的資料來源有兩部分,分別是:

  • MySQL的預讀執行緒預先載入的資料
  • 使用者的操作,例如Query查詢

預設情況下,由使用者操作影響而進入到Buffer Pool中的資料,會被立即放到連結串列的最前端,也就是 New Sublist 的 Head 部分。但如果是MySQL啟動時預載入的資料,則會放入MidPoint中,如果這部分資料被使用者訪問過之後,才會放到連結串列的最前端。

這樣一來,雖然這些頁資料在連結串列中了,但是由於沒有被訪問過,就會被移動到後1/4的 Old Sublist中去,直到被清理掉。

優化Buffer Pool的配置

在實際的生產環境中,我們可以通過變更某些設定,來提升Buffer Pool執行的效能。

  • 例如,我們可以分配儘量多的記憶體給Buffer Pool,如此就可以快取更多的資料在記憶體中
  • 當前有足夠的記憶體時,就可以搞多個Buffer Pool例項,減少併發操作所帶來的資料競爭
  • 當我們可以預測到即將到來的大量請求時,我們可以手動的執行這部分資料的預讀請求
  • 我們還可以控制Buffer Pool刷資料到磁碟的頻率,以根據當前MySQL的負載動態調整

那我們怎麼知道當前執行的 MySQL 中 Buffer Pool 的狀態呢?我們可以通過命令show engine innodb status來檢視。這個命令是看 InnoDB 整體的狀態的, Buffer Pool 相關的監控指標包含在了其中,在Buffer Pool And Memory模組中。

樣例如下。

----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 137428992
Dictionary memory allocated 972752
Buffer pool size   8191
Free buffers       4596
Database pages     3585
Old database pages 1303
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 1171, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 655, created 7139, written 173255
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 3585, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

解釋一些關鍵的指標所代表的含義:

  • Total memory allocated:分配給 Buffer Pool 的總記憶體
  • Dictionary memory allocated:分配給 InnoDB 資料字典的總記憶體
  • Buffer pool size:分配給 Buffer Pool 中頁的記憶體大小
  • Free buffers:分配給 Buffer Pool 中 Free List 的記憶體大小
  • Database pages:分配給 LRU 連結串列的記憶體大小
  • Old database pages:分配給 LRU 子連結串列的記憶體大小
  • Modified db pages:當前Buffer Pook中被更新的頁的數量
  • Pending reads:當前等待讀入 Buffer Pool 的頁的數量
  • Pending writes LRU:當前在 LRU 連結串列中等待被刷入磁碟的髒頁數量

都是些很常規的配置項,你可能會比較好奇什麼是 Free List,Free List 中存放的都是未被使用的頁。因為MySQL啟動的時候,InnoDB 會預先申請一部分頁。如果當前頁還未被使用,就會被儲存在 Free List 中。

知道了 Free List,那麼你也應該知道 Flush List,裡面儲存的是所有的髒頁,都是被更改後需要刷入到磁碟的。

自適應雜湊索引

自適應雜湊索引(Adaptive Hash Index)是配合Buffer Pool工作的一個功能。自適應雜湊索引使得MySQL的效能更加接近於記憶體伺服器。

如果要啟用自適應雜湊索引,可以通過更改配置innodb_adaptive_hash_index來開啟。如果不想啟用,也可以在啟動的時候,通過命令列引數--skip-innodb-adaptive-hash-index來關閉。

自適應雜湊索引是根據索引Key的字首來構建的,InnoDB 有自己的監控索引的機制,當其檢測到為當前某個索引頁建立雜湊索引能夠提升效率時,就會建立對應的雜湊索引。如果某張表資料量很少,其資料全部都在Buffer Pool中,那麼此時自適應雜湊索引就會變成我們所熟悉的指標這樣一個角色。

當然,建立、維護自適應雜湊索引是會帶來一定的開銷的,但是比起其帶來的效能上的提升,這點開銷可以直接忽略不計。但是,是否要開啟自適應雜湊索引還是需要看具體的業務情況的,例如當我們的業務特徵是有大量的併發Join查詢,此時訪問自適應雜湊索引被產生競爭。並且如果業務還使用了LIKE或者%等萬用字元,根本就不會用到雜湊索引,那麼此時自適應雜湊索引反而變成了系統的負擔。

所以,為了儘可能的減少併發情況下帶來的競爭,InnoDB對自適應雜湊索引進行了分割槽,每個索引都被繫結到了一個特定的分割槽,而每個分割槽都由單獨的鎖進行保護。其實通俗點理解,就是降低了鎖的粒度。分割槽的數量我們可以通過配置innodb_adaptive_hash_index_parts來改變,其可配置的區間範圍為[8, 512]。

Change Buffer

聊完了 Buffer Pool 中索引相關,剩下的就是 Change Buffer 了。Change Buffer是一塊比較特殊的區域,其作用是用於儲存那些當前不在 Buffer Pool 中的但是又被修改過的二級索引。

用流程來描述一下就是,當我們更新了非聚簇索引(二級索引)的資料時,此時應該是直接將其在Buffer Pool中的對應資料更新了即可,但是不湊巧的是,當前二級索引不在 Buffer Pool 中,此時將其從磁碟拉取到 Buffer Pool 中的話,並不是最優的解,因為該二級索引可能之後根本就不會被用到,那麼剛剛昂貴的磁碟I/O操作就白費了。

所以,我們需要這麼一個地方,來暫存對這些二級索引所做的改動。當被快取的二級索引頁被其他的請求載入到了Buffer Pool 中之後,就會將 Change Buffer 中快取的資料合併到 Buffer Pool 中去。

當然,Change Buffer也不是沒有缺點。當 Change Buffer 中有很多的資料時,全部合併到Buffer Pool可能會花上幾個小時的時間,並且在合併的期間,磁碟的I/O操作會比較頻繁,從而導致部分的CPU資源被佔用。

那你可能會問,難道只有被快取的頁載入到了 Buffer Pool 才會觸發合併操作嗎?那要是它一直沒有被載入進來,Change Buffer 不就被撐爆了?很顯然,InnoDB在設計的時候考慮到了這個點。除了對應的頁載入,提交事務、服務停機、服務重啟都會觸發合併。

歡迎微信搜尋關注【SH的全棧筆記】,回覆【佇列】獲取訊息佇列詳解,包含基礎概念解析和RocketMQ詳細的原始碼,持續更新中。

好了以上就是本篇部落格的全部內容了,如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

相關文章