本文基於 TiDB release-5.1進行分析,需要用到 Go 1.16以后的版本 ;
轉載請聲明出處哦~,本篇文章發布於luozhiyun的博客: https://www.luozhiyun.com/archives/592
啟動與調試
其實 TiDB 的調試非常的簡單,我這里用的是 TiDB release-5.1,那么需要將 Go 的版本更新到 1.16 之后。main 函數是在 tidb-server 包里面,直接運行就好了。為了保證環境的統一,我用的是 Linux 的環境。
如果想要對自己的代碼進行調試,只需要:
-
安裝 mysql 客戶端;
yum install mysql -
啟動 TiDB tidb-server 包里面 main 函數;
-
啟動 mysql 客戶端;
tidb 默認端口是 4000 ,賬號是 root ,庫我們選test
mysql -h 127.0.0.1 -P 4000 -u root -D test -
在對應的邏輯上斷點;
例如我們要看 insert 的執行邏輯,首先需要創建了一個表:
CREATE TABLE t ( id VARCHAR(31), name VARCHAR(50), age int, key id_idx (id) );在插入邏輯的地方斷點住:

然后執行插入指令即可
INSERT INTO t VALUES ("pingcap001", "pingcap", 3);
從 main 函數開始
學會了如何調試 TiDB 之后,下面看看 TiDB 的 main 函數執行邏輯,它是在 tidb-server 包下面:
func main() {
...
// 注冊store
registerStores()
// 注冊prometheus監控項
registerMetrics()
// 設置全局 config
config.InitializeConfig(*configPath, *configCheck, *configStrict, overrideConfig)
if config.GetGlobalConfig().OOMUseTmpStorage {
config.GetGlobalConfig().UpdateTempStoragePath()
err := disk.InitializeTempDir()
terror.MustNil(err)
checkTempStorageQuota()
}
setGlobalVars()
// 設置CPU親和性
setCPUAffinity()
//配置系統log
setupLog()
// 定時檢測堆內存有沒有超標
setHeapProfileTracker()
//注冊分布式系統追蹤鏈 jaeger
setupTracing() // Should before createServer and after setup config.
printInfo()
// 設置binlog信息
setupBinlogClient()
// 配置監控
setupMetrics()
storage, dom := createStoreAndDomain()
// 創建TiDB server
svr := createServer(storage, dom)
// 設置優雅關機
exited := make(chan struct{})
signal.SetupSignalHandler(func(graceful bool) {
svr.Close()
cleanup(svr, storage, dom, graceful)
close(exited)
})
topsql.SetupTopSQL()
//啟動服務
terror.MustNil(svr.Run())
<-exited
// 日志刷盤
syncLog()
}
從上面的 main 方法可以看出它主要是加載配置項,然后設置配置信息。從上面的信息配置中,有幾點我覺得可以借鑒到我們平時的項目中,一個是定時檢測堆內存檢測,另一個是優雅停機。
檢測堆內存檢測
堆內存檢測的實現邏輯是在 setHeapProfileTracker 方法中:
func setHeapProfileTracker() {
c := config.GetGlobalConfig()
// 默認1分鍾
d := parseDuration(c.Performance.MemProfileInterval)
// 異步運行
go profile.HeapProfileForGlobalMemTracker(d)
}
func HeapProfileForGlobalMemTracker(d time.Duration) {
log.Info("Mem Profile Tracker started")
// 設置 ticker 為1分鍾
t := time.NewTicker(d)
defer t.Stop()
for {
<-t.C
// 通過 pprof 獲取堆內存使用情況
err := heapProfileForGlobalMemTracker()
if err != nil {
log.Warn("profile memory into tracker failed", zap.Error(err))
}
}
}
從上面的代碼中可以看到 setHeapProfileTracker 里面實際上會啟動一個 Goroutine 異步去定時 ticker (不熟悉定時器原理的可以看這篇:https://www.luozhiyun.com/archives/458 )執行 heapProfileForGlobalMemTracker 函數通過 pprof 獲取堆內存使用情況。
func heapProfileForGlobalMemTracker() error {
// 調用 pprof 獲取堆內存使用情況
bytes, err := col.getFuncMemUsage(kvcache.ProfileName)
if err != nil {
return err
}
defer func() {
if p := recover(); p != nil {
log.Error("GlobalLRUMemUsageTracker meet panic", zap.Any("panic", p), zap.Stack("stack"))
}
}()
// 將內存放置到 cache 里
kvcache.GlobalLRUMemUsageTracker.ReplaceBytesUsed(bytes)
return nil
}
heapProfileForGlobalMemTracker 通過調用 pprof 獲取堆內存使用情況,然后將獲取到的信息傳遞給 GlobalLRUMemUsageTracker,這里比較有意思的是,GlobalLRUMemUsageTracker 是 Tracker 的實現類,會追蹤 Tracker 整條鏈路的內存使用情況,如果達到閾值,那么會觸發 父 Tracker 的 hook,拋出 panic 異常。

優雅停機
優雅停機在項目中就更加常用了,TiDB 在啟動時會調用 SetupSignalHandler 函數執行相應的信號監聽:
func SetupSignalHandler(shutdownFunc func(bool)) {
closeSignalChan := make(chan os.Signal, 1)
signal.Notify(closeSignalChan,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGQUIT)
go func() {
sig := <-closeSignalChan
logutil.BgLogger().Info("got signal to exit", zap.Stringer("signal", sig))
shutdownFunc(sig == syscall.SIGQUIT)
}()
}
當監聽到 SIGHUP 、SIGINT、SIGTERM、SIGQUIT 信號的時候,會執行傳入的 shutdownFunc 函數:
...
signal.SetupSignalHandler(func(graceful bool) {
svr.Close()
cleanup(svr, storage, dom, graceful)
close(exited)
})
...
傳入到 SetupSignalHandler 中的函數首先會執行 server 的關閉,graceful 會當監聽到 SIGQUIT 信號時為 true,然后會調用 cleanup 執行清理操作。
func cleanup(svr *server.Server, storage kv.Storage, dom *domain.Domain, graceful bool) {
// 是否是優雅停機
if graceful {
//優雅停機
svr.GracefulDown(context.Background(), nil)
} else {
// 嘗試優雅停機
svr.TryGracefulDown()
}
// 清理所有插件資源
plugin.Shutdown(context.Background())
closeDomainAndStorage(storage, dom)
disk.CleanUp()
topsql.Close()
}
cleanup 里面則會清理連接、插件、磁盤以及關閉tikv資源等。如果 graceful 是 true,那么會調用 GracefulDown 循環清理空閑連接,直到連接數為0;如果是 false,那么會調用 TryGracefulDown 清理連接,如果連接在15秒內還沒清理完畢則會強制清理。
啟動服務
啟動服務這個過程其實是和 net/http 的 server 非常的類似。入口在 main 函數的最下面,通過 server 的 Run 方法啟動:
func (s *Server) Run() error {
metrics.ServerEventCounter.WithLabelValues(metrics.EventStart).Inc()
s.reportConfig()
// 配置路由信息
if s.cfg.Status.ReportStatus {
s.startStatusHTTP()
}
for {
// 監聽客戶端請求
conn, err := s.listener.Accept()
if err != nil {
...
}
// 創建connection
clientConn := s.newConn(conn)
// 處理connection請求
go s.onConn(clientConn)
}
}
Run 方法這里留下了主要的邏輯:
- 配置路由信息;
- 監聽 connection;
- 為 connection 創建單獨的 Goroutine 進行處理。

獲取到的連接然后會調用 connection 的 Run 方法中讀取 connection 的數據,接着調用到 connection 的 dispatch 方法來做請求邏輯轉發處理。
func (cc *clientConn) dispatch(ctx context.Context, data []byte) error {
...
// 執行的命令
cmd := data[0]
// 命令相應的參數
data = data[1:]
...
// 將[]byte 轉為 string
dataStr := string(hack.String(data))
// 根據 cmd 選擇相應的執行邏輯
switch cmd {
case mysql.ComSleep:
case mysql.ComQuit:
case mysql.ComInitDB:
// 絕大多數sql 都會走這個邏輯
// 包括增刪改查
case mysql.ComQuery:
if len(data) > 0 && data[len(data)-1] == 0 {
data = data[:len(data)-1]
dataStr = string(hack.String(data))
}
return cc.handleQuery(ctx, dataStr)
case mysql.ComFieldList:
case mysql.ComRefresh:
case mysql.ComShutdown:
case mysql.ComStatistics:
case mysql.ComPing:
case mysql.ComChangeUser:
...
// ComEnd
default:
return mysql.NewErrf(mysql.ErrUnknown, "command %d not supported now", nil, cmd)
}
}
dispatch 里面會獲傳入的數組,第一個byte 為命令類型,后面的為執行命令,如我們插入一條 insert 語句:
INSERT INTO t VALUES ("pingcap001", "pingcap", 3);
在這條語句中 cmd 為 3 ,data 為 INSERT INTO t VALUES ("pingcap001", "pingcap", 3);。
然后根據 cmd 在 switch 判斷中找到對應的執行邏輯進行相應的處理。
需要注意的是這里 mysql.ComQuery這個分支其實是包含了增刪改查的,大家自己可以斷點看看。
總結
這一篇其實是非常簡單的,主要說一下如何配置環境進行相應的debug,然后就是介紹一下 main 方法里面主要做了些什么事情,以及我們可以從中學到什么。
對於 TiDB 的啟動環節我們還可以參照前幾次寫的文章:《一文說透 Go 語言 HTTP 標准庫》一起看看同樣是服務端,TiDB為啥要自己實現一個。
Reference
https://zhuanlan.zhihu.com/p/163607256
https://www.qikqiak.com/post/use-vscode-remote-dev-debug/
https://zh.wikipedia.org/wiki/Unix信號

