第3章
守护进程的初始化与运行
3.1 概述
节点初始化是节点首次使用时,根据用户传入的参数进行设置,并根据参数进行网络、数据库、本地区块链以及P2P分布式网络等模块的初始化,使得节点能够正常运行。节点初始化由bytomd守护进程执行,在初次运行时一次性完成。
本章主要内容:
- bytomd守护进程初始化流程。
- 守护进程初始化的具体实现,包括网络、数据库、本地区块链初始化等。
- 守护进程启动流程和停止流程。
3.2 bytomd守护进程初始化流程及命令参数
守护进程是一种特殊进程,启动后一直在后台运行,只有当触发特定的信号时,才会执行退出操作。比原链的守护进程是bytomd,初始化流程如图3-1所示。
在编写命令行程序时,通常需要对命令参数进行解析。不同语言一般都会提供解析命令行参数的方法或库,以方便程序员使用。在GO语言标准库中提供了flag包,方便进行命令行解析。
bytomd进程支持的传参如下:
3.3 bytomd守护进程的初始化实现
bytomd守护进程的Cobra流程与bytomcli过程非常相似,所以在此略去,后续主要对bytomd守护进程重要内容进行深入分析。在这里我们看一下bytomd预处理过程中使用到的代码文件结构,命令如下:
bytomd守护进程启动时,会根据不同的命令行flag参数,初始化不同的模块,最终以守护进程的方式运行。有关bytomd守护进程所有的运行工作都在node.NewNode(config)的具体实现中。下面介绍具体的实现过程。
3.3.1 Node对象
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)分析之前,我们需要先了解默认配置都有哪些。
首先,bytomd守护进程声明一个config全局变量,表示整个bytomd守护进程的配置信息。进程启动时config对象被赋予一个默认的配置参数。
默认的配置参数分有6个,每个针对不同的模块。下面对配置进行说明。我们将默认参数归纳为三块:Base基础配置、P2P网络配置、其他配置。
1. Base基础配置
BaseConfig用于配置bytomd节点所需的基础参数,包括数据目录、日志、监听地址等相关参数。
部分参数从配置文件中获取默认值,比如ApiAddress参数,它的tag是api_addr。我们可以从config/toml.go中获取默认值:
2. P2P网络配置
P2PConfig用于配置bytomd P2P通信协议中使用的参数,包括本机监听端口、通信节点超时、地址簿等相关参数。
注意,在比特币中,节点会采用DNS的方式来询问种子节点,进而查询到其他节点的IP地址。而在比原链中,种子节点是IP地址,一般会硬编码到代码里。技术细节我们会在后面的第10章详细讲解。
3. 其他配置
WalletConfig用于配置bytomd本地钱包使用的参数,包括是否启用本地钱包和更新等相关参数。
在bytomd守护进程声明config = DefaultConfig()之后,init()函数实现了config对象中各属性的赋值。具体实现代码如下:
在init()函数中定义了很多不同类型的flag参数,并将flag的参数值绑定到config对象上,比如:
这条语句的含义为:
- 定义一个Bool类型的flag参数。
- 该flag的名称为mining。
- 该flag的赋值对象为config.Mining。
- 该flag的描述信息为Enable mining。
至此,bytomd守护进程所需要的配置信息初始化完毕,程序运行真正进入初始阶段。下面对此进行深入分析。
3.3.3 创建文件锁
在比原链中,一份数据目录(--root参数指定)只能同时由一个bytomd守护进程读写,因为LevelDB高性能键值数据库是单进程模式,如果多个进程同时读写一份数据,会造成数据不一致的情况。因此,需要使用文件锁可以保证同一时间一个进程读写一份数据目录,代码如下:
bytomd启动时,lockDataDirectory函数使用flock在RootDir目录下创建一个LOCK文件。如果bytomd进程在一个文件的inode上加了锁,那么再次启动bytomd进程则会对errors.New中的内容报错并退出进程。flock的作用是检测进程是否已经存在。
flock主要有3种操作类型。
- LOCK_SH:共享锁,多个进程使用同一把锁用于读锁。
- LOCK_EX:排他锁,同时只允许一个进程使用,一般用于写锁。
- LOCK_UN:释放锁。
如果深入研究flock包的函数,我们可以看到,这里使用了LOCK_EX锁,即同时只允许一个进程使用,代码示例如下:
3.3.4 初始化网络类型
比原链的三种网络模式,分别是mainnet主网、testnet测试网和solonet单机模式。
其中initActiveNetParams函数根据用户传入的chain_id,初始化网络类型。consensus.ActiveNetParams对象保存了当前使用的网络模式。在比原链代码中会经常引用consensus.ActiveNetParams对象,用来识别当前节点连接的网络类型。
ActiveNetParams默认使用主网。MainNetParams中的参数说明如下。
- Bech32HRPSegwit:隔离见证,是一种协议升级,我们会在后面第5章讲解。
- Checkpoints:检查点,指定一个高度,以及与这个高度相匹配的hash值,用于快速同步时验证区块的正确性。通常在主网升级时,会将历史的块信息硬编码在Checkpoints中。
Checkpoints检查点有两种作用:第一是防止分叉,如果有人试图从检查点之前的区块进行分叉,当前节点不会接受这个分叉;也用于保护网络不受全网51%的算力攻击,因为攻击者不可能逆转检查点之前的交易。第二是用于节点间的快速同步,我们将在第10章中详细讲解。
3.3.5 初始化数据库(持久化存储)
创建一条公链,需要将链上的所有数据(包含块信息、交易信息等)存储在本地键值数据库中。在比原链中使用LevelDB来存储链上数据,代码如下:
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"提示。
protocol.NewTxPool()返回一个TxPool实例对象。此处我们只介绍交易池初始化部分,交易池实现原理的代码将在6.10节中深入剖析。
3.5.7 创建一条本地区块链
当节点第一次启动时,判断本地持久化存储的状态,当状态为初始化时会初始化本地的区块链。区块链的第一个区块(创世区块)会被加入到区块高度为0的地方。代码如下:
protocol.NewChain返回一个Chain对象,NewChain需要接收两个参数:Store区块链的存储对象,TxPool交易池。Chain对象管理着比原链的整个区块链条。代码如下:
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 初始化本地钱包
默认情况下比原链节点会启用本地钱包功能。代码实例如下:
在比原链的节点启动时,上述代码流程主要逻辑为:
1)创建加密机hsm对象,hsm对象管理keystore文件,该文件是存储私钥的一种格式(JSON)。keystore是一串代码,本质上是加密后的私钥,需配合钱包的密码来使用。
2)创建钱包数据库。
3)创建账户管理对象。
4)创建资产管理对象。
5)实例化Wallet对象。
6)RescanBlocks扫描本地所有区块,触发钱包更新操作。
3.3.9 初始化网络同步管理
P2P通信模块主要由SyncManager管理,SyncManager负责节点业务层信息的同步工作,即区块和交易信息的同步。代码如下:
主要参数说明如下:
- 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参数启动代码性能分析功能,代码示例如下:
3.3.11 初始化CPU挖矿功能
在比原链节点源码中,只提供了CPU设备的挖矿功能,以目前全网的算力来看,CPU设备挖矿几乎挖不到BTM币了。目前主流的挖矿设备,有比特大陆定制的挖矿芯片或各大矿池使用GPU设备挖矿。挖矿和矿池细节将在第13章中详细解读。代码实例如下:
其中,simd参数用于Tenaority CPU指令的优化。
3.4 bytomd守护进程的启动方式和停止方式
我们在GO语言下实现守护进程的方式一般是,监听标准的SIGTERM信号。在监听到SIGTERM信号后,进程处于阻塞状态,以实现守护进程。只有当进程收到来自外部的SIGTERM信号时,进程则处于非阻塞状态,实现进程退出。Linux信号参考http://man7.org/linux/man-pages/man7/signal.7.html 。代码实例如下:
signal.Notify监听中断和Term信号。启用goroutine取c对象,select进入阻塞状态。当进程接收到Term信号则通知c对象,执行os.Exit退出守护进程。
发送Term信号有两种方式:一种是执行命令kill -15 pid;另一种是进程运行在前台。
当守护进程接收到Term信号后就停止运行,在其退出之前需要做扫尾工作,如退出挖矿模式,退出P2P同步功能等。代码示例如下:
3.5 本章小结
本章从源码的角度分析了bytomd启动过程中的Node对象创建和初始化,以及总结bytomd实现的逻辑。