更新时间:2019-12-11 11:29
本文主要从减少网络带宽消耗和降低链码背书延迟这两个方面,介绍如何通过优化链码及SDK应用来提高Fabric区块链应用的吞吐量。这里首先为大家介绍Fabric中交易的基本概念,再从链码设计、SDK应用使用两个方面解析提升性能的关键点。
链码侧优化的目标主要是降低链码处理交易的时间,同时尽可能让链码在同一时间内能并发地处理更多的交易
在 Fabric 区块链账本中,数据是以 KV 的形式存储的,链码可以通过 GetState
、PutState
等方法对账本数据进行操作。每一个 Key 都有一个版本号,如果有两笔不同的交易对同一个版本的 Key 做不同的修改,其中一笔交易会因为 Key 冲突而失败。在 orderer 产生区块后,交易的顺序也确定了,由于此时第一笔交易已经让 Key 的版本发生了改变,当第二笔交易再次对 Key 进行修改时,就会失败了。
与其它区块链不同,Fabric 中的区块会包含非法的交易,如果业务产生了大量因为 Key 冲突而失败的交易,这些交易也会被记入各个节点的账本,占用节点的存储空间。同时由于冲突的原因,并行的交易很多会失败,不但会导致 SDK 的成功 TPS 大幅下降,失败的交易还会占用网络的吞吐量。
在进行链码设计时,可以通过链码的逻辑设计,减少不同交易对同一个 Key 进行写入的频率。例如,在链码调用阶段,对同一个Key进行写入的多笔不同交易,应避免间隔过短,即避免对Key进行频繁写入。建议在对该Key的上一笔写入交易成功(即 commit 到账本)后再发起下一笔写入交易。
Fabric 中的链码与 peer 节点之间的通信与 SDK 和区块链节点的通信类似,也是通过 GRPC 来进行的。当在链码中调用查询、写入账本的接口时(例如 GetState
、PutState
等),链码发送 GRPC 请求给 peer 节点,等待 peer 返回结果后再返回到链码的逻辑中。当链码在一次 Query/Invoke
中调用了多次账本的查询或写入接口时,会产生一定的网络通信成本和延迟,这对网络的整体吞吐率会有一定的影响,详细的原因在(减少链码运算量)中介绍。
我们在设计应用时,应尽量减少一次 Query/Invoke
中的查询和写入账本的次数。在一些对吞吐有很高要求的特殊场景下,可以在业务层对多个 Key 及对应的 Value 进行合并,将多次读写操作变成一次操作。
当链码被调用时,会在 peer 的账本上挂一把读锁,保证链码在处理该笔交易时,账本的状态不发生改变,当新的区块产生时,peer 将账本完全锁住,直到完成账本状态的更新操作。如果我们的链码在处理交易时花费了大量时间,会让 peer 验证区块等待更长的时间,从而降低整体的吞吐量。
在编写链码时,链码中最好只包含简单的逻辑、校验等必要的运算,将不太重要的逻辑放到链码外进行。
这里基于 fabric-sdk-java-1.4.0 版本来讨论,java sdk 相对比较灵活,同时也比较容易踩到坑,导致应用的性能极差。
SDK 在初始化 channel 对象阶段会有一定的资源及时间消耗,同时每一个 channel 对象都会建立自己的事件监听连接,向 peer 获取最新的区块及验证结果,从而消耗较多的网络带宽。应用程序在针对一个业务通道进行操作的时候,如果创建过多 channel 对象,可能会影响业务的响应时间,甚至会由于 TCP 连接数过多而引发业务阻塞。
在应用程序中,如果针对一个业务通道频繁发送交易,则创建该通道的第一个 channel 对象后应尽量复用。如果 channel 对象长时间闲置,可以使用channel.shutdown(true)
释放资源。
通过 HFCAClient 产生本地用户时,其中包含了用户私钥的生成和Enroll
操作,也有一定的时间消耗。应用程序不需要每次都产生新的 Enrollment
对象,可进行复用。
示例代码:
public class HttpHandler {
private HFClient client = null;
private Channel channel = null;
HttpHandler(user FabricUser) {
client = HFClient.createNewInstance();
client.setCryptoSuite(CryptoSuite.Factory.getCryptoSuite());
client.setUserContext(user);
NetworkConfig networkConfig = NetworkConfig.fromYamlFile("connection-profile.yaml");
channel = client.loadChannelFromConfig("mychannel", networkConfig);
channel.initialize();
}
}
阿里云BaaS(Fabric)中,每个组织都会有2个 peer 背书节点,如果一个业务通道内有 N 个组织,在使用 SDK 提交 Proposal 的时候,会默认发送给所有的 peer 背书节点(2*N个)。这时每个 peer 节点都要处理一遍P roposal,影响整体的吞吐量。而且当个别peer处理缓慢时,会拖慢交易的响应时间。
如果用户不需要在应用端对各个 peer 返回的读写集做一致性验证,可根据链码的背书策略选择性地提交 Proposal 到必要的 peer 节点,这样可节约 peer 的计算资源,提高性能。例如:
OR ('org1MSP.peer' , 'org2MSP.peer' , 'org3MSP.peer')
,则应用可选择6个 peer 节点中的任意一个,提交 proposal 获取背书返回即可;OutOf(2 , 'org1MSP.peer' , 'org2MSP.peer' , 'org3MSP.peer')
,则应用可选择6个 peer 节点中的任意2个,且来自不同组织,提交 proposal 获取背书返回即可。示例代码:
//背书策略为 OR ('org1MSP.peer' , 'org2MSP.peer' , 'org3MSP.peer')
//只需要发送到一个背书节点
Collection<Peer> peers = channel.getPeers(EnumSet.of(Peer.PeerRole.ENDORSING_PEER));
int size = peers.size();
int index = random.nextInt(size);
Peer[] endorsingPeers = new Peer[size];
peers.toArray(endorsingPeers);
Set partialPeers = new HashSet();
partialPeers.add(endorsingPeers[index]);
try {
transactionPropResp = channel.sendTransactionProposal(transactionProposalRequest, partialPeers);
} catch (ProposalException e) {
System.out.printf("invokeTransactionSync fail,ProposalException:{}", e.getLocalizedMessage());
e.printStackTrace();
} catch (InvalidArgumentException e) {
System.out.printf("InvalidArgumentException fail:{}", e.getLocalizedMessage());
e.printStackTrace();
}
也可以使用 Fabric 提供的 discovery 功能,自动选择必要的 peer 节点发送 Proposal。
使用 discovery 功能示例代码:
Channel.DiscoveryOptions discoveryOptions = Channel.DiscoveryOptions.createDiscoveryOptions();
discoveryOptions.setEndorsementSelector(ServiceDiscovery.EndorsementSelector.ENDORSEMENT_SELECTION_RANDOM); // 随机选择一个满足背书策略的组合发送请求
discoveryOptions.setForceDiscovery(false); // 使用 discovery 的缓存,默认2分钟刷新一次
discoveryOptions.setInspectResults(true); // 关闭 SDK 的背书策略检查,由我们的逻辑进行判断
Collection<ProposalResponse> transactionPropResp = channel.sendTransactionProposalToEndorsers(transactionProposalRequest, discoveryOptions);
应用端将 peer 返回的 proposal 读写集发送到 oderer 后,fabric 会进行一系列的排序-出块-验证-落盘等操作,根据通道的出块配置,最终交易落盘会有一定的延迟。
Java SDK 中默认会等待所有 eventSource
为 true 的节点事件,当所有节点验证均通过时,才会返回成功。这在一些业务场景下是可以优化的,一般选择自己所在组织的任意一个 peer 节点接受事件可以满足绝大多数场景下的需求。
channel.sendTransaction
方法返回 CompletableFuture<TransactionEvent>
,应用可使用多线程操作,当一个线程 sendTransaction
到 orderer 后则继续处理其他交易,另一个线程监听到 TransactionEvent
后进行相应的业务处理。NOfEvents
类,来控制 events 的接收策略,以判断发送到 orderer 的交易是否最终成功。建议将 NOfEvents
设为1,也就是只要任意一个节点返回 event 即可。应用不需要等待每一个peer都发出 TransactionEvent
才算交易成功。Channel.NOfEvents.createNoEvents()
创建 nofNoEvents
这种特殊的 NOfEvents
对象。将这个对象配置进 TransactionOptions
后,Orderer接收到应用发送的交易后会立即返回 CompletableFuture<TransactionEvent>
,但 TransactionEvent
会被置为null。我们可以通过使用 NOfEvents
来配置当收到任意一个节点验证通过的事件时,即返回成功:
Channel.TransactionOptions opts = new Channel.TransactionOptions();
Channel.NOfEvents nOfEvents = Channel.NOfEvents.createNofEvents();
Collection<EventHub> eventHubs = channel.getEventHubs();
if (!eventHubs.isEmpty()) {
nOfEvents.addEventHubs(eventHubs);
}
nOfEvents.addPeers(channel.getPeers(EnumSet.of(Peer.PeerRole.EVENT_SOURCE)));
nOfEvents.setN(1);
opts.nOfEvents(nOfEvents);
channel.sendTransaction(successful, opts).thenApply(transactionEvent -> {
logger.info("Orderer response: txid" + transactionEvent.getTransactionID());
logger.info("Orderer response: block number: " + transactionEvent.getBlockEvent().getBlockNumber());
return null;
}).exceptionally(e -> {
logger.error("Orderer exception happened: ", e);
return null;
}).get(60, TimeUnit.SECONDS);
除了使用 NOfEvents
来配置交易成功的验证方式,也可以在配置文件 connection-profile.yaml
中指定接收哪些 peer 节点的 eventSource
,下属示例中,只接收 peer1 节点的 eventSource
事件:
channels:
mychannel:
peers:
peer1.org1.aliyunbaas.top:31111:
chaincodeQuery: true
endorsingPeer: true
eventSource: true
ledgerQuery: true
discover: true
peer2.org2.aliyunbaas.top:31111:
chaincodeQuery: true
endorsingPeer: true
ledgerQuery: true
discover: true
orderers:
- orderer1
- orderer2
- orderer3
这里基于 Go SDK v1.0.0-alpha5 版本来讨论。Go SDK 相对来说对用户比较友好,默认内部实现了必要的 cache 以及负载均衡逻辑,能够帮助用户自动到必要的背书节点上收集签名,在交易合法性判断上,也会根据策略随机选择一个 peer 节点来监听事件。
在 Go SDK 的实现中,所有的 cache 都是基于对象 fabsdk.FabricSDK
的,不同的对象中会单独维护各自的 cache 和链接。我们在使用 Go SDK 时,应该避免创建过多的 fabsdk.FabricSDK
对象,与 java sdk 类似,对象在初始化时会向 peer 节点发送多个请求,消耗一些资源和较多时间。
在文档使用中是否遇到以下问题
更多建议
匿名提交