如果把區塊鏈比作一個只能讀寫,不能刪改的分散式資料庫的話,那麼事務和查詢就是對這個資料庫進行的最重要的操作。以比特幣來說,我們通過錢包或者Blockchain.info進行區塊鏈的查詢操作,而轉賬行為就是Transaction的處理。而HyperLedger Fabric在1.0對系統架構進行了升級,使得事務的處理更加複雜。
一、架構
讓我們來看看Fabric 0.6到1.0的架構圖:
這個圖來自IBM微課堂第三講,我們可以看到原來單一的peer節點在1.0中進行了拆分,分為peer(背書節點和提交節點)和orderer(排序節點)。membership也就是我們在1.0中說的CA節點,其中也涉及到很多密碼學和安全相關的知識,我們暫且按住不表,只說SDK、Peer和Orderer之間的關係。
二、賬本
要了解Fabric對事務的處理,首先我們需要了解Fabric中的賬本,也就是實際儲存和查詢資料的地方。這是IBM微講堂中對Fabric賬本的示意圖:
Fabric 1.0中的賬本分為3種:
- 區塊鏈資料,這是用檔案系統儲存在Committer節點上的。區塊鏈中儲存了Transaction的讀寫集。
- 為了檢索區塊鏈的方便,所以用LevelDB對其中的Transaction進行了索引。
- ChainCode操作的實際資料儲存在State Database中,這是一個Key Value的資料庫,預設採用的LevelDB,現在1.0也支援使用CouchDB作為State Database。
三、事務提交過程
瞭解了Fabric中的賬本,接下來我們來了解一下對這些賬本的操作涉及到的Transaction。
我們仍然以Example02為例,具體準備過程可參看我之前的部落格:http://www.cnblogs.com/studyzy/p/6973334.html
當執行a向b轉賬10元,我們在cli中執行的命令為:
peer chaincode invoke -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem -C mychannel -n devincc -c '{"Args":["invoke","a","b","10"]}'
當CLI中執行該命令時,發生了什麼呢?我們來看看IBM微講堂中PPT關於事務生命週期和相關賬本的示例圖:
其中peer chaincode invoke表明這是一個Transaction呼叫。-c '{"Args":["invoke","a","b","10"]}'中的”invoke”說明呼叫的是example02.go中的invoke函式,具體函式我們可以看看到底實現了什麼功能:
// Transaction makes payment of X units from A to B func (t *SimpleChaincode) invoke(stub shim.ChaincodeStubInterface, args []string) pb.Response { var A, B string // Entities var Aval, Bval int // Asset holdings var X int // Transaction value var err error if len(args) != 3 { return shim.Error("Incorrect number of arguments. Expecting 3") } A = args[0] B = args[1] // Get the state from the ledger // TODO: will be nice to have a GetAllState call to ledger Avalbytes, err := stub.GetState(A) if err != nil { return shim.Error("Failed to get state") } if Avalbytes == nil { return shim.Error("Entity not found") } Aval, _ = strconv.Atoi(string(Avalbytes)) Bvalbytes, err := stub.GetState(B) if err != nil { return shim.Error("Failed to get state") } if Bvalbytes == nil { return shim.Error("Entity not found") } Bval, _ = strconv.Atoi(string(Bvalbytes)) // Perform the execution X, err = strconv.Atoi(args[2]) if err != nil { return shim.Error("Invalid transaction amount, expecting a integer value") } Aval = Aval - X Bval = Bval + X fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval) // Write the state back to the ledger err = stub.PutState(A, []byte(strconv.Itoa(Aval))) if err != nil { return shim.Error(err.Error()) } err = stub.PutState(B, []byte(strconv.Itoa(Bval))) if err != nil { return shim.Error(err.Error()) } return shim.Success(nil) }
其中主要的4個關於StateDatabase呼叫是:
Avalbytes, err := stub.GetState(A) Bvalbytes, err := stub.GetState(B) err = stub.PutState(A, []byte(strconv.Itoa(Aval))) err = stub.PutState(B, []byte(strconv.Itoa(Bval)))
1.客戶端SDK把'{"Args":["invoke","a","b","10"]}'這些引數傳送到endorser peer節點,
2.endorser peer會與ChainCode的docker例項通訊,併為其提供模擬的State Database的讀寫集,也就是說ChainCode會執行完邏輯,但是並不會在stub.PutState的時候寫資料庫。
3.endorser把這些讀寫集連同簽名返回給Client SDK。
4.SDK再把讀寫集傳送給Orderer節點,Orderer節點是進行共識的排序節點,在測試的情況下,只啟動一個orderer節點,沒有容錯。在生產環境,要進行Crash容錯,需要啟用Zookeeper和Kafka。在1.0中移除了拜占庭容錯,沒有0.6的PBFT,也沒有傳說中的SBFT,不得不說是一個遺憾。
5.Orderer節點只是負責排序和打包工作,處理的結果是一個Batch的Transactions,也就是一個Block,這個Block的產生有兩種情況,一種情況是Transaction很多,Block的大小達到了設定的大小,而另一種情況是Transaction很少,沒有達到設定的大小,那麼Orderer就會等,等到大小足夠大或者超時時間。這些設定是在configtx.yaml中設定的。
6.打包好的一堆Transactions會傳送給Committer Peer提交節點,
7.提交節點收到Orderer節點的資料後,會先進行VSCC校驗,檢查Block的資料是否正確。接下來是對每個Transaction的驗證,主要是驗證Transaction中的讀寫資料集是否與State Database的資料版本一致。驗證完Block中的所有Transactions後,提交節點會把吧Block寫入區塊鏈。然後把所有驗證通過的Transaction的讀寫集中的寫的部分寫入State Database。另外對於區塊鏈,本身是檔案系統,不是資料庫,所有也會有把區塊中的資料在LevelDB中建立索引。
四、查詢
如果我們只是通過ChainCode查詢資料,而存在寫入資料,那麼會有什麼區別呢?在CLI中peer命令提供了query子命令,比如Example02中,查詢a賬戶的餘額是:
peer chaincode query -C mychannel -n devincc -c '{"Args":["query","a"]}'
這樣系統會呼叫ChainCode中的invoke函式,但是傳入的function name是query。也就是會執行如下程式碼:
} else if function == "query" { // the old "Query" is now implemtned in invoke return t.query(stub, args) } // query callback representing the query of a chaincode func (t *SimpleChaincode) query(stub shim.ChaincodeStubInterface, args []string) pb.Response { var A string // Entities var err error if len(args) != 1 { return shim.Error("Incorrect number of arguments. Expecting name of the person to query") } A = args[0] // Get the state from the ledger Avalbytes, err := stub.GetState(A) if err != nil { jsonResp := "{\"Error\":\"Failed to get state for " + A + "\"}" return shim.Error(jsonResp) } if Avalbytes == nil { jsonResp := "{\"Error\":\"Nil amount for " + A + "\"}" return shim.Error(jsonResp) } jsonResp := "{\"Name\":\"" + A + "\",\"Amount\":\"" + string(Avalbytes) + "\"}" fmt.Printf("Query Response:%s\n", jsonResp) return shim.Success(Avalbytes) }
我們可以看到,我們只是呼叫了stub.GetState(A),並沒有寫操作,那麼會像前面說的Transaction一樣那麼複雜嗎?答案是不會。
因為呼叫呼叫的是peer query,在程式碼中,只有invoke的時候才會執行Transaction步驟中的4、5、6、7.
但是如果我們使用peer invoke,那麼會怎麼樣呢?比如如下的命令:
peer chaincode invoke -o orderer.example.com:7050 --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem -C mychannel -n mycc -c '{"Args":["query","a"]}'
那麼從程式碼上來看,雖然我們是一個查詢,卻會以Transaction的生命週期來處理。
五、小結
通過對這個Transaction過程的分析,我們可以得出以下結論:
- Fabric不支援對同一個資料的併發事務處理,也就是說,如果我們同時執行了a->b 10元,b->a 10元,那麼只會第一條Transaction成功,而第二條失敗。因為在Committer節點進行讀寫集版本驗證的時候,第二條Transaction會驗證失敗。這是我完全無法接受的一點!
- Fabric是非同步的系統,在Endorser的時候a->b 10元,b->a 10元都會返回給SDK成功,而第二條Transaction在Committer驗證失敗後不進行State Database的寫入,但是並不會通知Client SDK,所以必須使用EventHub通知Client或者Client重新查詢才能知道是否寫入成功。
- 不管在提交節點對事務的讀寫資料版本驗證是否通過,因為Block已經在Orderer節點生成了,所以Block是被整塊寫入區塊鏈的,而在State Database不會寫入,所以會在Transaction之外的地方標識該Transaction是無效的。
- query沒有獨立的函式出來,並不是根據只有讀集沒有寫集而判斷是query還是Transaction。