orderer服务是单独拥有说明文档的组件,这说明这个服务应该是相当独立的。orderer提供的服务既涉及到chaincode,也涉及到账本,因此这时讲最合适。先回顾一下下列内容:
从以上回顾可以明确:(1) orderer服务是先于所有peer结点启动起来的单独的结点。(2) orderer服务与gossip服务密切相关。这里可以概述一下orderer所提供的服务,只做broadcast和deliver。orderer结点与各个peer结点通过grpc连接,orderer将所有peer结点通过broadcast客户端发来的消息(Envelope格式,比如peer部署后的数据)按照配置的大小依次封装到一个个block中并写入orderer自己的账本中,然后供各个peer结点的gossip服务通过deliver客户端来消费这个账本中的数据进行自身结点账本的同步。数据流向:
peer结点->grpc->Server->broadcast/deliver->broadcastSupport/deliverSupport->multichain->chainSupport->kafka/solo->chain->BlockCutter->chainSupport.CreateNextBlock->chainSupport.WriteBlock->ledger账本->kafka->peer结点。
在本文即后续讲解orderer的文中所涉及的图片都上传到了网盘,http://pan.baidu.com/s/1bpu91uf ,密码s51j,涉及的目录如下:
orderer服务的命令行使用了第三方库kingpin,v2版本,地址为gopkg.in/alecthomas/kingpin.v2。这里orderer不再像peer那样使用cobra的原因就不得而知了。kingpin本身比较简单,而且orderer也只使用了kingpin最表面的一层。只有两个子命令,start
和version
,且没有任何flag或参数。version
不用讲,start
是默认命令,即《fabric源码解析1》中提及的docker-compose-base.yaml中启动orderer结点容器时所执行的命令。当前版本中,docker-compose-base.yaml在examples/e2e_cli/base下,且执行的command是orderer
,但是start
是默认命令,所以执行orderer
相当于执行了orderer start
命令。
Server - 服务自身,包括broadcast和deliver。有以下三种类型的服务,分别用于不同情况下的部署:(1) Solo ordering service,Solo,针对开发测试环境的一种服务,特点是简单,不支持consensus,不够高效且不可测量。(2) Kafka-based ordering service,卡夫卡,一种经典的出版-订阅服务。orderer中使用了第三方库github.com/Shopify/sarama创建客户端,以连接kafka。在fabric Getting Started操作下载的镜像中,有一个kafka镜像以作为服务端处理消息(还有一个zookeeper镜像辅助kafka)。在产品级别的项目部署中,这种服务是当前最好的选择,有很高的吞吐量,非常的高效,但是不支持容错。理解orderer服务中的kafka客户端代码需要专门了解一些关于这个系统的概念,详见下文。(3) PBFT ordering service,拜占庭容错,当前处于开发之中,文档中的意思虽未明说,但意思明显是这种服务可以兼顾高效和容错。当前版本没有相关代码,不予讨论。这个模块是服务主体,包含多个子模块,如cutter,kafka,过滤器等。
Ledger - 账本。orderer服务必须允许客户端能够在排序过的块流中进行查询,因此orderer服务需要一个支持账本。有以下三种类型的账本,分别用于不同情况下的部署:(1) File ledger,文本账本,针对产品级别的项目部署使用,默认选择此类账本(下文也只讨论这类账本)。账本直接存储在文件系统上,通过轻量级LevelDB数据库以Index进行索引,以供客户端高效的检索指定的块。(2) RAM ledger,内存账本,可配置用于存储的内存的大小,即一切数据都存于内存中,针对测试,不容错,重启后将重置。(3) JSON ledger,JSON账本,即用JSON文本存储账本数据,也是针对测试环境的,特点在于简单明了,且可以容错,重启后不会重置。
Profiling - 监控orderer服务的模块,可供通过http(如浏览器)来监控orderer的情况。暂不讨论。
配置是orderer服务启动过程中所必须用到的,也直接影响了orderer如何服务。主要涉及到orderer.yaml,configtx.yaml两个配置文件。
orderer.yaml - 用于orderer服务自身。orderer.yaml的数据使用main.go中main
函数中的conf := config.Load()
被加载,形成了localconfig/config.go中的TopLevel对象conf(common/configtx/tool/localconfig/config.go中也有对应的一套TopLevel),过程稍显复杂,多个结构体一级一级的嵌套,但较方便的地方是取出来具体的某一项配置的值的变量与orderer.yaml中对应的配置key一样,如要取出orderer.yaml中GenesisFile项的值,则使用conf.General.GenesisFile
即可,且config.go中有一个默认的TopLevel对象defaults作为默认配置,可做参考。
configtx.yaml - 主要供orderer生成genesisblock作为orderer使用的链的第一块block。在main.go中initializeBootstrapChannel
函数中生成genesisblock时,genesisBlock = provisional.New(genesisconfig.Load(conf.General.GenesisProfile)).GenesisBlock()
(这个函数相当于Getting Started使用configtxgen手工生成的genesis.block,使用哪种方式生成gensisblock由orderer.yaml中的GenesisMethod项和启动orderer容器时所给的环境变量,即docker-compose-base.yaml中ORDERER_GENERAL_GENESISMETHOD项所决定,后者优先权应该大于前者),configtx.yaml被genesisconfig.Load(...)
加载,形成了ConfigEnvelope被写入genesisblock中。这个过程也是相等复杂,经历了多成结构的嵌套,具体参看图片gensisblock.png。基本的数据结构嵌套变化流程是:common/configtx/tool/localconfig/config.go中的TopLevel –> provisional/provisional.go中的bootstrapper(ConfigGroup) –> common/configtx/template.go中的Template –> Envelope(ConfigEnvelope) –> Block(genesisblock)。而具体哪些数据写入了gensisblock,可以求助于test函数将这个ConfigEnvelope打印出来。multichain/manager.go中的multiLedger对象是orderer服务直接使用的总管,所以初始化这个对象可以执行到orderer服务的各个子模块,自然也包含生成gensisblock这一步,所以在manager_test.go的TestManagerImpl
函数中,有初始化这个对象,可以以此测试。打印点设置在common/genesis/genesis.go的Block(...)
中的err = proto.Unmarshal(configEnv.ConfigUpdate, configUpdate)
下面,使用fmt.Printf("\033[43;36mconfigUpdate:%+v\033[0m\n",configUpdate)
以黄底绿字的形式打印。打印的数据已放入configupdate.txt稍作整理并上传至网盘。由打印的数据可知,写入gensisblock的配置数据主要是configtx.yaml中orderer部分的配置数据。
gprc服务 - orderer所基于的gprc服务AtomicBroadcast原型在protos/orderer/ab.proto中定义,对应生成的默认的客户端、服务端对象在ab.pb.go中,其中orderer所操作的服务端又单独实现在orderer/server.go中,peer点使用的客户端则基本沿用默认生成的客户端。server.go中的服务端对象实例server在main.go的main()
中由server := NewServer(manager, signer)
生成,使用ab.RegisterAtomicBroadcastServer(grpcServer.Server(), server)
进行了注册,随后grpcServer.Start()
启动起来。server也只是一个空架子,不做实事儿,其两个服务Broadcast、Deliver直接对应交由两个成员handlerImpl(orderer/common/broadcast/broadcast.go)和deliverServer(orderer/common/deliver/deliver.go)的Handle
处理。客户端的初始化,如peer chaincode和peer channel各自使用到的命令工厂ChaincodeCmdFactory/ChannelCmdFactory中使用到的BroadcastClient客户端,都会随着peer结点的初始化初始化。再如peer结点的gossip服务,所使用的Deliver客户端(core/deliverservice/deliverclient.go中的DefaultABCFactory
)也是默认生成的客户端,会随着gossip模块的初始化而初始化。
multiLedger总管 - 在orderer/multichain/manager.go中定义,是orderer直接使用的“总管家”,由其支配各个子模块。multiLedger包装chain集合,consenters集合,签名工具,账本生成工具,系统chain。在main.go的main()
中,manager := initializeMultiChainManager(conf, signer)
完成了multiLedger的初始化,初始化完成时,所有包含的子对象也相应的被初始化,所有orderer中现存的chain已经启动起来。这里的chain的概念比较模糊,既可以指账本本身,也可以指包含了账本的chainSupport,也可以指具体的处理消息的流程(如orderer/solo和orderer/kafka下各自实现的chain所执行的Enqueue
)。而multiLedger自身也人如其名,multi+Ledger,成了多条链的“总管家”。
chainSupport - 接口和实现均在orderer/multichain/chainsupport.go中定义,如同chaincode_support对chaincode一样,属于尽一切努力对chain操作提供支持的对象。既包含账本本身,也包含了账本用到的各种工具对象,如分割工具cutter,过滤工具filter,签名工具signer,最新配置在chain中的位置信息(lastConfig的值代表当前链中最新配置所在的block的编号,lastConfigSeq的值则代表当前链中最新配置消息自身的编号)等。chainSupport也实现了一些用于支持各个工具的小接口。这样,一些流程切换,数据流向的转变的工作,都交给了chainSupport来做,chainSupport也做一些杂活。比如A工具操作中需要调用B工具,则A包含的往往不是B本身,而是chainSupport,借chainSupport来使用B。
consenter - 分为solo和kafka两种类型,用于序列化生产(即各个peer点传送来的Envelope)出来的消息。solo在orderer/solo中实现,kafka在orderer/kafka中实现,两者也都是起到引导和配置的作用供chainSupport使用,真正干活的是solo/kafka封装进来的chain(orderer/solo/consensus.go)和chainImp(orderer/kafka/chain.go)。solo的chain是for循环 + select-case + chan组成的简单处理流程。kafka的chainImp则是一套使用第三方库github.com/Shopify/sarama实现的连接kafka服务端进行消息的订阅出版的方案,下文将详述。这里讲句题外话,solo和kafka都算不上真正的consenter,consenter意为同意者,与官方文档中的consensus mechanisms相对应,但是无论是solo和kafka都不具备容错性,因此也就谈不上是不是consenter了。这个模块之所以叫这个名字,估计是在等将来的SBFT。
chain的启动 - 这里chain指的是处理消息的线程。在orderer/multichain/manager.go中,当NewManagerImpl
初始化完毕一个chainSupport后,都会执行chain.start()
启动背后的chain处理消息的流程。以kafka为例,chain.start()
调用的是orderer/kafka/chain.go中chainImpl的Start()
,进而go startThread(chain)
启动了一个新线程,在startThread()
中:(1)chain.producer, err = setupProducerForChannel(...)
创建了kafka的生产者。(2)chain.parentConsumer, err = setupParentConsumerForChannel(...)
创建了消费者。(3)chain.channelConsumer, err = setupChannelConsumerForChannel(...)
分区消费者。(4)close(chain.startChan)
,chain.errorChan = make(chan struct{})分别开启处理peer结点通过Broadcast,Deliver发来的请求的开关(只有开启orderer才会开始处理peer发来的请求,这点在后续文章中会详解)。(5)chain.processMessagesToBlocks()
启动了for循环接收处理消息过程,该过程是用来接收kafka服务端(在orderer中就是kafka容器)序列化后返回给kafka客户端(在orderer中就是sarama)的消息流,然后依次分类处理。在processMessagesToBlocks()
中:(1)正常的kafka生产出来的消息,会从chain.channelConsumer.Messages()
这个通道中出来,根据不同的kafka消息类型,在switch-case
中分别使用processConnect
,processTimeToCut
,KafkaMessage_Regular
函数进行处理。
filter/committer - 过滤器/执行器,有几种,分散在orderer目录下,几种filter可以组装到一起形成一个过滤集合,orderer服务用此过滤集合(orderer/common/filter/filter.go中的RuleSet)中各个filter的Apply
函数对收到的消息进行过滤,比如有些类型的消息的一些字段不能为空,大小必须在配置的范围之内等。和filter对应的是一个提交对象committer,即一个消息通过了filter的Apply
,会返回一个committer,这个committer会在该消息写入账本之前执行Commit
操作,做一些事情。同时,committer的Isolated
的返回值标识着一条消息是否要单独成块,一些比较特殊的消息可能在不满足一定大小的情况下也要单独作为一个block,比如gensisblock。这么描述的话,一个个filter就有点儿模拟合同条款的味道了,一个filter集合,就是一个合同。而一个Envelope来到orderer中之后,会根据这个合同一条条的条款对照,如果符合了,提供执行一个对应的committer工具,以使Envelope能够拿到这个committer工具进一步做一些什么事情(这也是我们可以拓展的地方)。filter/committer接口在orderer/common/filter下定义,同时定义了三种基本的Apply
过滤的结果:Accept表示接受,Reject表示拒绝,Forward表示当前filter不清楚接受还是拒绝而需要进一步验证。filter有如下类型:(1)emptyRejectRule,验证不能为空的filter,orderer/common/filter/filter.go中定义。(2)sigFilter,验证签名的filter,orderer/common/sigfilter/sigfilter.go中定义。这里会使用公共签名策略(common/policies中定义)来验证进入orderer的Envelope中所携带的签名是否满足要求。(3)sizeFilter,验证大小的filter,orderer/common/sigfilter/sigfilter.go中定义。在configtx.yaml中Orderer配置中有许多关于大小多少的配置项,将在这个filter中验证。(4)systemChainFilter,系统链filter(orderer中系统链的概念先不说),orderer/multichain/systemchain.go中定义,作用在于验证一个Envelope是否是一个用于升级orderer配置的ConfigUpdate类型消息,若是,则会提供一个包含configtx工具的committer,在该Envelope写入账本前,执行committer,新建一条新配置的链。(5)configtxfilter,orderer/common/configtxfilter/filter.go中定义,作用与systemChainFilter类似,检测Envelope是否是一个HeaderType_CONFIG类型的消息,然后返回一个包含有configManager工具的committer供之后对消息做进一步操作。
blockcutter - 块分割工具,用于分割block,具体为orderer/common/blockcutter/blockcutter.go中定义的receiver。类似于工厂流水线的自动打包机,一条一条消息数据在流水线上被传送到cutter处,cutter按照configtx.yaml中的配置(主要是大小限制),把一条条消息打包成一批(一箱)消息,同时返回整理好的这批消息对应的committer集合,至此cutter的任务完成。后续的,每一批消息被当作一个block,执行完对应的committer集合后,被写入账本。在configtx.yaml中关于Orderer的配置项中,BatchSize部分规定了block的大小:MaxMessageCount(10)指定了block中最多存储的消息数量,AbsoluteMaxBytes(10 MB)指定了block最大的字节数,PreferredMaxBytes(512 KB)指定了一条消息的最优的最大字节数(blockcutter处理消息的过程中会努力使每一批消息尽量保持在这个值上)。根据这三个值,cutter在工作时(具体指blockcutter.go中的Ordered
函数):(1)若一个Envelope的数据大小(Payload+签名)大于PreferredMaxBytes时,无论当前缓存如何,立即Cut
;(2)若一个Envelope被要求单纯存储在一个block(即该消息对应的committer的Isolated()
返回为true),要立即Cut
;(3)若一个Envelope的大小加上blockcutter已有的消息的大小之和大于PreferredMaxBytes时,要立即Cut
;(4)若blockcutter当前缓存的消息数量大于MaxMessageCount了,要立即Cut
。(5)还有一个比较特殊的Cut
,由configtx.yaml中BatchTimeout配置项(默认2s)控制,当时间超过此值,chain启动的处理消息的流程中主动触发的Cut
(后续文章中会详解)。Cut
所做的工作,就是将当前缓存的消息和committer返回供blockcutter与当前处理的Envelope打包成一批或两批消息,然后清空缓存信息。在上述需要Cut
的情况中,只有(1)(2)会产生两批消息,且先是旧的消息(即blockcutter中之前缓存的消息)为一批,后是当前处理的最新的消息为一批。receiver中(如你设想的),filters是一个RuleSet,定义了过滤条件集合,均来自于orderer/multichain/chainsupport.go中的createStandardFilters
或createSystemChainFilters
(后者只是比前者多了一个systemChainFilter过滤对象,对看上一条(4)内容);pendingBatch是一个Envelope数组,用来缓存Envelope消息;pendingBatchSizeBytes来记录这些缓存的消息的大小,pendingCommitters是一个Committer数组,用来缓存每个Envelope对应的committer。
这里只讨论file形式的账本。orderer中使用的账本同peer结点中使用的一样,账本生成者BlockStoreProvider和账本自身BlockStore。只不过被稍微包装了一些,BlockStore被包装进了fileLedger(orderer/ledger/file/imp.go中),而fileLedger又是ReadWriter接口(orderer/ledger/ledger.go中)的实现。ReadWriter由Reader和Writer组成,也就是说,这可能是会拓展的部分,即将来在orderer中可能会实现只读账本,只写账本,读写账本的分类。fileLedger是同配置工具configResources一同封装到ledgerResources(orderer/multichain/manager.go)中,供chainSupport使用的。现阶段直接账本看作一个可写入可查询的数据库即可,详细的内部操作,将留在ledger主题文章中叙述。
与fileLedger相配合的是一个同文件中的fileLedgerIterator迭代器,由fileLedger的Iterator(startPosition)
生成,传入一个链上(实际是账本中)查询block开始迭代的位置赋值给迭代器的成员blockNumber,比如startPosition为0(即SeekPosition_Oldest),则说明从链的第一块block开始遍历迭代,再比如startPosition为SeekPosition_Newest,则说明从链的现存最新的一块block开始遍历迭代,其余的值均算为SeekPosition_Specified类型的位置,即任意指定链上的一个block块。任何查询fileLedger的行为,都是通过循环调用这个迭代器的Next()
进行的,每调用一次,blockNumber自增1。fileLedgerIterator迭代器有一个特征是Next()
会在迭代到还没有写入到账本block(即当前账本最新的block的接下来将要有的一个block)时阻塞等待,一直等待到该block被写入到账本中。这个阻塞控制涉及到两个chan,impl.go中的closedChan和fileLedger的成员signal。初始时closedChan为被close(closedChan)
掉。下面详述这个阻塞控制:
当迭代器当前遍历到的block序列号<=账本上当前最新的block序列号时(即ledger.Height()-1
),在Next()
中,会进入if i.blockNumber < i.ledger.Height()
分支(根据orderer对账本的操作,可知ledger.Height()
返回的是链上将要添加的下一个block的序列号),查询后返回。而当迭代器遍历到ledger.Height()
时,此时i.blockNumber == i.ledger.Height()
,Next()
会进入<-i.ledger.signal
等待。当一个新的block要被写入账本,则会调用fileLedger的Append(block)
,在Append(block)
中,把block写入账本成功之后,会调用一下close(fl.signal)
(此时Next()
的等待会结束再次进入if i.blockNumber < i.ledger.Height()
分支取block),然后紧接fl.signal = make(chan struct{})
再给signal创建一个chan(此时若再调用Next()
,仍会再次进入<-i.ledger.signal
等待)。
fileLedgerIterator迭代器的ReadyChan()
根据当前迭代器的实际情况,即情况1:当i.blockNumber > i.ledger.Height()-1
时,说明迭代器已经遍历到要查询账本的下一块还没有写入的block了,则返回signal;情况2:相反,则直接返回closedChan。这个控制会在Deliver服务处理客户端索要block时用到,即在orderer/common/deliver/deliver.go的Handle(...)
中,正常的会进入if seekInfo.Behavior == ab.SeekInfo_BLOCK_UNTIL_READY
分支进行select-case
选择,此时case <-erroredChan:
除了关闭Deliver时会来消息,erroredChan不会再来消息,只会等待case <-cursor.ReadyChan():
:若情况1,这里将等待signal,直到Append(block)
中写入新的block后close(fl.signal)
一下程序才会继续前进,在下文会调用到迭代器的cursor.Next()
;若情况2,由于closedChan是关闭的,程序将直接继续前进。
卡夫卡官方文档地址,http://kafka.apache.org/documentation/ 。这里非常粗线条的概述一下orderer/kafka中涉及到的地方。
1. kafka相关的概念
2. sarama
Sarama is an MIT-licensed Go client library for Apache Kafka,这是sarama库的README的第一句话,即是说,sarama是一个kafka的go客户端库。sarama使用的基本流程如下(所产生的对象均是客户端对象):
//1.创建一个kafka的综合配置对象,该对象可以配置生产者,消费者,网络等等等等。
brokerConfig := sarama.NewConfig()
//2.对配置对象逐一赋值,细节不叙,这一点是比较杂的一点。
brokerConfig.Producer.XXX = ...
brokerConfig.Consumer.XXX = ...
brokerConfig.Net.XXX = ...
brokerConfig.Metadata.XXX = ...
...
//3.根据配置创建生产者对象,其中brokers为字符串数组类型的broker的地址列表,
//在生成具体的生成者对象时,该对象已经根据brokers连接了kafka服务端。
//sarama的生产者对象有SyncProducer,AsyncProducer两种,即同步,异步两种。
//同步表现在出版一条kafka消息后会一直阻塞到收到出版成功的回复,异步则不存在阻塞。
//同步Producer
producer := sarama.NewSyncProducer(brokers, brokerConfig)
//异步Producer
producer := sarama.NewAsyncProducer(brokers, brokerConfig)
//4.根据配置创建消费者和分区消费者,参数与生产者一致。
//消费者是负责生成和管理(多个)分区消费者的对象,分区消费者是真正订阅消费消息的对象。
consumer := sarama.NewConsumer(brokers, brokerConfig)
//参数依次是哪个主题,哪个分区,从哪个offset开始(消费)
partitionConsumer := consumer.ConsumePartition(topic,partition,startFrom)
//5.生产者出版kafka消息,分区消费者订阅消费kafka消息
producer.SendMessage(&sarama.ProducerMessage{...})
msg_chan := partitionConsumer.Messages()
//从分区消费者chan中获取消息并使用
msg := <-msg_chan
...
//6.生产者,消费者关闭,为防止leak,必须进行此步操作。
producer.Close()
consumer.Close()
partitionConsumer.Close()
3. fabric中的kafka
fabric中使用kafka的基本目的是在多个peer结点同时向orderer服务发送消息时,能通过kafka形成一个串行的消息队列(队列中消息序号唯一,至此kafka的作用就算完成了),然后供cutter进行封装成一批批消息(即block),再将block写入orderer的ledger,也就形成了块链,最后供区域内各个peer结点中的leader获取orderer的ledger中的数据,然后由leader将数据在的区域中的各个peer结点间共享,形成了区块链。
fabric中关于sarama用到的关于kafka的配置集中在orderer.yaml中的Kafka配置区域,configtx.yaml中的Orderer部分中的Kafka部分。
sarama中主要的对象,生产者/消费者都封装在orderer/kafka/chain.go的chainImpl
对象中,而综合配置则封装在管理chainImpl
的在orderer/kafka/consenter.go的consenterImpl
对象中。
fabric中的kafka,具体来说是orderer中的kafka,是一个客户端,由sarama实现,通过网络连接的kafka服务端容器。sarama被包装进了orderer/kafka/chain.go中的chainImpl
中使用。chainImpl
成员中,producer即为生产者,parentConsumer为消费者(负责生成和管理分区消费者的),channelConsumer为分区消费者(实际消费消息的)。
官方文档的Bringing up a Kafka-based Ordering Service章节,介绍了如何启动一个以kafka服务为基础的orderer服务。还有就是,关于使用kafka,按官方文档的说法,将来肯定会被PBFT替换掉。在之后的orderer服务的讲述中,会把kafka服务端本身当作一个“暗盒”,即从消息进入到kafka服务端到从这里面出来的过程不会有所叙述,而还是把笔墨着于orderer服务自身。