簡介: 在 MySQL 8.0 之前,Server 層和存儲引擎(比如 InnoDB)會各自保留一份元數據(schema name, table definition 等),不僅在信息存儲上有着重復冗余,而且可能存在兩者之間存儲的元數據不同步的現象。不同存儲引擎之間(比如 InnoDB 和 MyISAM)有着不同的元數據存儲形式和位置(.FRM, .PAR, .OPT, .TRN and .TRG files),造成了元數據無法統一管理。此外,將元數據存放在不支持事務的表和文件中,使得 DDL 變更不會是原子的,crash recovery 也會成為一個問題。
來源 | 阿里技術公眾號
一 背景
在 MySQL 8.0 之前,Server 層和存儲引擎(比如 InnoDB)會各自保留一份元數據(schema name, table definition 等),不僅在信息存儲上有着重復冗余,而且可能存在兩者之間存儲的元數據不同步的現象。不同存儲引擎之間(比如 InnoDB 和 MyISAM)有着不同的元數據存儲形式和位置(.FRM, .PAR, .OPT, .TRN and .TRG files),造成了元數據無法統一管理。此外,將元數據存放在不支持事務的表和文件中,使得 DDL 變更不會是原子的,crash recovery 也會成為一個問題。
為了解決上述問題,MySQL 在 8.0 中引入了 data dictionary 來進行 Server 層和不同引擎間統一的元數據管理,這些元數據都存儲在 InnoDB 引擎的表中,自然的支持原子性,且 Server 層和引擎層共享一份元數據,不再存在不同步的問題。
二 整體架構
data dictionary 提供了統一的 client API 供 Server 層和引擎層使用,包含對元數據訪問的 acquire() / drop() / store() / update() 基本操作。底層實現了對 InnoDB 引擎存放的數據字典表的讀寫操作,包含開表(open table)、構造主鍵、主鍵查找等過程。client 和底層存儲之間通過兩級緩存來加速對元數據對象的內存訪問,兩級緩存都是基於 hash map 實現的,一層緩存是 local 的,由每個 client(每個線程對應一個 client)獨享;二級緩存是 share 的,為所有線程共享的全局緩存。下面我將對 data dictionary 的數據結構和實現架構做重點介紹,也會分享一個支持原子的 DDL 在 data dictionary 層面的實現過程。
三 metadata 在內存和引擎層面的表示
data dictionary (簡稱DD)中的數據結構是完全按照多態、接口/實現的形式來組織的,接口通過純虛類來實現(比如表示一個表的 Table),其實現類(Table_impl)為接口類的名字加 _impl 后綴。下面以 Table_impl 為例介紹一個表的元數據對象在 DD cache 中的表示。
1 Table_impl
Table_impl 類中包含一個表相關的元數據屬性定義,比如下列最基本引擎類型、comment、分區類型、分區表達式等。
Table_impl 也是代碼實現中 client 最常訪問的內存結構,開發者想要增加新的屬性,直接在這個類中添加和初始化即可,但是僅僅如此不會自動將該屬性持久化到存儲引擎中。除了上述簡單屬性之外,還包括與一個表相關的復雜屬性,比如列信息、索引信息、分區信息等,這些復雜屬性都是存在其他的 DD 表中,在內存 cache 中也都會集成到 Table_impl 對象里。
從 Abstract_table_impl 繼承來的 Collection m_columns 就表示表的所有列集合,集合中的每一個對象 Column_impl 表示該列的元信息,包括數值類型、是否為 NULL、是否自增、默認值等。同時也包含指向 Abstract_table_impl 的指針,將該列與其對應的表聯系起來。
此外 Table_impl 中也包含所有分區的元信息集合 Collection m_partitions,存放每個分區的 id、引擎、選項、范圍值、父子分區等。
因此獲取到一個表的 Table_impl,我們就可以獲取到與這個表相關聯的所有元信息。
2 Table_impl 是如何持久化存儲和訪問的
DD cache 中的元信息都是在 DD tables 中讀取和存儲的,每個表存放一類元信息的基本屬性字段,比如 tables、columns、indexes等,他們之間通過主外鍵關聯連接起來,組成 Table_impl 的全部元信息。DD tables 存放在 mysql 的表空間中,在 release 版本對用戶隱藏,只能通過 INFORMATION SCHEMA 的部分視圖查看;在 debug 版本可通過設置 SET debug='+d,skip_dd_table_access_check' 直接訪問查看。比如:
通過以上 mysql.tables 的表定義可以獲得存儲引擎中實際存儲的元信息字段。DD tables 包括 tables、schemata、columns、column_type_elements、indexes、index_column_usage、foreign_keys、foreign_key_column_usage、table_partitions、table_partition_values、index_partitions、triggers、check_constraints、view_table_usage、view_routine_usage 等。
Storage_adapter 是訪問持久存儲引擎的處理類,包括 get() / drop() / store() 等接口。當初次獲取一個表的元信息時,會調用 Storage_adapter::get() 接口,處理過程如下:
上述在獲取列和屬性的對應關系時,根據的是 Tables 對象的枚舉類型下標,按順序包含了該類型 DD 表中的所有列,與上述表定義是一一對應的。因此如果我們需要新增 DD 表中存儲的列時,也需要往下面枚舉類型定義中加入對應的列,並且在 Table_impl::restore_attributes() / Table_impl::store_attributes() 函數中添加對新增列的讀取和存儲操作。
四 多級緩存
為了避免每次對元數據對象的訪問都需要去持久存儲中讀取多個表的數據,使生成的元數據內存對象能夠復用,data dictionary 實現了兩級緩存的架構,第一級是 client local 獨享的,核心數據結構為 Local_multi_map,用於加速在當前線程中對於相同對象的重復訪問,同時在當前線程涉及對 DD 對象的修改(DDL)時管理 committed、uncommitted、dropped 幾種狀態的對象。第二級就是比較常見的多線程共享的緩存,核心數據結構為 Shared_multi_map,包含着所有線程都可以訪問到其中的對象,所以會做並發控制的處理。
兩級緩存的底層實現很統一,都是基於 hash map 的,目前的實現是 std::map。Local_multi_map 和 Shared_multi_map都是派生於 Multi_map_base。
Multi_map_base 對象實現了豐富的 m_map() 模板函數,可以很方便的根據 key 的類型不同選擇到對應的 hash map。
Shared_multi_map 與 Local_multi_map 的不同在於,Shared_multi_map 還引入了一組 latch 與 condition variable 用於並發訪問中的線程同步與 cache miss 的處理。同時對 Cache_element 對象做了內存管理和復用的相關能力。
1 局部緩存
一級緩存位於每個 Dictionary_client (每個 client 與線程 THD 一一對應)內部,由不同狀態(committed、uncommitted、dropped)的 Object_registry 組成。每個 Object_registry 由不同元數據類型的 Local_multi_map 組成,用於管理不同類型的對象(比如表、schema、字符集、統計數據、Event 等)緩存。
2 共享緩存
共享緩存是 Server 全局唯一的,使用單例 Shared_dictionary_cache 來實現。與上述局部緩存中 Object_registry 相似,Shared_dictionary_cache 也需要包含針對各種類型對象的緩存。與 Multi_map_base 實現根據 key 類型自動選取對應 hash map 的模版函數相似,Object_registry 和 Shared_dictionary_cache 也都實現了根據訪問對象的類型選擇對應緩存的 m_map() 函數,能夠很大程度上簡化函數調用。
3 緩存獲取過程
用戶通過 client 調用元數據對象獲取函數,傳入元數據的 name 字符串,然后構建出對應的 name key,通過 key 去緩存中獲取元數據對象。獲取的整體過程就是一級局部緩存 -> 二級共享緩存 -> 存儲引擎。
Cache miss
共享緩存的獲取過程在 Shared_multi_map::get() 中實現。就是加鎖后直接的 hash map 查找,如果存在則給引用計數遞增后返回;如果不存在,就會進入到 cache miss 的處理過程,調用上面介紹的存儲引擎的接口 Storage_adapter::get() 從 DD tables 中讀取,創建出來后依次加入共享緩存和局部緩存 committed registry 中。
當第一個 client 獲取到完整的 DD cache object,加入到共享緩存之后,移除 m_missed 集合中對應的 key,並通過廣播的方式通知之前等待的線程重新在共享緩存中獲取。
五 Auto_releaser
Auto_releaser 是一個 RAII 類,基本上在使用 client 訪問 DD cache 前都會做一個封裝,保證在整個 Auto_releaser 對象存在的作用域內,所獲取到的 DD cache 對象都會在局部緩存中存在不釋放。Auto_releaser 包含需要 release 的對象 registry,通過 auto_release() 函數收集着當前 client 從共享緩存中獲取到的 DD cache 對象,在超出其作用域進行析構時自動 release 對象,從局部緩存 committed 的 registry 中移除對象,並且在共享緩存中的引用計數遞減。
在嵌套函數調用過程中,可能在每一層都會有自己的 Auto_releaser,他們之間通過一個簡單的鏈表指針連接起來。在函數返回時將本層需要 release 的對象 release 掉,需要返回給上層使用的 DD cache 對象交給上層的 Auto_releaser 來負責。通過 transfer_release() 可以在不同層次的 Auto_releaser 對象間轉移需要 release 的對象,可以靈活的指定不再需要 DD cache 對象的層次。
六 應用舉例:inplace DDL 過程中對 DD 的操作
在 MySQL inplace DDL 執行過程中,會獲取當前表定義的 DD cache 對象,然后根據實際的 DDL 操作內容構造出新對應的 DD 對象。然后依次調用 client 的接口完成對當前表定義的刪除和新表定義的存儲。
在 drop() 過程中,會將當前表定義的 DD cache 對象對應的數據從存儲引擎中刪除,然后從共享緩存中移除(這要求當前對象的引用計數僅為1,即只有當前線程使用),之后加入到 dropped 局部緩存中。
在 store() 過程中,會將新的表定義寫入存儲引擎,並且將對應的 DD cache 對象加入 uncommitted 緩存中。
在事務提交或者回滾后,client 將局部緩存中的 dropped 和 uncommitted registry 清除。由於 InnoDB 引擎支持事務,持久存儲層面的數據會通過存儲引擎的接口提交或回滾,不需要 client 額外操作。
在這個過程中,由於 MDL(metadata lock) 的存在,不會有其他的線程嘗試訪問正在變更對象的 DD object,所以可以安全的對 Shared_dictionary_cache 進行操作。當 DDL 操作結束(提交或回滾),釋放 EXCLUSIVE 鎖之后,新的線程就可以重新從存儲引擎上加載新的表定義。
七 總結
MySQL data dictionary 解決了背景所述舊架構中的諸多問題,使元數據的訪問更加安全,存儲和管理成本更低。架構實現非常的精巧,通過大量的模版類實現使得代碼能夠最大程度上被復用。多層緩存的實現也能顯著提升訪問效率。通過 client 簡潔的接口,讓 Server 層和存儲層能在任何地方方便的訪問元數據。
本文為阿里雲原創內容,未經允許不得轉載。






