我们在支持用户在阿里云容器服务Kubernetes集群中使用容器服务区块链Hyperledger Fabric配置部署解决方案、或者使用自建的基于Hyperledger Fabric的区块链方案的过程中,逐渐积累了一些相关的进阶使用经验、技巧和最佳实践,涵盖了系统设计、资源规划、服务使用、错误诊断、运营维护等方面,适用于区块链Hyperledger Fabric应用和方案的开发测试、以及生产部署等用途。这些内容将以系列文章的形式陆续发布并更新,同时欢迎有兴趣、有经验的朋友不吝指正。
阿里云容器服务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协议)来实现以下两个需求:
实际上,用户可以根据实际业务需要和技术要求,在不同类型的存储之间选择:
容器服务为上述不同的存储类型提供了灵活的支持,具体使用和挂载示例在存储管理文档中进行了说明。
容器服务
在阿里云容器服务的Kubernetes集群上部署区块链,容器服务本身在大多数使用情况下是免费的,主要收取的是底层所使用的资源和服务(如云服务器、存储、负载均衡、公网地址等)的费用,详情请参见计费说明。
ECS云服务器
对于Kubernetes集群创建过程中自动创建的ECS云服务器,默认是按照使用量收费的。部分客户可能会考虑将ECS云服务器转为包年包月的资费方案以获得更经济的成本,不过需要注意的是,目前Kubernetes集群的创建尚未支持选择已有ECS云服务器,以及尚未支持将已有ECS云服务器添加进某一Kubernetes集群中,因此需要综合考虑。未来容器服务Kubernetes集群计划支持已有ECS云服务器加入集群的功能,请关注产品文档以获得进一步更新。
阿里云盘
用户如果选择了单独购买云盘给区块链使用的话,云盘默认的计费方式是按使用量收费的,并且可以重复进行挂载和卸载到不同的ECS云服务器上。但如果用户将云盘挂载到一台ECS云服务器上、并且将此ECS云服务器转成了包年包月的资费方案的话,那么这个云盘将变成ECS云服务器的一部分同样变为包年包月,但在这种方式下此云盘便不能再重复卸载和挂载了,这个使用上需要注意。详情可参见计费说明
实际上,云盘的按量使用和包年包月价格是一样的,默认使用按量计费即可,不必进行上述转换。
在进行区块链系统生产部署规划,用户可能需要选择在阿里云国内的哪个地域的容器服务集群上部署的问题。理论上容器服务在国内的各个地域所具备的能力都是一致的。用户可根据自身的业务或技术要求来自行选择。举例来说,假如我们选择的原则是让最终用户获得速度较快、延时较小的服务访问体验,那么:
在Kubernetes集群中部署Hyperledger Fabric的区块链网络之后,如需从集群内访问对应的服务(如CA, peer, orderer等等),可以直接使用service名称,kube-proxy会解析为对应的clusterIP并路由到对应的pod进行处理。
但如果需要从集群外部访问区块链网络各节点的服务,除了我们在外部访问列表(参见配置文档)中定义的NodePort外,我们还要配置外部可访问的公网IP。
在区块链解决方案的环境准备文档中,我们介绍了如何为任一worker节点创建和绑定弹性公网IP的方法。这种方式适用于开发测试环境、且不需要进行较多的配置(因为对VPC的安全组规则进行简单配置即可打开某一指定范围的NodePort端口访问),简单易上手。
但另一方面,上述方式也存在一定的局限性,例如:
因此对于生产级别的部署,我们更推荐使用创建负载均衡来将外部访问请求分发到所有worker节点上,以实现高可用、负载均衡以及端口安全的目的。
具体做法示例如下:
如对区块链网络各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适用的几点处理方式如下:
在使用区块链解决方案在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: ,
s: ,
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的工作机制。
最后,感谢Hyperledger社区的陈凯、董振华、戴建武等几位朋友在本文章撰写过程中所提供的宝贵信息和帮助!