本文主要從減少網絡帶寬消耗和降低鏈碼背書延遲這兩個方面,介紹如何通過優化鏈碼及SDK應用來提高Fabric區塊鏈應用的吞吐量。
設計高性能的BaaS應用程序
這里首先為大家介紹Fabric中交易的基本概念,再從鏈碼設計、SDK應用使用兩個方面解析提升性能的關鍵點。
交易的一生
SDK 生成 Proposal,其中包含調用鏈碼的相關參數等信息
SDK 將 Proposal 發送到多個不同的 peer 節點
peer 根據 Proposal 的信息,調用用戶上傳的鏈碼
鏈碼處理請求,將請求轉換為對賬本的讀集合和寫集合
peer 對讀集合和寫集合進行簽名,并將 ProposalResponse 返回給 SDK
SDK 收到多個 peer 節點的 ProposalResponse,并將讀集合與寫集合和不同節點的簽名拼接在一起,組成 Envelope
SDK 將 Envelope 發送給 orderer 節點,并監聽 peer 節點的塊事件
orderer 節點收到足夠的 Envelope 后,生成新的區塊,并將區塊廣播給所有 peer 節點
各個 peer 節點對收到區塊進行驗證,并向 SDK 發送新收到的區塊和驗證結果
SDK 根據事件中的驗證結果,判斷交易是否成功上鏈
鏈碼優化
鏈碼側優化的目標主要是降低鏈碼處理交易的時間,同時盡可能讓鏈碼在同一時間內能并發地處理更多的交易
避免 key 沖突
在 Fabric 區塊鏈賬本中,數據是以 KV 的形式存儲的,鏈碼可以通過 GetState
、PutState
等方法對賬本數據進行操作。每一個 Key 都有一個版本號,如果有兩筆不同的交易對同一個版本的 Key 做不同的修改,其中一筆交易會因為 Key 沖突而失敗。在 orderer 產生區塊后,交易的順序也確定了,由于此時第一筆交易已經讓 Key 的版本發生了改變,當第二筆交易再次對 Key 進行修改時,就會失敗了。
與其它區塊鏈不同,Fabric 中的區塊會包含非法的交易,如果業務產生了大量因為 Key 沖突而失敗的交易,這些交易也會被記入各個節點的賬本,占用節點的存儲空間。同時由于沖突的原因,并行的交易很多會失敗,不但會導致 SDK 的成功 TPS 大幅下降,失敗的交易還會占用網絡的吞吐量。
在進行鏈碼設計時,可以通過鏈碼的邏輯設計,減少不同交易對同一個 Key 進行寫入的頻率。例如,在鏈碼調用階段,對同一個Key進行寫入的多筆不同交易,應避免間隔過短,即避免對Key進行頻繁寫入。建議在對該Key的上一筆寫入交易成功(即 commit 到賬本)后再發起下一筆寫入交易。
減少 stub 讀取和寫入賬本的次數
Fabric 中的鏈碼與 peer 節點之間的通信與 SDK 和區塊鏈節點的通信類似,也是通過 GRPC 來進行的。當在鏈碼中調用查詢、寫入賬本的接口時(例如 GetState
、PutState
等),鏈碼發送 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
對象,可進行復用。
示例代碼:
public class HttpHandler {
private HFClient client = null;
private Channel channel = null;
HttpHandler(FabricUser user) {
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.只將交易發送給必要的節點背書
阿里云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);
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
來配置當收到任意一個節點驗證通過的事件時,即返回成功:
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
這里基于 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 節點發送多個請求,消耗一些資源和較多時間。