带你读《GO语言公链开发实战》之三:守护进程的初始化与运行

简介: 本书的目标是引导读者全面了解区块链技术实现原理,笔者也一直坚信,了解某一系统最直接的方式就是研读它的源码,所以本书并不是只介绍区块链技术,而是深入分析其背后的实现原理。通过阅读本书,读者可以全面地了解一条公链的技术实现。本书基于比原链的源代码进行分析,比原链是一个开源的有智能合约功能的公共区块链平台,是国内优秀的公链,目前比原链的代码量不多,而且源码结构清晰,特别适合初学者学习。

点击查看第一章
点击查看第二章

第3章

守护进程的初始化与运行

3.1 概述

节点初始化是节点首次使用时,根据用户传入的参数进行设置,并根据参数进行网络、数据库、本地区块链以及P2P分布式网络等模块的初始化,使得节点能够正常运行。节点初始化由bytomd守护进程执行,在初次运行时一次性完成。
本章主要内容:

  • bytomd守护进程初始化流程。
  • 守护进程初始化的具体实现,包括网络、数据库、本地区块链初始化等。
  • 守护进程启动流程和停止流程。

3.2 bytomd守护进程初始化流程及命令参数

守护进程是一种特殊进程,启动后一直在后台运行,只有当触发特定的信号时,才会执行退出操作。比原链的守护进程是bytomd,初始化流程如图3-1所示。
在编写命令行程序时,通常需要对命令参数进行解析。不同语言一般都会提供解析命令行参数的方法或库,以方便程序员使用。在GO语言标准库中提供了flag包,方便进行命令行解析。

image.png

bytomd进程支持的传参如下:
image.png
image.png

3.3 bytomd守护进程的初始化实现

bytomd守护进程的Cobra流程与bytomcli过程非常相似,所以在此略去,后续主要对bytomd守护进程重要内容进行深入分析。在这里我们看一下bytomd预处理过程中使用到的代码文件结构,命令如下:
image.png

bytomd守护进程启动时,会根据不同的命令行flag参数,初始化不同的模块,最终以守护进程的方式运行。有关bytomd守护进程所有的运行工作都在node.NewNode(config)的具体实现中。下面介绍具体的实现过程。

3.3.1 Node对象

image.png

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)分析之前,我们需要先了解默认配置都有哪些。
image.png

首先,bytomd守护进程声明一个config全局变量,表示整个bytomd守护进程的配置信息。进程启动时config对象被赋予一个默认的配置参数。
image.png

默认的配置参数分有6个,每个针对不同的模块。下面对配置进行说明。我们将默认参数归纳为三块:Base基础配置、P2P网络配置、其他配置。
1. Base基础配置
BaseConfig用于配置bytomd节点所需的基础参数,包括数据目录、日志、监听地址等相关参数。
image.png
image.png

部分参数从配置文件中获取默认值,比如ApiAddress参数,它的tag是api_addr。我们可以从config/toml.go中获取默认值:
image.png

2. P2P网络配置

P2PConfig用于配置bytomd P2P通信协议中使用的参数,包括本机监听端口、通信节点超时、地址簿等相关参数。
image.png
image.png

注意,在比特币中,节点会采用DNS的方式来询问种子节点,进而查询到其他节点的IP地址。而在比原链中,种子节点是IP地址,一般会硬编码到代码里。技术细节我们会在后面的第10章详细讲解。
3. 其他配置
WalletConfig用于配置bytomd本地钱包使用的参数,包括是否启用本地钱包和更新等相关参数。
image.png

在bytomd守护进程声明config = DefaultConfig()之后,init()函数实现了config对象中各属性的赋值。具体实现代码如下:
image.png
image.png

在init()函数中定义了很多不同类型的flag参数,并将flag的参数值绑定到config对象上,比如:
image.png

这条语句的含义为:

  • 定义一个Bool类型的flag参数。
  • 该flag的名称为mining。
  • 该flag的赋值对象为config.Mining。
  • 该flag的描述信息为Enable mining。

至此,bytomd守护进程所需要的配置信息初始化完毕,程序运行真正进入初始阶段。下面对此进行深入分析。

3.3.3 创建文件锁

在比原链中,一份数据目录(--root参数指定)只能同时由一个bytomd守护进程读写,因为LevelDB高性能键值数据库是单进程模式,如果多个进程同时读写一份数据,会造成数据不一致的情况。因此,需要使用文件锁可以保证同一时间一个进程读写一份数据目录,代码如下:
image.png

bytomd启动时,lockDataDirectory函数使用flock在RootDir目录下创建一个LOCK文件。如果bytomd进程在一个文件的inode上加了锁,那么再次启动bytomd进程则会对errors.New中的内容报错并退出进程。flock的作用是检测进程是否已经存在。
flock主要有3种操作类型。

  • LOCK_SH:共享锁,多个进程使用同一把锁用于读锁。
  • LOCK_EX:排他锁,同时只允许一个进程使用,一般用于写锁。
  • LOCK_UN:释放锁。

如果深入研究flock包的函数,我们可以看到,这里使用了LOCK_EX锁,即同时只允许一个进程使用,代码示例如下:
image.png

3.3.4 初始化网络类型

比原链的三种网络模式,分别是mainnet主网、testnet测试网和solonet单机模式。
image.png

其中initActiveNetParams函数根据用户传入的chain_id,初始化网络类型。consensus.ActiveNetParams对象保存了当前使用的网络模式。在比原链代码中会经常引用consensus.ActiveNetParams对象,用来识别当前节点连接的网络类型。
image.png

ActiveNetParams默认使用主网。MainNetParams中的参数说明如下。

  • Bech32HRPSegwit:隔离见证,是一种协议升级,我们会在后面第5章讲解。
  • Checkpoints:检查点,指定一个高度,以及与这个高度相匹配的hash值,用于快速同步时验证区块的正确性。通常在主网升级时,会将历史的块信息硬编码在Checkpoints中。

Checkpoints检查点有两种作用:第一是防止分叉,如果有人试图从检查点之前的区块进行分叉,当前节点不会接受这个分叉;也用于保护网络不受全网51%的算力攻击,因为攻击者不可能逆转检查点之前的交易。第二是用于节点间的快速同步,我们将在第10章中详细讲解。

3.3.5 初始化数据库(持久化存储)

创建一条公链,需要将链上的所有数据(包含块信息、交易信息等)存储在本地键值数据库中。在比原链中使用LevelDB来存储链上数据,代码如下:
image.png

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"提示。
image.png

protocol.NewTxPool()返回一个TxPool实例对象。此处我们只介绍交易池初始化部分,交易池实现原理的代码将在6.10节中深入剖析。

3.5.7 创建一条本地区块链

当节点第一次启动时,判断本地持久化存储的状态,当状态为初始化时会初始化本地的区块链。区块链的第一个区块(创世区块)会被加入到区块高度为0的地方。代码如下:
image.png

protocol.NewChain返回一个Chain对象,NewChain需要接收两个参数:Store区块链的存储对象,TxPool交易池。Chain对象管理着比原链的整个区块链条。代码如下:
image.png

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 初始化本地钱包

默认情况下比原链节点会启用本地钱包功能。代码实例如下:
image.png

在比原链的节点启动时,上述代码流程主要逻辑为:
1)创建加密机hsm对象,hsm对象管理keystore文件,该文件是存储私钥的一种格式(JSON)。keystore是一串代码,本质上是加密后的私钥,需配合钱包的密码来使用。
2)创建钱包数据库。
3)创建账户管理对象。
4)创建资产管理对象。
5)实例化Wallet对象。
6)RescanBlocks扫描本地所有区块,触发钱包更新操作。

3.3.9 初始化网络同步管理

P2P通信模块主要由SyncManager管理,SyncManager负责节点业务层信息的同步工作,即区块和交易信息的同步。代码如下:
image.png
image.png

主要参数说明如下:

  • 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参数启动代码性能分析功能,代码示例如下:
image.png

3.3.11 初始化CPU挖矿功能

在比原链节点源码中,只提供了CPU设备的挖矿功能,以目前全网的算力来看,CPU设备挖矿几乎挖不到BTM币了。目前主流的挖矿设备,有比特大陆定制的挖矿芯片或各大矿池使用GPU设备挖矿。挖矿和矿池细节将在第13章中详细解读。代码实例如下:
image.png

其中,simd参数用于Tenaority CPU指令的优化。

3.4 bytomd守护进程的启动方式和停止方式

我们在GO语言下实现守护进程的方式一般是,监听标准的SIGTERM信号。在监听到SIGTERM信号后,进程处于阻塞状态,以实现守护进程。只有当进程收到来自外部的SIGTERM信号时,进程则处于非阻塞状态,实现进程退出。Linux信号参考http://man7.org/linux/man-pages/man7/signal.7.html 。代码实例如下:
image.png

signal.Notify监听中断和Term信号。启用goroutine取c对象,select进入阻塞状态。当进程接收到Term信号则通知c对象,执行os.Exit退出守护进程。
发送Term信号有两种方式:一种是执行命令kill -15 pid;另一种是进程运行在前台。
当守护进程接收到Term信号后就停止运行,在其退出之前需要做扫尾工作,如退出挖矿模式,退出P2P同步功能等。代码示例如下:
image.png
image.png

3.5 本章小结

本章从源码的角度分析了bytomd启动过程中的Node对象创建和初始化,以及总结bytomd实现的逻辑。

相关文章
|
4天前
|
数据采集 存储 Go
使用Go语言和chromedp库下载Instagram图片:简易指南
Go语言爬虫示例使用chromedp库下载Instagram图片,关键步骤包括设置代理IP、创建带代理的浏览器上下文及执行任务,如导航至用户页面、截图并存储图片。代码中新增`analyzeAndStoreImage`函数对图片进行分析和分类后存储。注意Instagram的反爬策略可能需要代码适时调整。
使用Go语言和chromedp库下载Instagram图片:简易指南
|
1天前
|
安全 Go 开发者
Golang深入浅出之-Go语言并发编程面试:Goroutine简介与创建
【4月更文挑战第22天】Go语言的Goroutine是其并发模型的核心,是一种轻量级线程,能低成本创建和销毁,支持并发和并行执行。创建Goroutine使用`go`关键字,如`go sayHello("Alice")`。常见问题包括忘记使用`go`关键字、不正确处理通道同步和关闭、以及Goroutine泄漏。解决方法包括确保使用`go`启动函数、在发送完数据后关闭通道、设置Goroutine退出条件。理解并掌握这些能帮助开发者编写高效、安全的并发程序。
10 1
|
1天前
|
人工智能 Go 调度
掌握Go并发:Go语言并发编程深度解析
掌握Go并发:Go语言并发编程深度解析
|
1天前
|
SQL 关系型数据库 MySQL
Golang数据库编程详解 | 深入浅出Go语言原生数据库编程
Golang数据库编程详解 | 深入浅出Go语言原生数据库编程
|
2天前
|
Go 开发者
Golang深入浅出之-Go语言流程控制:if、switch、for循环详解
【4月更文挑战第21天】本文介绍了Go语言中的流程控制语句,包括`if`、`switch`和`for`循环。`if`语句支持简洁的语法和初始化语句,但需注意比较运算符的使用。`switch`语句提供多分支匹配,可省略`break`,同时支持不带表达式的形式。`for`循环有多种形式,如基本循环和`for-range`遍历,遍历时修改原集合可能导致未定义行为。理解并避免易错点能提高代码质量和稳定性。通过实践代码示例,可以更好地掌握Go语言的流程控制。
11 3
Golang深入浅出之-Go语言流程控制:if、switch、for循环详解
|
2天前
|
Go
Golang深入浅出之-Go语言函数基础:定义、调用与多返回值
【4月更文挑战第21天】Go语言函数是代码组织的基本单元,用于封装可重用逻辑。本文介绍了函数定义(包括基本形式、命名、参数列表和多返回值)、调用以及匿名函数与闭包。在函数定义时,注意参数命名和注释,避免参数顺序混淆。在调用时,要检查并处理多返回值中的错误。理解闭包原理,小心处理外部变量引用,以提升代码质量和可维护性。通过实践和示例,能更好地掌握Go语言函数。
16 1
Golang深入浅出之-Go语言函数基础:定义、调用与多返回值
|
3天前
|
程序员 Go API
【Go语言快速上手(二)】 分支与循环&函数讲解
【Go语言快速上手(二)】 分支与循环&函数讲解
|
3天前
|
Go
Golang深入浅出之-Go语言基础语法:变量声明与赋值
【4月更文挑战第20天】本文介绍了Go语言中变量声明与赋值的基础知识,包括使用`var`关键字和简短声明`:=`的方式,以及多变量声明与赋值。强调了变量作用域、遮蔽、初始化与零值的重要性,并提醒读者注意类型推断时的一致性。了解这些概念有助于避免常见错误,提高编程技能和面试表现。
18 0
|
3天前
|
编译器 Go 开发者
Go语言入门|包、关键字和标识符
Go语言入门|包、关键字和标识符
22 0
|
7天前
|
API Go
使用Go语言通过API获取代理IP并使用获取到的代理IP
使用Go语言通过API获取代理IP并使用获取到的代理IP