帶著問題讀 TiDB 原始碼:Hive 後設資料使用 TiDB 啟動報錯

PingCAP發表於2021-12-02

《帶著問題讀原始碼系列》- 開篇

在 TiDB 社群活躍較久的夥伴們應該知道,過去我們有被稱為 24 章經的《TiDB 原始碼閱讀系列文章》,也有面向 TiKV 的《TiKV 原始碼解析系列文章》以及 《Deep Dive TiKV 系列文章》。這些系列文章的內容非常深入,能夠幫助大家從非常細節的原理入手瞭解 TiDB 以及 TiKV 的實現方式和基礎原理。

然而在 TiDB 社群中活躍的許多夥伴還需要更簡單,並且同自己每天工作中使用 TiDB 時遇到的問題更相關的原始碼閱讀文章。本文是《帶著問題讀原始碼系列》的第一次嘗試,在定位並解決使用者所遇到的一個簡單問題的過程中,對相關的程式碼一併進行介紹。希望能夠從不同的視角,以不同的問題顆粒度來幫助大家更好的學習 TiDB 和 TiKV 的原始碼。

AskTUG 上有許多使用者日常使用 TiDB 過程中遇到的問題反饋,這些問題都能夠成為同本文類似的原始碼解析素材。如果本文能夠為大家創造價值,那麼我們一定努力將《帶著問題讀原始碼系列》持續建設成同前輩們一樣受大家歡迎的原始碼閱讀系列。

問題

近期在 AskTUG 論壇接到使用者反饋使用 TiDB 作為 Hive metastore 資料庫時設定 SERIALIZABLE 事務隔離級別失敗。並且使用者根據文件建議進行 SET GLOBAL tidb_skip_isolation_level_check=1 操作後仍然無法按照預期解決問題。考慮到知乎在一年前就已正式上線並一直使用著 4.0.x 系列的 TiDB 作為 Hive metastore 的資料庫,而使用者按照說明文件操作仍然無法順利在 TiDB 上部署 Hive metastore 意味著很可能 TiDB 在不同的版本間發生了不相容的行為改變。接下來就讓我們一起從問題的排查入手,學習瞭解相應功能背後的原始碼。

驗證流程

在 tiup 的幫助下我們能夠非常輕鬆的啟動多個不同版本的 TiDB 對事務隔離級別的行為進行測試和驗證。

首先我們先啟動 5.0.0 版本的 TiDB 叢集準備測試

接下來我們使用 tiup 提示的連線命令使用 mysql client 連線上測試叢集,在設定完 SET GLOBAL tidb_skip_isolation_level_check=1 之後使用 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 驗證行為符合預期。說明 TiDB 5.0 系列的行為同 4.0 一致,能夠支撐 Hive metastore 的運轉。

接下來我們啟動 5.1.0 版本的 TiDB 叢集準備測試

同樣我們使用 mysql client 連線上測試叢集,在設定完 SET GLOBAL tidb_skip_isolation_level_check=1 並重建連結確保設定生效後,使用 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 仍然會收到錯誤報告。說明從 TiDB 5.1 系列開始行為同以往版本不一致,無法滿足 Hive metastore 的要求。

問題分析

首先我們需要 checkout 一份最新 TiDB 程式碼(git hash: 649ed6abc9790cfdd2a17065118379d8abcc7595)檢視事務隔離級別校驗相關邏輯。為了快速定位到相關邏輯所在的程式碼,我們可以在 TiDB 程式碼的根目錄下對字串 SERIALIZABLE 進行文字檢索,快速定位到可能與此有關的程式碼檔案。

我們發現實際上包含字串 SERIALIZABLE 的檔案有兩個,而其中對隔離級別進行判斷進行處理的檔案只有 sessionctx/variable/varsutil.go 這一個檔案。開啟檔案後我們發現這裡正是對隔離級別進行判斷並根據 tidb_skip_isolation_level_check 設定決定是否通過的邏輯。

我們可以同行為符合預期的 5.0 版本 TiDB 程式碼(git hash: 53251a9731da02ad9ee5abed9f27a14c7dea33a4)進行對比來快速定位兩者間行為不同是由那些變化引起的。同樣我們通過字串匹配快速定位到 sessionctx/variable/sysvar.go 和 sessionctx/variable/session.go 兩個檔案都存在對隔離級別進行條件處理的情況。

這兩個不同的檢查邏輯非常類似,都是試圖獲取 TiDBSkipIsolationLevelCheck 變數的設定,根據設定值決定是否予以放行。當我們將這裡的邏輯同 master 程式碼中的邏輯進行對比時我們發現他們本質上的區別非常小。5.0 中使用了一個內建工具函式 GetSessionSystemVar 來獲取變數值,而 master 程式碼則直接訪問 SessionVars 的 systems 變數表進行訪問來獲取 TiDBSkipIsolationLevelCheck 變數的當前值。進一步檢視 5.0 中 GetSessionSystemVar 的實現我們發現這個工具函式負責在 session 變數未設定時進一步到全域性變數表中進行查詢並將查詢到的結果放置在 SessionVars 的 systems 變數表中供後續查詢使用。

根據目前的線索猜測,在 5.1 某次程式碼重構試圖將兩個相似的重複隔離級別檢查邏輯合併成一個通用邏輯的時候繞過了工具函式直接訪問 systems 變數表。這種方式訪問變數表不具備從前工具函式自動回退全域性變數設定的能力。瞭解到這裡修復非常簡單,只需使用當前 TiDB 中類似工具函式 GetSessionOrGlobalSystemVar 來讀取 TiDBSkipIsolationLevelCheck 的變數值就能恢復預期行為。

修復並完成構建後再次測試 TiDB 的行為已符合預期。

提交修復

根據 TiDB 社群標準的程式碼貢獻流程,我們首先建立一個新的 Issue 對發現的問題、復現方式以及期望的行為做清晰的描述。

建立完 issue 後我們就可以將修復邏輯提交到自己 fork 的倉庫並建立 PR,建立過程中需要根據實際情況填充 PR 資訊模版。

建立完成後 CI 系統會對提交的 PR 進行一系列的負責檢查並執行必要的測試,除了這些系統自動化的驗證之外。其他社群貢獻者會對 PR 進行 code review,在有足夠來自於 TiDB Reviewer 及以上許可權的貢獻者對 PR 點贊後變更才能夠被合併到專案主幹中。

在 PR 提交後不久就得到了 @morgo 的 review 反饋,反饋一針見血的指出了問題背後的真正原因是 PR #24836 中對 TiDBSkipIsolationLevelCheck 變數初始化行為的錯誤變更,去掉 TiDBSkipIsolationLevelCheck 變數定義中的 skipInit: true 初始化欄位即可確保 session 初始化時正確的將 global 變數值複製到 session 中,讓前面的隔離級別檢查邏輯行為恢復正常。根據這個線索進行程式碼修改並實際測試證明表現符合預期,接下來讓我們繼續分析 skipInit 相關的原始碼探個究竟。

程式碼中所有對 skipInit 變數的讀取操作都封裝在上圖的 SkipInit 函式中,從下圖中我們可以看到 SkipInit 方法用於在初始化新的 session 變數 cache 的過程跳過部分變數。

接下來 newSessionCache 被更新到 session 變數中並通過下圖中的 GetSessionCache 方法對外提供訪問。

而 GetSessionCache 方法只有一個呼叫方 loadCommonGlobalVariablesIfNeeded,到這裡 skipInit 對系統變數初始化流程的影響就非常清晰了。

當 session 建立完成後,沒有標記為 skipInit 的變數都會以變數的初始值的形式更新到會話變數表中,也就是前面提到的 systems 變數表中。當我們將 TiDBSkipIsolationLevelCheck 的 skipInit 恢復為 false 之後,全域性變數 tidb_skip_isolation_level_check 能夠在這個初始化的過程中被正確的複製到使用者會話,使得調整會話事務隔離級別的行為符合使用者預期。

在問題得到解決後,大家可能還會問在什麼樣的情況下 skipInit 需要被設定成 true。在引入這個功能的 PR #24836 中我們可以得知部分不適合在初始化過程中複製到會話中的變數會利用這個標記實現黑名單功能。而在這次重構過程中 TiDBSkipIsolationLevelCheck 被錯誤的設定在黑名單中導致了 5.1 開始版本行為的異常。

What problem does this PR solve?

Problem Summary:Currently the builtinGlobalVariable feature is a source of bugs because even though a sysvar is added, it is not automatically copied to new sessions. This behavior is also not MySQL compatible, where it is expected a sysvar of session scope should be copied on session init.Fixing the full incompatibility is a little bit more complicated, but this takes the initial step of inverting from an allow list to a deny list, but is otherwise functionally compatible.This also includes the fix from #24835 should this PR supercede it.

What is changed and how it works?

What's Changed:A variable on the SysVar struct can now be set to skipInit. By default it will not skip for session-scope variables, which is why it is now a deny list.However, it will always skip for noop variables, which helps keep the memory footprint of new sessions slightly lower.

Related changes

None
There will need to be followup PRs to handle the specific skipInit variables; some probably don't need to be on this list. The global-only variables that are hard-coded into the SkipInit function will also need removing.
後記

感謝向社群報告 TiDB 行為異常的熱心使用者,非常遺憾沒能在故障發生的第一時間定位並解決問題。但我們仍然希望在新版本釋出修復這個問題後 TiDB 能夠為你支撐 Hive metastore 乃至更多業務場景起到積極作用。

相關文章