上圖是InnoDB存儲引擎的結構。
1、緩衝池
InnoDB存儲引擎是基於磁盤存儲的,並將其中的記錄按照頁的方式進行管理。因此可以看作是基於磁盤的數據庫系統。在數據庫系統中,由於CPU速度和磁盤速度之間的鴻溝,基於磁盤的數據庫系統通常使用緩衝池技術來提高數據庫的性能。
緩衝池簡單來說就是一塊內存區域,通過內存的速度來彌補磁盤速度較慢對數據庫性能的影響。在數據庫進行讀取頁的操作時,首先將從磁盤讀取的頁存放在緩衝池中,這個過程稱爲將頁“FIX”在緩衝池中。下一次再讀相同的頁是,首先判斷該頁是否在緩衝池中。若在緩衝池中,稱該頁在緩衝池中被命中,直接讀取該頁。否則,讀取磁盤上的頁。
對於數據庫中頁的修改操作,則首先修改在緩衝池中的頁,然後再以一定的頻率刷新到磁盤中。但是,頁從緩衝池刷新回磁盤的操作並不是在每次頁發生更新時觸發,而是通過一種稱爲Checkpoint的機制刷新回磁盤。這是爲了提高數據庫的整體性能。
可以通過下面的方式查看MySql數據庫服務器中InnoDB存儲引擎的緩衝池設置的大小,從結果可以看出大小爲134MB左右。
mysql> show variables like 'innodb_buffer_pool_size';
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| innodb_buffer_pool_size | 134217728 |
+-------------------------+-----------+
1 row in set (0.00 sec)
緩衝池中緩存的數據類型有:索引頁,數據頁,undo頁,插入緩衝,自適應哈希索引,InnoDB存儲的鎖信息,數據字典信息等。
下面是InnoDB存儲引擎中內存的結構:
InnoDB存儲引擎中允許有多個緩衝池實例。每個頁根據哈希值平均分配到不同緩衝池實例中。這樣可以減少數據庫內部的資源競爭,增加數據庫的併發處理能力。可以通過設置參數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.00 sec)
2、LRU List、Free List和Flush List
緩衝池是一個很大的內存區域,其中存放了各種類型的頁。那麼InnoDB存儲引擎是怎麼對其進行管理的呢?
- LRU List
數據庫中的緩衝池是通過LRU(Latest Recent Used,最近最少使用)算法來進行管理的。即最頻繁使用的頁在LRU列表的前端,而最少使用的頁在LRU列表的尾端。當緩衝池不能存放新讀取到的頁時,將首先釋放LRU列表中尾部的頁。
InnoDB存儲引擎中,緩衝池中頁的大小默認是16KB,同樣使用LRU算法對緩衝池進行管理。不過,InnoDB存儲引擎對傳統的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.00 sec)
innodb_old_blocks_pct默認是37,表示新讀取的頁插入到LRU列表尾部的37%的位置(其實就是距離尾部3/8的位置,距離首部5/8的位置)。把midpoint之後的LRU列表稱爲old列表,之前的列表稱爲new列表。可以簡單地理解爲new列表中的頁都是最爲活躍的熱點數據。
爲什麼不用樸素的LRU算法,直接將讀取的頁放入到LRU列表的首部呢?
如果直接將讀取的頁放入到LRU的首部,會影響緩衝池的效率。比如,索引或數據的掃描操作需要訪問表中的許多頁,但是這些頁僅僅是在本次查詢時需要,它們並不是活躍的熱點數據。所以,如果把這些非熱點數據頁直接放入LRU列表的首部,那麼非常可能會把之前的熱點數據頁重LRU列表中移除,那麼,在下一次需要讀取該頁的時候,InnoDB存儲引擎需要再次訪問磁盤。
InnoDB存儲引擎引入另一個參數innodb_old_blocks_time,該參數用來表示頁讀取到mid位置後需要等待多久纔會被加入到LRU列表的熱端,也就是LRU列表的new列表。如果增加innodb_old_blocks_time的值,那麼會使LRU列表的熱點數據不那麼快被換出。
mysql> show variables like 'innodb_old_blocks_time';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| innodb_old_blocks_time | 0 |
+------------------------+-------+
1 row in set (0.00 sec)
mysql> set global innodb_old_blocks_time=1000;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'innodb_old_blocks_time';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000 |
+------------------------+-------+
1 row in set (0.00 sec)
如果活躍的熱點數據比較多,可能大於63%,除了上面增加innodb_old_blocks_time的值外,還可以減少innodb_old_blocks_pct的值來減少熱點數據被換出的概率。減少innodb_old_blocks_pct的值使得midpoint的位置後移,new列表的長度增加。
mysql>
mysql> show variables like 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 37 |
+-----------------------+-------+
1 row in set (0.00 sec)
mysql> set global innodb_old_blocks_pct=20;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'innodb_old_blocks_pct';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_old_blocks_pct | 20 |
+-----------------------+-------+
1 row in set (0.00 sec)
- Free List
LRU列表是用來管理已經讀取的頁,但是當數據庫剛剛啓動時,LRU列表是空的,即沒有任何的頁。這時頁都存放在Free列表中,也就是說,用Free列表來管理所有的空閒頁。
當需要從緩衝池中分頁時,首先從Free列表中查找是否有可用的空閒頁,若有則將該頁從Free列表中刪除,放入到LRU列表中。如果Free列表中沒有可用的空閒頁,則根據LRU算法,淘汰LRU列表末尾的頁,將該內存空間分配給新的頁。
當從LRU列表的old部分加入到new部分時,稱此時發生的操作爲page made young,而因爲innodb_old_blocks_time的設置爲導致頁沒有從old部分移動到new部分的操作稱爲page not made young。
通過下面的命令可以查看LRU列表和Free列表的使用情況和運行狀態:
mysql> show engine innodb status\G
下面是部分輸出:
----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 137363456; in additional pool allocated 0
Dictionary memory allocated 256619
Buffer pool size 8191
Free buffers 7734
Database pages 457
Old database pages 0
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 457, created 0, written 0
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: 457, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
可以看出當前Buffer pool size共有8191個頁,即8192*16K,共128M的緩衝池。
Free buffers 表示當前Free列表中頁的數量爲:7734
Database pages表示LRU列表中頁的數量爲:457
(Free buffers 和Database pages的總和可能不等於Buffer pool size,因爲緩衝池中的頁還可能會分配給自適應哈希索引、Lock信息、Insert Buffer等頁,這部分的頁不需要LRU算法進行維護,因此不存在於LRU列表中。)
Pages made young顯示了LRU列表中頁移動到前端的次數。
innodb_old_blocks_time的設置爲導致頁沒有從old部分移動到new部分的操作稱爲page not made young。因爲沒有設置innodb_old_blocks_time,所以not young等於0。
youngs/s, non-youngs/s分別表示每秒這兩類操作的次數。
No buffer pool page gets since the last printout,表示沒有數據從緩衝池中獲取。如果有的話,會顯示變量Buffer pool hit rate,表示緩衝池的命中率。
上面的結果不是當前的狀態,而是過去36秒內的平均值。下面的語句是命令輸出的前面的一行提示:
Per second averages calculated from the last 36 seconds
從InnoDB 1.2版本開始,還可以通過INNODB_BUFFER_POOL_STATUS來觀察緩衝池的運行狀態,如下面的命令:
mysql> use information_schema;
Database changed
mysql> SELECT * FROM INNODB_BUFFER_POOL_STATS\G
*************************** 1. row ***************************
POOL_ID: 0
POOL_SIZE: 8191
FREE_BUFFERS: 7734
DATABASE_PAGES: 457
OLD_DATABASE_PAGES: 0
MODIFIED_DATABASE_PAGES: 0
PENDING_DECOMPRESS: 0
PENDING_READS: 0
PENDING_FLUSH_LRU: 0
PENDING_FLUSH_LIST: 0
PAGES_MADE_YOUNG: 0
PAGES_NOT_MADE_YOUNG: 0
PAGES_MADE_YOUNG_RATE: 0
PAGES_MADE_NOT_YOUNG_RATE: 0
NUMBER_PAGES_READ: 457
NUMBER_PAGES_CREATED: 0
NUMBER_PAGES_WRITTEN: 0
PAGES_READ_RATE: 0
PAGES_CREATE_RATE: 0
PAGES_WRITTEN_RATE: 0
NUMBER_PAGES_GET: 7653
HIT_RATE: 0
YOUNG_MAKE_PER_THOUSAND_GETS: 0
NOT_YOUNG_MAKE_PER_THOUSAND_GETS: 0
NUMBER_PAGES_READ_AHEAD: 0
NUMBER_READ_AHEAD_EVICTED: 0
READ_AHEAD_RATE: 0
READ_AHEAD_EVICTED_RATE: 0
LRU_IO_TOTAL: 0
LRU_IO_CURRENT: 0
UNCOMPRESS_TOTAL: 0
UNCOMPRESS_CURRENT: 0
1 row in set (0.00 sec)
還可以通過INNODB_BUFFER_PAGE_LRU表來觀察每個LRU列表中每個頁的具體信息。
mysql> select * from INNODB_BUFFER_PAGE_LRU\G
下面是命令的輸出的最後兩行:
*************************** 456. row ***************************
POOL_ID: 0
LRU_POSITION: 455
SPACE: 0
PAGE_NUMBER: 658
PAGE_TYPE: INDEX
FLUSH_TYPE: 0
FIX_COUNT: 0
IS_HASHED: NO
NEWEST_MODIFICATION: 0
OLDEST_MODIFICATION: 0
ACCESS_TIME: 3374755154
TABLE_NAME: ch9/operate
INDEX_NAME: op_id
NUMBER_RECORDS: 4
DATA_SIZE: 36
COMPRESSED_SIZE: 0
COMPRESSED: NO
IO_FIX: IO_NONE
IS_OLD: NO
FREE_PAGE_CLOCK: 0
*************************** 457. row ***************************
POOL_ID: 0
LRU_POSITION: 456
SPACE: 0
PAGE_NUMBER: 654
PAGE_TYPE: INDEX
FLUSH_TYPE: 0
FIX_COUNT: 0
IS_HASHED: NO
NEWEST_MODIFICATION: 0
OLDEST_MODIFICATION: 0
ACCESS_TIME: 3374755155
TABLE_NAME: ch9/trigger_time
INDEX_NAME: GEN_CLUST_INDEX
NUMBER_RECORDS: 4
DATA_SIZE: 132
COMPRESSED_SIZE: 0
COMPRESSED: NO
IO_FIX: IO_NONE
IS_OLD: NO
FREE_PAGE_CLOCK: 0
457 rows in set (0.00 sec)
- unzip_LRU列表
InnoDB存儲引擎從1.0.x版本開始支持壓縮頁的功能,即將原本16KB的頁壓縮爲1KB,2KB,4KB,8KB。而由於頁的大小發生了變化,LRU列表也有了一些變化。對於非16KB的頁,是通過unzip_LRU列表進行管理的。
通過下面命令可以查看:
mysql> show engine innodb status\G
下面是部分輸出:
----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 137363456; in additional pool allocated 0
Dictionary memory allocated 256619
Buffer pool size 8191
Free buffers 7734
Database pages 457
Old database pages 0
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 457, created 0, written 0
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: 457, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
可以看到LRU列表有457個頁,而unzip_LRU列表中沒有頁。需要注意的是,LRU中的頁包含了unzip_LRU列表中的頁。
對於壓縮頁的表,每個表的壓縮比率可能不一樣。有的表頁大小爲8K,有的可能是2K。那麼unzip_LRU是怎樣從緩衝池中分配內存的呢?
首先,在unzip_LRU列表中對不同壓縮頁大小的頁進行分別管理。其次,通過夥伴算法進行內存的分配。例如,對需要從緩衝池中申請頁爲4K的大小,其過程如下:
1)檢查4K的unzip_LRU列表,檢查是否有可用的空閒頁;
2)若有,則直接使用;
3)否則,檢查8K的unzip_LRU列表;
4)若能夠得到空閒頁,將頁分成2個4KB的頁,存放到4K的unzip_LRU列表;
5)若不能得到空閒頁,從LRU列表中申請一個16K的頁,將頁分爲1個8K的頁,2個4K的頁,分別存放到對應的unzip_LRU列表中。
可以通過INNODB_BUFFER_PAGE_LRU表來看哪些頁的COMPRESSED_SIZE不等於0。如果不等於0,則表示該頁是unzip_LRU列表的頁。
- Flush列表
在LRU列表中的頁被修改之後,稱該頁爲髒頁,即緩衝池中的頁和磁盤上的頁的數據發生了不一致。這時數據庫會通過Checkpoint機制將髒頁刷新回磁盤,而Flush列表中的頁即爲髒頁列表。需要注意的是,髒頁既存在於LRU列表中,也存在於Flush列表中。LRU列表用來管理緩衝池中頁的可用性,Flush列表用來管理將頁刷新回磁盤,二者互不影響。
通過下面命令查看:
mysql> show engine innodb status\G
下面是命令的部分輸出:
----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 137363456; in additional pool allocated 0
Dictionary memory allocated 256619
Buffer pool size 8191
Free buffers 7734
Database pages 457
Old database pages 0
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 457, created 0, written 0
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: 457, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
其中Modified db pages 顯示的是髒頁的數量。
3、重做日誌緩衝
InnoDB存儲引擎的內存區域除了有緩衝池外,還有重做日誌緩衝(redo log buffer)。InnoDB存儲引擎首先將重做日誌信息先放入到這個緩衝區,然後將其按一定頻率刷新到重做日誌文件中。
重做日誌緩衝一般不需要設置很大,因爲一般情況下每一秒鐘會將重做日誌緩衝刷新到日誌文件,因此用戶只需要保證每秒產生的事務量在這個緩衝大小之內即可。該值可由配置參數innodb_log_buffer_size控制,默認是8MB。
mysql> show variables like 'innodb_log_buffer_size';
+------------------------+---------+
| Variable_name | Value |
+------------------------+---------+
| innodb_log_buffer_size | 8388608 |
+------------------------+---------+
1 row in set (0.00 sec)
通常情況下,8MB的重做日誌緩衝池足以滿足絕大部分的應用,因爲重做日誌在下列三種情況下會做重做日誌緩衝中的內容刷新到外部磁盤的重做日誌文件中。
1)Master Thread每一秒將重做日誌緩衝刷新到重做日誌文件;
2)每個事務提交時會將重做日誌緩衝刷新到重做日誌文件;
3)當重做日誌緩衝池剩餘空間小於1/2時,重做日誌緩衝刷新到重做日誌文件;
4、額外的內存池
額外的內存池通常被DBA忽略。
在InnoDB存儲引擎中,對內存的管理是通過一種稱爲內存堆的方式進行的。在對一些數據結構本身的內存進行分配時,需要從額外的內存池中進行申請,當該區域的內存不夠時,會從緩衝池中進行申請。
比如,分配了緩衝池,但是每個緩衝池中的幀緩衝還有對應的緩衝控制對象,這些對象記錄了一些諸如LRU、鎖、等待等信息,而這個對象的內存需要從額外內存池中申請。因此,在申請了很大的InnoDB緩衝池時,也應考慮相應地增加這個值。