前言
Tendermint学习随笔(2)P2P源码阅读与分析 中,对 p2p 相关源码的调用关系和业务逻辑进行了分析,可以发现整个 p2p 通信的最小单位就是 node,故而本文对 node 进行分析。而 node 涉及到了 tendermint 系统的全生命周期所以将以 node 启动流程为切入点,对相关功能进行分析。一、源码分析
1.1 tendermint 启动入口
与传统 go 语言项目相同,tendermint 的启动主入口在 cmd 目录中,使用cobra
作为命令行的构建工具,cmd/tendermint 目录的目录结构如下:
1 | . |
结合cobra
和对目录结构的分析,不难看出应该从main.go
作为分析的入口,commands
目录下是各个命令的具体实现。接下来先看该文件源码:
1 | package main |
从代码中不难看出,各种具体命令的实现都在github.com/tendermint/tendermint/cmd/tendermint/commands
包中,对应前文看到的commands
目录,启动 node 对应的代码入口为cmd.NewRunNodeCmd(nodeFunc)
,这里我们先看nodeFunc
是如何创建的,对应的github.com/tendermint/tendermint/node
包就是前文提到的node
目录。
首先看NewRunNodeCmd
函数,该函数是 tendermint 节点启动的直接入口,对应实现在run_node.go
文件中:
1 | // NewRunNodeCmd returns the command that allows the CLI to start a node. |
这个函数通过传入的nodeProvider
构建节点n
,构建过程中传入的config
和logger
两个变量是在构建命令行的过程中优先构建的,创建代码在commands
目录的root.go
中:
1 | var ( |
随后通过调用n.Start()
启动节点,Start
函数是 tendermint 系统构建的统一服务类,用于实现各个服务的统一启动和停止,具体实现在/libs/sevice/service.go
中:
1 | // Start implements Service by calling OnStart (if defined). An error will be |
不难看出,该实现最后调用了对应实现的OnStart()
函数,Start
作为BaseService
的成员函数能被调用的原因,是Node
结构体包含了service.BaseService
结构体,这个结构体中有Service
的定义impl
,只要node
匿名实现了该方法,就可以进行调用.
1 | type Node struct { |
最后通过自定义的TrapSignal
函数监听系统的终止信号.
1 | // TrapSignal catches the SIGTERM/SIGINT and executes cb function. After that it exits |
另一方面,再先看DefaultNewNode
这个函数,这个函数是直接被作为对象传入NewRunNodeCmd
函数中,而该函数的定义也在github.com/tendermint/tendermint/node
这个包中,综上所属,各种调用的最后逻辑都指向了node
这个包,接下来对该包进行分析.
1.2 Node 创建过程
node 目录下,只有如下四个文件,具体发挥功能的只有node.go
这个文件.
1 | . |
顺着上一节,我们首先分析DefaultNewNode
这个函数:
1 | // DefaultNewNode returns a Tendermint node with default settings for the |
该函数首先通过p2p.LoadOrGenNodeKey
函数获取节点密钥,然后调用NewNode
函数构建节点,传入的各个函数的构建方法如上所示,都是普通的构建逻辑.在分析NewNode
函数前,先看一下Node
的结构体定义:
1 | // Node is the highest level interface to a full Tendermint node. |
在Node
的结构体定义中,保存了所有节点的网络信息,同时保存了各类存储句柄,reactor 句柄,通信相关服务句柄,监控相关句柄,具体的构建都在NewNode
中进行:
1 | // NewNode returns a new, ready to go, Tendermint Node. |
NewNode
函数首先通过initDBs
初始化区块存储和状态数据库,默认的存储后端为goleveldb
.然后再通过sm.NewStore
封装并初始化当前的状态存储,最后通过LoadStateFromDBOrGenesisDocProvider
函数从创世文件和数据库中获取当前状态,具体的状态存储设计,放在后续章节中分析.
1 | func initDBs(config *cfg.Config, dbProvider DBProvider) (blockStore *store.BlockStore, stateDB dbm.DB, err error) { |
在构建完存储和数据库后,NewNode
函数将创建用于通信的代理服务.其中createAndStartProxyAppConns
创建 ABCI 通信,createAndStartEventBus
和createAndStartIndexerService
用以监听事件和构建事务与区块索引.
1 | func createAndStartProxyAppConns(clientCreator proxy.ClientCreator, logger log.Logger) (proxy.AppConns, error) { |
接下来构建验证者,默认是将自己作为验证者,但如果配置文件中设置了外部验证者的地址,则会尝试通过createAndStartPrivValidatorSocketClient
函数进行连接:
1 | func createAndStartPrivValidatorSocketClient( |
在验证者构建完成并获得pubKey
以后,则将根据配置文件信息,检查是否进行状态同步,当本地存在状态数据时,将直接通过doHandshake
构建本地数据,其核心是调用Handshake
函数,该函数会对所有区块数据进行重放.具体的重放流程将会在后续分析共识模块的过程中进行分析.
1 | func doHandshake( |
在状态数据准备完毕后,检测是否启用快速同步,然后将当前节点启动状态信息输出:
1 | func logNodeStartupInfo(state sm.State, pubKey crypto.PubKey, logger, consensusLogger log.Logger) { |
输出信息后,就进入正式的 reactor 等各类功能模块的构建环节,这个环节中,首先建立各个组件的监控工具,然后通过createMempoolAndMempoolReactor
,createEvidenceReactor
,createBlockchainReactor
,createConsensusReactor
,statesync.NewReactor
构建 tendermint 核心的几个 reactor
1 | //检查配置文件,构建指定版本mempool |
在构建各个相关组件之后,将会构建节点通信功能,对应上一篇文章提到的 p2p 功能中的transport
,switch
等通信组件并通过AddPersistentPeers
,AddUnconditionalPeerIDs
添加对等节点和利用createAddrBookAndSetOnSwitch
构件地址簿:
1 | func createTransport( |
在通信功能构建完成以后,会再根据配置文件调用createPEXReactorAndAddToSwitch
函数创建一个特殊的pexReactor
,这个reactor
会接管节点之间的连接和信息交换…
1 | func createPEXReactorAndAddToSwitch(addrBook pex.AddrBook, config *cfg.Config, |
至此,node
的构建流程就基本结束,NewNode
函数后续代码是构建node
对象等操作,这里就不再详细赘述,总结下来,创建节点主要分为以下几个过程:
- 初始化区块存储与状态存储
- 初始化通信服务和索引服务
- 初始化验证者角色
- 恢复存储状态
- 构建
mempool
,Evidence
,Blockchain
,Consensus
,statesync
等核心功能 - 建立节点节通信并构建
pexReactor
- 构建节点
1.3 Node 启动过程
在 tendermint 系统中,核心服务都是通过OnStart
,OnStop
两个函数进行统一的启动和停止管理,我们这里首先看启动过程:
1 | // OnStart starts the Node. It implements service.Service. |
这个启动过程中,首先同步时间,然后通过startRPC
函数启动 RPC 通信:
1 | // ConfigureRPC makes sure RPC has all the objects it needs to operate. |
随后通过startPrometheusServer
函数启动监控服务:
1 | // startPrometheusServer starts a Prometheus HTTP server, listening for metrics |
最后启动 p2p 通信并设置节点信息同步:
1 | // startStateSync starts an asynchronous state sync process, then switches to fast sync mode. |
在整个启动过程中,最核心的是
swtich的启动,该启动同样服务了基本服务的
Start函数并最终调用了
swtich的
OnStart`函数.
1.4 Node 停止过程
停止的核心函数是OnStop
,没有过多需要分析的地方,就是一个个 reactor 和通信服务的停止.
1 | // OnStop stops the Node. It implements service.Service. |
二、总结
- 结合上一篇文章对 P2P 源码的分析,不难看出 Tendermint 的节点中,各个 Reactor 启动最后均是由 Switch 的启动引发的,每个 Reactor 启动时同时也会将自己子模块的各个功能启动。相应的 reactor 停止功能也是由 Switch 的停止引发的.