PD源码阅读系列:PD节点启动
李雷,神州數(shù)碼武漢云基地,目前在研究TiDB的PD模塊。
在TiDB生態(tài)中,PD作為調(diào)度模塊,負(fù)責(zé)整個集群的調(diào)度以及保存整個集群的云信息。這篇文章將從PD的啟動作為入手點(diǎn),簡單剖析PD節(jié)點(diǎn)啟動的步驟,了解PD啟動的流程,學(xué)習(xí)PD讀取配置、啟動日志和監(jiān)控、設(shè)置并啟動PD節(jié)點(diǎn)服務(wù)并通過協(xié)程的方式監(jiān)聽退出命令等知識點(diǎn)。PD簡介
Placement Driver (后續(xù)以 PD 簡稱) 是 TiDB 里面全局中心總控節(jié)點(diǎn),它負(fù)責(zé)整個集群的調(diào)度,負(fù)責(zé)全局 ID 的生成,以及全局時間戳 TSO 的生成等。PD 還保存著整個集群 TiKV 的元信息,負(fù)責(zé)給 client 提供路由功能。
在架構(gòu)上面,PD 所有的數(shù)據(jù)都是通過 TiKV 主動上報獲知的。同時,PD 對整個 TiKV 集群的調(diào)度等操作,也只會在 TiKV 發(fā)送 heartbeat 命令的結(jié)果里面返回相關(guān)的命令,讓 TiKV 自行去處理,而不是主動去給 TiKV 發(fā)命令。這樣設(shè)計上面就非常簡單,我們完全可以認(rèn)為 PD 是一個無狀態(tài)的服務(wù)(當(dāng)然,PD 仍然會將一些信息持久化到 etcd),所有的操作都是被動觸發(fā),即使 PD 掛掉,新選出的 PD leader 也能立刻對外服務(wù),無需考慮任何之前的中間狀態(tài)。
Why PD?
根據(jù)上文,我們了解到PD節(jié)點(diǎn)的主要作用在于元數(shù)據(jù)的存儲以及TiKV節(jié)點(diǎn)的調(diào)度。那么我們不禁要問,為什么需要PD?
當(dāng)我們只有一個TiKV時,那就根本不需要調(diào)度,因?yàn)閿?shù)據(jù)只可能存在于這一臺機(jī)器上,各種客戶端也只可能與這一個TiKV節(jié)點(diǎn)進(jìn)行交互。在分布式存儲領(lǐng)域,這種情況不可能一直持續(xù)下去,因?yàn)閿?shù)據(jù)的增量一定會超過這臺機(jī)器的存儲極限。到時我們必須將部分?jǐn)?shù)據(jù)遷移到其他機(jī)器上去。
了解過TiKV的同學(xué)們都知道TiKV使用range的方式將數(shù)據(jù)進(jìn)行切分。我們使用Region來表示一個數(shù)據(jù)range。每個Region都有多個副本Peer。通常為了數(shù)據(jù)可靠性,我們至少使用三個副本。
最開始系統(tǒng)初始化的時候,我們只有一個Region。當(dāng)數(shù)據(jù)量持續(xù)增大而超過Region設(shè)置的最大Size(64MB)閾值時,Region就會分裂,生成兩個新的Region。Region是調(diào)度TiKV的基本單位。當(dāng)我們新增一個TiKV的時候,PD就會將原來TiKV中的一些Region調(diào)度到這個新增的TiKV中去。這樣就能保證整個數(shù)據(jù)均衡的分布在TiKV集群上面。因?yàn)橐粋€Region通常是64MB,將一個Region從一個TiKV移動到另一個TiKV的過程中,數(shù)據(jù)量變更其實(shí)不大。所以可以直接使用Region的數(shù)量來大概的做數(shù)據(jù)的平衡。
上面我們對TiKV數(shù)據(jù)的調(diào)度做了簡單的介紹,但是實(shí)際的情況要比這個復(fù)雜很多。我們不僅要考慮數(shù)據(jù)的均衡,也要考慮計算的均衡。這樣才能保證整個TiKV集群更快更好的對外提供服務(wù)。因?yàn)門iKV使用的是Raft一致性算法。Raft有一個強(qiáng)約束就是為了保證線性一致性。所有的讀寫都必須通過Leader發(fā)起。假設(shè)現(xiàn)在有三個TiKV,如果幾乎所有的Leader都集中在某一個TiKV上,那么會造成這個TiKV成為性能瓶頸,最好的做法是Leader也能夠均衡地分布在不同的TiKV上,這樣整個系統(tǒng)都能對外提供服務(wù)。
總的來說,在分布式存儲TiKV中,調(diào)度任務(wù)及其重要。這關(guān)乎系統(tǒng)向外提供服務(wù)的質(zhì)量。我們必須同時考慮存儲Storage和計算Leader等資源。所以我們得出一個觀點(diǎn),分布式存儲系統(tǒng)是必須要有一個調(diào)度模塊的。那么,調(diào)度模塊的實(shí)現(xiàn)形式是什么樣的?今天我們都知道了在TIDB生態(tài)中,有PD作為TiKV集群的調(diào)度模塊。那么為什么需要單獨(dú)拿出來作為一個項(xiàng)目?我認(rèn)為這樣做的最大好處就是降低耦合。TiDB生態(tài)中,TiDB server負(fù)責(zé)查詢,TiKV負(fù)責(zé)存儲,PD則負(fù)責(zé)TiKV調(diào)度。如果將調(diào)度模塊寫在TiDB或者TiKV里,當(dāng)TiDB或TiKV擴(kuò)展節(jié)點(diǎn)時,PD也會跟著1:1地擴(kuò)展。這將會造成一定的性能浪費(fèi),因?yàn)槲覀儗?shí)際上并不一定需要與TIDB或TiKV節(jié)點(diǎn)數(shù)一樣多的PD模塊。另外也可以說這是遵守了軟件設(shè)計原則中的單一職責(zé)原則。
PD相關(guān)技術(shù)
- Go:PD完全由Go開發(fā)。Go語言簡單易用,天生支持高并發(fā)。PD源碼體積很小,不到5M,但是性能相當(dāng)不錯。
- Etcd:分布式系統(tǒng)中最關(guān)鍵的分布式可靠鍵值存儲。PD將Region meta信息持久化在etcd,以保證切換 PD Leader 節(jié)點(diǎn)后能快速繼續(xù)提供 Region 路由服務(wù)。
- Raft:Etcd實(shí)現(xiàn)數(shù)據(jù)可靠性靠的是分布式一致性算法Raft。
- Prometheus:PD集成Prometheus來達(dá)到指標(biāo)監(jiān)控的目的。每個PD啟動時都會配置Prometheus,將系統(tǒng)運(yùn)行的指標(biāo)傳給Prometheus。
- Zap Logger:Go系統(tǒng)庫自帶的日志包存在一定的性能與功能缺乏。PD集成了由 Ubder 開源的高性能日志框架Zap Logger來提高PD的性能。
- TOML:PD配置文件書寫語法,由前GitHub CEO, Tom Preston-Werner,于2013年創(chuàng)建。其目標(biāo)是成為一個小規(guī)模的易于使用的語義化配置文件格式。
PD本地編譯運(yùn)行
PD代碼開源,可以從github獲取:
https://github.com/tikv/pd
源碼閱讀需要在本地編譯運(yùn)行PD源碼。首先需要準(zhǔn)備PD所需環(huán)境。我本地運(yùn)行的是Win10 系統(tǒng),安裝了如下依賴:go 1.14.7 + cmake3 + mingw64,使用intellij idea本地編譯運(yùn)行。
這里需要注意的是,我一開始安裝的 go 版本為1.15。結(jié)果每次本地編譯都會報類似于內(nèi)存泄漏等問題。解決方法是降低 go 的版本。我降到1.14版本后即可正常編譯運(yùn)行PD server。
還有另外一點(diǎn)是PD源碼有個ui模塊中文件 embedded_assets_rewriter 可能會報錯,報錯原因是未識別的變量。我在相關(guān)論壇提問也沒得到回應(yīng),于是只能選擇注釋掉未聲明的變量并將相關(guān)方法返回nil。處理完這些問題就能跑起PD server來了。
PD源碼閱讀
今天將解讀pd源碼的開始部分:啟動一個pd server。
閱讀從根目錄下的cmd/pd-server/main.go開始,由此展開。
一、讀取配置
PD的配置信息有三個來源。分別是Config對象默認(rèn)配置,外部配置文件和命令行參數(shù)。它們的優(yōu)先級分別是命令行參數(shù) > 外部配置文件 > 默認(rèn)。下面第一塊代碼就是讀取配置的兩行代碼。config.NewConfig()獲取到系統(tǒng)的默認(rèn)配置。系統(tǒng)默認(rèn)配置文件在/conf/config.toml里。在Config 的結(jié)構(gòu)體中,可以利用第三方包 http://github.com/BurntSushi/toml 直接讀取 toml 格式的配置文件中的值。下面的第二段代碼就是config結(jié)構(gòu)體中使用 toml 工具包讀取 toml 格式的配置文件中的值來設(shè)置屬性的默認(rèn)值的部分代碼。通過 toml:"配置文件中屬性名"的形式獲取到配置的值。從而設(shè)置為該屬性的默認(rèn)值。Parse 方法讀取命令行參數(shù)并將參數(shù)設(shè)置到config對象中去。
讀取配置
cfg := config.NewConfig()err := cfg.Parse(os.Args[1:])Config結(jié)構(gòu)體部分代碼
type Config struct {flagSet *flag.FlagSetVersion bool `json:"-"`ConfigCheck bool `json:"-"`ClientUrls string`toml:"client-urls" json:"client-urls"`PeerUrls string`toml:"peer-urls" json:"peer-urls"`AdvertiseClientUrlsstring `toml:"advertise-client-urls"json:"advertise-client-urls"`AdvertisePeerUrls string`toml:"advertise-peer-urls" json:"advertise-peer-urls"`}創(chuàng)建默認(rèn)配置對象cfg時,NewConfig 方法內(nèi)部還將利用 flagSet 對象對cfg各個屬性做屬性說明。對于bool類型的屬性將調(diào)用flagSet的BoolVar方法對其進(jìn)行說明。具體過程會聲明該變量的簡稱,值以及用處。同理 StringVar 就是對 string 類型的變量做說明的。
下面的示例代碼就展示了 BoolVar 和 StringVar 的內(nèi)部邏輯以及使用這些方法對config對象的屬性做說明的過程。我們可以看到使用 StringVar 對屬性 configFile 做了說明。其簡稱為 config 。它的值默認(rèn)為 "" 。它的用處就是作為配置文件。同理,BoolVar也對bool類型的屬性 ConfigCheck 做了說明。說明它是檢查配置文件的合規(guī)性的。
New Config()
cfg := &Config{}cfg.flagSet =flag.NewFlagSet("pd", flag.ContinueOnError)fs := cfg.flagSetfs.StringVar(&cfg.configFile,"config", "", "config file")fs.BoolVar(&cfg.ConfigCheck,"config-check", false, "check config file validity and exit")func (f *FlagSet) BoolVar(p *bool, namestring, value bool, usage string) {f.Var(newBoolValue(value, p), name, usage)}func (f *FlagSet) StringVar(p *string,name string, value string, usage string) {f.Var(newStringValue(value, p), name, usage)}以上是默認(rèn)配置的一些處理操作。接下來講講獲取外部配置文件和命令行中的配置信息。
PD 在啟動時可以攜帶外部的配置文件對 PD 的屬性做配置。具體操作是用命令行啟動 PD 時,使用命令行參數(shù) --config 指明外部配置文件的位置。例如 --config "/usr/local/config.toml" 將指定 PD 啟動時讀取本機(jī)文件目錄 /usr/local/config.toml 的配置文件。
接著我們在代碼層面看一下這個過程:
首先在 main 方法中獲取命令行參數(shù)信息。這一步驟是通過 go 的os包支持的。通過 os.Args獲取命令行參數(shù)數(shù)組。然后傳入到 config 對象的 Parse 方法中。
接著在 Parse 方法中,調(diào)用 flagSet 的 Parse 方法將命令行參數(shù)都設(shè)置到config對象對應(yīng)的屬性上。在隨后的代碼中將判斷 config 對象中 configFile 屬性是否非空。因?yàn)檫@個屬性默認(rèn)是空字符串,只有設(shè)置了值,才能進(jìn)行下一步讀取指定路徑的配置文件。當(dāng)它的值非空時將調(diào)用 configFromFile 方法讀取指定目錄的配置文件,讀取的結(jié)果放到 toml.MetaData 對象中。然后將這個對象傳入到 config 對象的Adjust 方法中用于調(diào)整 config 的各個屬性值。
PD 的配置文件描述全面的資料可以參考:
PD 配置文件描述
命令行參數(shù)描述可以參考:
PD 配置參數(shù)
讀取完配置后,Parse 方法將返回err對象以幫助判斷Parse過程是否成功。err 如果是 nil,則說明Parse是沒有問題的。如果是ErrHelp,則說明輸入命令行的是-h或者是-help。輸入這個命令說明我只是想查看pd啟動時可以攜帶哪些配置參數(shù)而不是直接啟動pd。所以在這個case下將調(diào)用 exit 方法退出啟動程序。除此之外,其他情況就是parse過程錯誤,輸出錯誤提示信息。
Parse結(jié)果檢查
switch errors.Cause(err) { case nil: case flag.ErrHelp:exit(0) default:log.Fatal("parse cmd flags error", errs.ZapError(errs.ErrParseFlags)) }二、啟動logger服務(wù)并打印PD Server的信息和警告信息
PD使用zap Logger替代go原生的log組件以此來提高整體運(yùn)行的性能。我們都知道go原生的logger使用起來十分簡單。我們通過設(shè)置任何io.writer作為日志記錄輸出并向其發(fā)送要寫入的日志就行。但是簡單歸簡單,原生logger也有很多不足的地方。例如:僅限基本日志級別、只有一個Print選項(xiàng)、Fatal日志通過調(diào)用os.Exit(1)來結(jié)束程序、Panic日志在寫入日志消息之后拋出一個panic、不提供日志切割的能力、缺乏日志格式化能力等。綜合這些原因,pd使用uber開源的日志框架zap logger來替換原生的logger。zap logger有兩個優(yōu)點(diǎn)。其一是提供了結(jié)構(gòu)化日志記錄和printf風(fēng)格的日志記錄。其二是它非常的快。關(guān)于zap logger高性能的設(shè)計思路可以參考它家github地址:
https://github.com/uber-go/zap#performance
下方代碼就是PD創(chuàng)建zap logger來替換原生logger的過程:
首先調(diào)用cfg對象的 SetupLogger 方法設(shè)置cfg的logger和logProps屬性。在SetupLogger 方法內(nèi)部,使用PingCAP自家的log包里的初始化方法 InitLogger獲得zap.logger 和ZapProperties對象并將二者分別賦給cfg的 logger 和 logProps屬性。接著使用 ReplaceGlobals替換全局的logger。然后刷新緩存,最后使用 InitLogger 初始化zap logger。
logger組件設(shè)置啟動好之后,打印PD信息和警告。
啟動logger:
err = cfg.SetupLogger() if err == nil {log.ReplaceGlobals(cfg.GetZapLogger(), cfg.GetZapLogProperties()) } else {log.Fatal("initialize logger error", errs.ZapError(err)) } // Flushing any buffered log entries defer log.Sync()// The old logger err = logutil.InitLogger(&cfg.Log) if err != nil {log.Fatal("initialize logger error", errs.ZapError(err)) }server.LogPDInfo()for _, msg := range cfg.WarningMsgs {log.Warn(msg) }三、Prometheus監(jiān)控
在 main 方法中調(diào)用 EnableHandlingTimeHistogram 。在 PD 啟動時,會初始化一個默認(rèn)的 ServerMetrics 對象來記錄 PD server服務(wù)運(yùn)行的指標(biāo)。默認(rèn)不開啟 Histogram metrics 這個指標(biāo)監(jiān)控。因?yàn)檫@個指標(biāo)監(jiān)控耗費(fèi)性能較高。在源碼的注釋中也說明,開啟 Histogram metrics 監(jiān)控可能會耗費(fèi)較大性能。如果機(jī)器性能有限,那么可以選擇不開啟。
接著就會調(diào)用 Push 方法將指標(biāo)發(fā)送到 Prometheus 的推送網(wǎng)關(guān)上。具體推送方法是 prometheusPushClinet。在該方法內(nèi)首先構(gòu)造推送者對象pusher。pusher的構(gòu)造使用了建造者模式。首先使用推送的地址和任務(wù)初始化pusher,添加了為其添加了收集器以及分組標(biāo)簽。
Prometheus監(jiān)控:
grpcprometheus.EnableHandlingTimeHistogram()metricutil.Push(&cfg.Metric) Gatherer(prometheus.DefaultGatherer).Grouping("instance", instanceName())for {err := pusher.Push()if err != nil {log.Error("could not push metrics to Prometheus Pushgateway", errs.ZapError(errs.ErrPrometheusPushMetrics, err))}time.Sleep(interval)} }四、動態(tài)添加節(jié)點(diǎn)
PD使用 PrepareJoinCluster 方法將當(dāng)前節(jié)點(diǎn) Join指定的集群當(dāng)中去并且在Join成功后持久化Join配置,當(dāng)PD節(jié)點(diǎn)宕機(jī)后重啟時,讀取本地配置就能快速重新加入集群。
下面簡單聊聊從PD節(jié)點(diǎn)首次加入到一個集群以及PD停機(jī)再次加入集群的情況。
當(dāng)PD節(jié)點(diǎn)首次Join某集群時,我們進(jìn)入PrepareJoinCluster 方法,攜帶的參數(shù)時cfg,也就是PD的配置對象。當(dāng)我們想Join某個集群時,首先保證目標(biāo)集群能夠正常工作。在啟動PD節(jié)點(diǎn)時。命令行攜帶參數(shù)--join="target-urls",target-urls就是目標(biāo)集群里任意PD的advertise-clinet-url。PD啟動時通過os.Args讀取這些額外參數(shù)并設(shè)置到cfg對象中去。首先要做基本的差錯檢測,排除Join信息錯誤的情況。然后嘗試讀取本地保存的Join信息。我們是第一次Join到一個陌生的集群,這些信息以及目錄還沒有創(chuàng)建。接下來將創(chuàng)建一個etcd的client,創(chuàng)建時傳入Join信息、TLS憑證配置、超時限制等信息。下一步,ListEtcdMember 方法列出目標(biāo)集群所有的etcd成員。隨后判斷當(dāng)前PD節(jié)點(diǎn)是否與集群中的節(jié)點(diǎn)重名。重名則無法加入集群,直接退出。如果滿足條件名字不沖突。隨后使用 AddEtcdMenber方法嘗試加入集群。結(jié)果將返回到類型為*clientv3.MenberAddResponse的對象中。隨后再次調(diào)用 ListEtcdMenber 獲取最新的etcd集群成員信息并對集群情況進(jìn)行驗(yàn)證,并將最新的集群信息更新到cfg對象中。最后將節(jié)點(diǎn)配置信息保存到本地。
當(dāng)PD停機(jī)再次重啟時,直接讀取本地文件獲取集群信息并加入到集群中去。
Join節(jié)點(diǎn):
err = join.PrepareJoinCluster(cfg) if err != nil {log.Fatal("join meet error", errs.ZapError(err)) }五、創(chuàng)建并運(yùn)行PD Server
這一步驟主要做兩件事情。第一個就是創(chuàng)建PD Server并運(yùn)行。第二就是監(jiān)聽退出信號。
首先使用 CreateServer 方法創(chuàng)建Server對象并且傳入所需要的參數(shù):上下文對象ctx、配置cfg、服務(wù)數(shù)組servcieBuilders。接著調(diào)用server的Run方法啟動Server。在Run方法內(nèi),首先會通過協(xié)程開啟監(jiān)控。隨后開啟etcd和Server服務(wù)。最后通過Server的startServerLoop方法使得服務(wù)處于不斷運(yùn)行的狀態(tài)而不退出。
另外一個部分就是監(jiān)聽退出信號。通過監(jiān)聽四種信號來判斷是否要中止服務(wù)。這四種信號及含義如下表所示。監(jiān)聽程序通過協(xié)程的方式監(jiān)聽退出信號,一旦監(jiān)聽到退出信號,調(diào)用cancle方法即會向ctx對象的Done通道發(fā)送消息。Done通道一旦接收到消息運(yùn)行Server的線程就會退出。接著就會打印退出信息返回退出碼。
| SIGHUP | 1 | Term | 終端控制進(jìn)程結(jié)束(終端連接斷開) |
| SIGHINT | 2 | Term | 用戶發(fā)送INTR字符(Ctrl+C)觸發(fā) |
| SIGTERM | 15 | Term | 結(jié)束程序(可以被捕獲、阻塞或忽略) |
| SIGQUIT | 3 | Core | 用戶發(fā)送QUIT字符(Ctrl+/)觸發(fā) |
創(chuàng)建 PD Server:
ctx, cancel := context.WithCancel(context.Background()) serviceBuilders := []server.HandlerBuilder{api.NewHandler, swaggerserver.NewHandler, autoscaling.NewHandler} serviceBuilders = append(serviceBuilders, dashboard.GetServiceBuilders()...) svr, err := server.CreateServer(ctx, cfg, serviceBuilders...) if err != nil {log.Fatal("create server failed", errs.ZapError(err)) }總的來說,PD節(jié)點(diǎn)的啟動會經(jīng)歷讀取配置、設(shè)置logger、啟動prometheus監(jiān)控、join集群、啟動server、監(jiān)聽退出命令后退出等步驟。
我們今天主要了解了PD節(jié)點(diǎn)啟動的基本步驟,也了解到PD對zap logger和Prometheus等中間件的集成使用。最后學(xué)習(xí)了使用協(xié)程監(jiān)聽退出命令。
整個PD的啟動流程用下面流程圖表示一下:
?本篇文章只是對PD節(jié)點(diǎn)啟動做的一個粗略的解讀,有些地方可能存在錯誤希望有真知灼見的大神能不吝賜教,指出我的問題,多多交流。
總結(jié)
以上是生活随笔為你收集整理的PD源码阅读系列:PD节点启动的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 365抽奖软件 v6.1.7
- 下一篇: 利用red5搭建一个简单的流媒体直播系统