性能篇

设计高性能的BaaS应用程序

本文主要从减少网络带宽消耗和降低链码背书延迟这两个方面,介绍如何通过优化链码及SDK应用来提高Fabric区块链应用的吞吐量。这里首先为大家介绍Fabric中交易的基本概念,再从链码设计、SDK应用使用两个方面解析提升性能的关键点。

交易的一生

  1. SDK 生成 Proposal,其中包含调用链码的相关参数等信息
  2. SDK 将 Proposal 发送到多个不同的 peer 节点
    1. peer 根据 Proposal 的信息,调用用户上传的链码
    2. 链码处理请求,将请求转换为对账本的读集合和写集合
    3. peer 对读集合和写集合进行签名,并将 ProposalResponse 返回给 SDK
  3. SDK 收到多个 peer 节点的 ProposalResponse,并将读集合与写集合和不同节点的签名拼接在一起,组成 Envelope
  4. SDK 将 Envelope 发送给 orderer 节点,并监听 peer 节点的块事件
  5. orderer 节点收到足够的 Envelope 后,生成新的区块,并将区块广播给所有 peer 节点
  6. 各个 peer 节点对收到区块进行验证,并向 SDK 发送新收到的区块和验证结果
  7. SDK 根据事件中的验证结果,判断交易是否成功上链

链码优化

链码侧优化的目标主要是降低链码处理交易的时间,同时尽可能让链码在同一时间内能并发地处理更多的交易

避免 key 冲突

在 Fabric 区块链账本中,数据是以 KV 的形式存储的,链码可以通过 GetStatePutState 等方法对账本数据进行操作。每一个 Key 都有一个版本号,如果有两笔不同的交易对同一个版本的 Key 做不同的修改,其中一笔交易会因为 Key 冲突而失败。在 orderer 产生区块后,交易的顺序也确定了,由于此时第一笔交易已经让 Key 的版本发生了改变,当第二笔交易再次对 Key 进行修改时,就会失败了。

与其它区块链不同,Fabric 中的区块会包含非法的交易,如果业务产生了大量因为 Key 冲突而失败的交易,这些交易也会被记入各个节点的账本,占用节点的存储空间。同时由于冲突的原因,并行的交易很多会失败,不但会导致 SDK 的成功 TPS 大幅下降,失败的交易还会占用网络的吞吐量。

在进行链码设计时,可以通过链码的逻辑设计,减少不同交易对同一个 Key 进行写入的频率。例如,在链码调用阶段,对同一个Key进行写入的多笔不同交易,应避免间隔过短,即避免对Key进行频繁写入。建议在对该Key的上一笔写入交易成功(即 commit 到账本)后再发起下一笔写入交易

减少 stub 读取和写入账本的次数

Fabric 中的链码与 peer 节点之间的通信与 SDK 和区块链节点的通信类似,也是通过 GRPC 来进行的。当在链码中调用查询、写入账本的接口时(例如 GetStatePutState 等),链码发送 GRPC 请求给 peer 节点,等待 peer 返回结果后再返回到链码的逻辑中。当链码在一次 Query/Invoke 中调用了多次账本的查询或写入接口时,会产生一定的网络通信成本和延迟,这对网络的整体吞吐率会有一定的影响,详细的原因在(减少链码运算量)中介绍。

我们在设计应用时,应尽量减少一次 Query/Invoke 中的查询和写入账本的次数。在一些对吞吐有很高要求的特殊场景下,可以在业务层对多个 Key 及对应的 Value 进行合并,将多次读写操作变成一次操作。

减少链码的运算量

当链码被调用时,会在 peer 的账本上挂一把读锁,保证链码在处理该笔交易时,账本的状态不发生改变,当新的区块产生时,peer 将账本完全锁住,直到完成账本状态的更新操作。如果我们的链码在处理交易时花费了大量时间,会让 peer 验证区块等待更长的时间,从而降低整体的吞吐量。

在编写链码时,链码中最好只包含简单的逻辑、校验等必要的运算,将不太重要的逻辑放到链码外进行。

SDK 优化

Java SDK

这里基于 fabric-sdk-java-1.4.0 版本来讨论,java sdk 相对比较灵活,同时也比较容易踩到坑,导致应用的性能极差。

1.复用 channel 及 client 对象

SDK 在初始化 channel 对象阶段会有一定的资源及时间消耗,同时每一个 channel 对象都会建立自己的事件监听连接,向 peer 获取最新的区块及验证结果,从而消耗较多的网络带宽。应用程序在针对一个业务通道进行操作的时候,如果创建过多 channel 对象,可能会影响业务的响应时间,甚至会由于 TCP 连接数过多而引发业务阻塞。

在应用程序中,如果针对一个业务通道频繁发送交易,则创建该通道的第一个 channel 对象后应尽量复用。如果 channel 对象长时间闲置,可以使用channel.shutdown(true)释放资源。

通过 HFCAClient 产生本地用户时,其中包含了用户私钥的生成和Enroll操作,也有一定的时间消耗。应用程序不需要每次都产生新的 Enrollment 对象,可进行复用。

示例代码:

  1. public class HttpHandler {
  2. private HFClient client = null;
  3. private Channel channel = null;
  4. HttpHandler(user FabricUser) {
  5. client = HFClient.createNewInstance();
  6. client.setCryptoSuite(CryptoSuite.Factory.getCryptoSuite());
  7. client.setUserContext(user);
  8. NetworkConfig networkConfig = NetworkConfig.fromYamlFile("connection-profile.yaml");
  9. channel = client.loadChannelFromConfig("mychannel", networkConfig);
  10. channel.initialize();
  11. }
  12. }

2.只将交易发送给必要的节点背书

阿里云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 获取背书返回即可。

示例代码:

  1. //背书策略为 OR ('org1MSP.peer' , 'org2MSP.peer' , 'org3MSP.peer')
  2. //只需要发送到一个背书节点
  3. Collection<Peer> peers = channel.getPeers(EnumSet.of(Peer.PeerRole.ENDORSING_PEER));
  4. int size = peers.size();
  5. int index = random.nextInt(size);
  6. Peer[] endorsingPeers = new Peer[size];
  7. peers.toArray(endorsingPeers);
  8. Set partialPeers = new HashSet();
  9. partialPeers.add(endorsingPeers[index]);
  10. try {
  11. transactionPropResp = channel.sendTransactionProposal(transactionProposalRequest, partialPeers);
  12. } catch (ProposalException e) {
  13. System.out.printf("invokeTransactionSync fail,ProposalException:{}", e.getLocalizedMessage());
  14. e.printStackTrace();
  15. } catch (InvalidArgumentException e) {
  16. System.out.printf("InvalidArgumentException fail:{}", e.getLocalizedMessage());
  17. e.printStackTrace();
  18. }

也可以使用 Fabric 提供的 discovery 功能,自动选择必要的 peer 节点发送 Proposal。

使用 discovery 功能示例代码:

  1. Channel.DiscoveryOptions discoveryOptions = Channel.DiscoveryOptions.createDiscoveryOptions();
  2. discoveryOptions.setEndorsementSelector(ServiceDiscovery.EndorsementSelector.ENDORSEMENT_SELECTION_RANDOM); // 随机选择一个满足背书策略的组合发送请求
  3. discoveryOptions.setForceDiscovery(false); // 使用 discovery 的缓存,默认2分钟刷新一次
  4. discoveryOptions.setInspectResults(true); // 关闭 SDK 的背书策略检查,由我们的逻辑进行判断
  5. Collection<ProposalResponse> transactionPropResp = channel.sendTransactionProposalToEndorsers(transactionProposalRequest, discoveryOptions);

3.异步等待必要的节点事件

应用端将 peer 返回的 proposal 读写集发送到 oderer 后,fabric 会进行一系列的排序-出块-验证-落盘等操作,根据通道的出块配置,最终交易落盘会有一定的延迟。

Java SDK 中默认会等待所有 eventSource 为 true 的节点事件,当所有节点验证均通过时,才会返回成功。这在一些业务场景下是可以优化的,一般选择自己所在组织的任意一个 peer 节点接受事件可以满足绝大多数场景下的需求。

  • Java SDK 的 channel.sendTransaction 方法返回 CompletableFuture<TransactionEvent>,应用可使用多线程操作,当一个线程 sendTransaction 到 orderer 后则继续处理其他交易,另一个线程监听到 TransactionEvent 后进行相应的业务处理。
  • Java SDK 还提供了 NOfEvents 类,来控制 events 的接收策略,以判断发送到 orderer 的交易是否最终成功。建议将 NOfEvents 设为1,也就是只要任意一个节点返回 event 即可。应用不需要等待每一个peer都发出 TransactionEvent 才算交易成功。
  • 如果应用不需要处理 transaction event,可通过 Channel.NOfEvents.createNoEvents() 创建 nofNoEvents 这种特殊的 NOfEvents 对象。将这个对象配置进 TransactionOptions 后,Orderer接收到应用发送的交易后会立即返回 CompletableFuture<TransactionEvent>,但 TransactionEvent 会被置为null。

我们可以通过使用 NOfEvents 来配置当收到任意一个节点验证通过的事件时,即返回成功:

  1. Channel.TransactionOptions opts = new Channel.TransactionOptions();
  2. Channel.NOfEvents nOfEvents = Channel.NOfEvents.createNofEvents();
  3. Collection<EventHub> eventHubs = channel.getEventHubs();
  4. if (!eventHubs.isEmpty()) {
  5. nOfEvents.addEventHubs(eventHubs);
  6. }
  7. nOfEvents.addPeers(channel.getPeers(EnumSet.of(Peer.PeerRole.EVENT_SOURCE)));
  8. nOfEvents.setN(1);
  9. opts.nOfEvents(nOfEvents);
  10. channel.sendTransaction(successful, opts).thenApply(transactionEvent -> {
  11. logger.info("Orderer response: txid" + transactionEvent.getTransactionID());
  12. logger.info("Orderer response: block number: " + transactionEvent.getBlockEvent().getBlockNumber());
  13. return null;
  14. }).exceptionally(e -> {
  15. logger.error("Orderer exception happened: ", e);
  16. return null;
  17. }).get(60, TimeUnit.SECONDS);

除了使用 NOfEvents来配置交易成功的验证方式,也可以在配置文件 connection-profile.yaml 中指定接收哪些 peer 节点的 eventSource,下属示例中,只接收 peer1 节点的 eventSource 事件:

  1. channels:
  2. mychannel:
  3. peers:
  4. peer1.org1.aliyunbaas.top:31111:
  5. chaincodeQuery: true
  6. endorsingPeer: true
  7. eventSource: true
  8. ledgerQuery: true
  9. discover: true
  10. peer2.org2.aliyunbaas.top:31111:
  11. chaincodeQuery: true
  12. endorsingPeer: true
  13. ledgerQuery: true
  14. discover: true
  15. orderers:
  16. - orderer1
  17. - orderer2
  18. - orderer3

Go SDK

这里基于 Go SDK v1.0.0-alpha5 版本来讨论。Go SDK 相对来说对用户比较友好,默认内部实现了必要的 cache 以及负载均衡逻辑,能够帮助用户自动到必要的背书节点上收集签名,在交易合法性判断上,也会根据策略随机选择一个 peer 节点来监听事件。

1.全局复用sdk实例

在 Go SDK 的实现中,所有的 cache 都是基于对象 fabsdk.FabricSDK 的,不同的对象中会单独维护各自的 cache 和链接。我们在使用 Go SDK 时,应该避免创建过多的 fabsdk.FabricSDK 对象,与 java sdk 类似,对象在初始化时会向 peer 节点发送多个请求,消耗一些资源和较多时间。