2.深入TiDB:入口代碼分析及調試 TiDB


本文基於 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 的環境。

如果想要對自己的代碼進行調試,只需要:

  1. 安裝 mysql 客戶端;

    yum install mysql
    
  2. 啟動 TiDB tidb-server 包里面 main 函數;

  3. 啟動 mysql 客戶端;

    tidb 默認端口是 4000 ,賬號是 root ,庫我們選test

    mysql -h 127.0.0.1 -P 4000 -u root -D test
    
  4. 在對應的邏輯上斷點;

    例如我們要看 insert 的執行邏輯,首先需要創建了一個表:

    CREATE TABLE t (
    id      VARCHAR(31),
    name    VARCHAR(50),
    age     int,
    key     id_idx (id)
    );
    

    在插入邏輯的地方斷點住:

    image-20210718164645543

    然后執行插入指令即可

    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 異常。

tracker

優雅停機

優雅停機在項目中就更加常用了,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 方法這里留下了主要的邏輯:

  1. 配置路由信息;
  2. 監聽 connection;
  3. 為 connection 創建單獨的 Goroutine 進行處理。

server

獲取到的連接然后會調用 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信號

掃碼_搜索聯合傳播樣式-白色版 1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM