本篇文章分为下列四个部分:
Broadcast
服务。Deliver
服务。这里给的前提是,orderer服务已按照orderer.yaml/configtx.yaml的配置正常启动。讲解orderer服务可以按照服务分类来讲,也可以依据数据的流向按步骤讲。本文综合一下,既分服务,也按数据流向的步骤。在每个服务中按数据流向讲解,服务与服务之间的步骤前后衔接。
grpc自然分为客户端和服务端,对于peer连接orderer来说,peer是客户端,orderer为服务端。
Broadcast服务主要集中在peer结点的命令中,如peer chaincode,peer channel命令。在peer/common/common.go中,GetBroadcastClientFnc
的值为peer/common/ordererclient.go中的GetBroadcastClient
函数,peer chaincode和peer channel命令中使用到的命令工厂中的broadcast客户端BroadcastClient(在peer/common/common.go中的ChaincodeCmdFactory中)均由此函数生成。同时,peer chaincode和peer channel命令执行时,都有一个Flag,-o
,来指定所要连接的orderer服务结点的地址。当命令(第一次)执行时,peer结点会建立与orderer结点的Broadcast服务的grpc连接。
Deliver服务较复杂和绕一点,对象之间相互嵌套,主要集中在peer结点的gossip服务中,且包裹在core/deliverservice/blocksprovider/blocksprovider.go中的blocksProviderImpl中的client中的createClient,client的类型是core/deliverservice/client.go中的broadcastClient,createClient也不是Deliver服务本身,而是用于生成Deliver服务实例的,而core/deliverservice/deliverclient.go中的deliverServiceImpl按chainID存储不同的blocksProviderImpl(也就是说每个chain都会有一个自己的client),是gossip服务直接使用的deliver服务的对象。
serve
函数中,service.InitGossipService(...)
初始化了gossip服务,将一个专门生成deliver服务对象的deliver工厂赋值给gossip服务的的成员deliveryFactory,这个deliver工厂的值是gossip/service/gossip_service.go中的deliveryFactoryImpl。serve
中随后的peer.Initialize(...)
(core/peer/peer.go)调用了createChain
,在createChain
中调用了service.GetGossipService().InitializeChannel(...)
(gossip/service/gossip_service.go),传给InitializeChannel
的最后一个参数ordererAddresses就是orderer的地址,对gossip服务的进一步进行了初始化。InitializeChannel
中,使用了第1点中deliveryFactoryImpl的Service
函数,进而使用deliverclient.NewDeliverService(config)
(core/deliverservice/deliverclient.go中实现)生成了一个Deliver服务实例deliverServiceImpl赋值给gossip服务中的成员deliveryService,所给的配置config中Endpoints,ConnFactory,ABCFactory被分别赋值为传进来的orderer的地址,core/deliverservice.go中的DefaultConnectionFactory
,DefaultABCFactory
。InitializeChannel
中,无论是静态指定leader还是动态选举leader,最终都会调用deliveryService.StartDeliverForChannel(...)
,即在core/deliverservice/deliverclient.go中,StartDeliverForChannel会使用newClient
->NewBroadcastClient
(core/deliverservice/client.go),创建一个client,再使用blocksprovider.NewBlocksProvider(...)
创建一个包含client的blocksProviderImpl,随后go d.blockProviders[chainID].DeliverBlocks()
启动client的接收线程。NewBroadcastClient
中,createClient就被赋值为上述config的ABCFactory,而生成的broadcastClient就是blocksProviderImpl中的成员client。也就是说,blocksProviderImpl中的createClient的值是core/deliverservice/deliverclient.go中的DefaultABCFactory
,DefaultABCFactory
即是使用了protos/orderer/ab.pb.go中生成的默认的AtomicBroadcastClient(自然包含默认的Deliver客户端)。启动client的接收线程后,在client接收消息时,即core/deliverservice/client.go中的Recv()
,会执行try(...)
,进而执行bc.doAction(action)
,在doAction
中会查看client是否已经和orderer通过grpc连接上,若未连接则会执行connect()
进行grpc连接。可回看《fabric源码解析20》,以peer chaincode instantiate为例,假设名为peerA的结点将example02部署完毕后,生成了一个Envelope(参看图片EnvelopeSendToOrderer.png)发送给orderer(参看peer/chaincode/instantiate.go的chaincodeDeploy
中的cf.BroadcastClient.Send(env)
)。cf即为peer的命令工厂ChaincodeCmdFactory的实例,cf成员BroadcastClient即为使用/protos/orderer/ab.pb.go中默认生成的Broadcast服务客户端AtomicBroadcast_BroadcastClient。
cf.BroadcastClient.Send(env)
,通过grpc将消息以Envelope的形式发送到orderer结点中。Broadcast
收到peer点发来的Envelope消息,直接交由成员bh的Handle
处理。Handler
函数也在这里实现,在Handler
函数中,会在for中循环接收来自peer结点的消息:(1)msg, err := srv.Recv()
接收消息。(2) Unmarshal并检查Envelope消息中的一些字段(如ChannelId),如果是HeaderType_CONFIG_UPDATE类型的消息,则会将消息经过bh.sm.Process(msg)
,实际是调用orderer/configupdate/configupdate.go中的Process
对消息进行进一步的加工,将消息加工成一个orderer可以处理的交易消息(加工成 创建新chain的配置 或 对已存在的chain更新配置)。(3)support, ok := bh.sm.GetChain(chdr.ChannelId)
,获取消息对应ChannelId的链的chainSupport,然后support.Filters().Apply(msg)
,使用该chainSupport的过滤器过滤该消息,以查看该消息是否达标,这里没有使用过滤器一并返回的committer集合,这是第一次过滤。(4)support.Enqueue(msg)
,进而调用了support对应的chain的cs.chain.Enqueue(env)
处理消息,这个chain是对应的consenter(这里只以kafka为例进行介绍)的HandleChain
生成的(在创建chainSupport的时候,orderer/multichain/chainsupport.go的newChainSupport
中,cs.chain, err = consenter.HandleChain(cs, metadata)
)。Enqueue(env)
,在最外层的select-case
中,如果startChan
已经被close(对看《fabric源码解析22》chain的启动章节关于开关的文字),则说明orderer的kafka客户端部分(即orderer/kafka/chain.go的chainImpl)已经一切准备就绪,则会进入case <-chain.startChan:
分支,否则进入default:
分支,什么都不做(即不处理消息)而返回。在case <-chain.startChan:
分支中,又是一个select-case
,如果该chain没有停止,即haltChan没有被close,则会进入default:
分支,消息也会在此进行处理。default:
分支中:(1)message := newProducerMessage(chain.channel, payload)
把消息打包成kafka消息类型,也是使用的sarama预定义的ProducerMessage类型,Topic定义了消息的主题,Key和Value,key为分区号,value为Marshal过的peer点发来的原始的消息。(2)chain.producer.SendMessage(message)
,使用sarama的生产者对象将kafka消息发送到kafka服务端。processMessagesToBlocks()
,kafka服务端生产的消息从case in, ok := <-chain.channelConsumer.Messages()
处被消费出来,得到ConsumerMessage类型的消费消息,并进入该分支。case in, ok := <-chain.channelConsumer.Messages()
分支中,首先出现的一个select-case
是一个关于errorChan通道的开关(也对看《fabric源码解析22》chain的启动章节关于开关的文字),errorChan通道在chain初始化之初是关闭的(参看newChain(...)
中的close(errorChan)
),而后在startThread()
中的chain.errorChan = make(chan struct{})
中再度生成,算是再度开启。如果errorChan是关闭的,则在select-case
中会进入case <-chain.errorChan:
分支而返回,否则进入default:
分支,程序继续。随后进入一个switch-case
,根据解压消费消息所携带的数据的类型,进入不同分支,处理消息。正常的,数据会进入case *ab.KafkaMessage_Regular:
分支,被processRegular(...)
处理。这里注意两个数据:(1)一是传入processRegular(...)
的消费消息的in.Offset
,该值是消息在kafka服务端分区中的offset,在整个生产消费过程中是序列化排序依次分配给每个消息的,因此每个消息具有唯一的offset,也因此in.Offset可以代表每个具体的消费消息。在block的结构中,一方面,Metadata是一个数组,用来存储与block块有关的基础信息,其中下标BlockMetadataIndex_ORDERER处存储的就是block中最后一条消息的offset值+1(也相当于下一个block中第一条消息的offset)。另一方面,block中实际存储数据的Data是一个[]byte数组,序列化过后的消息都被二进制化后依次存放在这个数组中,我们可以很容易的算出block中具体有多少条消息。因此通过< offset+1的条件,就可以准确定位block中的每条消息。(2)二是传入processRegular(...)
的time计时器,这个计时器对应控制《fabric源码解析22》blockcutter章节描述的关于Cut
操作的第(5)点情况。processRegular()
中,将原始数据(即peer结点发来的消息)从kafka类型消息中分解出来后,交由blockcutter模块的Ordered
进行分块处理。Ordered(env)
,先r.filters.Apply(msg)
进行了第二次的过滤(对看第2步(3)的第一次过滤,这主要是防止从第2步到此步期间orderer的配置有所更新),然后就是一个根据配置信息进行分割成block的过程。这里假设此时满足生成一批消息的条件,该批消息和对应的committer集合一同被返回。processRegular()
,当返回了一或二批消息,程序会进入for i, batch := range batches
循环依次处理每批消息。在循环中:(1)计算当批消息的offset值(即最后一条消息的offset值+1),offset := receivedOffset - int64(len(batches)-i-1)
,对看上文第7点对传入的消息的offset的描述和《fabric源码解析22》blockcutter章节所描述的批量消息是如何返回的,就可以明白为何这么计算。(2)block := support.CreateNextBlock(batch)
,将当批消息打包成block,至此消息正式成块。(3)support.WriteBlock(block, committers[i], encodedLastOffsetPersisted)
,将block对应的committer集合,然后将block写入账本。processRegular()
中的time计时器和该计时器控制的主动Cut
操作。调用processRegular(...)
是chainImpl启动的processMessagesToBlocks
,而消息来源于kafka服务器,倘若kafka服务器长时间没有消息被消费出来,要么是kafka服务器出了故障,要么是客户端没有新的生产消息发给kafka服务器。在这种情况下,blockcutter可能已经缓存了一些之前的消息,为了不使这部分消息丢失和及时记录到账本中(比如kafka处理的数据量很小或消息流很不稳定,一条消息后很长时间都不来下一条消息,会造成已缓存的数据不能及时记录到账本,有丢失的风险,也有交易已经产生了好长时间,但是包裹交易的消息一直悬空在blockcutter缓存中无法被消费,进而无法被记录和查询),configtx.yaml中配置的BatchTimeout规定了超时时间,默认2s。当下一条消息超过2s还没被消费出来,将会主动Cut
操作将现有缓存打包成一个block写入账本。具体的操作是:(1) timer初始状态为nil,当消息进入processRegular()
中,若缓存进blockcutter中,则会进入if ok && len(batches) == 0 && *timer == nil
分支,设timer计时器为2s后触发然后返回。(2)下一条消息若在2s之前再次进入processRegular()
,若仍是被放入缓存,则依然会进入if ok && len(batches) == 0 && *timer == nil
重置timer为2s计时然后返回。(3)当timer没有及时被重置,即超过了2s,processMessagesToBlocks
会进入case <-timer:
分支,调用sendTimeToCut
向kafka服务器发送一条TimeToCutMessage消息(包裹着chain.lastCutBlockNumber+1,即若主动Cut
,会使用的block的序号),接着kafka服务器收到,然后被消费出来(这里存在一个时间过程),这条TimeToCutMessage消息会再次进入processMessagesToBlocks
的case in, ok := <-chain.channelConsumer.Messages():
分支,进而进入case *ab.KafkaMessage_TimeToCut:
分支,调用processTimeToCut(...)
处理这条消息,也就是主动Cut
。(4)在processTimeToCut(...)
中,倘若此时if ttcNumber == *lastCutBlockNumber+1
(ttcNumber即为第(3)点TimeToCutMessage消息包裹的block序列号),说明在第(3)点提到的时间过程中,chain.lastCutBlockNumber
的值没变,也就是没有发生新的Cut
,因为一旦有新的Cut
,chain.lastCutBlockNumber
就会增1。没有发生新的Cut
,则说明blockcutter中现在缓存的数据依然是发送TimeToCutMessage消息时的数据,或者有新数据但是依旧没有达到Cut
的条件。此时就会进行主动的Cut
,blockcutter中现有的缓存被打包出来后清空,timer计时器也会重新的置为nil。另外,在processRegular(...)
中,当常规的触发了Cut
后blockcutter中将不存在缓存消息,之后会进入if len(batches) > 0
分支,会将timer计时器置为nil,在processMessagesToBlocks
中的for-select-case
等待再次从kafka中消费出消息。block := support.CreateNextBlock(batch)
(参看图片EnvelopeToBlockToWriteToLedger.png)和support.WriteBlock(block, committers[i], encodedLastOffsetPersisted)
,调用了support中的ledger进行创建block和写入block,在写入block之前会逐一执行该block对应的committer集合,这里不再详述。至此,orderer端处理peer结点发送来的消息的流程结束。Deliver服务较Broadcast服务复杂,我们知道最终orderer中ledger账本的数据会通过Deliver服务推送leader结点,现在还存在一些的问题:是orderer主动向peer结点推送数据?还是peer结点主动向orderer索要数据,要多少给多少?如果是peer结点向orderer索要数据,是在何处且索要方式是什么,定时索要固定量,还是orderer端一有block产生就索要?
RequestBlocks(...)
函数中,该函数传入一个包裹了高度值的LedgerInfo,若该高度值为向orderer索要的开始的block的序列号,如果高度值>0,则调用seekLatestFromCommitter
,否则调用seekOldest()
,两个函数过程基本一致,先创建一个包装了SeekInfo信息(即索要的block的起止范围信息)的HeaderType_CONFIG_UPDATE类型的Envelope消息,然后b.client.Send(env)
向orderer端的Deliver服务端索要block。注意,这两个函数索要的block范围的起点可能不一样,但是止点都一致:math.MaxUint64,即最大极限值,这相当于向orderer端索要现在以及将来所产生的所有block。newClient
中,broadcastSetup := func(...
中调用了RequestBlocks(...)
。(2)broadcastSetup
作为一个参数通过NewBroadcastClient
赋值给了bClient(原型是core/deliverservice/client.go中的broadcastClient)的成员onConnect
。(3)随后bClient通过newClient
返回,在StartDeliverForChannel(...)
中的blocksprovider.NewBlocksProvider(...)
,bClient赋值给了对应chainID的BlocksProvider(core/deliverservice/blocksprovider/blocksprovider.go)的成员client。(4)接着go d.blockProviders[chainID].DeliverBlocks()
,调用了b.client.Recv()
,这个client,就是第(3)点提到的bClient。(5)Recv()
在core/deliverservice/client.go中,调用了bc.try(...)
,传给try(...)
参数是一个执行接收动作的函数,即使用bc.BlocksDeliverer.Recv()
接收消息(此时BlocksDeliverer还没有被赋值)。(6)在try(...)
中,在for循环中不断尝试执行bc.doAction(action)
,这个action是上一步传入的接收消息的动作。在doAction(action)
中,会首先查看连接conn是否已经建立,此时没有建立,则会调用bc.connect()
建立连接。随后resp, err := action()
执行action动作。(7)对看建立连接章节Deliver部分,追溯一下可知,在connect()
中,使用了conn, endpoint, err := bc.prod.NewConnection()
建立了一个连接orderer结点的grpc连接,abc, err := bc.createClient(conn).Deliver(ctx)
使用建立的grpc连接创建了一个Deliver服务客户端,随后调用了bc.afterConnect(conn, abc, cf)
。(8)在afterConnect
中,将创建与orderer结点的连接和Deliver客户端分别赋值给broadcastClient的conn(可使之后的所有doAction(...)
中的connect()
不再执行)和BlocksDeliverer(对看在(5)时BlocksDeliverer还没有被赋值),然后调用bc.onConnect(bc)
,这个onConnect就是(1)(2)中的broadcastSetup
,即在这里发生了索要行为,向orderer结点索要所有的block。(9)随后开始返回,重新返回到doAction
中,与orderer结点的连接建立并发送了索要请求后,继续开始执行动作resp, err := action()
,这个action即为开始接收orderer结点发来的block数据(对看(5))。(10)总结一下,以上步骤都是随者gossip服务运行起来,调用deliverServiceImpl对象的StartDeliverForChannel
,所启动的go d.blockProviders[chainID].DeliverBlocks()
中的for循环中持续调用的,其中建立连接和索要行为只会发生一次。bc.onConnect(bc)
后,即执行的是core/deliverservice/requester.go中的RequestBlocks
后,orderer结点在orderer/common/deliver/deliver.go的Handle
中的envelope, err := srv.Recv()
处被接收,之后:(1)一系列从Envelope的解压收取数据的操作,然后chain, ok := ds.sm.GetChain(chdr.ChannelId)
获取Envelope中对应chainID的chainSupport。(2)erroredChan := chain.Errored()
,select {case <-erroredChan:...
,这也是一个erroredChan控制的开关,对看上文Orderer章节第7点,用法是一样的。此刻erroredChan开关处于开启状态,程序将继续向下走。(3)sigfilter.New(...)
,sf.Apply(envelope)
,创建了一个签名过滤对象验证Envelope消息,不详述。(4)从Envelope解压出来携带的索要的block的起止信息SeekInfo,cursor, number := chain.Reader().Iterator(seekInfo.Start)
,根据起止信息创建一个账本的查询迭代器cursor,这个迭代器是在orderer/ledger/file/impl.go中定义的fileLedgerIterator,存储了起点,有一个特性是Next()
会在迭代到还没有写入到账本block(即当前账本最新的block的接下来将要有的一个block)时阻塞等待,一直等待到该block被写入到账本中。(5)在for循环中,block, status := cursor.Next()
,不断使用迭代器cursor获取一个block,随后sendBlockReply(srv, block)
向Deliver客户端回复该block。(6)向Deliver客户端回复一条block后,会进行if stopNum == block.Header.Number
判断,若刚刚发送的block已经是客户端索要的最后一块block,则break跳出循环等待客户端下次的索要行为。但是前面已经提到,gossip服务索要的block的止点是math.MaxUint64,即stopNum的值是math.MaxUint64,所以向gossip服务发送block的for循环“永远”不会退出。doAction
中,resp, err := action()
接收到block,继续返回至try(...)
,再返回至Recv()
,再返回core/deliverservice/blocksprovider/blocksprovider.go的DeliverBlocks()
中的msg, err := b.client.Recv()
,然后进入case *orderer.DeliverResponse_Block:
分支:(1)gossipMsg := createGossipMsg(b.chainID, payload)
将block包装成gossip服务能处理的消息类型。(2)b.gossip.AddPayload(b.chainID, payload)
,将block添加到peer结点的gossip服务模块的本地,目的在于添加到本地的ledger中。再次强调,接收block的是peer结点中的leader,所以这里的gossip服务模块是leader的。(3)b.gossip.Gossip(gossipMsg)
,leader的gossip服务模块向其他peer结点散播block。if stopNum == block.Header.Number
分支跳出回复block的for循环(对看第3点(5)的描述)。总结一下:peer结点中的leader的gossip服务起来之后就建立了与orderer的Deliver服务的连接(这期间peer结点只会在开始的时候向orderer的Deliver服务索要一次block),之后当orderer中的账本中存在block数据后,就开始主动的向leader的Deliver客户端发送block数据,这个推送行为将一直持续。leader的Deliver客户端收到block流之后会使用gossip服务向自身和其他peer结点散播这些block数据。另外,peer channel等命令也会向Deliver服务端请求某一段block数据,但该推送是非持续性的。这一部分是在群中交流之后追加的。分享一篇文章 A Kafka-based Ordering Service for Fabric,原文在docs.google上,鉴于地址可能会失效且FQ问题,在此不贴链接了,可自行搜索或加入群中,群中该文件有共享,该文章也是官方文档Bringing up a Kafka-based Ordering Service章节中的链接。文章讲述的内容大概为如何考虑问题并一步步设计解决问题实现以kafka为基础的orderer服务。这是个自上而下的过程。源码解析是一个自下而上的过程,从代码的实现去推敲作者的设计意图和所要解决的问题。理解了一个方向后再去从另一个方向去理解,感觉还是很受益的。在此感谢悠悠浮云的共享。同时,建议读一下官方文档的Bringing up a Kafka-based Ordering Service章节,指导了如何部署多个orderer结点的步骤(若觉与本文有矛盾之处,乃笔者未理解透彻之过,已官方文档为准)。
从文章中受益而来的,是关于orderer服务的源码解析中没有讲述到多个orderer结点的情形。即区域链中,服务于每个“区域”的一个链中peer结点的多个orderer结点共同连接一个kafka服务器。每个orderer结点的服务过程如文章之前描述的那样不变,多个orderer连接kafka服务器也没有其他更多的不同之处,只是接收的“生产点”和“消费点”多了而已。但是考虑多个orderer结点的情况,可以提高了各位审视区域链orderer服务的高度,也出现了一个此前忽略的重要的概念(可以说是精髓):共识机制。这里的共识问题就是:kafka输出的消息被消费至多个orderer结点后产生的block块序列最终一致。
若同一个chain存在多个orderer结点服务于众peer结点,即“生产点”和“消费点”多了的情况,则在共识问题上主要需要解决下列两个问题:
Cut
操作,因此如何保证一个chain中的某个orderer配置更新后,其他的orderer结点得到更新但不影响共识?这里有几个前提:
Cut
操作的控制。orderer服务中的Cut
操作受configtx.yaml中的BatchTimeout/BatchSize两项配置的影响。(1)由BatchTimeout触发的Cut
将以TimeToCutMessage消息的形式发送到kafka服务器的分区中,当一个结点接收到TimeToCutMessage消息时,无论该消息是否是自身发出的,只要TimeToCutMessage携带的BlockNumber值等于自身账本的下一块block的序列号(对看上文Orderer章节第11点(4)),则Cut
。这相当于利用分区消息序列的唯一性设置了一个统一Cut
的命令,任何一个orderer结点无论先后,只要读到这个消息就进行Cut
,这样被Cut
出来的block是一致的,只是在时间早晚罢了。(2)由BatchSize触发的Cut
操作,只要每个orderer结点的该配置一致,则顺序消费同一个分区的消息,所产生的block块序列自然一致。newChain(...)
生成的chainImpl实例时,成员lastOffsetPersisted
所赋的值为-3。startThread(...)
中在生成分区消费对象,也是chainImpl的成员channelConsumer时,chain.channelConsumer, err = setupChannelConsumerForChannel(..., chain.lastOffsetPersisted+1)
,最后一个参数指定的是channelConsumer从分区的何处开始消费(参看sarama库对ConsumePartition
函数的说明),此时chain.lastOffsetPersisted+1
值为-2。Cut
命令一样,一个orderer通过向分区中插入ConfigUpdate消息(这里ConfigUpdate消息算作使一条正常的交易消息),对各个orderer结点进行统一的配置升级。需要明确的是,这个ConfigUpdate消息其实是链中具有写权限的peer结点通过Broadcast
服务发给orderer结点的。Handle(...)
时,会进入if chdr.Type == int32(cb.HeaderType_CONFIG_UPDATE)
分支进行bh.sm.Process(msg)
加工处理,使这个消息在发送到分区后再被各个orderer或前或后消费出来后到达orderer/kafka/chain.go的processRegular(...)
中,ConfigUpdate消息进入orderer/common/blockcutter/blockcutter.go的Ordered(...)
中执行了committer, err := r.filters.Apply(msg)
而产生了消息对应的committer。这个committer的Commit()
就是最终用来执行配置升级的。ConfigUpdate和对应的committer最终会被包裹在一批消息中返回出来,假设该批消息为batchA,对应的committer集合为committerA。在processRegular(...)
中,block := support.CreateNextBlock(batch)
将batchA打包成block,随后在执行support.WriteBlock(...)
中被执行依次执行了committerA,其中自然包括ConfigUpdate对应的committer。也就是说,bathA生成的block是以旧的配置信息生成的最后一块block,之后再进行Cut
,均是新的配置下所生成的block。对于任何一个orderer结点,均是这样。《fabric源码解析》中提及到,以kafka为基础的orderer服务的共识机制不具备容错性,这点也是官方文档中所述的。在此的体会是,这个容错性可能指两方面的容错性:(1)orderer服务自身的容错性(而不是kafka服务的容错性)。kafka服务自身具有一点的容错性,在集群中,可以容忍一定数量的代理失效。但是多个orderer自身不存在容错性,一个orderer结点发生故障,假设是名为orderer_ERROR的结点,则orderer_ERROR结点所服务的所有peer结点将无法再继续同orderer_ERROR通信,也就自然无法再向orderer_ERROR发送交易数据或获取block数据。(2)多个orderer结点消费顺序同一分区的消息,正常工作自然好,但倘或某一个orderer结点在消费的过程中由于某些原因丢失了已经消费出来的一个消息或者根本就没有消费到某一个消息呢?如此之后该orderer所产生的block将同其他orderer结点产生差异。而据此推测,PBFT ordering service将实现应该是:多个orderer的情况下,允许一定数量的orderer结点失效,而这些失效的orderer结点背后服务的所有peer结点的通信将被及时转移到依然有效的orderer结点上来,即这些peer结点及时重新连接仍然有效的orderer结点,从而继续工作。而且可能会增加检查各个orderer间所产生的block序列的行为。还有一点,就是orderer结点是否可以自由加入的问题,目前从代码,kafka服务的配置显示,orderer的结点是不能像peer结点那样自由加入的,但这样又有点悖于对于区域链这种分布式的松散和自由,比如你设计启动一个区域链,需要预先设计好orderer结点的数量。