fabric源码解析23——Orderer服务

fabric源码解析23——Orderer服务

本篇文章分为下列四个部分:

  1. peer结点与orderer结点间的grpc服务是如何建立的。
  2. 消息从peer结点发给orderer服务的过程,核心是Broadcast服务。
  3. 消息在orderer结点中被处理的过程。
  4. peer结点从orderer结点索要服务的过程,核心是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服务的对象。

  1. 在peer/node/start.go的serve函数中,service.InitGossipService(...)初始化了gossip服务,将一个专门生成deliver服务对象的deliver工厂赋值给gossip服务的的成员deliveryFactory,这个deliver工厂的值是gossip/service/gossip_service.go中的deliveryFactoryImplserve中随后的peer.Initialize(...)(core/peer/peer.go)调用了createChain,在createChain中调用了service.GetGossipService().InitializeChannel(...)(gossip/service/gossip_service.go),传给InitializeChannel的最后一个参数ordererAddresses就是orderer的地址,对gossip服务的进一步进行了初始化。
  2. InitializeChannel中,使用了第1点中deliveryFactoryImpl的Service函数,进而使用deliverclient.NewDeliverService(config)(core/deliverservice/deliverclient.go中实现)生成了一个Deliver服务实例deliverServiceImpl赋值给gossip服务中的成员deliveryService,所给的配置configEndpointsConnFactoryABCFactory被分别赋值为传进来的orderer的地址,core/deliverservice.go中的DefaultConnectionFactoryDefaultABCFactory
  3. 重回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的接收线程
  4. NewBroadcastClient中,createClient就被赋值为上述config的ABCFactory,而生成的broadcastClient就是blocksProviderImpl中的成员client。也就是说,blocksProviderImpl中的createClient的值是core/deliverservice/deliverclient.go中的DefaultABCFactoryDefaultABCFactory即是使用了protos/orderer/ab.pb.go中生成的默认的AtomicBroadcastClient(自然包含默认的Deliver客户端)。启动client的接收线程后,在client接收消息时,即core/deliverservice/client.go中的Recv(),会执行try(...),进而执行bc.doAction(action),在doAction中会查看client是否已经和orderer通过grpc连接上,若未连接则会执行connect()进行grpc连接。
  5. 总结一下:当peer结点的容器启动起来的时候,peer结点的gossip服务随着peer node start命令启动,其中角色是leader的peer结点的gossip服务中使用到的deliver服务对象deliverServiceImpl也会使用默认生成的Deliver客户端通过grpc连接orderer结点并启动循环接收的线程。至于orderer如何不断的给peer结点中的leader发送block消息,将在下文Deliver服务中详述。

Broadcast

可回看《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。

  1. cf.BroadcastClient.Send(env),通过grpc将消息以Envelope的形式发送到orderer结点中。

Orderer

  1. orderer/server.go中的Broadcast收到peer点发来的Envelope消息,直接交由成员bh的Handle处理。
  2. bh原型为orderer/common/broadcast/broadcast.go中的handlerImpl,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))。
  3. 消息进入orderer/kafka/chain.go的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:分支,消息也会在此进行处理。
  4. 在这个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服务端。
  5. kafka服务端(即kafka容器)这个“暗盒”接收到消息并生产消息
  6. 在之前chain启动起来的接收kafka服务端消息的线程中(对看《fabric源码解析22》chain的启动章节),即orderer/kafka/chain.go中的processMessagesToBlocks(),kafka服务端生产的消息从case in, ok := <-chain.channelConsumer.Messages()被消费出来,得到ConsumerMessage类型的消费消息,并进入该分支。
  7. 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)点情况。
  8. processRegular()中,将原始数据(即peer结点发来的消息)从kafka类型消息中分解出来后,交由blockcutter模块的Ordered进行分块处理。
  9. 对看《fabric源码解析22》blockcutter章节,消息进入orderer/common/blockcutter/blockcutter.go的Ordered(env),先r.filters.Apply(msg)进行了第二次的过滤(对看第2步(3)的第一次过滤,这主要是防止从第2步到此步期间orderer的配置有所更新),然后就是一个根据配置信息进行分割成block的过程。这里假设此时满足生成一批消息的条件,该批消息和对应的committer集合一同被返回。
  10. 重回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写入账本。
  11. 这里单独描述一下传入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消息会再次进入processMessagesToBlockscase 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,因为一旦有新的Cutchain.lastCutBlockNumber就会增1。没有发生新的Cut,则说明blockcutter中现在缓存的数据依然是发送TimeToCutMessage消息时的数据,或者有新数据但是依旧没有达到Cut的条件。此时就会进行主动的Cut,blockcutter中现有的缓存被打包出来后清空,timer计时器也会重新的置为nil。另外,在processRegular(...)中,当常规的触发了Cut后blockcutter中将不存在缓存消息,之后会进入if len(batches) > 0分支,会将timer计时器置为nil,在processMessagesToBlocks中的for-select-case等待再次从kafka中消费出消息。
  12. block := support.CreateNextBlock(batch)(参看图片EnvelopeToBlockToWriteToLedger.png)和support.WriteBlock(block, committers[i], encodedLastOffsetPersisted),调用了support中的ledger进行创建block和写入block,在写入block之前会逐一执行该block对应的committer集合,这里不再详述。至此,orderer端处理peer结点发送来的消息的流程结束。

Deliver

Deliver服务较Broadcast服务复杂,我们知道最终orderer中ledger账本的数据会通过Deliver服务推送leader结点,现在还存在一些的问题:是orderer主动向peer结点推送数据?还是peer结点主动向orderer索要数据,要多少给多少?如果是peer结点向orderer索要数据,是在何处且索要方式是什么,定时索要固定量,还是orderer端一有block产生就索要?

  1. 既然Deliver服务是建立grpc之上,且orderer为Deliver的服务端,则基本上可以推断是peer结点向orderer索要block数据。gossip服务的索要行为的对象是core/deliverservice/requester.go中的blocksRequester,索要行为发生在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。
  2. 索要全部的block的行为在何时发生呢。对看上文建立连接章节,(1)在core/deliverservice/deliverclient.go的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循环中持续调用的,其中建立连接和索要行为只会发生一次。
  3. 第2点(8)中执行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循环“永远”不会退出。
  4. orderer的Deliver服务端向peer结点的Deliver客户端发送block,block进入第2点(9)的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。
  5. 向Orderer的Deliver服务端索要block消息的,除了gossip服务,还有peer channel的几个子命令,但peer channel索要的都是一定量的block,即非持续性的,在此不做详述,流程与上述一致,只是Deliver服务端最后会进入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数据,但该推送是非持续性的。

拓展:orderer实现的共识机制

这一部分是在群中交流之后追加的。分享一篇文章 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结点,即“生产点”和“消费点”多了的情况,则在共识问题上主要需要解决下列两个问题:

  • A. 每个orderer结点不能保证在同一时刻启动开始服务(或者有新的orderer加入),因此,先启动的orderer结点可能会先产生block块,但后启动的orderer与前启动的orderer间相差的block怎么弥补?
  • B. 当更新orderer服务的配置,每个orderer结点不能保证同时更新配置,因此在某一时刻每个orderer的配置可能不同,又因为配置会影响到Cut操作,因此如何保证一个chain中的某个orderer配置更新后,其他的orderer结点得到更新但不影响共识?

这里有几个前提:

  • fabric中以kafka为基础的orderer实现,从实现上看是每一条链一个分区(因为每个orderer/kafka/chain.go中的chainImpl都只有一个sarama.PartitionConsumer成员)。在同一条链上多个orderer结点共同连接一个kafka服务器,消费kafka服务器上的同一个分区(可直接当成同一个文件)中的消息。
  • kafka同一分区生产存储的消息序列无论在哪一个orderer结点,被消费出来的序列都是一样的。这也是kafka服务自身实现的特性。也就是说,多个orderer结点从kafka服务器出得到的消息序列,无论各个orderer结点在消费消息的速率的快慢,最终得到的消息序列是一致的。再也就是说,无论各个orderer结点在生产消息的速率上的差异如何,只要将不同的消息成功写入kafka分区,各个orderer结点消费出来的消息序列依旧一致。
  • 要达到每个orderer结点所产生的block序列一致,关键在于对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块序列自然一致。

问题A

  1. 在orderer/kafka/chain.go中,newChain(...)生成的chainImpl实例时,成员lastOffsetPersisted所赋的值为-3。startThread(...)中在生成分区消费对象,也是chainImpl的成员channelConsumer时,chain.channelConsumer, err = setupChannelConsumerForChannel(..., chain.lastOffsetPersisted+1),最后一个参数指定的是channelConsumer从分区的何处开始消费(参看sarama库对ConsumePartition函数的说明),此时chain.lastOffsetPersisted+1值为-2。
  2. sarama库中,提及了两个常量:OffsetNewest,OffsetOldest,前者指从分区的最新偏移处开始消费,后者指从分区最开始偏移处开始消费。而OffsetOldest的常量值就是-2,即第1点所提及的chainImpl的channelConsumer默认是从分区的最开始出开始消费的。
  3. 因此,同一个chain有多个orderer结点时,每个orderer结点中都有自己的一个chainImpl对象,但一个chain对应一个分区,所有每个orderer的chainImpl中的channelConsumer都是消费同一个分区下的消息。无论每个orderer结点的chainImpl何时开始消费消息,其都是从分区的最开始处开始消费消息的,因此不会错过任何一个之前(在该orderer结点开始消费消息之前)其他orderer结点产生的消息。
  4. 另外需要注意的是,kafka服务器自身对分区消息保留的时长或大小是可以设置的,分别由.server.properties文件中的log.retention.hours和log.retention.bytes指定,超过设置的时间或大小,分区中之前旧的消息会被清除。Getting Started中下载的kafka镜像所启动的kafka服务器的配置中,log.retention.hours值为默认的168(一周),log.retention.bytes值为-1,即不开启此阈值(可以docker run一下kafka镜像,无论是否启动,都会将配置打印出来)。因此,orderer结点最晚加入的时间也将受此限制。

问题B

  1. 同理,如同在分区中插入TimeToCutMessage消息作为统一的Cut命令一样,一个orderer通过向分区中插入ConfigUpdate消息(这里ConfigUpdate消息算作使一条正常的交易消息),对各个orderer结点进行统一的配置升级。需要明确的是,这个ConfigUpdate消息其实是链中具有写权限的peer结点通过Broadcast服务发给orderer结点的。
  2. ConfigUpdate消息途径orderer/common/broadcast/broadcast.go的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结点的数量。

你可能感兴趣的:(Fabric)