我们在支持用户在阿里云容器服务Kubernetes集群中使用容器服务[区块链Hyperledger Fabric配置部署解决方案]、或者使用自建的基于Hyperledger Fabric的区块链方案的过程中,逐渐积累了一些相关的进阶使用经验、技巧和最佳实践,涵盖了系统设计、资源规划、服务使用、错误诊断、运营维护等方面,适用于区块链Hyperledger Fabric应用和方案的开发测试、以及生产部署等用途。这些内容将以系列文章的形式陆续发布并更新,同时欢迎有兴趣、有经验的朋友不吝指正。
利用区块链解决方案生成Hyperledger Fabric的Kubernetes部署yaml示例
阿里云容器服务Kubernetes集群的区块链解决方案是以Helm Chart的形式发布于容器服务的[应用目录]。实际上,我们除了可以用它来实现在阿里云容器服务的Kubernetes集群上一键自动配置和部署区块链网络外,还可以将它作为一个辅助的yaml生成工具,为我们生成可定制的Kubernetes示例yaml。这一技巧可以帮助那些刚开始学习Kubernetes和Hyperledger Fabric的朋友,为他们提供一套经过验证的示例yaml作为对照,以便快速开发出满足自身需求的定制化yaml。
这一技巧需要用到helm install --dry-run
命令以及schelm工具。具体操作流程如下:
在阿里云容器服务Kubernetes集群的master节点上安装golang和git(假定以root账户进行下述操作)
```
yum install golang
yum install git
```
将golang的bin目录加入到PATH环境变量中:
```
vi ~/.bash_profile
```
为PATH添加如下值:
```
PATH=$PATH:$HOME/bin:$HOME/go/bin
```
保存退出。然后运行以下命令以使变量生效:
```
source ~/.bash_profile
```
使用以下命令安装schelm
```
go get -u github.com/databus23/schelm
```
生成默认配置的Hyperledger Fabric部署yaml
```
helm install --name blockchain-network01 --dry-run --debug incubator/acs-hyperledger-fabric 2>&1 | schelm -f output/
```
如看到以下输出信息、以及有output文件夹生成,则说明命令成功执行:
```
2017/12/29 11:16:41 Creating output/acs-hyperledger-fabric/templates/ca-deploy-svc.yaml
2017/12/29 11:16:41 Creating output/acs-hyperledger-fabric/templates/fabric-network-generator-deploy-svc.yaml
2017/12/29 11:16:41 Creating output/acs-hyperledger-fabric/templates/kafka-deploy-svc.yaml
2017/12/29 11:16:41 Creating output/acs-hyperledger-fabric/templates/orderer-deploy-svc.yaml
2017/12/29 11:16:41 Creating output/acs-hyperledger-fabric/templates/peer-deploy-svc.yaml
2017/12/29 11:16:41 Creating output/acs-hyperledger-fabric/templates/zookeeper-deploy-svc.yaml
2017/12/29 11:16:41 Creating output/acs-hyperledger-fabric/templates/fabric-image-downloader-ds.yaml
2017/12/29 11:16:41 Creating output/acs-hyperledger-fabric/templates/cli-pod.yaml
2017/12/29 11:16:41 Creating output/acs-hyperledger-fabric/templates/fabric-init-pod.yaml
2017/12/29 11:16:41 Creating output/acs-hyperledger-fabric/templates/fabric-utils-pod.yaml
```
如无输出或无output文件夹生成,可重新运行命令(去掉 schelm部分)查看报错信息,例如:
```
helm install --name blockchain-network01 --dry-run --debug incubator/acs-hyperledger-fabric
```
以下是正确生成的yaml文件列表:
```
|____output
| |____acs-hyperledger-fabric
| | |____templates
| | | |____kafka-deploy-svc.yaml
| | | |____peer-deploy-svc.yaml
| | | |____fabric-network-generator-deploy-svc.yaml
| | | |____ca-deploy-svc.yaml
| | | |____orderer-deploy-svc.yaml
| | | |____fabric-image-downloader-ds.yaml
| | | |____fabric-init-pod.yaml
| | | |____cli-pod.yaml
| | | |____fabric-utils-pod.yaml
| | | |____zookeeper-deploy-svc.yaml
```
如果我们需要个性化的区块链网络配置,可以参照[区块链解决方案的配置部署文档],编写定制的value yaml文件,例如:
```
# sample network01.yaml
fabricNetwork: network01
fabricChannel: tradechannel
orgNum: 3
ordererNum: 4
ordererDomain: shop
peerDomain: shop
externalAddress: 11.22.33.44
caExternalPortList: ["31054", "31064", "31074"]
ordererExternalPortList: ["31050", "31060", "31070", "31080"]
peerExternalGrpcPortList: ["31051", "31061", "31071", "31081", "31091", "31101"]
peerExternalEventPortList: ["31053", "31063", "31073", "31083", "31093", "31103"]
```
然后用如下命令传入values yaml文件,生成Kubernetes的yaml部署文件:
```
helm install --name blockchain-network01 --values network01.yaml --dry-run --debug incubator/acs-hyperledger-fabric 2>&1 | schelm -f output/
```
需要说明的是,区块链解决方案仅适用于阿里云环境的部署,而按照上述方法生成的yaml并不能直接在其他环境(非阿里云)运行,主要用作参考。
区块链在阿里云容器服务部署的存储选择
目前区块链解决方案采用了NAS文件存储(NFS协议)来实现以下两个需求:
- 在不同区块链节点之间共享区块链网络配置、证书、密钥等
- 为主要的区块链节点提供数据持久化存储
实际上,用户可以根据实际业务需要和技术要求,在不同类型的存储之间选择:
- 块存储:如阿里云盘。主要适用于节点的持久化存储需求。
- 文件存储:如阿里云NAS文件存储。基于文件路径访问的方式,挂载和使用上较为灵活。
- 对象存储:如阿里云OSS。适用于基于文件对象访问的场景。
容器服务为上述不同的存储类型提供了灵活的支持,具体使用和挂载示例在存储管理文档中进行了说明。
区块链在阿里云容器服务部署的资费方案的选择
容器服务
在阿里云容器服务的Kubernetes集群上部署区块链,容器服务本身在大多数使用情况下是免费的,主要收取的是底层所使用的资源和服务(如云服务器、存储、负载均衡、公网地址等)的费用。
ECS云服务器
对于Kubernetes集群创建过程中自动创建的ECS云服务器,默认是按照使用量收费的。部分客户可能会考虑将ECS云服务器转为包年包月的资费方案以获得更经济的成本,不过需要注意的是,目前Kubernetes集群的创建尚未支持选择已有ECS云服务器,以及尚未支持将已有ECS云服务器添加进某一Kubernetes集群中,因此需要综合考虑。未来容器服务Kubernetes集群计划支持已有ECS云服务器加入集群的功能,请关注[产品文档]以获得进一步更新。
阿里云盘
用户如果选择了单独购买云盘给区块链使用的话,云盘默认的计费方式是按使用量收费的,并且可以重复进行挂载和卸载到不同的ECS云服务器上。但如果用户将云盘挂载到一台ECS云服务器上、并且将此ECS云服务器转成了包年包月的资费方案的话,那么这个云盘将变成ECS云服务器的一部分同样变为包年包月,但在这种方式下此云盘便不能再重复卸载和挂载了,这个使用上需要注意。详情可参见计费说明
实际上,云盘的按量使用和包年包月价格是一样的,默认使用按量计费即可,不必进行上述转换。
区块链在阿里云容器服务部署的地域选择
在进行区块链系统生产部署规划,用户可能需要选择在阿里云国内的哪个地域的容器服务集群上部署的问题。理论上容器服务在国内的各个地域所具备的能力都是一致的。用户可根据自身的业务或技术要求来自行选择。举例来说,假如我们选择的原则是让最终用户获得速度较快、延时较小的服务访问体验,那么:
- 如果最终用户遍布全国各省,那么可以选择地理位置上比较居中的地域,例如华东1(杭州)或华东2(上海)
- 如果最终用户有比较集中的分布,那么可选择距离较近的地域
公网访问区块链服务
在Kubernetes集群中部署Hyperledger Fabric的区块链网络之后,如需从集群内访问对应的服务(如CA, peer, orderer等等),可以直接使用service名称,kube-proxy会解析为对应的clusterIP并路由到对应的pod进行处理。
但如果需要从集群外部访问区块链网络各节点的服务,除了我们在外部访问列表中定义的NodePort外,我们还要配置外部可访问的公网IP。
在区块链解决方案的[环境准备文档]中,我们介绍了如何为任一worker节点创建和绑定弹性公网IP的方法。这种方式适用于开发测试环境、且不需要进行较多的配置(因为对VPC的安全组规则进行简单配置即可打开某一指定范围的NodePort端口访问),简单易上手。
但另一方面,上述方式也存在一定的局限性,例如:
- 该worker节点如果发生故障的话,整个集群对外便无法提供服务
- 指定范围的NodePort(无论是否有运行的service)都暴露在公网下
因此对于生产级别的部署,我们更推荐使用创建负载均衡来将外部访问请求分发到所有worker节点上,以实现高可用、负载均衡以及端口安全的目的。
具体做法示例如下:
- 创建新的负载均衡实例
- 记录其公网IP地址,作为区块链解决方案的外部访问地址(externalAddress)
- 对负载均衡的后端服务器,批量添加所有的worker节点
- 对负载均衡的监听端口,选择TCP协议,对前端和后端的端口均填成相同的NodePort。其他均使用默认配置
- 为区块链网络中需要外网访问的service的NodePort按照步骤4逐一创建监听端口。
区块链日志中的connection reset by peer消息原因分析和处理
如对区块链网络各service的NodePort开启了外部负载均衡的监听,那么在某些类型Service(如CA、orderer)的log中(使用kubectl logs或者docker logs命令查看时)可能会出现大量类似下面的消息:
2017-12-28 08:09:16.321 UTC [grpc] Printf -> DEBU 173 grpc: Server.Serve failed to complete security handshake from "172.20.3.1:50028": read tcp 172.20.3.160:7050->172.20.3.1:50028: read: connection reset by peer
2017-12-28 08:09:16.494 UTC [grpc] Printf -> DEBU 174 grpc: Server.Serve failed to complete security handshake from "172.20.3.1:24688": read tcp 172.20.3.160:7050->172.20.3.1:24688: read: connection reset by peer
2017-12-28 08:09:16.599 UTC [grpc] Printf -> DEBU 175 grpc: Server.Serve failed to complete security handshake from "172.20.3.1:58678": read tcp 172.20.3.160:7050->172.20.3.1:58678: read: connection reset by peer
此类消息在log中的大量存在有可能为日志运维以及错误诊断带来一定的干扰。
经过分析和资料查阅后,我们确认了此类消息是由于负载均衡中的TCP健康检查所引起的。
在负载均衡的[健康检查原理]文档中有如下说明:
TCP监听的检查机制如下:
1. LVS节点服务器根据监听的健康检查配置,向后端ECS的内网IP+【健康检查端口】发送TCP SYN数据包。
2. 后端ECS收到请求后,如果相应端口正在正常监听,则会返回SYN+ACK数据包。
3. 如果在【响应超时时间】之内,LVS节点服务器没有收到后端ECS返回的数据包,则认为服务无响应,判定健康检查失败;并向后端ECS发送RST数据包中断TCP连接。
4. 如果在【响应超时时间】之内,LVS节点服务器成功收到后端ECS返回的数据包,则认为服务正常运行,判定健康检查成功,而后向后端ECS发送RST数据包中断TCP连接。
注意:正常的TCP三次握手,LVS节点服务器在收到后端ECS返回的SYN+ACK数据包后,会进一步发送ACK数据包,随后立即发送RST数据包中断TCP连接。
该实现机制可能会导致后端ECS认为相关TCP连接出现异常(非正常退出),并在业务软件如Java连接池等日志中抛出相应的错误信息(如“Connection reset by peer”)。
在对比了以上文档以及健康检查导致大量日志的处理文档所提供的各种处理建议之后,我们总结出对Hyperledger Fabric适用的几点处理方式如下:
- 因Hyperledger Fabric的service端口访问需要使用TCP协议,而负载均衡的TCP协议健康检查不能像HTTP协议健康检查那样可以关闭
- 在Hyperledger Fabric内部源代码未实现对指定类型log消息进行过滤或抑制的情况下,我们可考虑的是调大健康检查的时间间隔
- 更改负载均衡实例中的监听端口的健康检查属性,从默认的2秒调大为更长的间隔(最大值50秒),这样便可以降低相关日志产生的频率
区块链SDK应用访问中的x509: certificate has expired or is not yet valid错误原因分析和处理
在使用区块链解决方案在Kubernetes集群上部署区块链网络并进行SDK应用的测试过程中,在invoke chaincode的环节我们遇到了x509: certificate has expired or is not yet valid
错误,更奇怪的是,该错误是偶然发生的,并不是每次都能出现。详细的Node.js SDK应用中的错误信息如下:
[2017-12-15 19:49:06.410] [INFO] Helper - Successfully loaded member from persistence
[2017-12-15 19:49:06.410] [DEBUG] invoke-chaincode - Sending transaction "{"_nonce":{"type":"Buffer","data":[78,166,195,28,221,100,129,247,157,103,26,157,64,16,173,220,177,46,245,144,92,3,233,64]},"_transaction_id":"8e77f6a9a2b7ce5db73350f1493e3ef4fe9e7137d370e4e9d9b9e50c25aab92a"}"
[2017-12-15 19:49:06.416] [DEBUG] Helper - [crypto_ecdsa_aes]: ecdsa signature: Signature {
r: <BN: 19b5aeb922b56a83e1bc79b5a24a18d98104ad30a62682738e9b6f6cd2d741a3>,
s: <BN: 681eb988eb292b78d6063fdc61157db389828efefdb5d264d83bdcbc293a5aca>,
recoveryParam: 0 }
error: [client-utils.js]: sendPeersProposal - Promise is rejected: Error: Failed to deserialize creator identity, err The supplied identity is not valid, Verify() returned x509: certificate has expired or is not yet valid
at /Users/yushan/Temp/solution-blockchain-demo/balance-transfer-app/node_modules/grpc/src/node/src/client.js:554:15
error: [client-utils.js]: sendPeersProposal - Promise is rejected: Error: Failed to deserialize creator identity, err The supplied identity is not valid, Verify() returned x509: certificate has expired or is not yet valid
at /Users/yushan/Temp/solution-blockchain-demo/balance-transfer-app/node_modules/grpc/src/node/src/client.js:554:15
[2017-12-15 19:49:06.583] [ERROR] invoke-chaincode - transaction proposal was bad
[2017-12-15 19:49:06.584] [ERROR] invoke-chaincode - transaction proposal was bad
[2017-12-15 19:49:06.585] [ERROR] invoke-chaincode - Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...
[2017-12-15 19:49:06.585] [ERROR] invoke-chaincode - Failed to order the transaction. Error code: undefined
经过大量对比测试和深入的代码分析,我们发现SDK应用enroll的用户证书的开始生效时间(即NotBefore时间)要早于为其签发证书的fabric-ca的证书的开始生效时间。以下是一个示例:
SDK应用的用户证书(有效期从Dec 17 06:34:00 2017开始)
Issuer: C=US, ST=California, L=San Francisco, O=org1.alibaba.com, CN=ca.org1.alibaba.com
Validity
Not Before: Dec 17 06:34:00 2017 GMT
Not After : Dec 17 06:34:00 2018 GMT
Subject: CN=Jim
fabric-ca的证书(有效期从Dec 17 06:34:32 2017开始):
Issuer: C=US, ST=California, L=San Francisco, O=org1.alibaba.com, CN=ca.org1.alibaba.com
Validity
Not Before: Dec 17 06:34:32 2017 GMT
Not After : Dec 15 06:34:32 2027 GMT
Subject: C=US, ST=California, L=San Francisco, O=org1.alibaba.com, CN=ca.org1.alibaba.com
而在进一步分析后,我们发现直接原因在于,fabric-ca为SDK用户签发证书时,会在NotBefore时间上引入一个5分钟的backdate,即在实际签发的时间基础上减去5分钟。核心代码如下所示,这里省略了我们对方法调用链的展开以及对进入对应条件分支的可能性分析,有兴趣的读者可以联系我们进行交流探讨。
hyperledger/fabric-ca/vendor/github.com/cloudflare/cfssl/signer/signer.go
func FillTemplate(template *x509.Certificate, defaultProfile, profile *config.SigningProfile) error
...
if backdate = profile.Backdate; backdate == 0 {
backdate = -5 * time.Minute
} else {
backdate = -1 * profile.Backdate
}
if !profile.NotBefore.IsZero() {
notBefore = profile.NotBefore.UTC()
} else {
notBefore = time.Now().Round(time.Minute).Add(backdate).UTC()
}
对于为什么问题是偶发的,我们一度也很疑惑。但在分析了整个过程的时间消耗,真相总算是水落石出:
首先,我们的基于区块链解决方案的测试是一个完整的“端到端”过程,其中包括:根据用户参数自动调用工具 > 生成证书和网络配置 > Hyperledger Fabric的Kubernete部署yaml文件动态生成 > 在Kubernetes上根据yaml文件创建区块链网络 > 下载证书到SDK应用端并配置应用访问区块链网络的配置 > 运行SDK应用进行区块链网络测试
在传统的Hyperledger Fabric示例中,如果使用的是现成的证书,这样fabric-ca的证书开始生效时间一般是很久以前,即使SDK用户的证书实际签发时间减去5分钟的backdate也不会早于fabric-ca证书的开始生效时间
在手工部署Hyperledger Fabric方式中,手工完成自定义配置以及完整的“端到端”过程,所花费的时间往往远远超过5分钟(数十分钟、数小时到数天),到了SDK用户的证书签发时间再减去5分钟的backdate,也不会早于fabric-ca证书的开始生效时间
而容器服务Kubernetes的区块链解决方案,其优势在于完成这一自定义配置的“端到端”流程所需时间非常短,往往仅需要2、3分钟(包括了人工操作的环节)。因此当SDK用户的证书签发时间再减去5分钟的backdate之后,便有可能早于fabric-ca证书的开始生效时间,从而导致错误的出现。
之所以是“偶发”的,是因为上述人工操作环节在每次测试中耗时不同(例如测试人员被其他事情占用)引起的。
基于上面的分析,对应的解决办法有以下一些选择:
- 在Hyperledger Fabric 1.0.3及之后的版本,官方在cryptogen工具中提供了一个fix,思路是将生成fabric-ca证书的开始生效时间(NotBefore)默认进行了在当前时间基础上减5分钟的处理,详情可参见https://jira.hyperledger.org/browse/FAB-6251。
- 如果使用的是Hyperledger Fabric 1.0.3以前的版本,在完成了Kubernetes集群的区块链网络创建后,等待超过5分钟后,再运行SDK应用进行测试,便可以避免该问题。
不过用户可以放心的是,以上场景仅会发生在开发测试环境中且进行快速“端到端”测试的极端情况才可能出现,这与实际使用场景、特别是生产部署后的业务使用场景有所不同。此问题的分析过程和处理方式主要是为了帮助用户更深入了解Hyperledger Fabric的工作机制。
最后,感谢Hyperledger社区的陈凯、董振华、戴建武等几位朋友在本文章撰写过程中所提供的宝贵信息和帮助!