《帶着問題讀源碼系列》- 開篇
在 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 叢集準備測試
1.png
接下來我們使用 tiup 提示的連接配接指令使用 mysql client 連接配接上測試叢集,在設定完 SET GLOBAL tidb_skip_isolation_level_check=1 之後使用 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 驗證行為符合預期。說明 TiDB 5.0 系列的行為同 4.0 一緻,能夠支撐 Hive metastore 的運轉。
2.jpeg
接下來我們啟動 5.1.0 版本的 TiDB 叢集準備測試
3.png
同樣我們使用 mysql client 連接配接上測試叢集,在設定完 SET GLOBAL tidb_skip_isolation_level_check=1 并重建連結確定設定生效後,使用 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 仍然會收到錯誤報告。說明從 TiDB 5.1 系列開始行為同以往版本不一緻,無法滿足 Hive metastore 的要求。
4.jpeg
問題分析
首先我們需要 checkout 一份最新 TiDB 代碼(git hash: 649ed6abc9790cfdd2a17065118379d8abcc7595)檢視事務隔離級别校驗相關邏輯。為了快速定位到相關邏輯所在的代碼,我們可以在 TiDB 代碼的根目錄下對字元串 SERIALIZABLE 進行文字檢索,快速定位到可能與此有關的代碼檔案。
5.png
我們發現實際上包含字元串 SERIALIZABLE 的檔案有兩個,而其中對隔離級别進行判斷進行處理的檔案隻有 sessionctx/variable/varsutil.go 這一個檔案。打開檔案後我們發現這裡正是對隔離級别進行判斷并根據 tidb_skip_isolation_level_check 設定決定是否通過的邏輯。
6.png
我們可以同行為符合預期的 5.0 版本 TiDB 代碼(git hash: 53251a9731da02ad9ee5abed9f27a14c7dea33a4)進行對比來快速定位兩者間行為不同是由那些變化引起的。同樣我們通過字元串比對快速定位到 sessionctx/variable/sysvar.go 和 sessionctx/variable/session.go 兩個檔案都存在對隔離級别進行條件處理的情況。
7.jpeg
這兩個不同的檢查邏輯非常類似,都是試圖擷取 TiDBSkipIsolationLevelCheck 變量的設定,根據設定值決定是否予以放行。當我們将這裡的邏輯同 master 代碼中的邏輯進行對比時我們發現他們本質上的差別非常小。5.0 中使用了一個内置工具函數 GetSessionSystemVar 來擷取變量值,而 master 代碼則直接通路 SessionVars 的 systems 變量表進行通路來擷取 TiDBSkipIsolationLevelCheck 變量的目前值。進一步檢視 5.0 中 GetSessionSystemVar 的實作我們發現這個工具函數負責在 session 變量未設定時進一步到全局變量表中進行查找并将查找到的結果放置在 SessionVars 的 systems 變量表中供後續查找使用。
8.png
根據目前的線索猜測,在 5.1 某次代碼重構試圖将兩個相似的重複隔離級别檢查邏輯合并成一個通用邏輯的時候繞過了工具函數直接通路 systems 變量表。這種方式通路變量表不具備從前工具函數自動回退全局變量設定的能力。了解到這裡修複非常簡單,隻需使用目前 TiDB 中類似工具函數 GetSessionOrGlobalSystemVar 來讀取 TiDBSkipIsolationLevelCheck 的變量值就能恢複預期行為。
9.jpeg
修複并完成建構後再次測試 TiDB 的行為已符合預期。
10.jpeg
送出修複
根據 TiDB 社群标準的代碼貢獻流程,我們首先建立一個新的 Issue 對發現的問題、複現方式以及期望的行為做清晰的描述。
11.png
建立完 issue 後我們就可以将修複邏輯送出到自己 fork 的倉庫并建立 PR,建立過程中需要根據實際情況填充 PR 資訊模版。
12.jpeg
建立完成後 CI 系統會對送出的 PR 進行一系列的負責檢查并執行必要的測試,除了這些系統自動化的驗證之外。其他社群貢獻者會對 PR 進行 code review,在有足夠來自于 TiDB Reviewer 及以上權限的貢獻者對 PR 點贊後變更才能夠被合并到項目主幹中。
在 PR 送出後不久就得到了 @morgo 的 review 回報,回報一針見血的指出了問題背後的真正原因是 PR #24836 中對 TiDBSkipIsolationLevelCheck 變量初始化行為的錯誤變更,去掉 TiDBSkipIsolationLevelCheck 變量定義中的 skipInit: true 初始化字段即可確定 session 初始化時正确的将 global 變量值複制到 session 中,讓前面的隔離級别檢查邏輯行為恢複正常。根據這個線索進行代碼修改并實際測試證明表現符合預期,接下來讓我們繼續分析 skipInit 相關的源碼探個究竟。
13.jpeg
代碼中所有對 skipInit 變量的讀取操作都封裝在上圖的 SkipInit 函數中,從下圖中我們可以看到 SkipInit 方法用于在初始化新的 session 變量 cache 的過程跳過部分變量。
14.jpeg
接下來 newSessionCache 被更新到 session 變量中并通過下圖中的 GetSessionCache 方法對外提供通路。
15.jpeg
而 GetSessionCache 方法隻有一個調用方 loadCommonGlobalVariablesIfNeeded,到這裡 skipInit 對系統變量初始化流程的影響就非常清晰了。
16.jpeg
當 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 乃至更多業務場景起到積極作用。