詳細瞭解 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

該鏈表存儲的數據來源有兩部分,分別是:

默認情況下,由用戶操作影響而進入到 Buffer Pool 中的數據,會被立即放到鏈表的最前端,也就是 New Sublist 的 Head 部分。但如果是 MySQL 啓動時預加載的數據,則會放入 MidPoint 中,如果這部分數據被用戶訪問過之後,纔會放到鏈表的最前端。

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

優化 Buffer Pool 的配置

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

那我們怎麼知道當前運行的 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]

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

都是些很常規的配置項,你可能會比較好奇什麼是 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