Fabric基於Kafka的共識機制剖析

鏈客區塊鏈技術問答社群發表於2019-03-28
想知道更多關於區塊鏈技術知識,請百度【鏈客區塊鏈技術問答社群】
鏈客,有問必答!

在Hyperledger Fabric最新發布的1.0版本里,分拆出來Orderer元件用於交易的排序及共識。現階段提供solo及kafka兩種方式的實現。solo模式不用多講,即整個叢集就一個Orderer節點,區塊鏈的交易順序即為它收到交易的順序。而kafka模式的Orderer相對較複雜,在實現之初都有多種備選方案,但最終選擇了現在大家所看到的實現方式。那麼其中的選型過程是怎麼樣的呢?我想將開發者Kostas Christidis的設計思路給大家解析一番,既是翻譯也是我自身的理解。
原文:A Kafka-based Ordering Service for Fabric
Kafka模式的Orderer服務包含Kafka叢集及相關聯的Zookeeper叢集,以及許多OSN(ordering service node)。

ordering service client可以與多個OSN連線,OSN之間並不直接通訊,他們僅僅和Kafka叢集通訊。
OSN的主要作用:
1.client認證
2.允許client使用SDK與Channel互動
3.過濾並驗證configure transactions,比如重新配置channel或者建立新channel的transaction。
我們都知道,Messages(Records)是被寫入到Kafka的某個Topic Partition。Kafka叢集可以有多個Topic,每個Topic也可以有多個Partition。每個Partition是一個排序的、持久化的Record序列,並可以持續新增。
假設每個channel有不同的Partition。那麼OSNs通過client認證及transaction過濾之後,可以將發過來的transaction放到特定channel的相關Partition中。之後,OSNs就可以消費這些Partition的資料,並得到經過排序後的Transaction列表。這對所有的OSN都是通用的。

(假設所有的TX都屬於同一channel)
在這種情況下,每一TX都是不同的Block。OSNs將每一個TX都打包成一個區塊,區塊的編號就是Kafka叢集分配給TX的偏移編號,然後簽名該區塊。任意建立了Kafka消費者的Deliver RPCs都可以消費該區塊。
這種解決方案是可以執行的,但是會有以下問題:
1.假設1秒有1000個Transactions,Ordering 現在就必須1秒生成1000個簽名。並且接收端的clients必須能夠在1秒裡驗證1000個簽名。但通常簽名和驗證都是非常耗時的,這種情況下會非常棘手。所以為了避免一個區塊只有一個交易的情況,引入batch機制。假設一個batch包含1000個交易,那麼Ordering現在只需要生成1個簽名,client也只需要驗證1個簽名。
2.如果發往Ordering的交易速度並不均勻,假設Ordering需要發出包含1000個交易的batch,現在已經有了999個交易儲存在記憶體中,只需要等待1個交易就可以生成新的batch,但就是沒有交易發往Ordering了。這時候前面的999個交易就被動延遲了,這當然是不可接受的。所以我們需要batch定時器,當有交易作為新batch的首個交易時啟動定時器,只要batch達到最大交易數量或者定時器到點了,該batch都會形成新的區塊。
3.但是呢,基於時間分割區塊需要在Orderer之間協調時間。以交易數量分割區塊是容易的,對任意的OSN來說,都會得到相同的區塊。現在假設batch定時器設定為1秒,有兩個OSN。剛剛生成了batch,這時候一筆新的交易通過OSN1進入到Kafka中。OSN2在時間t=5s的時候讀取該交易,並設定了在t=6s的時候timeout。OSN1在時間t=5.6s的時候讀取該交易,並設定了自己相應的timeout時間。下一筆交易被髮送到Partition的末端,OSN2在t=6.2s的時候讀取了該tx,而OSN1在t=6.5s的時候讀取了該交易。這時候就會發現,OSN1的當前區塊包含了2筆交易,而OSN2的區塊只包含了前一筆交易。現在這兩個OSN產生了不同的區塊序列,這是不可接受的。因此,依照時間分割區塊需要明確的協調訊號。我們假設每個OSN在分割區塊之前都向Partition發出訊息“是時候分割區塊X啦”(X是區塊序列的下一個編號,TTC-X),並且不會真正的分割區塊直到接收到任意TTC-X訊息。接受到的這個訊息不必一定是自己發出的,如果每個OSN都等待自己的訊息,我們又得不到相同序列了。每個OSN分割區塊,要麼獲取batchSize筆交易,或者接收到第一個TTC-X訊息,無論哪種方式都會得到一致的區塊,這也意味著所有的子序列TTC-X訊息都會被忽略。
4.與例子中的每筆交易放在不同的區塊不同,區塊的編號現在沒有被轉換為Kafka的偏移量編號。所以如果Ordering服務接收到一個Deliver請求,讓從區塊5開始返回區塊,這時候就根本不知道讓消費者查詢哪個偏移量。或許我們可以使用區塊訊息裡的Metadata欄位,讓OSN標記該區塊的偏移量區間(區塊4的Metadata:offsets:63-64)。這樣如果client想獲取從區塊5到區塊9的資料,這樣就可以從65開始讀取,OSN重置Partition的日誌到偏移量65,按照之前定義的batchsize和batchtimeout讀取區塊。但是有兩個問題,1、我們違背了Deliver API協議,需要區塊編號作為引數;2、如果client已經丟失了很多區塊,並只想從區塊 X開始同步區塊,這時候就不知道正確的偏移量編號了,OSN也同樣不能解決這個問題。所以每個OSN需要每個channel維護一張表,內容為區塊編號到該區塊的偏移量起始值的對映。這也就意味著一個OSN除非維護一張lookup表,否則不能應答Deliver請求。lookup表移除了區塊metadata,也能快速定位區塊偏移編號。OSN將請求的區塊編號轉換成正確的偏移編號,並啟動Kafka消費者獲取該區塊。

  1. 無論何時OSN收到Deliver請求,都會從請求的區塊編號開始查詢所有的交易,並簽名。打包操作和簽名操作每次都被重複觸發,代價很高。為解決這個問題,我們建立另一個Partition(Partition1),之前的Partition我們標記為Partition0。現在無論什麼時候OSN分割區塊的時候,都將分割後的結果放入Partition1,這樣所有的Deliver請求都使用Partition1. 因為每個OSN都將其分割區塊結果放入到Partition1中,因此Partition1中的區塊序列並不是真正的channel的區塊序列,且有重複。

這就意味著Kafka的偏移編號不能和OSN的區塊編號對應起來,所以也需要建立區塊編號到偏移量的lookup表。

  1. 現在在Partition1中現在有冗餘的區塊。Deliver請求不僅僅是建立Kafka消費者,從偏移量開始向後查詢交易記錄那麼簡單。lookup表需要隨時被查詢,deliver的邏輯變得更加複雜,查詢lookup表增加了額外的延遲。是什麼造成了Partition接受了冗餘的資料呢?是Partition0的TTC-X訊息麼?還是被髮往Partition1的訊息和之前的訊息一致或者相似?如何解決冗餘的訊息呢?我們先定義一條規則:如果Partition1已經接收到相同的訊息(不算簽名),那麼就不再向Partition1新增該訊息。在上面的例子中,如果OSN1已經知道Block3已經被OSN2放入到Partition1中了,OSN1將終止該操作。那麼這樣就可以降低冗餘訊息。當然不能完全消除他們,因為肯定有OSNs在相同時間插入相同的訊息,這是無法避免的。
    如果我們選舉Leader OSN,它負責將區塊寫入到Partition1呢?有幾種方法選舉Leader:可以讓所有的OSNs競爭ZooKeeper的znode,或者第一個傳送TTC-X訊息到Partition0的OSN。另外一個有趣的方法就是讓所有的OSNs都屬於相同的Kafka消費者組,意味著每筆交易只會被消費一次,那麼無論哪個OSN消費了該交易,都會生成相同的區塊序列。
    如果Leader傳送區塊X訊息,訊息還未到達Partition1時Leader崩潰了,這時候會如何呢?其他OSNs意識到Leader崩潰了,因為Leader已經不再擁有znode,這時候會選舉新的Leader。這時候新的Leader發現區塊X還在他這裡,還沒有被髮送到Partition1,所以他傳送區塊X到Partition1。同時,舊Leader的區塊X訊息也傳送到了Partition1,訊息又冗餘了。

我們可以使用Kafka的日誌壓縮功能。

如果我們啟用日誌壓縮,我們完全可以刪除所有的冗餘訊息。當然我們假設所有的區塊X訊息擁有相同的key,X不同時,key也不同。但是因為日誌壓縮儲存的是最新版本的key,所以OSNs可能會擁有陳舊的lookup表。假設上圖的中key對應的是區塊。OSN收到的前兩個訊息在本地的lookup表中有對映關係,同時,Partition被壓縮成上圖下方的部分,這時候查詢偏移0/1會返回錯誤訊息。另外一個問題就是Partition1中的區塊不能逆向儲存,所以Deliver邏輯同樣複雜。事實上,僅僅考慮到lookup表的過期問題,日誌壓縮就不是一個好的方案。
所以沒有一個很好的解決方案解決這個問題,我們回到問題5,建立另一個Partition1,解決重複分割、簽名block的問題,我們可以摒棄這種方案,讓每個OSN在本地儲存每個channel的區塊檔案。

Delivery請求現在只需要順序的讀取本地ledger,沒有冗餘資料,沒有lookup表。OSN只需要儲存最後讀取的偏移量,這樣在重聯之後就可以知道從哪裡開始重新消費Kafka的訊息。
一個缺點可能就是比直接通過Kafka提供服務慢,但是我們也從來不是直接從Kafka提供服務,本來就有一些操作需要OSNs本地進行,比如簽名。
綜上,ordering 服務使用一個單Partition(每channel)接收客戶端的交易訊息和TTC-X訊息,在本地儲存區塊(每channel),這種解決方案能夠在效能和複雜度之間取得較好的平衡。

相關文章