tendermint多节点组网

多节点组网概述

在之前的课程内容中,我们的关注点集中在单一节点上的状态机逻辑实现上,这是因为 状态机复制的问题,是由tendermint负责完成的:我们在任何一个节点提交的交易请求, tendermint可以透明地帮我们在多个节点间实现同步。

在这一章,我们将通过前面已经学习过的简单的ABCI计数应用来学习如何部署多个tendermint节点, 并进一步理解tendermint共识的建立过程。为了简化操作,我们将首先了解如何将ABCI 应用与tendermint库集成为单一可执行文件,然后利用这个单一完整的程序来部署4个 节点旳区块链:

networking

在tendermint节点组成的区块链中有两种节点:验证节点和观察节点,只有验证节点 参与共识的建立。在这一章,我们将同时学习如何部署验证节点和观察节点。

tendermint是拜占庭容错的共识机制,因此在3f+1个验证节点中,允许不超过f个节点 出现拜占庭错误。在活性与可靠性中tendermint选择了可靠性,因此当3f+1个节点中 超过f个节点出现拜占庭错误后,整个系统将停机,我们也将通过实验观察到这一点。

实现abci接口

使用go语言开发tendermint链上应用有一个额外的优势,就是可以将tendermint节点 与ABCI应用集成在单一程序内发布。

我们还是使用计数状态机作为ABCI应用示例,在多个节点之间维护单一的计数值:

sm-counte

为简化问题,我们仅实现三个主要的ABCI接口 —— CheckTx、DeliverTx和Query, 同时,使用0x01~0x03分别表示递增、递减和复位交易:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type App struct {
types.BaseApplication
Value int
}

func (app *App) CheckTx(tx []byte) (rsp types.ResponseCheckTx) {
if tx[0] < 0x04 { return }
rsp.Code = 1
return
}

func (app *App) DeliverTx(tx []byte) (rsp types.ResponseDeliverTx) {
switch tx[0] {
case 0x01 : app.Value += 1
case 0x02 : app.Value -= 1
case 0x03 : app.Value = 0
}
return
}

func (app *App) Query(req types.RequestQuery) (rsp types.ResponseQuery) {
rsp.Log = fmt.Sprintf("counter: %d",app.Value)
return
}

你可以查看~/repo/go/src/hubwiz.com/c8/smr.go中的完整示例代码。

命令行封装:cobra子命令

Tendermint的命令行程序是基于spf13/cobra开发包开发的,每个子命令都对应着 一个cobra/Command结构的实例,例如,初始化节点子命令init对应着InitFilesCmd这个 对象,而RootCmd则对应着根命令:

cobra-commands

因此我们可以利用这些封装好的cobra命令,非常快速地生成一个具有节点同样功能 的命令行程序:

1
2
3
4
5
6
7
8
9
func main(){
root := commands.RootCmd

root.AddCommand(commands.InitFilesCmd)
root.AddCommand(commands.ResetAllCmd)

exec := cli.PrepareBaseCmd(root,"WIZ",".")
exec.Execute()
}

PrepareBaseCmd()方法将指定的Command实例转换为一个Executor对象,它进行 必要的环境变量设置(根据第二个参数指定的前缀),并确定命令行的默认工作目录 (第三个参数)。

在上面代码中,我们将默认工作目录设置为当前目录,这意味着在默认情况下, 当执行init子命令时,将在当前目录下生成data和config子目录。不过在RootCmd 中定义了全局标志–home,可以在命令行指定其他目录为节点工作目录。例如, 下面的代码将在当前目录下的n1子目录中执行初始化命令:

~/repo/go/src/hubwiz.com/c8$ go run smr.go init –home n1

实现节点提供器

与其他cobra命令不同,用于启动节点旳node子命令对应于一个NewRunNodeCmd()方法, 它是一个构建cobra/Command实例的工厂方法,需要传入一个NodeProvider类型的变量:

node-provide

NodeProvider其实是返回节点实例的函数,其原型定义如下:

1
type NodeProvider func(config *cfg.Config, logger log.Logger) (*node.Node, error)

其中,参数config(节点配置)和logger(日志)都是由tendermint框架在启动时传入的参数。

NodeProvider是基于具体的abci应用创建的。例如,下面的代码利用abci应用实例创建一个 NodeProvider实例,并最终得到cobra/Command实例添加到根命令中:

1
2
3
app := NewApp()
nodeProvider := makeNodeProvider(app)
root.AddCommand(commands.NewRunNodeCmd(nodeProvider))

makeNodeProvider()函数返回NodeProvider实例,其实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func makeNodeProvider(app types.Application) node.NodeProvider {
return func(config *cfg.Config, logger log.Logger) (*node.Node, error) {
nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile())
if err != nil { return nil, err }

return node.NewNode(config,
privval.LoadOrGenFilePV(config.PrivValidatorFile()),
nodeKey,
proxy.NewLocalClientCreator(app),
node.DefaultGenesisDocProviderFunc(config),
node.DefaultDBProvider,
node.DefaultMetricsProvider(config.Instrumentation),
logger)
}
}

编译安装

在整合了abci应用和cobra命令代码后,为了简化后续的键盘输入,我们将其 编译安装:

~/repo/go/src/hubwiz.com/c8$ go install smr.go
现在可以很简单地执行节点程序:

~/repo/go/hubwiz.com/c8$ smr

单节点实验

Tendermint支持两种类型的节点:验证节点(Validator)和观察节点(Observer)。显然,对于 网络中的第一个节点而言,必须是一个验证节点 —— 否则没有办法出块了:

node-provide

第一个节点旳基本参数是:

P2P通信端口:26656
RPC服务端口:26657
节点目录:n1
节点初始化与启动

进入1#终端,执行init子命令初始化第一个节点,注意使用–home标志指定节点目录:

~/repo/go/src/hubwiz.com/c8$ smr init –home n1
执行show_node_id子命令显示并记录第一个节点旳ID:

~/repo/go/src/hubwiz.com/c8$ smr show_node_id –home n1
输出结果为节点旳ID,我们后续会用到,请记下来(你的ID应该与此不同):

fd8debec2b97adfa0f6e8bae939c22a69cda9741
然后执行node子命令启动第一个节点:

~/repo/go/src/hubwiz.com/c8$ smr node –home n1
测试状态机交易

进入2#终端,执行如下交易并分别查询:

增加:

~$ curl localhost:26657/broadcast_tx_commit?tx=0x0101
~$ curl localhost:26657/abci_query
减少:

~$ curl localhost:26657/broadcast_tx_commit?tx=0x0201
~$ curl localhost:26657/abci_query
复位:

~$ curl localhost:26657/broadcast_tx_commit?tx=0x0301
~$ curl localhost:26657/abci_query

添加观察节点

现在我们在网络中添加一个观察节点,它不会参与到共识的建立:

two-node

第二个节点旳基本参数是:

P2P通信端口:36656
RPC服务端口:36657
节点目录:n2
节点初始化配置与启动

进入2#终端,执行init子命令初始化第二个节点:

~/repo/go/src/hubwiz.com/c8$ smr init –home n2
由于这个节点是观察节点,因此我们可以直接使用验证节点旳创世文件:

~/repo/go/src/hubwiz.com/c8$ cp n1/config/genesis.json n2/config/genesis.json
同时修改n2/config/config.toml,设置其rpc监听端口为36657,p2p监听端口为36656, 并使其主动连接第一个节点,其中fd8d…41为第一个节点旳ID(使用show_node_id 子命令获取):

[rpc]
laddr = “tcp://0.0.0.0:36657”

[p2p]
laddr = “tcp://0.0.0.0:36656”
persistent_peers = “fd8debec2b97adfa0f6e8bae939c22a69cda9741@127.0.0.1:26656”
现在2#终端启动第二个节点:

~/repo/go/src/hubwiz.com/c8$ smr node –home n2
交易测试

无论利用哪个节点提交交易,都可以保证所有的节点状态同步。

首先在5#终端分别查看节点一和节点二的状态:

~/repo/go/src/hubwiz.com/c8$ curl localhost:26657/abci_query
~/repo/go/src/hubwiz.com/c8$ curl localhost:36657/abci_query
然后利用节点一的rpc接口提交交易:

~/repo/go/src/hubwiz.com/c8$ curl localhost:26657/broadcast_tx_commit?tx=0x01
再次查看节点一和节点二的状态,你可以观察到同样的最新状态

停机测试

由于我们只有一个验证节点,显然当该节点停机时,整个集群将无法达成共识。

首先在1#终端按ctrl+c停止smr的运行,然后在5#终端通过节点二提交交易:

~/repo/go/src/hubwiz.com/c8$ curl localhost:26657/broadcast_tx_commit?tx=0x01
该命令会一直挂起直至超时,因为交易始终无法确认。

添加验证节点

现在我们继续向网络中添加第三个节点,这次是一个验证节点:

three-node

第三个节点旳基本参数是:

P2P通信端口:46656
RPC服务端口:46657
节点目录:n3
节点初始化配置与启动

进入3#终端,执行init子命令初始化节点三的目录n3:

~/repo/go/src/hubwiz.com/c8$ smr init –home n3
由于这个节点是验证节点,因此我们需要在节点一现有的创世文件中添加该节点 的priv_validator.json文件中的公钥和地址,其中每个验证节点旳power值用来 表征其代表的权益:

validator-genesis

在上面的配置中,由于每个节点旳power都是10,因此每个节点都有50%(10/(10+10)) 的机率出块。

然后将新的创世文件分发给节点二和节点三:

~/repo/go/src/hubwiz.com/c8$ cp n1/config/genesis.json n2/config/genesis.json
~/repo/go/src/hubwiz.com/c8$ cp n1/config/genesis.json n3/config/genesis.json
同时修改n3/config/config.toml,设置其rpc监听端口为46657,p2p监听端口为46656, 并使其主动连接第一个节点,其中fd8d…41为第一个节点旳ID(使用show_node_id 子命令获取):

[rpc]
laddr = “tcp://0.0.0.0:46657”

[p2p]
laddr = “tcp://0.0.0.0:46656”
persistent_peers = “fd8debec2b97adfa0f6e8bae939c22a69cda9741@127.0.0.1:26656”
现在3#终端启动第二个节点:

~/repo/go/src/hubwiz.com/c8$ smr node –home n3
交易测试

无论利用哪个节点提交交易,都可以保证所有的节点状态同步。

首先在5#终端分别查看三个节点的状态:

~/repo/go/src/hubwiz.com/c8$ curl localhost:26657/abci_query
~/repo/go/src/hubwiz.com/c8$ curl localhost:36657/abci_query
然后利用节点一的rpc接口提交交易:

~/repo/go/src/hubwiz.com/c8$ curl localhost:26657/broadcast_tx_commit?tx=0x0101
再次查看三个节点的状态,你可以观察到同样的最新状态

停机测试

由于3f+1个验证节点中,才允许f个发生拜占庭故障,因此两个验证节点旳任一个 出现故障,整个集群都将停止共识。

首先在3#终端按ctrl+c停止smr的运行,然后在5#终端通过节点二提交交易:

~/repo/go/src/hubwiz.com/c8$ curl localhost:36657/broadcast_tx_commit?tx=0x0102
该命令会一直挂起直至超时,因为交易始终无法确认。

但是观测节点旳停机不会影响共识的达成。

拜占庭容错实验

现在我们添加第四个节点,并将全部节点都设置为验证节点,来测试当其中某个 节点故障时,系统的容错能力:

four-node

第四个节点旳基本参数是:

P2P通信端口:56656
RPC服务端口:56657
节点目录:n4
节点初始化配置与启动

进入1#终端,首先初始化第四个节点目录:

~/repo/go/src/hubwiz.com/c8$ smr init –home n4
修改节点一的创世文件,将其他三个节点的priv_validator.json文件中的公钥和地址 添加进去,然后将新的创世文件分发给其他节点

~/repo/go/src/hubwiz.com/c8$ cp n1/config/genesis.json n2/config/genesis.json
~/repo/go/src/hubwiz.com/c8$ cp n1/config/genesis.json n3/config/genesis.json
~/repo/go/src/hubwiz.com/c8$ cp n1/config/genesis.json n4/config/genesis.json
同时修改节点2/3/4的config/config.toml,设置其rpc监听端口分别为36657/46657/56657, p2p监听端口分别为36656/46656/56656,并使其主动连接第一个节点,其中fd8d…41为第一 个节点旳ID(使用show_node_id子命令获取)。以节点2为例:

[rpc]
laddr = “tcp://0.0.0.0:56657”

[p2p]
laddr = “tcp://0.0.0.0:56656”
persistent_peers = “fd8debec2b97adfa0f6e8bae939c22a69cda9741@127.0.0.1:26656”
分别在四个终端启动四个目录对应的节点,例如,在2#终端启动节点2:

~/repo/go/src/hubwiz.com/c8$ smr node –home n2
容错测试

由于3f+1个验证节点中,允许f个发生拜占庭故障,因此我们的四个节点旳小集群, 允许任一个出现拜占庭错误。

首先在1#终端按ctrl+c停止smr的运行,然后在5#终端通过节点二提交交易:

~/repo/go/src/hubwiz.com/c8$ curl localhost:36657/broadcast_tx_commit?tx=0x0101
可以观察到交易正常完成。

坚持原创技术分享,您的支持将鼓励我继续创作!