《MySQL 性能優化》之 InnoDB 存儲引擎

上一篇我們介紹了 MySQL 服務器的體系結構,其中插件式存儲引擎是 MySQL 與其他數據庫管理系統的最大區別。InnoDB 作爲 MySQL 默認的存儲引擎應用最爲廣泛;因此,本篇我們來介紹一下 InnoDB 存儲引擎。

InnoDB 概述

InnoDB 是一個具有高可靠性和高性能的通用存儲引擎,也是 MySQL 5.5 之後的默認存儲引擎。因此,如果CREATE TABLE語句沒有指定ENGINE選項,默認創建的就是 InnoDB 表。

📝使用SHOW VARIABLES LIKE 'default_storage_engine';命令可以查看默認的存儲引擎。

在進一步討論 InnoDB 體系結構之前,我們先介紹幾個 InnoDB 存儲引擎的關鍵特性:

  • InnoDB 表的數據修改操作(DML)具有事務安全性(ACID),支持事務提交、事務回滾以及故障恢復,能夠保障數據的一致性和完整性;
  • InnoDB 採用更細粒度的行級鎖和類似 Oracle 的一致性讀(MVCC),能夠提高併發性和性能。
  • InnoDB 按照主鍵索引(clustered index)的順序組織表中的數據,優化了基於主鍵字段的查詢。
  • InnoDB 支持外鍵約束(FOREIGN KEY),能夠維護多個表之間的數據完整性。

當然,InnoDB 存儲引擎提供的功能遠遠不止與此;正是由於這些強大的功能,使得 MySQL 能夠像 Oracle、Microsoft SQL Server 等商業數據庫一樣大量應用在企業系統中。

InnoDB 系統結構

下圖顯示了 InnoDB 存儲引擎的內存結構和磁盤結構。
InnoDB
記住這張圖可以幫助我們理解 InnoDB 的體系結構,接下來我們分別討論 InnoDB 的內存結構和磁盤結構。

InnoDB 內存結構

InnoDB 提供了自己的內存組件,主要包括緩衝池(Buffer Pool)、變更緩衝(Change Buffer)、日誌緩衝(Log Buffer)以及自適應哈希索引(Adaptive Hash Index)技術。

緩衝池

緩衝池是 InnoDB 在內存中的一個緩衝區域,主要用於緩存訪問過的表和索引等數據。緩衝池利用內存直接處理數據,避免磁盤操作,從而加快了數據處理的速度。

📝在專用的 MySQL 服務器上,通常會給緩衝池分配多達 80% 的物理內存。

以下命令顯示了 InnoDB 緩衝池相關的配置:

mysql> SHOW VARIABLES LIKE 'innodb_buffer_pool%';
+-------------------------------------+----------------+
| Variable_name                       | Value          |
+-------------------------------------+----------------+
| innodb_buffer_pool_chunk_size       | 8388608        |
| innodb_buffer_pool_dump_at_shutdown | ON             |
| innodb_buffer_pool_dump_now         | OFF            |
| innodb_buffer_pool_dump_pct         | 25             |
| innodb_buffer_pool_filename         | ib_buffer_pool |
| innodb_buffer_pool_in_core_file     | ON             |
| innodb_buffer_pool_instances        | 1              |
| innodb_buffer_pool_load_abort       | OFF            |
| innodb_buffer_pool_load_at_startup  | ON             |
| innodb_buffer_pool_load_now         | OFF            |
| innodb_buffer_pool_size             | 8388608        |
+-------------------------------------+----------------+
11 rows in set (0.00 sec)

其中,innodb_buffer_pool_chunk_size 表示每個緩衝塊的大小;innodb_buffer_pool_instances 表示緩衝池的實例個數,每個實例由數量相同的緩衝塊組成;innodb_buffer_pool_size 表示總的緩衝池大小,是 innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances 的倍數。

緩衝池管理算法

爲了提高大量讀取操作時的效率,緩衝池被劃分爲頁(page),每個頁可能包含多行數據。爲了提高緩存管理的效率,緩衝池被實現爲頁組成的鏈接列表。最終緩衝池使用特定的 LRU(最近最少使用)算法進行管理,從而將頻繁訪問的數據保留在緩存中,將最少使用的緩存頁移除。

下圖演示了緩衝池管理的 LRU 算法。
buffer pool
InnoDB 使用 LRU 算法略有改動,緩存池被分爲兩個部分:頭部的 5/8 是最近被訪問過的一個新的子列表,尾部的 3/8 是最近較少訪問的一箇舊的子列表。這個比例由系統變量 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)

當一個新的頁需要被緩存時,最近最少使用的頁將被剔除,新頁將被放入緩存池的新舊子列表的中間;這種方式被稱爲中間點插入策略(midpoint insertion strategy)。用戶提交的操作(例如 SQL 查詢)或者 InnoDB 的預讀(read-ahead)操作都會導致新頁的緩存。

一方面,訪問舊子列表中的頁將會使得它被移動到新子列表的頭部,變得更新。如果是用戶操作引起的訪問,該頁將會立即被移動到新的子列表中;如果是預讀操作引起的訪問,不會立即導致移動,也可能根本不會移動。

另一方面,沒有被訪問的緩存頁將會逐漸被移動到列表的尾部,變得更舊。新子列表和舊子列表中的頁都會隨着其他頁的前移變得更舊;舊子列表中的頁還會隨着新頁的加入變得更舊,最終到達列表的最尾部並且被剔除。

我們可以輸入SHOW ENGINE INNODB STATUS命令,利用 InnoDB 標準監控輸出查看緩衝池的使用指標,相關信息顯示在 BUFFER POOL AND MEMORY 部分:

----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 8585216
Dictionary memory allocated 380485
Buffer pool size   512
Free buffers       249
Database pages     259
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 997, created 142, written 156
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: 259, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

其中的 Buffer pool size 是緩衝池分配的數據頁數量(512),乘以 innodb_page_size(16384)等於緩衝池的大小(8388608)。

📝關於 Buffer Pool 的配置和優化我們將會在 MySQL 實例優化的部分進行介紹。

變更緩衝

變更緩衝緩存了那些不在緩衝池中的二級索引(secondary index)頁的修改操作。INSERTUPDATE或者DELETE操作導致的變更將會在此緩衝,隨後再合併(由其他讀取操作引起)到緩衝池中。下圖演示了變更緩衝的作用過程。

change buffer
與聚集索引(clustered index)不同,二級索引通常是非唯一索引,索引的插入、更新、刪除通常是順序隨機的操作。將變更進行緩存,並且在隨後讀入緩衝池時進行合併,能夠避免將輔助索引頁從磁盤讀入緩衝池所需的大量隨機 I/O。

當系統處於空閒狀態或在緩慢關閉期間運行清除操作,定期將更新後的索引頁寫入磁盤。相對於每次將數據即寫入磁盤,這種清除操作可以更有效地寫入多個連續的索引值。

在內存中,變更緩衝屬於緩衝池的一部分。在磁盤上,變更緩衝屬於系統表空間的一部分;當數據庫服務器關閉時,索引變更將會被緩衝到磁盤中。

系統變量 innodb_change_buffering 決定了何種類型的操作會被緩衝,默認爲 ALL。

如果索引中包含降序索引列或主鍵中包含降序索引列,就不會對二級索引進行變更緩衝。

我們同樣可以輸入SHOW ENGINE INNODB STATUS命令,利用 InnoDB 標準監控輸出查看變更緩衝的狀態信息:

-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
 insert 0, delete mark 0, delete 0
discarded operations:
 insert 0, delete mark 0, delete 0
Hash table size 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 0 buffer(s)
Hash table size 2267, node heap has 1 buffer(s)
Hash table size 2267, node heap has 3 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s

因爲變更緩衝最開始只支持插入操作,所以顯示爲 INSERT BUFFER AND ADAPTIVE HASH INDEX。

📝關於 Change Buffer 的配置和優化我們將會在 MySQL 實例優化的部分進行介紹。

日誌緩衝

日誌緩衝是重做日誌(Redo Log)的內存緩衝,日誌緩衝的大小由變量 innodb_log_buffer_size 決定,默認爲 16 MB。日誌緩衝的內容會定期刷新到磁盤文件。設置一個大的日誌緩衝使得大型事務不必在提交之前將重做日誌數據寫入磁盤。因此,如果存在需要更新、插入或者刪除大量數據的事務,可以通過增加日誌緩衝的大小減少磁盤 I/O。

系統變量 innodb_flush_log_at_trx_commit 用於控制日誌緩衝寫入磁盤的方式。默認值爲 1,即每次事務提交都會刷新緩衝到磁盤,滿足 ACID 特性。

系統變量 innodb_flush_log_at_timeout 用於控制日誌緩衝刷新到磁盤的頻率。默認值爲 1 秒,即每隔 1 秒刷新一次。

📝關於 Log Buffer 的配置和優化我們將會在 MySQL 實例優化的部分進行介紹。

自適應哈希索引

InnoDB 包含了一個監控索引查找的機制,當 InnoDB 發現哈希索引可以提高查詢的性能時會自動創建哈希索引。哈希索引基於索引鍵的一個前綴部分創建,可能只包含了 B+樹索引中的一些值,通常時頻繁訪問的索引頁。

當一個表能夠差不多完全加載到內存中,哈希索引可以直接定位到所有數據,因此能夠提高查詢性能。自適應哈希索引特性由變量 innodb_adaptive_hash_index 設置,默認爲 ON。但是由於它需要佔用緩衝池的內存,只能用於等值查詢,而且只在特定的情況下有效,因此 MySQL 5.6 開始建議關閉該選項。

我們可以利用SHOW ENGINE INNODB STATUS命令查看自適應哈希索引的使用情況,相關的數據也顯示在 INSERT BUFFER AND ADAPTIVE HASH INDEX 部分。

InnoDB 磁盤結構

InnoDB 提供的磁盤存儲組件主要包括表空間(Buffer Pool)、(Table)、索引(Index)、重做日誌(Redo Log)、回滾日誌(Undo Logs)以及雙寫緩衝(Doublewrite Buffer)。

表空間

表空間是一個邏輯上的存儲概念,用於存儲數據表、索引、回滾(Undo)數據等。一個表空間對應操作系統上的一個或者多個文件。從邏輯概念上來說,表空間又是由段(Segment)組成,段由區間(Extent)組成,區間由頁(Page)組成,頁最終由行(Row)組成。
tablespace
一個 InndoDB 表通常對應一個數據段,而區間是磁盤分配的基本單位,頁(默認爲 16 KB)是 InndoDB 管理磁盤的最小單位,與操作系統的頁(通常是 4 KB)概念不同。

InnoDB 提供的表空間包括:系統表空間(System Tablespace)、獨立表空間(File-Per-Table Tablespaces)、通用表空間(General Tablespaces)、回滾表空間(Undo Tablespaces)以及臨時表空間(Temporary Tablespaces)。

系統表空間

系統表空間用於存儲雙寫緩衝和變更緩衝。如果創建表和索引時不使用獨立表空間或通用表空間,它們也會被存儲到系統表空間;不推薦這種做法。在 MySQL 8.0 之前,系統表空間中還包含了 InnoDB 數據字典信息;從 MySQL 8.0 開始, InnoDB 使用統一的 MySQL 數據字典存儲元數據。

系統表空間可以擁有一個或多個數據文件。默認情況下在數據目錄中創建一個名爲 ibdata1 的系統表空間數據文件。系統表空間數據文件的大小和數量由系統參數 innodb_data_file_path 進行控制。

獨立表空間

獨立表空間(File-Per-Table Tablespaces)用於存儲單個 InnoDB 表的數據和索引,每個表空間在文件系統中對應單個數據文件。舉例來說,如果我們爲 test 數據庫創建一個表 t1,MySQL 會在數據目錄下的 test 子目錄中創建一個數據文件 t1.idb。

InnoDB 默認使用獨立表空間創建表,可以使用系統變量 innodb_file_per_table 進行控制。如果禁用該參數,InnoDB 將會默認在系統表空間中創建表。

通用表空間

通用表空間是一種共享的 InnoDB 表空間,可以供多個表和索引使用。通用表空間比獨立表空間具有更高的內存利用率。MySQL 服務器將會緩存表空間的元數據,包含多個表的通用表空間需要的內存比多個獨立表空間更少。

通用表空間可以像獨立表空間一樣在 MySQL 數據目錄內部或者外部創建數據文件,從而爲關鍵的表指定單獨的存儲,例如 RAID 或者 DRBD,提高數據訪問的性能。

通用表空間使用 CREATE TABLESPACE 語句創建。

回滾表空間

回滾表空間用於存儲回滾日誌,回滾日誌記錄中包含了撤銷事務對聚集索引記錄所作的最新修改所需的信息。回滾記錄存儲在回滾日誌段中,回滾日誌段存儲在回滾段中。系統變量 innodb_rollback_segments 決定了每個回滾表空間分配的回滾段數量。

MySQL 實例初始化時會創建兩個回滾表空間。 默認的回滾表空間在 innodb_undo_directory 參數指定的目錄中創建,如果沒有定義該參數,則在數據目錄中創建。默認回滾表空間的數據文件名爲 undo_001 和 undo_002,對應數據字典中的回滾表空間名爲 innodb_undo_001 和 innodb_undo_002。

從 MySQL 8.0.14 開始,可以使用 CREATE UNDO TABLESPACE 增加額外的回滾表空間;一個 MySQL 實例最多可以存在 127 個回滾表空間,包括默認的兩個回滾表空間。

臨時表空間

InnoDB 存在兩種臨時表空間:會話臨時表空間(session temporary tablespaces)和一個全局臨時表空間(global temporary tablespace)。

會話臨時表空間用於存儲用戶創建的臨時表;當 InnoDB 被設置爲磁盤內部臨時表的存儲引擎時,會話臨時表空間也用於優化器創建的內部臨時表。從 MySQL 8.0.16 開始,磁盤內部臨時表的存儲引擎永遠都是 InnoDB;在此之前由參數 internal_tmp_disk_storage_engine 決定 。

系統變量 innodb_temp_tablespaces_dir 決定了會話臨時表空間的文件目錄,默認爲數據目錄下的 #innodb_temp 子目錄。 表 INFORMATION_SCHEMA.INNODB_SESSION_TEMP_TABLESPACES 存儲了會話臨時表空間的元數據,表 INFORMATION_SCHEMA.INNODB_TEMP_TABLE_INFO 存儲了當前活動的用戶臨時表的元數據。

全局臨時表空間(ibtmp1)存儲了用戶臨時表修改信息的回滾段數據。系統變量 innodb_temp_data_file_path 定義了全局臨時表空間數據文件的相對路徑、名稱、大小以及屬性。如果沒有指定該參數,默認在 innodb_data_home_dir 目錄中創建一個名爲 ibtmp1 的自動擴展的數據文件,初始大小略微大於 12 MB。

表和索引

表是數據庫中存儲數據的主要對象,使用CREATE TABLE語句創建。

CREATE TABLE t1 (a INT, b CHAR (20), PRIMARY KEY (a)) ENGINE=InnoDB;

其中,ENGINE 用於指定表的存儲類型;如果不指定,MySQL 默認使用 InnoDB 存儲引擎。使用以下命令查看錶的信息:

mysql> SHOW TABLE STATUS LIKE 't%' \G
*************************** 1. row ***************************
           Name: t1
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 0
 Avg_row_length: 0
    Data_length: 16384
Max_data_length: 0
   Index_length: 0
      Data_free: 0
 Auto_increment: NULL
    Create_time: 2020-02-17 14:32:40
    Update_time: NULL
     Check_time: NULL
      Collation: utf8mb4_0900_ai_ci
       Checksum: NULL
 Create_options:
        Comment:
1 row in set (0.00 sec)

InnoDB 表按照索引的組織方式存儲數據,被稱爲聚簇索引(clustered index)。具體來說,

  • 如果指定了 PRIMARY KEY,InnoDB 使用主鍵作爲聚簇索引。推薦使用生成之後不會更改、不爲空且不重複、經常作爲查詢條件的字段作爲主鍵,例如各種編號。如果沒有邏輯上唯一且非空的字段,可以使用自增字段 AUTO_INCREMENT 作爲主鍵。
  • 如果沒有定義 PRIMARY KEY,MySQL 使用第一個非空且唯一的索引字段作爲 InnoDB 表的聚集索引。
  • 如果沒有定義 PRIMARY KEY 也沒有合適的 UNIQUE,InnoDB 會在內部生成一個 行 ID 字段,並且創建一個隱藏的聚集索引 GEN_CLUST_INDEX 。InnoDB 爲表中的每一行生成一個遞增的 ID 值,並且按照該順序存儲數據。

除了聚簇索引之外的索引被稱爲二級索引(secondary indexes)。InnoDB 二級索引中的每個索引記錄都包含了主鍵索引列的值,以及二級索引的字段。InnoDB 使用主鍵值查找聚集索引中的數據行。因此,如果主鍵字段很長,二級索引就需要佔用更多的磁盤空間,查找的效率就會更低。這也就是爲什麼 InnoDB 推薦使用簡單的數字作爲主鍵。

📝關於索引和優化我們將會在 MySQL 模式優化的部分進行介紹。

雙寫緩衝

雙寫緩衝是系統表空間中的一個存儲區域;在 InnoDB 將緩衝池刷新到數據文件之前,會先將緩衝頁寫入該區域。如果在寫入數據頁的過程中,出現了操作系統、存儲系統或者 mysqld 進程崩潰,InnoDB 可以利用雙寫緩衝存儲的緩衝頁進行故障恢復。

由於 InnoDB 的數據頁大小往往和操作系統數據頁大小不一致,例如 InnoDB 爲 16 KB,操作系統爲 4 KB;此時 InnoDB 刷新一個數據頁,操作系統需要刷新 4 個數據頁,在系統故障時可能只刷新了部分數據頁。雙寫緩衝會先把緩衝池的數據寫入共享表空間,然後再刷新數據頁;如果在這個過程中發生系統崩潰,InnoDB 可以從共享表空間獲取到要刷新的數據,然後重新執行寫入。

雖然數據需要寫入兩次,雙寫緩衝並不會導致兩倍的 I/O 負載或者操作,因爲雙寫緩衝只需要寫入一個連續的數據塊,只有一次 fsync() 系統調用。

雙寫緩衝由系統變量 innodb_doublewrite 控制,默認值爲 ON。如果文件系統或者存儲設備提供了防止部分寫失效的功能,可以禁用雙寫緩衝。

重做日誌

重做日誌用於故障恢復時修復未完成事務的數據,它位於磁盤中,與內存中的日誌緩衝相對應。在正常操作過程中,重做日誌記錄了表中的數據修改信息。當系統出現異常關閉後,重新啓動時自動利用重做日誌恢復未更新到數據文件中的修改。

默認情況下,重做日誌物理上由兩個文件 ib_logfile0 和 ib_logfile1 組成。MySQL 使用循環的方式寫入重做日誌文件。

回滾日誌

回滾日誌由一組回滾日誌記錄組成,這些記錄屬於單個讀寫事務。回滾日誌記錄包含了回滾一個事務對聚集索引記錄的最新修改所需的信息。另外,如果另一個事務需要查看原始的數據(一致性讀),將會從回滾日誌記錄中返回未修改前的數據。

回滾日誌存儲在回滾日誌段中,後者包含在回滾段中;回滾段存儲在回滾表空間以及全局臨時表空間中。

下一篇我們將會討論 MySQL 中的事務管理與併發控制,歡迎關注❤️、點贊👍、轉發📣!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章