Hyperledger Fabric v1.4.3 Orderer模塊部分源碼分析(2)—solo共識機制

寫在前面

  • 本系列博客主要用於記錄總結自己在做一個區塊鏈相關項目中所涉及到的源碼知識,一方面加深自己的記憶,另一方面也可以鍛鍊自己的知識敘述能力,博客內容僅供參考,真誠的歡迎對Fabric源碼感興趣的dalao來與我討論問題。
  • 因自己所做項目的原因,該系列博客側重點在於Orderer模塊中共識機制的分析以及修改,因此源碼中的其他部分可能不會做特別深入的研究,使用的平臺爲Ubuntu16.04+Windows10雙系統,源代碼爲Hyperledger Fabric項目的1.4.3版本。點擊跳轉源碼。
  • 碼字不易,轉載請說明出處:)

Orderer模塊中的共識機制

在當前版本中,共內置了三種共識機制:solo、kafka和raft,每一種共識機制都需要實現fabric/orderer/consensus/consensus.go文件中的Consenter接口和Chain接口,其中Consenter接口代表着共識機制,需要實現HandleChain函數,通過調用HandleChain函數可以產生Chain接口,Chain接口代表了在這一共識機制下的區塊鏈,共識算法的實現直接依賴於Chain接口而非Consenter接口,Chain接口的運行又依賴於Consenter接口,Chain接口實現了對交易信息的處理,區塊的生成,區塊的共識,區塊的上鍊等一系列操作,啓動Chain後,會循環運行一個go協程。
在fabric/orderer/consensus/consensus.go文件中還有另外一個接口ConsenterSupport,其實現爲結構體ChainSupport,該接口用於幫助開發者實現共識算法,提供了一些用於操作區塊鏈的基本函數,如使用blockcutter產生交易batch,通過交易batch生成新的區塊,將區塊寫入區塊鏈等,這些函數的實現均在結構體ChainSupport中,我們在實現自己的共識算法時,可以直接使用ConsenterSupport接口中的函數。

solo共識機制

內容分散,敘述略長,所以最好能對照着源碼來看@_@

回憶“Orderer模塊啓動”博客中,在創建“大雜燴”管理器的時候,會將所有共識算法的Consenter結構體存放在consenters映射中,因此會有如下代碼:

consenters["solo"] = solo.New()

因此我們將從solo.New函數切入到solo共識模塊來進行梳理。

代碼片段一

func New() consensus.Consenter {
	return &consenter{}
}

上述代碼爲solo.New函數,會創建一個本地的consenter結構體返回,consenter結構體如下:

type consenter struct{}

func (solo *consenter) HandleChain(support consensus.ConsenterSupport, metadata *cb.Metadata) (consensus.Chain, error) {
	return newChain(support), nil
}

func newChain(support consensus.ConsenterSupport) *chain {
	return &chain{
		support:  support,
		sendChan: make(chan *message),
		exitChan: make(chan struct{}),
	}
}

solo共識中的consenter結構體實現了consensus.Consenter接口,其本身並不包含任何變量。
通過調用consenter結構體中的HandleChain函數可以創建chain結構體,chain結構體結構如下:

type chain struct {
	support  consensus.ConsenterSupport
	sendChan chan *message
	exitChan chan struct{}
}

chain結構體包括一個consensus.ConsenterSupport接口和兩個通道,實現了consensus.Chain接口。

在solo共識機制中,沒有涉及到Metadata,Metadata爲存儲在區塊中的元數據,包含多種類型,在kafka和raft中都有涉及,在這裏先不做討論。

在“Orderer模塊啓動”博客中,我們瞭解到在生成共識機制的Consenter之後,會通過Consenter產生相應的ChainSupport結構體,在產生ChainSupport結構體的過程中,會調用ConsenterHandleChain函數產生Chain保存在ChainSupport結構體的Chain變量中,最終會調用ChainSupport結構體的start函數,如下代碼:

type ChainSupport struct {
	*ledgerResources
	msgprocessor.Processor
	*BlockWriter
	consensus.Chain
	cutter blockcutter.Receiver
	crypto.LocalSigner
}

func (cs *ChainSupport) start() {
	cs.Chain.Start()
}

從上面代碼可知,ChainSupportstart函數,實質爲Chain接口的Start函數。
因此我們現在將視角轉向共識機制中Chain接口的Start函數。

代碼片段二

func (ch *chain) Start() {  // ChainSupport初始化完成後,會啓動共識機制
	go ch.main()
}

上述代碼爲solo共識中chain結構體的Start函數,在調用後,會啓動一個go協程運行chain.main函數,如下:

func (ch *chain) main() {
	var timer <-chan time.Time 
	var err error

	for { 
		seq := ch.support.Sequence()  // 返回當前的配置序號,嵌套了很多層結構體
		err = nil
		select {
		case msg := <-ch.sendChan:
			if msg.configMsg == nil {
				// NormalMsg
				if msg.configSeq < seq {  
					_, err = ch.support.ProcessNormalMsg(msg.normalMsg)  //基於當前的配置對傳入的消息進行有效性驗證,返回配置序號以及驗證結果
					if err != nil {
						logger.Warningf("Discarding bad normal message: %s", err)
						continue
					}
				}

				batches, pending := ch.support.BlockCutter().Ordered(msg.normalMsg)  

				for _, batch := range batches {
					block := ch.support.CreateNextBlock(batch)  // 創建區塊
					ch.support.WriteBlock(block, nil)
				}

				switch {
				case timer != nil && !pending:
					// Timer is already running but there are no messages pending, stop the timer
					timer = nil
				case timer == nil && pending:
					// Timer is not already running and there are messages pending, so start it
					timer = time.After(ch.support.SharedConfig().BatchTimeout())  
					logger.Debugf("Just began %s batch timer", ch.support.SharedConfig().BatchTimeout().String())
				default:
					// Do nothing when:
					// 1. Timer is already running and there are messages pending
					// 2. Timer is not set and there are no messages pending
				}

			} else {
				// ConfigMsg
				if msg.configSeq < seq {  // 消息中的配置序號小於通道最新配置序號,那麼該消息就有可能會存在失效的問題
					msg.configMsg, _, err = ch.support.ProcessConfigMsg(msg.configMsg)
					if err != nil {
						logger.Warningf("Discarding bad config message: %s", err)
						continue
					}
				}

				batch := ch.support.BlockCutter().Cut() 
				if batch != nil {
					block := ch.support.CreateNextBlock(batch)
					ch.support.WriteBlock(block, nil)
				}

				block := ch.support.CreateNextBlock([]*cb.Envelope{msg.configMsg})
				ch.support.WriteConfigBlock(block, nil) 
				timer = nil 
			}
		case <-timer:
			//clear the timer
			timer = nil

			batch := ch.support.BlockCutter().Cut()
			if len(batch) == 0 {
				logger.Warningf("Batch timer expired with no pending requests, this might indicate a bug")
				continue
			}
			logger.Debugf("Batch timer expired, creating block")
			block := ch.support.CreateNextBlock(batch)
			ch.support.WriteBlock(block, nil)
		case <-ch.exitChan:
			logger.Debugf("Exiting")
			return
		}
	}
}

通過go協程循環運行一個for死循環,來對交易信息進行處理。
main函數中,首先通過ChainSupportSequence函數返回當前的配置區塊序號seq,然後是一個select選擇語句,共有三個分支:

  • msg := <-ch.sendChan,即chain結構體的sendChan通道中有數據傳輸過來
  • <-timer,計時器時間到
  • <-ch.exitChan,chain結構體的exitChan通道中有數據傳輸過來

當沒有條件滿足時,就一直滯留在這裏,直到有一個條件滿足。

我們首先看sendChan通道中所傳輸的數據,是message類型,如下所示:

type message struct {
	configSeq uint64
	normalMsg *cb.Envelope
	configMsg *cb.Envelope
}

通過上面的結構體可以看到,在message結構體中會包含有交易信息cb.Envelope,因此,main函數中的第一個分支即爲處理提交到Orderer節點的交易信息。

我們現在假設進入了第一個分支,即啓動之後,在sendChan通道中有新的message傳輸過來了,這時,會進入第一個分支語句,首先會判斷message中的configMsg是否爲空:

  • 若爲空,則說明該消息是正常的數據信息
  • 若不爲空,則說明該消息是配置信息

當消息爲正常的數據信息時,會首先對該消息的有效性進行檢測,當消息中的配置序號msg.configSeq小於當前通道配置序號seq時,那麼該消息就有可能會存在失效的問題,這時就需要調用ChainSupportProcessNormalMsg函數來對消息進行有效性驗證,並返回驗證結果,當返回有錯誤時,就continue,忽視掉這個消息,否則就對該消息進行下一步的處理。
調用ChainSupportBlockCutter函數,會返回之前在ChainSupport中創建的blockcutter.receiver結構體(該結構體由batchSize控制,用於對交易信息進行區塊化處理),然後調用blockcutter.receiver結構體的Ordered函數,根據batchSize對傳入的交易消息進行處理,在blockcutter.receiver結構體中會有一個pendingBatch變量,用於臨時存儲交易消息,Ordered函數會根據pendingBatch變量中保存的交易消息以及傳入的交易消息,結合batchSize來返回需要產生區塊的交易(並非一個交易信息就產生一個區塊,而是由batchSize控制),Ordered函數共有兩個返回值messageBatchespending,這裏除去錯誤情況之外,共分爲四種情況,其中messageBatches是一個二維切片,它的長度最多爲2,表示需要產生兩個區塊,pending爲布爾值,當其爲true時,表示在pendingBatch中還存有交易信息,當其爲false時,表明pendingBatch當前爲空。
在產生messageBatches之後,會先後調用ChainSupportCreateNextBlock函數和WriteBlock函數,由交易batch產生區塊並加入到區塊鏈中,在這之後需要判斷計時器和pending變量的值,當pending爲true並且計時器未開啓時,需要開啓一個由BatchTimeout控制的計時器timertimer倒計時結束後,會令之前的select語句進入第二個分支,表明到點需要將現有保存在pendingBatch變量中的交易信息切割生成區塊了,具體如何切割生塊將在下文中詳述。

timerblockcutter.receiver結構體共同控制生成區塊,可以體現出區塊生成是由BatchTimeout和BatchSize共同控制的。

此時,Chain區塊鏈對正常交易信息的處理就完成了,共涉及到,“信息有效性的檢驗”、“使用blockCutter產生區塊batches”、“由區塊batches生成區塊”和“將區塊加入到區塊鏈中”這四個步驟。
當消息爲配置消息時,同樣也是先對消息的有效性進行檢測,合格則進一步處理,否則就忽視這一配置消息,與正常消息不同的是,配置消息不能傳入blockcutter.receiver結構體的Ordered函數中由BatchSize控制來進行切塊,因爲配置消息需要獨佔一個區塊,因此直接調用blockcutter.receiver結構體的Cut函數,將pendingBatch變量中的交易信息返回保存在batch變量中,然後由batch變量中的交易信息產生區塊,並將區塊加入到區塊鏈中,這時,因爲配置信息前面不再有其他未上賬的交易信息了,所以直接通過該配置信息構造出新的區塊,並調用ChainSupportWriteConfigBlock函數,將配置區塊寫入區塊鏈,並應用新的配置,因爲此時pendingBatch變量中沒有保存的交易信息了,所以將timer清空。
至此,select的第一個分支的分析就完成了,該分支即爲solo共識對接收到的交易信息的處理過程。
timer沒有被取消,並倒計時結束時,會進入第二個分支,此時由於BatchTimeout的控制,需要將
pendingBatch變量中保存的交易信息進行切塊上賬,調用blockcutter.receiver結構體的Cut函數,將pendingBatch變量中的交易信息返回,然後調用ChainSupportCreateNextBlock函數生成交易區塊,最後調用WriteBlock函數將區塊加入到區塊鏈中。
該分支表明區塊的產生不僅僅是受配置文件中的BlockSize所控制,同時也受到BlockTimeout的控制,timer只有在上文中的pending爲true時纔會開啓。

代碼片段三

func (ch *chain) Order(env *cb.Envelope, configSeq uint64) error {
	select {
	case ch.sendChan <- &message{
		configSeq: configSeq,
		normalMsg: env,
	}:
		return nil
	case <-ch.exitChan:
		return fmt.Errorf("Exiting")
	}
}

solo共識機制chain結構體的Order函數是在grpcServer接收到來自於peer節點的Broadcast數據流時所調用的函數之一,當Broadcast數據流傳輸過來的數據爲正常的交易信息,最終就會調用Order函數;當傳輸過來的數據爲配置信息,則會調用Configure函數。
Order函數中,會利用接收到的交易信息cb.Envelope構造message結構體,並將其輸入到sendChan通道。
因此,Order函數可以喚醒chain.main函數中的第一個select分支。
同理,當grpcServer接收到配置信息,則會調用Configure函數,構造交易message,同樣也可以喚醒第一個select分支。
Configure函數代碼如下所示。

func (ch *chain) Configure(config *cb.Envelope, configSeq uint64) error {
	select {
	case ch.sendChan <- &message{
		configSeq: configSeq,
		configMsg: config,
	}:
		return nil
	case <-ch.exitChan:
		return fmt.Errorf("Exiting")
	}
}

總結

solo共識機制只能用於單節點模式,即只能有一個Orderer節點,因此,其共識過程很簡單,每接收到一個交易信息,就在BatchSize和BatchTimeout的控制下產生區塊並上賬,在源代碼中的體現如下:

  1. grpcServer接收到來自peer節點的交易信息
  2. 最終調用chain.Order函數或chain.Configure函數進行響應
  3. 喚醒chain.main函數中的第一個select分支進行共識處理,共包含四個主要步驟
  4. 可能會喚醒chain.main函數的第二個select分支,在BatchTimeout的控制下進行區塊的生成

solo共識的實現是最簡單的,但“麻雀雖小五臟俱全”,通過分析solo共識的源碼實現,我們可以瞭解到Fabric中的共識模塊在實現時的基本接口以及數據的基本流向,這對我們後續研究raft源碼以及實現自己的共識機制均有很大的意義。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章