詳細瞭解 InnoDB 內存結構及其原理
InnoDB 內存結構
其大致結構如下圖。
InnoDB 內存的兩個主要區域分別爲 Buffer Pool 和 Log 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 在設計的時候考慮到了這個點。除了對應的頁加載,提交事務、服務停機、服務重啓都會觸發合併。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/KOt4_xpuArfswP7GmidoZQ