InnoDB儲存引擎——記憶體

readyao發表於2017-03-10

這裡寫圖片描述

上圖是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緩衝池時,也應考慮相應地增加這個值。

相關文章