天天看點

帶你讀《GO語言公鍊開發實戰》之三:守護程序的初始化與運作第3章

點選檢視第一章 點選檢視第二章

第3章

守護程序的初始化與運作

3.1 概述

節點初始化是節點首次使用時,根據使用者傳入的參數進行設定,并根據參數進行網絡、資料庫、本地區塊鍊以及P2P分布式網絡等子產品的初始化,使得節點能夠正常運作。節點初始化由bytomd守護程序執行,在初次運作時一次性完成。

本章主要内容:

  • bytomd守護程序初始化流程。
  • 守護程序初始化的具體實作,包括網絡、資料庫、本地區塊鍊初始化等。
  • 守護程序啟動流程和停止流程。

3.2 bytomd守護程序初始化流程及指令參數

守護程序是一種特殊程序,啟動後一直在背景運作,隻有當觸發特定的信号時,才會執行退出操作。比原鍊的守護程序是bytomd,初始化流程如圖3-1所示。

在編寫指令行程式時,通常需要對指令參數進行解析。不同語言一般都會提供解析指令行參數的方法或庫,以友善程式員使用。在GO語言标準庫中提供了flag包,友善進行指令行解析。

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

bytomd程序支援的傳參如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章
帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

3.3 bytomd守護程序的初始化實作

bytomd守護程序的Cobra流程與bytomcli過程非常相似,是以在此略去,後續主要對bytomd守護程序重要内容進行深入分析。在這裡我們看一下bytomd預處理過程中使用到的代碼檔案結構,指令如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

bytomd守護程序啟動時,會根據不同的指令行flag參數,初始化不同的子產品,最終以守護程序的方式運作。有關bytomd守護程序所有的運作工作都在node.NewNode(config)的具體實作中。下面介紹具體的實作過程。

3.3.1 Node對象

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

Node對象說明如下。

  • cmn.BaseService:服務管理。
  • config:目前節點的全局配置。
  • syncManager:區塊和交易同步管理。
  • wallet:本地錢包管理。
  • accessTokens:token管理,使用者通路憑證。
  • api:API Server接口服務。
  • chain:本地區塊鍊管理對象。
  • txfeed:目前版本中該功能未使用。
  • cpuMiner:CPU挖礦管理對象。
  • miningPool:礦池管理對象。
  • miningEnable:是否啟用挖礦模式。

node.NewNode(config)整個過程是為了建立Node對象,Node對象是整個bytomd所有子產品運作的基礎。

cmn.BaseService是tendermint架構的一個服務管理子產品,在這裡我們可以把Node作為一個服務,對該服務進行OnStart/OnStop/IsRunning等操作管理。tendermint架構可以保證這些操作不會被重複執行多次。

3.3.2 配置初始化

在執行node.NewNode(config)之前,config的預設配置就已經定義好了。在深入node.NewNode(config)分析之前,我們需要先了解預設配置都有哪些。

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

首先,bytomd守護程序聲明一個config全局變量,表示整個bytomd守護程序的配置資訊。程序啟動時config對象被賦予一個預設的配置參數。

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

預設的配置參數分有6個,每個針對不同的子產品。下面對配置進行說明。我們将預設參數歸納為三塊:Base基礎配置、P2P網絡配置、其他配置。

1. Base基礎配置

BaseConfig用于配置bytomd節點所需的基礎參數,包括資料目錄、日志、監聽位址等相關參數。

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章
帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

部分參數從配置檔案中擷取預設值,比如ApiAddress參數,它的tag是api_addr。我們可以從config/toml.go中擷取預設值:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

2. P2P網絡配置

P2PConfig用于配置bytomd P2P通信協定中使用的參數,包括本機監聽端口、通信節點逾時、位址簿等相關參數。

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章
帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

注意,在比特币中,節點會采用DNS的方式來詢問種子節點,進而查詢到其他節點的IP位址。而在比原鍊中,種子節點是IP位址,一般會寫死到代碼裡。技術細節我們會在後面的第10章詳細講解。

3. 其他配置

WalletConfig用于配置bytomd本地錢包使用的參數,包括是否啟用本地錢包和更新等相關參數。

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

在bytomd守護程序聲明config = DefaultConfig()之後,init()函數實作了config對象中各屬性的指派。具體實作代碼如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章
帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

在init()函數中定義了很多不同類型的flag參數,并将flag的參數值綁定到config對象上,比如:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

這條語句的含義為:

  • 定義一個Bool類型的flag參數。
  • 該flag的名稱為mining。
  • 該flag的指派對象為config.Mining。
  • 該flag的描述資訊為Enable mining。

至此,bytomd守護程序所需要的配置資訊初始化完畢,程式運作真正進入初始階段。下面對此進行深入分析。

3.3.3 建立檔案鎖

在比原鍊中,一份資料目錄(--root參數指定)隻能同時由一個bytomd守護程序讀寫,因為LevelDB高性能鍵值資料庫是單程序模式,如果多個程序同時讀寫一份資料,會造成資料不一緻的情況。是以,需要使用檔案鎖可以保證同一時間一個程序讀寫一份資料目錄,代碼如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

bytomd啟動時,lockDataDirectory函數使用flock在RootDir目錄下建立一個LOCK檔案。如果bytomd程序在一個檔案的inode上加了鎖,那麼再次啟動bytomd程序則會對errors.New中的内容報錯并退出程序。flock的作用是檢測程序是否已經存在。

flock主要有3種操作類型。

  • LOCK_SH:共享鎖,多個程序使用同一把鎖用于讀鎖。
  • LOCK_EX:排他鎖,同時隻允許一個程序使用,一般用于寫鎖。
  • LOCK_UN:釋放鎖。

如果深入研究flock包的函數,我們可以看到,這裡使用了LOCK_EX鎖,即同時隻允許一個程序使用,代碼示例如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

3.3.4 初始化網絡類型

比原鍊的三種網絡模式,分别是mainnet主網、testnet測試網和solonet單機模式。

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

其中initActiveNetParams函數根據使用者傳入的chain_id,初始化網絡類型。consensus.ActiveNetParams對象儲存了目前使用的網絡模式。在比原鍊代碼中會經常引用consensus.ActiveNetParams對象,用來識别目前節點連接配接的網絡類型。

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

ActiveNetParams預設使用主網。MainNetParams中的參數說明如下。

  • Bech32HRPSegwit:隔離見證,是一種協定更新,我們會在後面第5章講解。
  • Checkpoints:檢查點,指定一個高度,以及與這個高度相比對的hash值,用于快速同步時驗證區塊的正确性。通常在主網更新時,會将曆史的塊資訊寫死在Checkpoints中。

Checkpoints檢查點有兩種作用:第一是防止分叉,如果有人試圖從檢查點之前的區塊進行分叉,目前節點不會接受這個分叉;也用于保護網絡不受全網51%的算力攻擊,因為攻擊者不可能逆轉檢查點之前的交易。第二是用于節點間的快速同步,我們将在第10章中詳細講解。

3.3.5 初始化資料庫(持久化存儲)

建立一條公鍊,需要将鍊上的所有資料(包含塊資訊、交易資訊等)存儲在本地鍵值資料庫中。在比原鍊中使用LevelDB來存儲鍊上資料,代碼如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

dbm使用tendermint架構的db管理庫。dbm.NewDB傳回一個DB對象,DB對象提供了資料庫接口和許多方法實作,包括使用記憶體映射、檔案系統目錄結構、GO中LevelDB等的實作。

dbm.NewDB傳回一個DB對象,需要傳入三個參數:db的名稱,db使用的鍵值資料庫(預設為LevelDB),db資料存儲的路徑。leveldb.NewStore函數傳回一個Store對象,即比原鍊對LevelDB進行了封裝,在LevelDB的基礎上增加了區塊緩存(cache)、區塊驗證、區塊狀态、區塊查詢等功能。

3.3.6 初始化交易池

當交易被廣播到網絡中并且被礦工接收到時,礦工會将接收到的交易加入到本地的TxPool交易池中,TxPool對象的作用是管理本地交易池。交易池相當于一個緩沖區,它并不是無限大。預設情況下比原鍊中交易池最大可以存儲10 000筆交易。如果超出這個門檻值,則會傳回"transaction pool reach the max number"提示。

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

protocol.NewTxPool()傳回一個TxPool執行個體對象。此處我們隻介紹交易池初始化部分,交易池實作原理的代碼将在6.10節中深入剖析。

3.5.7 建立一條本地區塊鍊

當節點第一次啟動時,判斷本地持久化存儲的狀态,當狀态為初始化時會初始化本地的區塊鍊。區塊鍊的第一個區塊(創世區塊)會被加入到區塊高度為0的地方。代碼如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

protocol.NewChain傳回一個Chain對象,NewChain需要接收兩個參數:Store區塊鍊的存儲對象,TxPool交易池。Chain對象管理着比原鍊的整個區塊鍊條。代碼如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

NewChain函數的執行可分為下面幾個步驟:

1)執行個體化Chain對象。

2)store.GetStoreStatus擷取本地區塊鍊的存儲狀态,如果狀态為nil則說明區塊鍊未被初始化。執行initChainStatus初始化本地區塊鍊,該函數初始化創世區塊(第一個區塊)并添加到本地鍊上。

3)store.LoadBlockIndex加載塊索引,從資料庫中讀取所有Block Header資訊并緩存在記憶體中,目的是加速通路區塊頭資訊。

4)c.index.SetMainChain,設定目前節點已同步的最新區塊。

5)go c.blockProcesser(),啟動一個go rutine,用于更新本地區塊鍊上的區塊資訊。

3.3.8 初始化本地錢包

預設情況下比原鍊節點會啟用本地錢包功能。代碼執行個體如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

在比原鍊的節點啟動時,上述代碼流程主要邏輯為:

1)建立加密機hsm對象,hsm對象管理keystore檔案,該檔案是存儲私鑰的一種格式(JSON)。keystore是一串代碼,本質上是加密後的私鑰,需配合錢包的密碼來使用。

2)建立錢包資料庫。

3)建立賬戶管理對象。

4)建立資産管理對象。

5)執行個體化Wallet對象。

6)RescanBlocks掃描本地所有區塊,觸發錢包更新操作。

3.3.9 初始化網絡同步管理

P2P通信子產品主要由SyncManager管理,SyncManager負責節點業務層資訊的同步工作,即區塊和交易資訊的同步。代碼如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章
帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

主要參數說明如下:

  • newBlockCh:通道用于新挖掘出的區塊進行快速廣播給其他節點。通道大小為1024。
  • netsync.NewSyncManager:執行個體化syncManager同步管理對象,它管理節點與節點之間的區塊、交易資訊同步。
  • newPoolTxListenner:啟動一個goroutine,監聽交易池中的交易,将交易發送給syncManager同步管理對象或本地錢包。

詳細實作機制将在第10章進行講解。

3.3.10 初始化Pprof性能分析工具

Pprof是GO語言标準庫中自帶的性能分析工具。用于記憶體分析、CPU分析、代碼追蹤等,還可以生成性能分析圖表。(詳細參考

https://golang.org/pkg/net/http/pprof/

)。在比原鍊中預設不啟用該功能,可以使用--prof_laddr參數啟動代碼性能分析功能,代碼示例如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

3.3.11 初始化CPU挖礦功能

在比原鍊節點源碼中,隻提供了CPU裝置的挖礦功能,以目前全網的算力來看,CPU裝置挖礦幾乎挖不到BTM币了。目前主流的挖礦裝置,有比特大陸定制的挖礦晶片或各大礦池使用GPU裝置挖礦。挖礦和礦池細節将在第13章中詳細解讀。代碼執行個體如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

其中,simd參數用于Tenaority CPU指令的優化。

3.4 bytomd守護程序的啟動方式和停止方式

我們在GO語言下實作守護程序的方式一般是,監聽标準的SIGTERM信号。在監聽到SIGTERM信号後,程序處于阻塞狀态,以實作守護程序。隻有當程序收到來自外部的SIGTERM信号時,程序則處于非阻塞狀态,實作程序退出。Linux信号參考

http://man7.org/linux/man-pages/man7/signal.7.html

。代碼執行個體如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

signal.Notify監聽中斷和Term信号。啟用goroutine取c對象,select進入阻塞狀态。當程序接收到Term信号則通知c對象,執行os.Exit退出守護程序。

發送Term信号有兩種方式:一種是執行指令kill -15 pid;另一種是程序運作在前台。

當守護程序接收到Term信号後就停止運作,在其退出之前需要做掃尾工作,如退出挖礦模式,退出P2P同步功能等。代碼示例如下:

帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章
帶你讀《GO語言公鍊開發實戰》之三:守護程式的初始化與運作第3章

3.5 本章小結

本章從源碼的角度分析了bytomd啟動過程中的Node對象建立和初始化,以及總結bytomd實作的邏輯。