文章目錄
1、簡述
InnoDB 存儲引擎是基於磁盤存儲的,對數據記錄的管理是以頁爲單位的。由於CPU與磁盤之間在速度上的巨大差距,那麼緩衝池就應運而生了,它的存在提高數據庫的整體性能。
InnoDB 緩衝池是用於在內存中緩存數據和索引的存儲區。與InnoDB Redo Log一起使用,並將數據頁保留在磁盤上,它們構成了I / O的低層抽象:可以將數據視爲頁面,其中每個頁面都通過其表空間ID和頁面ID以及InnoDB進行標識可以以原子方式加載,修改和存儲此類頁面。在此抽象之上,才構建各種主索引和輔助索引的更復雜的結構,這些結構使用這些頁面來存儲樹的節點。
2、數據頁操作邏輯
2.1、讀取頁操作
在數據庫中讀取頁的操作,首先將從磁盤讀取的頁存放在緩衝池中。下一次再讀取數據頁時,先判斷該數據頁是否在緩衝池中,如果在緩衝池中,會直接讀取該數據頁,否則,讀取磁盤上的頁。
2.2、修改頁操作
對數據庫中頁的修改操作,會首先修改在緩衝池中的頁,然後再以一定的頻率刷新到磁盤上。這裏有一點需要注意,頁從緩衝池刷新到磁盤的操作並不是在每次頁發生更新時觸發的,而是通過 Checkpoint 機制刷新到磁盤。
綜上所述,緩衝池的大小直接影響了數據庫的整體性能。隨着內存技術的成熟,內存成本也在不斷下降,因此強烈建議在數據庫專用服務器上,將盡可能多的物理內存分配給緩衝池。
3、緩衝池中數據頁類型
緩衝池中不是隻有緩衝數據頁和緩存索引頁,只是這部分佔緩衝池很大的一部分而已。緩衝池中緩存的數據頁類型有:
索引頁、數據頁、undo頁、插入緩衝(insert buffer)、自適應哈希索引(adaptive hash index)、InnoDB存儲的鎖信息(lock info)、數據字典信息(data dictionary)等。
4、緩衝池組件
4.1、緩衝池實例(Buffer Pool Instance)
在每一個Buffer Pool Instance中,實際都會維護一個自己的Buffer Pool模塊,InnoDB通過16KB Page的方式將數據從文件中讀取到Buffer Pool中,並通過一個LRU List來緩存這些Page,經常訪問的Page在LRU List的前面,不經常訪問的Page在後面。InnoDB訪問一個Page時,首先會從Buffer Pool中獲取,如果未找到,則會訪問數據文件,讀取到Page,並將其put到LRU List中,當一個Instance的Buffer Pool中沒有可用的空閒Page時,會對LRU List中的Page進行淘汰。
InnoDB啓動時會加載配置srv_buf_pool_size和srv_buf_pool_instances,分別是Buffer Pool總大小和需要劃分的Instance數量,當srv_buf_pool_size小於1G時,srv_buf_pool_instances會被重置爲1,單個Buffer Pool Instance的大小計算規則爲:size=srv_buf_pool_size/srv_buf_pool_instances,每個Buffer Pool Instance的大小均相等。在Mysql 8.0中,最大支持64個Buffer Pool Instance,實際Instance在初始化時,爲了加快分配速度,會根據運行環境進行調整並行初始化的數量,詳細流程見Buffer Pool初始化。
在每個Buffer Pool Instance中都有包含自己的鎖,mutex,Buffer chunks,各個頁鏈表,每個Instance之間都是獨立的,支持多線程併發訪問,且一個page只會被存放在一個固定的Instance中,後續會詳細介紹這個算法。
在每個Buffer Pool Instance中還包含一個page_hash的hash table,通過這個page_hash能快速找到LRU List中的page,避免掃描整個LRU List,極大提升了Page的訪問效率。
可以通過參數 innodb_buffer_pool_instances 對緩衝池實例進行配置,默認值是1.
mysql> SHOW VARIABLES LIKE 'innodb_buffer_pool_instances'
-> ;
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| innodb_buffer_pool_instances | 1 |
+------------------------------+-------+
1 row in set (0.01 sec)
4.2、緩衝塊(Buffer chunks)
Buffer chunks是每個Buffer Pool Instance中實際的物理存儲塊數組,一個Buffer Pool Instance中有一個或多個chunk,每個chunk的大小默認爲128MB,最小爲1MB,且這個值在8.0中時可以動態調整生效的。每個Buffer chunk中包含一個buf_block_t的blocks數組(即Page),Buffer chunk主要存儲數據頁和數據頁控制體,blocks數組中的每個buf_block_t是一個數據頁控制體,其中包含了一個指向具體數據頁的*frame指針,以及具體的控制體buf_page_t,後面在數據結構中詳細闡述他們的關係。
4.3、頁鏈表
以下所有的鏈表中的每個節點都是數據頁控制體(buf_page_t)
1、Free List: Free List中存放的都是未曾使用的空閒Page,InnoDB需要Page時從Free List中獲取,如果Free List爲空,即沒有任何空閒Page,則會從LRU List和Flush List中通過淘汰舊Page和Flush髒Page來回收Page。在InnoDB初始化時,會將Buffer chunks中的所有Page加入到Free List中。
2、LRU List: 所有從數據文件中新讀取進來的Page都會緩存在LRU List,並通過LRU策略對這些Page進行管理。LRU List實際劃分爲Young和Old兩個部分,其中Young區保存的是較熱的數據,Old區保存的是剛從數據文件中讀取出來的數據,如果LRU List的長度小於512,則不會將其拆分爲Young和Old區。當InnoDB讀取Page時,首先會從當前Buffer Pool Instance的page_hash查找,並分爲三種情況來處理:
如果在page_hash找到,即Page在LRU List中,則會判斷Page是在Old區還是Young區,如果是在Old區,在讀取完Page後會把它添加到Young區的鏈表頭部
如果在page_hash找到,並且Page在Young區,需要判斷Page所在Young區的位置,只有Page處於Young區總長度大約1/4的位置之後,纔會將其添加到Young區的鏈表頭部
如果未能在page_hash找到,則需要去數據文件中讀取Page,並將其添加到Old區的頭部
LRU List採用非常精細的LRU淘汰策略來管理Page,並且用以上機制避免了頻繁對LRU 鏈表的調整。
3、Flush List: 所有被修改過且還沒來得及被flush到磁盤上的Page(髒頁),都會被保存在這個鏈表中。所有保存在Flush List上的數據都會在LRU List中,但在LRU List中的數據不一定都在Flush List中。在Flush List上的每個Page都會保存其最早修改的lsn,即oldest_modification,雖然一個Page可能被修改多次,但只記錄最早的修改。Flush List上的Page會按照其各自的oldest_modification進行降序排序,鏈表尾部保存oldest_modification最小的Page,在需要從Flush List中回收Page時,從尾部開始回收。
4.4、Mutex
爲保證各個頁鏈表訪問時的互斥,Buffer Pool中提供了對幾個List的Mutex,如LRU_list_mutex用來保護LRU List的訪問,free_list_mutex用來保護Free List的訪問,flush_list_mutex用來保護Flush List的訪問。
4.5、Page_hash
在每個Buffer Pool Instance中都會包含一個獨立的Page_hash,其作用主要是爲了避免對LRU List的全鏈表掃描,通過使用space_id和page_no就能快速找到已經被讀入Buffer Pool的Page。
5、緩衝池LRU算法
爲了提高大容量讀取操作的效率,緩衝池分爲多個頁面,這些頁面可以容納多行數據。爲了提高緩存管理的效率,緩衝池被實現爲頁面的鏈接列表。使用LRU算法的變體將很少使用的數據從緩存中淘汰掉。
普通的LRU(Latest Recent Used,最近最少使用)算法是,頻繁訪問的頁在LRU列表的前端,很少使用的頁在LRU列表的尾端。當緩衝池不能存放新讀取的頁時,將優先釋放LRU列表尾端的頁。
在InnoDB存儲引擎中,LRU 列表中加入了 midpoint 位置。最新訪問的頁並不直接放入LRU列表的頭部,而是放入LRU列表的midpoint位置。這個算法在InnoDB存儲引擎中稱爲 midpoint insertion strategy(中點插入策略)。默認配置中,該位置在LRU列表長度的 5/8 處。midpoint 位置可以由參數 innodb_old_blocks_pct 控制。
mysql> show variables like 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37 |
+-----------------------+-------+
1 row in set (0.01 sec)
由上示例看到,參數 innodb_old_blocks_pct 的默認值是 37,表示新讀取的頁插入到LRU列表尾端的37%的位置(大概3/8位置)。
在InnoDB存儲引擎中,midpoint 之後的列表稱爲old列表,之前的列表稱爲new列表。
該算法將大量頁面保留在新的子列表中。舊的子列表包含較少使用的頁面;這些頁面是驅逐對象。
-
默認情況下,該算法的運行方式如下:
-
3/8的緩衝池專用於舊的子列表。
-
列表的中點是新子列表的尾部與舊子列表的頭相交的邊界。
-
當InnoDB將頁面讀入緩衝池時,它首先將其插入中點(舊子列表的頭部)。
-
訪問舊子列表中的頁面會使其“年輕”,並將其移至新子列表的開頭。如果由於用戶啓動的操作而需要讀取頁面,則立即進行首次訪問,並使頁面年輕。如果由於預讀操作而讀取了該頁面,則第一次訪問不會立即發生,並且在退出該頁面之前可能根本不會發生。
-
隨着數據庫的運行,通過移至列表的末尾,緩衝池中未被訪問的頁面將“老化”。新的和舊的子列表中的頁面都會隨着其他頁面的更新而老化。隨着將頁面插入中點,舊子列表中的頁面也會老化。最終,未使用的頁面到達舊子列表的尾部並被逐出。
默認情況下,查詢讀取的頁面會立即移入新的子列表,這意味着它們在緩衝池中的停留時間更長。例如,針對mysqldump操作或不帶WHERE子句的SELECT語句執行的表掃描會將大量數據帶入緩衝池,並驅逐相當數量的舊數據。同樣,由預讀後臺線程加載且僅訪問一次的頁面將移至新列表的開頭。這些情況可能會將常用頁面推到舊的子列表,在此它們會被逐出。
6、緩衝池配置
可以通過配置緩衝池的各個指標值以提高性能:
1、理想情況下,可以將緩衝池的大小設置爲與實際一樣大的值,從而爲服務器上的其他進程留出足夠的內存以運行而不會進行過多的分頁。緩衝池越大,InnoDB越像內存數據庫,從磁盤讀取一次數據,然後在後續讀取期間從內存訪問數據。參見: Configuring InnoDB Buffer Pool Size
2、在具有足夠內存的64位系統上,可以將緩衝池分成多個部分,以最大程度地減少併發操作之間的內存結構爭用。通俗點說就是增加緩衝池實例的數量。參見:Configuring Multiple Buffer Pool Instances
3、可以將頻繁訪問的數據保留在內存中,而不必考慮操作突然導致的活動高峯,這些操作會將大量不經常訪問的數據帶入緩衝池。參見: Making the Buffer Pool Scan Resistant
4、可以控制何時以及如何執行預讀請求,以異步方式將頁面預取到緩衝池中,從而預期很快將需要這些頁面。參見:Configuring InnoDB Buffer Pool Prefetching (Read-Ahead)
5、可以控制何時進行後臺flush,以及是否根據工作負荷動態調整flush速率。參見:Configuring Buffer Pool Flushing
6、可以配置InnoDB如何保留當前的緩衝池狀態,以避免服務器重啓後的漫長的預熱時間。參見:Saving and Restoring the Buffer Pool State
7、使用InnoDB標準監視器監視緩衝池
可以使用 SHOW ENGINE INNODB STATUS 訪問 InnoDB Standard Monitor 輸出提供有關緩衝池操作的度量。緩衝池指標位於InnoDB Standard Monitor輸出的BUFFER POOL AND MEMORY部分中,如下內容:
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 2198863872
Dictionary memory allocated 776332
Buffer pool size 131072
Free buffers 124908
Database pages 5720
Old database pages 2071
Modified db pages 910
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 4, not young 0
0.10 youngs/s, 0.00 non-youngs/s
Pages read 197, created 5523, written 5060
0.00 reads/s, 190.89 creates/s, 244.94 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not
0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read
ahead 0.00/s
LRU len: 5720, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
InnoDB Standard Monitor 輸出的不是當前 INNODB 存儲引擎的狀態,而是過去某個時間範圍內INNODB 存儲引擎的狀態。
7.1、InnoDB緩衝池指標
注意事項:
1、該youngs/s指標僅適用於舊頁面。它基於對頁面的訪問次數而不是頁面數。可以對給定頁面進行多次訪問,所有訪問都計入在內。如果youngs/s在不進行大掃描時看到非常低的值,則可能需要減少延遲時間或增加用於舊子列表的緩衝池的百分比。增加百分比會使舊的子列表變大,因此該子列表中的頁面需要更長的時間才能移到尾部,這增加了再次訪問這些頁面並使它們變年輕的可能性。
2、該non-youngs/s指標僅適用於舊頁面。它基於對頁面的訪問次數而不是頁面數。可以對給定頁面進行多次訪問,所有訪問都計入在內。如果non-youngs/s在執行大表掃描時看不到較高的值(較高的youngs/s 值),請增加延遲值。
3、該young-making比率說明了對所有緩衝池頁面的訪問,而不僅僅是訪問了舊子列表中的頁面。該young-making速率和 not速率通常不會加總到整個緩衝池的命中率。舊子列表中的頁面命中會導致頁面移動到新的子列表,但是新子列表中的頁面命中只會導致頁面與列表的頭部保持一定距離時才移動到列表的頭部。
4、not (young-making rate)是由於innodb_old_blocks_time未滿足所定義的延遲,或者由於新子列表中的頁面命中並未導致頁面移動到頭部而導致頁面訪問未使頁面年輕化的平均命中率 。此速率說明了對所有緩衝池頁面的訪問,而不僅僅是訪問舊子列表中的頁面。