Hands-on! 如何給 TiDB 添加新系統表

做者:黃東旭mysql

「TiDB,你已是一個成熟的數據庫了,該學會用本身的 SQL 查本身的狀態了。」

對於一個成熟的數據庫來講,經過 SQL 來查詢系統自己的狀態再正常不過,對於 MySQL 來講 INFOMATION_SCHEMAPERFORMANCE_SCHEMA 裏面有大量的信息,基本上經過查詢些信息,DBA 就能對整個系統的運行狀態一目瞭然。最棒的是,查詢的接口正是 SQL,不須要依賴其餘的第三方工具,運用表達力強大的 SQL 甚至能夠對這些信息進行二次加工或者過濾,另外接入第三方的運維監控工具也很天然,不須要引入新的依賴。sql

過去因爲種種緣由,TiDB 不少的內部狀態信息是經過不一樣組件暴露 RESTFul API 來實現,這個方案也不是很差,可是隨着 API 的增多,管理成本愈來愈高,舉一個例子:在不參考文檔的前提下,用戶是很難記住那麼多 RESTFul API 的路徑的,只能經過將這些 API 封裝成命令行工具來使用,可是若是這是一張系統表,只須要一句 SHOW TABLES 和幾條 SELECT 就可以了。固然選擇 RESTFul API 還有其餘的緣由,例若有些操做並非只讀的,是相似命令的形式,例如:手動 split region 這類操做,使用 RESTFul API 會更好,這二者其實並不矛盾,系統表固然是一個很好的補充,這是提高總體軟件易用性的一個好例子。數據庫

今天正好有一些時間,花了幾十分鐘完整的走了一遍流程,給 TiDB 的 INFORMATION_SCHEMA 添加了一張名爲 TIDB_SERVERS_INFO 的表,用來顯示集羣中全部活着的 tidb-server 的狀態信息(基本和 /info/all 作的事情差很少),意在拋磚引玉,社區的小夥伴能夠參照這篇博客添加新的有用的信息。設計模式

有這個想法後,個人直覺是去找 information_schema 的代碼看看別的系統表是怎麼實現的,照貓畫虎就 OK 了(😁沒毛病)。 TiDB 的代碼組織還算比較直觀,在 tidb repo 的根目錄下直接看到了一個包叫 infoschema,感受就是它,打開 inforschema/table.go 後確實應證了個人猜測,文件開頭集中定義了不少字符串常量:session

...
tableTiKVStoreStatus                    = "TIKV_STORE_STATUS"
tableAnalyzeStatus                      = "ANALYZE_STATUS"
tableTiKVRegionStatus                   = "TIKV_REGION_STATUS"
tableTiKVRegionPeers                    = "TIKV_REGION_PEERS"
...

這些常量正是 TiDB 的 INFOMATION_SCHEMA 中的表名,根據這些變量順藤摸瓜能夠找到同文件裏面的 tableNameToColumns 這個 map,顧名思義應該是這個 map 經過表名映射到表結構定義,隨便打開一個,果真如此:運維

var columnStatisticsCols = []columnInfo{
    {"SCHEMA_NAME", mysql.TypeVarchar, 64, mysql.NotNullFlag, nil, nil}, 
    {"TABLE_NAME", mysql.TypeVarchar, 64, mysql.NotNullFlag, nil, nil}, 
    {"COLUMN_NAME", mysql.TypeVarchar, 64, mysql.NotNullFlag, nil, nil}, 
    {"HISTOGRAM", mysql.TypeJSON, 51, 0, nil, nil}, 
}

下一步須要如何填充數據返回給 TiDB 的 SQL Engine,咱們注意到 infoschemaTable 這個類實現了 table.Table interface,很顯然這個 interface 就是 TiDB 中對於 Table 獲取數據/修改數據的接口,有關獲取數據的方法是 IterRecords,咱們只須要看到 IterRecords 中的實現就能知道這些系統表的數據是如何返回給 SQL Engine 的,果真在 IterRecords 裏面有一個方法,inforschemaTable.getRows(),這個方法的定義中有一個巨大的 switch 語句,用於判斷是在哪一個系統表上,根據這個信息而後返回不一樣的數據:dom

...
switch it.meta.Name.O {
    case tableSchemata:
        fullRows = dataForSchemata(dbs)
    case tableTables:
        fullRows, err = dataForTables(ctx, dbs) 
    case tableTiDBIndexes: 
        fullRows, err = dataForIndexes(ctx, dbs) 
...
}

Bingo! 感受就是咱們須要的東西。函數

如今步驟就很清楚了:工具

  1. infoschema/tables.go 中添加一個新的字符串常量 tableTiDBServersInfo 用於定義表名;
  2. 定義一個 []columnInfo:tableTiDBServersInfoCols,用於定義這張系統表的結構;
  3. tableNameToColumns 這個 map 中添加一個新的映射關係 tableTiDBServersInfo => tableTiDBServersInfoCols
  4. infoschemaTable.getRows() 方法中加入一個新的 dataForTableTiDBServersInfo 的 swtich case;
  5. 搞定。

下一個目標是實現 dataForTableTiDBServersInfo,很顯然,大體的思路是:ui

  1. 找到這個集羣的 PD,由於這些集羣拓撲信息;
  2. 將這些信息封裝成 tableTiDBServersInfoCols 中定義的形式,返回給 getRows 方法。

經過傳入的 ctx 對象,獲取到 Store 的信息, sessionctx.Context 是 TiDB 中一個很重要的對象,也是 TiDB 貫穿整個 SQL 引擎的一個設計模式,這個 Context 中間存儲在這個 session 生命週期中的一些重要信息,例如咱們能夠經過 sessionctx.Context 獲取底層的 Storage 對象,拿到 Storage 對象後,能幹的事情就不少了。

本着照貓畫虎的原則,參考了一下 dataForTiDBHotRegions 的實現:

tikvStore, ok := ctx.GetStore().(tikv.Storage)

由於咱們的目標是獲取 PD 對象,必然地,只有 TiKV 做爲 backend 的時候纔有 PD,因此這裏的類型轉換判斷是必要的。

其實,經過 PD 獲取集羣信息這樣的邏輯已經在 TiDB 中封裝好了,我發如今 domain/info.go 中的這個方法正是咱們想要的:

// GetAllServerInfo gets all servers static information from etcd. func (is *InfoSyncer) 
GetAllServerInfo(ctx context.Context) (map[string]*ServerInfo, error)

實際上,TiDB 的 /info/all 這個 REST API 正是經過調用這個函數實現,咱們只須要調用這個方法,將返回值封裝好就完成了。

自此,咱們就完成了一個新的系統表的添加。在本身添加的新表上 SELECT 一下,是否是頗有成就感 :) 歡迎你們在此基礎上添加更多有用的信息。

閱讀原文https://pingcap.com/blog-cn/hands-on-build-a-new-system-table-for-tidb/

相關文章
相關標籤/搜索