All Products
Search
Document Center

Blockchain as a Service:Hyperledger Fabric Performance

Last Updated:Oct 13, 2022

Design high-performance BaaS applications

This topic describes how to optimize the chaincodes and SDK applications to improve the throughput of Fabric blockchain applications. This topic focuses on reducing the network bandwidth consumption and the endorsement delay. This topic introduces the basic concepts of transactions in Fabric, and analyzes the key points to improve the application performance, including the chaincode design and the use of SDK applications.

Transaction flow

  1. The SDK generates a transaction proposal. The proposal is a request to invoke a chaincode function with certain input parameters.

  2. SDK sends the proposal to multiple peer nodes.

    1. The peer nodes call the chaincode uploaded by the user according to the information provided by the proposal.

    2. The chaincode is executed to produce transaction results including a response value, read set, and write set.

    3. The set of these values, along with the endorsing peer signature is passed back as a roposal response to the SDK.

  3. The SDK receives proposal responses from multiple peers, and packages the read set, the write set, and the signatures of different nodes to form an envelope.

  4. The SDK sends the envelope to the orderer node and listens to block events of the peer node.

  5. After the orderer node receives enough envelopes, it generates a new block and broadcasts the block to all peers.

  6. Each peer verifies the received block and sends the verification results of the new block to the SDK.

  7. The SDK determines whether the transaction is successfully chained based on the verification results in the event.

Optimize chaincodes

The optimization of chaincodes can reduce the time used by the chaincode to process transactions and allow the chaincode to handle more transactions concurrently.

Avoid key conflicts

In a Fabric blockchain ledger, data is stored in key/value pairs, and the chaincode can operate on the ledger data by calling GetState, PutState and other methods. Each key has a version number. If two different transactions of the same block update the same key version, one of the transactions will fail due to the key conflict. The sequence of transactions is determined when the orderer node generates a block. The first transaction has updated the key version, so when the second transaction updates the key, it will fail due to the key conflict.

Unlike other blockchains, blocks in the Fabric can contain invalid transactions. If a large number of invalid transactions are generated due to key conflicts, these transactions will be recorded in the ledger of each node and consume the storage space of the node. Due to key conflicts, many parallel transactions will fail, which greatly reduces the number of successful transactions per second. At the same time, the invalid transactions still consume the network throughput.

The frequency of updating the same key by different transactions can be reduced with an improved chaincode logic. For example, increase the time interval for transactions to update the same key to reduce frequent updates. We recommend that you allow a transaction to update the key only after the previous transaction has updated the key and the information is committed to the ledger.

Reduce the number of stub reads and ledger updates

Similar to the communications between the SDK and the blockchain nodes, the communications between the chaincode and peers are made through gRPC. Chaincode allows you to query and update the ledger by using APIs, for example, the GetState API and the PutState API. The chaincode sends gRPC requests to the peer node. The peer node returns the result and sends it to the chaincode logic. If these APIs are called multiple times in a Query/Invoke, the communication cost will increase. This delays the network communication and affects the throughput. For more information about the reasons, see Reduce the computation workload of chaincodes.

When we design an application, we need to reduce the query and update operations on a ledger in one Query/Invoke. In specific scenarios that require high throughput, multiple key/value pairs can be combined at the business layer to turn multiple read and write operations into one operation.

Reduce the computation of chaincodes

When the chaincode is called, only read query is allowed. This ensures that the called chaincode does not participate in state validation checks in subsequent commit phase. When a new block is generated, the peer keeps the ledger locked until the ledger is updated. The validation on peers has to wait for a longer period of time if the chaincode spends too much time handling transactions, which reduces the overall throughput.

It is best to include only the necessary computations, such as simple logic and verification.

Optimize SDK

Java SDK

This topic focuses on version fabric-sdk-java-1.4.0. The Java SDK is flexible in use, but it has disadvantages that can seriously affect the performance of applications.

1. Reuse channel and client objects

The SDK will consume a certain amount of resources and time during the initialization process of the channel object. At the same time, each channel object will establish its own connection for event listening and obtain the latest block and verification results from the peer, which consumes a large amount of network bandwidth. If the application creates too many channel objects when operating on a business channel, it may affect the business response time and even cause the blocking of business code due to too many TCP connections.

If transactions are sent frequently to a channel, it is best to reuse the first channel object to reduce the resource consumption. You can use channel.shutdown(true) to release idle channel objects.

Generating a local user through HFCAClient includes the generation of the user’s private key and the Enroll operation, which also consumes a certain amount of time. The Enrollment object can be reused.

Example code:

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();
    }
}

2. Send transactions to necessary peers for endorsement

In Alibaba Cloud BaaS (Fabric), each organization has two endorsing peers. If there are N organizations in a business channel, when you use the SDK to submit a proposal, it will be sent to all (2 * N) endorsing peers by default. Each peer has to receive and process the proposal. This consumes a large amount of time and affects the overall throughput. The transaction response will be slower if some peers are processing slowly.

It will greatly save the computation resources and improve the performance if the user does not need to verify the read-write sets returned by each peer on the application side and can selectively submit the proposal to the necessary peers according to the endorsement policy of the chaincode.Example:

  • If the endorsement policy of the chaincode is OR ('org1MSP.peer' , 'org2MSP.peer' , 'org3MSP.peer'), the application can choose one peer from six peers, submit the proposal, and wait for the endorsement response.

  • If the endorsement policy of the chaincode is OutOf(2 , 'org1MSP.peer' , 'org2MSP.peer' , 'org3MSP.peer'), the application can choose two peers from six peers, submit the proposal, and wait for the endorsement response.

Sample code:

//The endorsement policy is OR ('org1MSP.peer' , 'org2MSP.peer' , 'org3MSP.peer')
 //Send the proposal to one peer for endorsement
  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();
  }

You can use the discovery feature provided by Fabric to send the proposal to necessary peers.

Sample code for using the discovery feature:

Channel.DiscoveryOptions discoveryOptions = Channel.DiscoveryOptions.createDiscoveryOptions();
discoveryOptions.setEndorsementSelector(ServiceDiscovery.EndorsementSelector.ENDORSEMENT_SELECTION_RANDOM); //Randomly selects peers that satisfy the endorsement policy.
discoveryOptions.setForceDiscovery(false); //Uses the cache of the discovery feature. The cache is refreshed every 2 minutes by default.
discoveryOptions.setInspectResults(true); //Disables the endorsement policy check of the SDK and uses the logic check.
Collection<ProposalResponse> transactionPropResp = channel.sendTransactionProposalToEndorsers(transactionProposalRequest, discoveryOptions);

3. Wait asynchronously for necessary node events

After the application sends the proposal read-write sets returned by the peer to the orderer node, Fabric will perform a series of operations, including ordering, blocking, validating, and committing transactions. The commitment of transactions may delay, which depends on the configurations for block generation in the channel.

By default, the Java SDK waits for all node events with true eventSource and returns success when all node validations are passed. This process can be optimized in specific business scenarios. Selecting one peer in the organization to receive events is enough to meet your business requirements in most scenarios.

  • The channel.sendTransaction method of the Java SDK returns CompletableFuture<TransactionEvent>. The application can perform concurrent operations in multiple threads. After a thread sends a transaction (sendTransaction) to an orderer node, the application can continue to handle other transactions. Another thread can perform related operations after it has listened to a TransactionEvent.

  • The Java SDK also provides the NOfEvents class to control the policies used to receive events and check whether the transactions that are sent to the orderer node are successful. We recommend that you set the value of NOfEvents to 1. This means to allow a random node to return events. The application does not need to wait for each peer to send a TransactionEvent to consider the transaction as successful.

  • You can use Channel.NOfEvents.createNoEvents () to create the nofNoEvents object if the application does not need to handle transactions events. In this case, the orderer node returns CompletableFuture<TransactionEvent> as soon as it receives a transaction, but TransactionEvent will be set to null.

You can configure NOfEvents to return success when any node receives an event that has passed the verification.

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);

Apart from NOfEvents, you can also specify the eventSource of specific peers in connection-profile.yaml. In the following example, the application only receives eventSource events from the peer1 node.

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

This topic is based on version Go SDK v1.0.0-alpha5. The Go SDK is user-friendly. It implements the cache and load balancing logic by default, which helps users automatically collect signatures on the required endorsement nodes. To determine the transaction legality, the Go SDK will randomly select a peer node based on the policy to listen to events.

1. Reuse the SDK instance globally

In the Go SDK, all caches are based on the fabsdk.FabricSDK object and different caches and links are maintained separately in different objects. You must avoid creating too many fabsdk.FabricSDK objects. Similar to the Java SDK, objects send multiple requests to peers during the initialization process, which consumes resources and time.