做者:黃東旭mysql
「TiDB,你已是一個成熟的數據庫了,該學會用本身的 SQL 查本身的狀態了。」
對於一個成熟的數據庫來講,經過 SQL 來查詢系統自己的狀態再正常不過,對於 MySQL 來講 INFOMATION_SCHEMA
和 PERFORMANCE_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! 感受就是咱們須要的東西。函數
如今步驟就很清楚了:工具
infoschema/tables.go
中添加一個新的字符串常量 tableTiDBServersInfo
用於定義表名;[]columnInfo:tableTiDBServersInfoCols
,用於定義這張系統表的結構;tableNameToColumns
這個 map 中添加一個新的映射關係 tableTiDBServersInfo => tableTiDBServersInfoCols
;infoschemaTable.getRows()
方法中加入一個新的 dataForTableTiDBServersInfo
的 swtich case;下一個目標是實現 dataForTableTiDBServersInfo
,很顯然,大體的思路是:ui
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/