MySQL·8.0新特性·Newdatadictionary嚐鮮篇

zhaiwx_yinfeng發表於2016-10-07

眾所周知,由於MySQL採用統一Server層+不同的底層引擎外掛的架構模式,在Server層為每個表建立了frm檔案,以儲存與表定義相關的後設資料資訊。然而某些引擎(例如InnoDB)本身也會儲存後設資料,這樣不僅產生了後設資料冗餘,而且由於Server層和引擎層分別各自管理,在執行DDL之類的操作時,很難做到crash-safe,更別說讓DDL具備事務性了。

為了解決這些問題(尤其是DDL無法做到atomic),從MySQL8.0開始取消了FRM檔案及其他server層的後設資料檔案(frm, par, trn, trg, isl,db.opt),所有的後設資料都用InnoDB引擎進行儲存, 另外一些諸如許可權表之類的系統表也改用InnoDB引擎。

本文是筆者初次瞭解這塊內容,因此不會過多深入,由於涉及的改動太多,後面有空再逐個展開。

本文所有測試和程式碼相關部分都是基於MySQL8.0.0版本,由於這是8.0大版本的第一個開發版本,不排除未來行為會發生變化。

測試

首先我們建立一個新庫,並在庫下建立兩個表來開啟我們的測試

mysql> CREATE DATABASE sbtest;
Query OK, 1 row affected (0.00 sec)

mysql> USE sbtest
Database changed
mysql> CREATE TABLE t1 (a int primary key);
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE t2 (a int primary key, b int);
Query OK, 0 rows affected (0.00 sec)

$ls -lh /u01/my80/data/sbtest
total 256K
-rw-r----- 1 yinfeng.zwx users 128K Oct  5 19:44 t1.ibd
-rw-r----- 1 yinfeng.zwx users 128K Oct  5 19:44 t2.ibd

$ls  /u01/my80/data/sbtest_9.SDI
/u01/my80/data/sbtest_9.SDI

$cat /u01/my80/data/sbtest_9.SDI
{
    "sdi_version": 1,
    "dd_version": 1,
    "dd_object_type": "Schema",
    "dd_object": {
        "name": "sbtest",
        "default_collation_id": 33,
        "created": 0,
        "last_altered": 0
    }
}

可以看到在庫目錄下只有ibd檔案,並沒有frm檔案,而在資料目錄下,相應的生成了一個SDI檔案,來描述這個sbtest庫的資訊。

我們再來看看建立一個MYISAM引擎的表:

mysql> create database my;
Query OK, 1 row affected (0.00 sec)

mysql> use my
Database changed
mysql> create table t1 (a int, b varchar(320)) engine=myisam;
Query OK, 0 rows affected (0.00 sec)

$ls my/
t1_435.SDI  t1.MYD  t1.MYI

{
    "sdi_version": 1,
    "dd_version": 1,
    "dd_object_type": "Table",
    "dd_object": {
        "name": "t1",
        "mysql_version_id": 80000,
        "created": 20161005201935,
        "last_altered": 20161005201935,
        "options": "avg_row_length=0;key_block_size=0;keys_disabled=0;pack_record=1;stats_auto_recalc=0;stats_sample_pages=0;",
        "columns": [
            {
                "name": "a",
                "type": 4,
                "is_nullable": true,
                "is_zerofill": false,
                "is_unsigned": false,
                "is_auto_increment": false,
                "is_virtual": false,
                "hidden": false,
                "ordinal_position": 1,
                "char_length": 11,
                "numeric_precision": 10,
                "numeric_scale": 0,
                "datetime_precision": 0,
                "has_no_default": false,
                "default_value_null": true,
                "default_value": "",
                "default_option": "",
                "update_option": "",
                "comment": "",
                "generation_expression": "",
                "generation_expression_utf8": "",
                "options": "interval_count=0;",
                "se_private_data": "",
                "column_key": 1,
                "column_type_utf8": "int(11)",
                "elements": [],
                "collation_id": 33
            },
            {
                "name": "b",
                "type": 16,
                "is_nullable": true,
                "is_zerofill": false,
                "is_unsigned": false,
                "is_auto_increment": false,
                "is_virtual": false,
                "hidden": false,
                "ordinal_position": 2,
                "char_length": 960,
                "numeric_precision": 0,
                "numeric_scale": 0,
                "datetime_precision": 0,
                "has_no_default": false,
                "default_value_null": true,
                "default_value": "",
                "default_option": "",
                "update_option": "",
                "comment": "",
                "generation_expression": "",
                "generation_expression_utf8": "",
                "options": "interval_count=0;",
                "se_private_data": "",
                "column_key": 1,
                "column_type_utf8": "varchar(320)",
                "elements": [],
                "collation_id": 33
            }
        ],
        "schema_ref": "my",
        "hidden": false,
        "se_private_id": 18446744073709551615,
        "engine": "MyISAM",
        "comment": "",
        "se_private_data": "",
        "row_format": 2,
        "partition_type": 0,
        "partition_expression": "",
        "default_partitioning": 0,
        "subpartition_type": 0,
        "subpartition_expression": "",
        "default_subpartitioning": 0,
        "indexes": [],
        "foreign_keys": [],
        "partitions": [],
        "collation_id": 33
    }
}

這裡我們建立了一個MyISAM表t1,相應的一個SDI檔案被建立,檔案中以JSON的格式記錄了該表的詳細資訊。根據官方檔案的描述,這個檔案的存在是為了一個還未完全實現的功能。

新的Information Schema定義

一些新IS表使用View進行了重新設計,主要包括這些表:

CHARACTER_SETS
COLLATIONS
COLLATION_CHARACTER_SET_APPLICABILITY
COLUMNS
KEY_COLUMN_USAGE
SCHEMATA
STATISTICS
TABLES
TABLE_CONSTRAINTS
VIEWS

#例如SCHEMATA

mysql> show create table information_schema.schemataG
*************************** 1. row ***************************
                View: SCHEMATA
         Create View: CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `information_schema`.`SCHEMATA` AS select `cat`.`name` AS `CATALOG_NAME`,`sch`.`name` AS `SCHEMA_NAME`,`cs`.`name` AS `DEFAULT_CHARACTER_SET_NAME`,`col`.`name` AS `DEFAULT_COLLATION_NAME`,NULL AS `SQL_PATH` from (((`mysql`.`schemata` `sch` join `mysql`.`catalogs` `cat` on((`cat`.`id` = `sch`.`catalog_id`))) join `mysql`.`collations` `col` on((`sch`.`default_collation_id` = `col`.`id`))) join `mysql`.`character_sets` `cs` on((`col`.`character_set_id` = `cs`.`id`))) where can_access_database(`sch`.`name`)
character_set_client: utf8
collation_connection: utf8_general_ci
1 row in set (0.01 sec)

也就是說,雖然DD系統表被隱藏不可見了,但你依然可以通過檢視獲得大部分資訊。這種方式實際上大大加快了IS表的查詢速度,轉換成物理表的查詢後,將無需為每個IS表的查詢建立臨時表(臨時表的操作包含了server層建立frm, 引擎層獲取資料or需要鎖保護的全域性資料)。另外優化器也能為IS表的查詢選擇更好的執行計劃(例如使用系統表上的索引進行查詢)。

官方對此做了測試,結果顯示對IS表的查詢效能大幅度提升,官方部落格傳送門:
MySQL 8.0: Improvements to Information_schema
MySQL 8.0: Scaling and Performance of INFORMATION_SCHEMA

新選項: information_schema_stats: CACHED | LATEST

目前表的後設資料資訊快取在statistics及tables表中以加速對IS表的查詢效能。你可以通過引數information_schema_stats來直接讀取已經快取到記憶體的資料(cached),還是從儲存引擎中獲取最新的資料(latest). 很顯然後者要慢一點。

而從is庫下,可以看到對應兩種表:TABLES及TABLES_DYNAMIC, 以及STATISTICS及STATISTICS_DYNAMIC。當被設定為LATEST時,就會去從**_DYNAMIC表中去讀取資料。

該選項也會影響到SHOW TABLES等語句的行為。

Data Dictionary Cache

資料詞典的結構發生巨大的變化後,相應的對於記憶體資料詞典Cache也做改動,

mysql> show variables like `%defin%`;
+---------------------------------+-------+
| Variable_name                   | Value |
+---------------------------------+-------+
| schema_definition_cache         | 256   |
| stored_program_definition_cache | 256   |
| table_definition_cache          | 1400  |
| tablespace_definition_cache     | 256   |
+---------------------------------+-------+
4 rows in set (0.00 sec)

tablespace_definition_cache: tablespace cache的大小,儲存了tablespace的定義. 一個tablespace中可能包含多個table。

table_definition_cache:

stored_program_definition_cache: 儲存過程&&function的定義cache.

schema_definition_cache: 儲存schema定義的cache

hardcode的字符集cache:

character set definition cache partition: Stores character set definition objects and has a hardcoded object limit of 256.
collation definition cache partition: Stores collation definition objects and has a hardcoded object limit of 256.

系統表變化

  • 和許可權相關的錶轉換成InnoDB引擎

// 包括:user, db, tables_priv, columns_priv, procs_priv, proxies_priv

// 官方部落格介紹

  • func錶轉換成InnoDB事務表

// 基於此變化,對function的操作(例如CREATE FUNCTION或者DROP FUNCTION, 或者使用者定義的UDF)可能會導致一次隱式提交

  • mysql庫下的routine表及event表不再使用,這些資訊被儲存到新的DD table中,並且在mysql庫下是不可見的。
  • 外來鍵系統表

// 使用兩個不可見的系統表foreign_keys和foreign_key_column_usage來儲存外來鍵資訊
// 由於這兩個系統表不可見,你需要通過IS庫下的REFERENTIAL_CONSTRAINTS和KEY_COLUMN_USAGE表來獲得外來鍵資訊
// 引入的不相容:foreign key的名字不可以超過64個字元(之前版本是允許的)

原始碼概覽

我們回到原始碼目錄下,大量的新程式碼檔案被引入,以從server層管理New DD,主要定義了一系列統一的API,程式碼存於sql/dd目錄下,函式和類定義在namespace dd下

針對不同的後設資料分別定義了不同的類及其繼承關係:

namespace dd {
    Weak_object
        Entity_object   
            Dictionary_object
                Tablespace
                Schema
                Event
                Routine
                    Function
                    Procedure
                Charset
                Collation
                Abstract_table
                    Table
                Spatial_reference_system
                Index_stat
                    View
                Table_stat
            Partition
            Trigger
            Index
            Foreign_key
            Parameter
            Column
        Partition_index 
        Partition_value
        View_routine
        View_table
        Tablespace_file
        Foreign_key_element
        Index_element
        Column_type_element
        Parameter_type_element
    Object_table
        Dictionary_object_table
    Object_type
    Object_table_definition   
    
}

資料詞典Cache管理類:

dd::cache {
    dd::cache::Dictionary_client
    Object_registry
    Element_map
    Multi_map_base
        Local_multi_map
        Shared_multi_map
    
    Cache_element
    Free_list
    Shared_dictionary_cache
    
    Storage_adapter
}

mysql庫儲存的是系統表,但通過show tables命令,我們只能看到37個表,而從磁碟來看mysql目錄下ibd檔案遠遠超過37個,這意味著有些系統表對使用者是不可見的,這些表也是用於管理核心資料詞典資訊,不可見的原因是避免使用者不恰當的操作。(當然也不排除未來這一行為發生變化),關於這些表的訪問,在目錄sql/dd/impl/tables/中進行了介面定義,這些隱藏的表包括:

$grep `std::string s_table_name` sql/dd/impl/tables/* | awk `{ print $4}`
s_table_name("catalogs");
s_table_name("character_sets");
s_table_name("collations");
s_table_name("columns");
s_table_name("column_type_elements");
s_table_name("events");
s_table_name("foreign_key_column_usage");
s_table_name("foreign_keys");
s_table_name("index_column_usage");
s_table_name("indexes");
s_table_name("index_partitions");
s_table_name("index_stats");
s_table_name("parameters");
s_table_name("parameter_type_elements");
s_table_name("routines");
s_table_name("schemata");
s_table_name("st_spatial_reference_systems");
s_table_name("table_partitions");
s_table_name("table_partition_values");
s_table_name("tables");
s_table_name("tablespace_files");
s_table_name("tablespaces");
s_table_name("table_stats");
s_table_name("triggers");
s_table_name("version");
s_table_name("view_routine_usage");
s_table_name("view_table_usage");

我們以對一個表的常見操作為例,看看其中一些程式碼是如何被呼叫的。
(由於New DD的程式碼改動很大,相關的worklog有幾十個,筆者通過測試+程式碼debug的方式第一步先熟悉程式碼,記錄的比較凌亂)

庫級操作

  1. 建立database
mysql> create database db1;
Query OK, 1 row affected (2.87 sec)

mysql> create database db2;
Query OK, 1 row affected (3.05 sec)

入口函式:mysql_create_db

— 建立database目錄
— 構建binlog並寫入檔案
— 呼叫DD API介面: dd::create_schema

* 構建物件dd::Schema
* 儲存到資料詞典中mysql.schemata表中,相關堆疊:
dd::create_schema
|--> dd::cache::Dictionary_client::store<dd::Schema>
    |--> dd::cache::Storage_adapter::store<dd::Schema>
        |--> dd::Weak_object_impl::store
            |--> dd::Raw_new_record::insert
            

Note: schemata表對使用者是不可見的
    mysql> desc schemata;
    ERROR 3554 (HY000): Access to system table `mysql.schemata` is rejected.
* 建立並儲存當前庫的資訊到SDI檔案中,sdi檔案命名以庫名為字首,堆疊如下
    
dd::create_schema
|--> dd::store_sdi
    |--> dd::sdi_file::store
        |--> write_sdi_file
    
* 成功則commit,失敗則rollback
  1. 修改database
mysql> alter database db1 default charset gbk;
Query OK, 1 row affected (2 min 17.54 sec)

入口函式: mysql_alter_db

— 呼叫DD API介面: dd::alter_schema

* 更新資料詞典資訊,相關堆疊:
dd::alter_schema
|--> dd::cache::Dictionary_client::update<dd::Schema>
    |--> dd::cache::Dictionary_client::store<dd::Schema>
        |--> dd::cache::Storage_adapter::store<dd::Schema> 
            |--> dd::Weak_object_impl::store
                |--> dd::Raw_record::update

*更新sdi檔案, 相關堆疊
dd::alter_schema
|--> dd::Sdi_updater::operator()
    |--> dd::update_sdi
        |--> dd::sdi_file::store
            |--> write_sdi_file
*但奇怪的是,更新後很快就刪除了 ?? (8.0.0版本,why ??)
看起來sdi檔案的序列號沒有遞增,導致檔案被快速刪除了,實際上的目的是建立一個新的檔案,寫入新的資料,然後將老的SDI刪掉
ref: http://bugs.mysql.com/bug.php?id=83281

— 寫Binlog

  1. show databases
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| db1                |
| db2                |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
6 rows in set (1.40 sec)

執行該命令時,實際上會對其進行一個SQL轉換,將其轉換成一個標準的查詢語句,堆疊如下:

dispatch_command
|-->mysql_parse
    |-->parse_sql
        |-->MYSQLparse
            |--> dd::info_schema::build_show_databases_query

轉換後的SQL類似:

       SELECT SCHEMA_NAME as `Database`,
          FROM information_schema.schemata;

由於直接從系統表中讀取, 這意味著在資料目錄下建立一個資料夾將不會當作新的資料庫目錄。

  1. 刪除database
mysql> drop database db2;
Query OK, 0 rows affected (1 min 1.86 sec)

— 刪除相關檔案

— 刪除系統表mysql/schemata中記錄

mysql_rm_db
|--> dd::drop_schema
    |--> dd::cache::Dictionary_client::drop<dd::Schema>
        |-->dd::cache::Storage_adapter::drop<dd::Schema>
            |--> dd::Weak_object_impl::drop
                |--> dd::Raw_record::drop
                    |--> handler::ha_delete_row

表級操作

  1. 建立表
mysql> create table t1 (a int primary key, b int, c int, key(b));
Query OK, 0 rows affected (7 min 12.29 sec)

入口函式:

mysql_create_table_no_lock
|--> create_table_impl
    |--> rea_create_table

— 先在dd中插入新的記錄(dd::create_table –> dd::create_dd_user_table)

// 根據建表語句初始化`dd::Table` 物件,包括表的列定義,各個屬性和選項,索引定義
// 存到系統表中
dd::create_dd_user_table
|-->  dd::cache::Dictionary_client::store<dd::Table> 
    |-->dd::cache::Storage_adapter::store<dd::Table>
        |-->dd::Weak_object_impl::store
            // 先插入到mysql/tables系統表中
            
            // 再插入到其他系統表中,如"mysql/columns", 
            |-->dd::Table_impl::store_children
                |--> dd::Abstract_table_impl::store_children      // mysql/columns
                    |--> dd::Collection<dd::Column*>::store_items
                        |--> Weak_object_impl::store
            |-->dd::Collection<dd::Index*>::store_items           // mysql/indexes
                |--> dd::Weak_object_impl::store
                    |-->dd::Index_impl::store_children
                        |--> dd::Collection<dd::Index_element*>::store_items    // mysql/index_column_usage

— 然後再建立引擎檔案

  1. Open table

— 將例項重啟後,然後再開啟表,表定義第一次載入記憶體,需要先去訪問系統表拿到表定義:

open_and_process_table
|-->open_table
    |-->get_table_share_with_discover
        |-->get_table_share
            |-->open_table_def
                // 先看schema是否存在,並從系統表`mysql/schemata`載入記憶體cache中
                |-->dd::schema_exists   
                    |--> dd::cache::Dictionary_client::acquire<dd::Schema> 
                        |-->dd::cache::Dictionary_client::acquire<dd::Item_name_key, dd::Schema>
                            |-->dd::cache::Shared_dictionary_cache::get<dd::Item_name_key, dd::Schema>
                                |-->dd::cache::Shared_dictionary_cache::get_uncached<dd::Item_name_key, dd::Schema>
                                    |-->dd::cache::Storage_adapter::get<dd::Item_name_key, dd::Schema>
                                        |-->dd::Raw_table::find_record
                // 再獲取表的定義並從系統表mysql/tables載入
                |-->dd::abstract_table_type
                    |-->dd::cache::Dictionary_client::acquire<dd::Abstract_table>
                        |-->dd::cache::Dictionary_client::acquire<dd::Item_name_key, dd::Abstract_table>
                            |-->dd::cache::Shared_dictionary_cache::get<dd::Item_name_key, dd::Abstract_table>
                                |-->dd::cache::Shared_dictionary_cache::get_uncached<dd::Item_name_key, dd::Abstract_table>
                                    |-->dd::cache::Storage_adapter::get<dd::Item_name_key, dd::Abstract_table>
                                        |--> dd::Raw_table::find_record
                                        // 獲取表上的屬性資訊
                                        |-->Dictionary_object_table_impl::restore_object_from_record
                                            |-->dd::Table_impl::restore_children
                                                |-->dd::Abstract_table_impl::restore_children
                                                    // 從mysql/columns系統表獲得列資訊
                                                    |-->dd::Collection<dd::Column*>::restore_items<dd::Abstract_table_impl>
                                                    // 從mysql/indexs系統表獲得索引資訊
                                                    |-->dd::Collection<dd::Index*>::restore_items<dd::Table_impl>
                                                    //從mysql/index_column_usage獲取索引資訊
                                                    |-->dd::Collection<dd::Index_element*>::restore_items<dd::Index_impl>
                                                    // 從mysql/foreign_keys獲得外來鍵資訊
                                                    |-->dd::Collection<dd::Foreign_key*>::restore_items<dd::Table_impl>
                                                    // 從mysql/table_partitions獲得分割槽資訊
                                                    |-->dd::Collection<dd::Partition*>::restore_items<dd::Table_impl>
                                                    //從"mysql/triggers獲得觸發器資訊
                                                    |-->dd::Collection<dd::Trigger*>::restore_items<dd::Table_impl>

相關WorkLog

WL#6379: Schema definitions for new DD
WL#6380: Formulate framework for API for DD
WL#6381: Handler API changes for new dictionary
WL#6382: Define and Implement API for Table objects
WL#6383: Define and Implement API for Triggers
WL#6384: Define and Implement API for Stored Routines
WL#6385: Define and Implement API for Schema
WL#6387: Define and Implement API for Tablespaces
WL#6388: Define and Implement API for Events
WL#6389: Define and Implement API for Views
WL#6390: Use new DD API for handling non-partitioned tables
WL#6391: Protect Data Dictionary tables
WL#6392: Upgrade to Transactional Data Dictionary
WL#6394: Bootstrap code for new DD
WL#6416: InnoDB: Remove the use of *.isl files
WL#6599: New Data Dictionary and I_S integration
WL#6929: Move FOREIGN KEY constraints to the global data dictionary
WL#7053: InnoDB: Provide storage for tablespace dictionary
WL#7066: External tool to extract InnoDB tablespace dictionary information
WL#7069: Provide data dictionary information in serialized form
WL#7167: Change DDL to update rows for view columns in DD.COLUMNS and other dependent values.
WL#7284: Implement common code for different DD APIs
WL#7464: InnoDB: provide a way to do non-locking reads
WL#7488: InnoDB startup refactoring
WL#7630: Define and Implement API for Table Partition Info
WL#7771: Make sure errors are properly handled in DD API
WL#7784: Store temporary table metadata in memory
WL#7836: Use new DD API for handling partitioned tables
WL#7896: Use DD API to work with triggers
WL#7897: Use DD API to work with stored routines
WL#7898: Use DD API to work with events
WL#7907: Runtime: Use non-locking reads for DD tables under I_S view.
WL#8150: Dictionary object cache
WL#8433: Separate DD commands from regular SQL queries in the parser grammar
WL#8980: Move UDF table from MyISAM to Transactional Storage
WL#9045: Make user management DDLs atomic

官方部落格:
https://mysqlserverteam.com/mysql-server-bootstrapping-and-dictionary-initialization/
https://mysqlserverteam.com/bootstrapping-the-transactional-data-dictionary/


相關文章