调试hyperledger fabric中的orderer服务
## 背景
上一篇我们对hyperledger fabric orderer源码进行了分析《hyperledger fabric orderer源码分析》,在已经阅读过代码之后,我们已经有了一些源码基础;在此基础上,开始尝试动手调试orderer,一方面能够避免一直阅读代码容易产生枯燥的感觉,另一方面通过实际操作,增加对代码逻辑的理解,进而理解架构设计的意图。
关于hyperledger fabric orderer,我们已经上上一篇《hyperledger fabric orderer源码分析》有介绍,这里不再赘述。本篇我们对orderer服务进行调试。
## 调试准备工作
### 调试工具Delve
在调试这块,官网似乎对GDB不是太满意,golang的官网推荐使用Delve。
> Note that Delve is a better alternative to GDB when debugging Go programs built with the standard toolchain. It understands the Go runtime, data structures, and expressions better than GDB. Delve currently supports Linux, OSX, and Windows on amd64. For the most up-to-date list of supported platforms, please see the Delve documentation.
>
> GDB does not understand Go programs well. The stack management, threading, and runtime contain aspects that differ enough from the execution model GDB expects that they can confuse the debugger and cause incorrect results even when the program is compiled with gccgo. As a consequence, although GDB can be useful in some situations (e.g., debugging Cgo code, or debugging the runtime itself), it is not a reliable debugger for Go programs, particularly heavily concurrent ones. Moreover, it is not a priority for the Go project to address these issues, which are difficult.
著名的IDE厂商,jetbrains出品的Goland,最好用的 Golang IDE之一,调试器内置的就是delve。
安装Delve:
1. go get -u [github.com/go-delve/delve/cmd/dlv](http://github.com/go-delve/delve/cmd/dlv),改命令会进行编译和安装,安装目录为GOPATH/bin;
2. 将GOPATH/bin加入到path目录当中,以便dlv命令可以被正常调用。
dlv的部分命令和gdb一致,dlv常用的命令:
```shell
The following commands are available:
break (alias: b) ------------ Sets a breakpoint.
breakpoints (alias: bp) ----- Print out info for active breakpoints.
continue (alias: c) --------- Run until breakpoint or program termination.
exit (alias: quit | q) ------ Exit the debugger.
goroutine ------------------- Shows or changes current goroutine
goroutines ------------------ List program goroutines.
next (alias: n) ------------- Step over to next source line.
print (alias: p) ------------ Evaluate an expression.
stack (alias: bt) ----------- Print stack trace.
step (alias: s) ------------- Single step through program.
step-instruction (alias: si) Single step a single cpu instruction.
stepout --------------------- Step out of the current function.
thread (alias: tr) ---------- Switch to the specified thread.
threads --------------------- Print out info for every traced thread.
```
详细的可以在进入dlv之后,输入help查看;或者查阅文档
接下来我们使用工具delve对hyperledger fabric 1.0中的orderer进行调试分析。
### 用于测试的客户端
为了方便进行调试时测试,orderer程序自带了用于调试时测试的程序,以下两个程序分别可以用于测试Broadcast和Deliver接口,在fabric/orderer/sample_clients目录下,我们找到两个对应的测试程序:
- broadcast_timestamp
- 发送一个包含时间的消息到Broadcast接口
- deliver_stdout
- 调用Deliver接口将接收的批量信息输出到屏幕上
### 编译orderer以及测试程序
编译选项
- -gcflags="-N -l",禁止编译器优化和内联
- -N,禁止编译器优化
- -l,禁止内联
编译orderer
```
cd hyperledger/fabric/orderer
go build -gcflags="-N -l"
```
编译broadcast_timestamp
```
cd fabric/orderer/sample_clients/broadcast_timestamp
go build -gcflags="-N -l"
```
编译deliver_stdout
```
fabric/orderer/sample_clients/deliver_stdout
go build -gcflags="-N -l"
```
## 调试过程
经过以上的准备工作,我们已经生成了工具和程序,接下来进入调试阶段。
### 开始调试
设置环境变量FABRIC_CFG_PATH,在fabric/sampleconfig当中有配置文件示例,例如我的fabric源码放在`~/go/src/github.com/hyperledger`目录下,orderer.yaml示例配置文件在`~/go/src/github.com/hyperledger/fabric/sampleconfig/`目录下。我们可以设置为该目录,可以在~/.profile中通过export设置该变量
orderer.yaml配置文件可以设置
- LedgerType: file,存储账本类型
- ListenAddress: 127.0.0.1,orderer服务地址
- ListenPort: 7050,orderer服务端口
broadcast_timestamp和deliver_stdout也读取该配置文件,主要是服务地址和端口两个配置项。
### 调试
通过dlv exec启动编译好的二进制程序
```
dlv exec ./orderer
```
进入到dlv的调试界面之后,设置断点,我们以broadcast接口为例,设置两个端点:
1. fabric/orderer/common/broadcast/broadcast.go中handlerImpl结构的Handle函数,该函数从接收消息开始处理
2. fabric/orderer/solo/consensus.go中的chain结构的main函数,该函数将上一步处理好的交易消息,完成排序 ,并且出块
```shell
b broadcast.go:77
b consensus.go:91
```
继续执行
```shell
c
```
关于两个函数的详细逻辑,我们在上一篇《hyperledger fabric orderer源码分析》已经分析。接下来我们通过测试示例来跑下,触发上述两个端点,从而跟踪具体的实现。
### 使用broadcast_timestamp测试
broadcast_timestamp发送发一个包含时间信息内容的数据包到orderer,orderer将其写入到区块。
我们到broadcast_timestamp目录下执行./broadcast_timestamp完成一次测试:
```shell
cd fabric/orderer/sample_clients/broadcast_timestamp
./broadcast_timestamp
```
可以看到orderer输出日志:
```
2019-01-14 02:16:06.767 UTC [orderer/main] initializeMultiChainManager -> INFO 002 Not bootstrapping because of existing chains
2019-01-14 02:16:08.304 UTC [orderer/multichain] NewManagerImpl -> INFO 003 Starting with system channel testchainid and orderer type solo
2019-01-14 02:16:08.304 UTC [orderer/main] main -> INFO 004 Beginning to serve requests
2019-01-14 02:16:21.438 UTC [orderer/common/broadcast] Handle -> INFO 005 88, recv msg ok
```
说明消息发送成功,orderer也成功处理。
背后发生了什么事情?我们先从头开始过一下broadcast_timestamp源代码,来探索下:
broadcast_timestamp代码比较简单,首先加载配置文件
```go
config := config.Load()
```
该过程是使用viper来读取配置信息,viper是一个功能强大的库,既能从环境变量,又能从配置文件中读取,并且有优先级区分。
我们提前通过FABRIC_CFG_PATH设置好配置文件路径,FABRIC_CFG_PATH设置的是一个包含配置文件的目录;因为我们使用的是自带的sampleconfig目录下的配置文件,因此可以吧该目录设置为FABRIC_CFG_PATH。
在~/.profile中设置:
```shell
export FABRIC_CFG_PATH=/home/fabric/go/src/github.com/hyperledger/fabric/sampleconfig/
```
这里我们只使用配置文件来配置选项信息,这样简单明了。
broadcast_timestamp支持命令行参数,包含以下参数
- 命令行参数
- serverAddr,ip:port形式,默认从配置文件中读取
- chainID,字符类型,默认为testchainid
- messages,消息数量,数字类型,即调用多少次broadcast来发送对应多少次消息
- 程序解析
1. grpc连接服务,如果连接失败,进行报错
2. 调用接口,返回对应Broadcast的stream流
3. 构造broadcastClient
- client ab.AtomicBroadcast_BroadcastClient
- chainID string
4. 在broadcastClient中包装了消息打包过程
- 整个打包过程非常简单,只构造了Payload,Payload当中Data字段是一个时间字符串,内容为fmt.Sprintf("Testing %v", time.Now()),即"Testing 2009-11-10 23:00:00 +0000 UTC m=+0.000000001"
- 而Header字段构造也是非常简洁,SignatureHeader为空结构体构造,而ChannelHeader多个字段中,只赋值了ChannelID字段,该字段即为flag参数的值chainID即“testchainid”
- 以及send过程,发送的为一个common.Envelope的消息包
整个打包过程如下图所示:
![5c3bef5041008.png](https://tech.voiceads.cn/usr/uploads/2019/04/2540758134.png)
5. getAck,获取返回响应
- 调用recv过程接收消息,调用完之后,我们可以将响应消息BroadcastResponse打印出来,因为BroadcastResponse结构体中只有一个Status字段,我们可以加一句日志fmt.Println("getAck", msg.Status),从而看到返回内容
- Status,200表示成功,其他值表示存在问题
- 其他
- 为stream增加关闭函数defer client.CloseSend(),确保流正常关闭。
### 使用deliver_stdout测试
deliver_stdout发送获取区块信息的消息,包含起始位置,然后从orderer接收区块信息,并且输出到屏幕上。
我们到deliver_stdout目录下执行./deliver_stdout完成一次测试:
```shell
cd fabric/orderer/sample_clients/deliver_stdout
./deliver_stdout
```
我们将返回信息输出出来,可以看到屏幕输出日志:
```
...
t.Block.Header
32 4ce01e74dfadfad452fd23c5b3b4db8a2434e3f457d288d69cef5d303e13ddac 9a90df7d8dd694402e7ed7bba2d9ac4764f6a31fff5f8d631de20c25cd2483e6
t.Block.Data
data:"\nQ\n\017\n\r\"\013testchainid\022>Testing 2019-01-14 02:39:15.071977498 +0000 UTC m=+0.020535514"
t.Block.Metadata
metadata:"\022\230\007\n\315\006\n\260\006\n\007DEFAULT\022\244\006-----BEGIN -----\nMIICNjCCAd2gAwIBAgIRAMnf9/dmV9RvCCVw9pZQUfUwCgYIKoZIzj0EAwIwgYEx\nCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4g\nRnJhbmNpc2NvMRkwFwYDVQQKExBvcmcxLmV4YW1wbGUuY29tMQwwCgYDVQQLEwND\nT1AxHDAaBgNVBAMTE2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMTcxMTEyMTM0MTEx\nWhcNMjcxMTEwMTM0MTExWjBpMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv\ncm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEMMAoGA1UECxMDQ09QMR8wHQYD\nVQQDExZwZWVyMC5vcmcxLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D\nAQcDQgAEZ8S4V71OBJpyMIVZdwYdFXAckItrpvSrCf0HQg40WW9XSoOOO76I+Umf\nEkmTlIJXP7/AyRRSRU38oI8Ivtu4M6NNMEswDgYDVR0PAQH/BAQDAgeAMAwGA1Ud\nEwEB/wQCMAAwKwYDVR0jBCQwIoAginORIhnPEFZUhXm6eWBkm7K7Zc8R4/z7LW4H\nossDlCswCgYIKoZIzj0EAwIDRwAwRAIgVikIUZzgfuFsGLQHWJUVJCU7pDaETkaz\nPzFgsCiLxUACICgzJYlW7nvZxP7b6tbeu3t8mrhMXQs956mD4+BoKuNI\n-----END -----
...
```
说明消息发送成功,并且获取到了响应,上图输出当前我们的32个区块;改程序不会退出,会一直等待接收新区块,并且打印出来。
接下来我们来看下deliver_stdout的具体实现:
- 加载配置文件,和broadcast_timestamp加载配置文件过程一致
- 参数获取
- server,ip:port形式,默认从配置文件中读取
- chainID,默认为testchainid
- seek,默认值为-2,-2或者表示从最早的区块开始获取,-1表示从最新的区块开始获取,大于等于0时表示只获取该区块号值对应的区块
- 程序解析
1. grpc连接服务,如果连接失败,进行报错
2. 调用接口,返回对应Broadcast的stream流
3. deliverClient构造
- client AtomicBroadcast_DeliverClient
- chainID string
4. 消息打包过程
发送的是一个SeekInfo结构,结构定义了三个字段:
- Start
- 起始位置
- Stop
- 结束位置
- Behavior
- 如果没有对应区块号的区块产生时的行为,一直等待,还是直接返回
- SeekInfo_BLOCK_UNTIL_READY SeekInfo_SeekBehavior = 0, BLOCK_UNTIL_READY
- 对应orderer服务行为,如果对应的区块还没有产生,一直等待,直到区块产生
- SeekInfo_FAIL_IF_NOT_READY SeekInfo_SeekBehavior = 1,FAIL_IF_NOT_READY
- 对应orderer服务行为,如果对应的区块还没有产生,不再等待,发送Status_NOT_FOUND,表示该区块没有找到
打包过程如下图所示:
![orderer_smapleclient-deliver.png](https://i.loli.net/2019/01/14/5c3bfc7417953.png)
seek起始参数的进一步说明:
- -2
- 起始位置为:oldest = &ab.SeekPosition{Type: &ab.SeekPosition_Oldest{Oldest: &ab.SeekOldest{}}}
- 结束位置为:maxStop = &ab.SeekPosition{Type: &ab.SeekPosition_Specified{Specified: &ab.SeekSpecified{Number: math.MaxUint64}}}
- stop设置了为Unint64的最大值MaxUint64,如果流不断开,其实就是表示一直不断的获取区块
- 以最早的区块为起始位置(应该是从0开始),不再断开连接,保持该流,一直接收在通道上新的区块
- -1
- 起始位置为:newest = &ab.SeekPosition{Type: &ab.SeekPosition_Newest{Newest: &ab.SeekNewest{}}}
- 结束位置为:maxStop = &ab.SeekPosition{Type: &ab.SeekPosition_Specified{Specified: &ab.SeekSpecified{Number: math.MaxUint64}}}
- stop设置了为Unint64的最大值MaxUint64,如果流不断开,其实就是表示一直不断的获取区块
- 因为behavior已经设置SeekInfo_BLOCK_UNTIL_READY,以最新的区块为起始位置,不再断开连接,保持该流,一直接收在通道上新的区块
- \>=0
- 获取某一个区块号对应的区块,起始位置和结束位置都是该值,该值只获取一个位置即退出。
- readUntilClose,调用Recv一直接收返回信息
- 首先发送block信息,发送区块信息完成之后再发送status状态
- ab.DeliverResponse_Block,屏幕上直接输出DeliverResponse_Block结构体内容
- ab.DeliverResponse_Status,200表示成功,其他值表示存在问题
- 问题
- 和broadcast_timestamp例子一样,也存在流没有关闭的情况,因此我们在流创建好之后,使用defer来添加关闭流操作,我们在client创建好之后,加上语句:
```go
defer client.CloseSend()
```
### 调试注意
- 非root账户下,没有权限创建文件
- 或者在配置文件中,修改FileLedger子选项Location值,由默认的/var/hyperledger/production/orderer,比如可以改为/home/fabric/var/hyperledger/production/orderer目录下,从而可以对目录进行的读写
- “Error reading from stream: rpc error: code = Canceled desc = context canceled”
- 我们发现,每次跑着跑着就出现“:Error reading from stream: rpc error: code = Canceled desc = context canceled”,虽然是WARN级别的,但是查看orderer源代码,fabric/orderer/common/broadcast/broadcast.go中Handle函数实现,找对对应代码
- 首先,不是io.EOF,而是Canceled错误
- 猜测,可能是客户端没有及时关闭导致
- 存在connction的关闭conn.Close()
- 但是此处用的是stream模式,而stream模式,客户端发送完消息,需要调用CloseSend关闭发送流,我们添加defer client.CloseSend()
- ok,正常关闭流之后,不再报错
## 总结
我们成功的使用两个测试程序对orderer的两个接口进行调试,跟踪了测试程序和orderer服务接口的源码细节,加深了对orderer服务的理解。学习和实践是分不开的,后续我们将继续对peer的源码进行分析,并且对peer进行调试,加深对整个fabric系统的理解;系列的下一篇《peer源码分析》。
## 参考资料
1. [Hyperledger Fabric Ordering Service](https://github.com/hyperledger/fabric/blob/release-1.0/orderer/README.md)
2. [Command Line Interface](https://github.com/go-delve/delve/blob/master/Documentation/cli)
3. [Delve Documentation](https://github.com/go-delve/delve/blob/master/Documentation/README.md)