要理清一条消息在频道中的各个结点中如何散播的,重点在于三点:
需要说明的是,首先,这里消息指的是ordering服务经deliverservice服务发送给gossip服务器后在众结点间散播的block块消息(即把block作为payload封装到DataMessage类型的GossipMessage),而gossip服务器中处理的其他类型的消息,如state消息,关系消息等,都具有辅助性质,即都是为了block块消息能够在众结点间散播并达到最终一致所服务的。其次,预设的情景是:fabric网络中存在一个ID为chainID的频道,该频道包含一个名为orgID的组织和一个名为ordererID的ordering服务结点,orgID组织中包含ID为nodeA,nodeB,nodeC,nodeD四个结点,每个结点的gossip服务器都已初始化,nodeA被指定或暂时选举为leader。留意这些预设对象的名字,下文中将直接使用。再者,消息传播基于的grpc服务在下文所述的deliverservice模块初始化中应该还尚未与服务器端建立连接,这里只是假设已连接。最后,在讲述消息散播的过程中,重点是消息如何散播,由于这个过程还是比较曲折的,为了不冗杂且把注意力放在流程展现上,所以描述中文字不可能过细,函数所在文件也不可能一一列出,所涉及的函数基本集中在如下目录中。后文中若非绝对路径,则均以下列所提到的路径为基础,读者若找不到函数所在文件或文件所在目录,可与文章《fabirc源码解析14》中相应模块对看,或自行用grep,locate命令搜索。
一块块block消息由ordering服务序列化后,使用deliverservice服务的分发客户端发送给gossip服务器。即,消息直接来自于deliverservice模块。这里假设deliverservice模块会从ordering服务依次接收序号在11-20之间共10块的block,在传播过程中,这些数据在各个模块间传送,会变化或被封装成不同的消息类型,但均用M11,M12,…,M20表示。
deliverservice模块代码集中在deliverservice目录下,原型为deliverServiceImpl,在deliveryclient.go中定义,利用成员blockProviders提供对每一个频道BlocksProvider对象的管理。BlocksProvider对象利用grpc客户端从ordererID结点出接收消息后使用gossip服务器开始传送消息,原型为blocksProviderImpl,在blocksprovider/blocksprovider.go中定义。
因为只有leader结点才会启动该模块,因此以nodeA结点为例。在start.go的serve
中,InitGossipService()
实例化了gossip服务器,但此时并未初始化deliverservice模块,直到后文的Initialize()
才间接在/fabric/core/peer/peer.go的createChain()
中调用service.GetGossipService().InitializeChannel()
将deliverservice模块初始化:在InitializeChannel()
中,调用g.deliveryFactory.Service(...)
将gossip服务器实例和指定的ordererID的IP地址等封入配置Config后传入deliverservice模块中,然后调用StartDeliverForChannel()
启动了模块。
在StartDeliverForChannel()
中,步骤如下:
StopDeliverForChannel()
,一旦nodeA停止模块,则把BlocksProvider从blockProviders中删除),则直接返回。newClient
新建一个grpc客户端client。这个客户端原型为broadcastClient,在client.go中定义,实现的是/fabirc/protos/orderer/ab.pb.go中定义的AtomicBroadcast_DeliverClient这个Deliver服务的grpc流客户端接口。这里不直接使用ab.pb.go中的atomicBroadcastDeliverClient,自然是因为这个自动生成的结构不能满足功能的需要。而且可以臆测一下,Deliver服务的grpc流服务端应该实现在ordering服务中。NewBlocksProvider(...)
新建一个属于chainID的BlocksProvider对象,并把2中新建的client、deliverservice模块Config中的gossip服务器,签名对象CryptoSvc传给这个对象的各个成员,然后把这个对象放入blockProviders中。go d.blockProviders[chainID].DeliverBlocks()
,执行3中新建的BlocksProvider对象的DeliverBlocks()
服务。DeliverBlocks()
就是实际办事儿的函数,在这个函数中,循环使用client客户端从ordering结点接收11块block消息,msg, err := b.client.Recv()
,然后使用switch-case根据消息的类型分别处理每块消息。这里只关注DeliverResponse_Block类型的消息,即我们要传播的原始的block块数据,在此分支中,先调用createPayload()
将block数据包装成可存储在本地账本的payload,再调用createGossipMsg()
将payload包装成可用于传播的DataMessage类型的GossipMessage消息gossipMsg(gossipMsg的Tag值为CHAN_AND_ORG),然后调用gossip服务器的AddPayload()
直接将payload存储在本地的账本中并更新chanState模块中对应chainID的channel对象的状态(此状态指channel实例成员stateInfoMsg,包含当前从payload抽取的高度和时间戳),再调用gossip服务器的Gossip()
将gossipMsg散播出去。而gossipMsg被传播到其他结点,比如nodeB后,目的也是把gossipMsg中包含的payload抽取出来后存储到nodeB本地的账本中。Gossip(gossipMsg)
中,先把gossipMsg包装成SignedGossipMessage类型的sMsg,然后使用MessageCryptoService对sMsg签名,使sMsg包含了nodeA的身份信息。if msg.IsChannelRestricted()
,若gossipMsg指定可以在频道范围内散播,则nodeA在chanState模块中使用chainID对应的channel对象的AddToMsgStore(sMsg)
函数,将sMsg在blockMsgStore存储一份,也在blocksPuller中的itemID2Msg以PKI-ID为key存储了sMsg,并把sMsg的序号在engine中的state中存储一份。这里提一句,在blocksPuller中存sMsg和sMsg的序号是为了pull消息的过程中使用,但是在blockMsgStore存储一份是为了什么目前笔者还没搞清楚,因为事实上只见往blockMsgStore中存了但是压根没找有在哪里使用。g.emitter.Add(sMsg)
,将sMsg包装成batchedMessage后添加到emitter模块的buff中,准备发送。我们知道emitter是一批批发送的,指定一批大小的burstSize的值默认为10,当序号为20的sMsg被Add进emitter后,就会触发emitter模块的emit()
函数。emit()
将现存的消息,即序列号为11-20的消息,抽取出data放入“发射数组”msgs2beEmitted,然后先调用倒钩发送函数cb(msgs2beEmitted)
,再调用decrementCounters()
清除emitter模块中剩余发送次数为0的消息,因为将sMsg包装成batchedMessage时所给的发送次数默认为1,因此这发送过的10条block消息都会从buff中删除。这里需要说明的是,在同一时段,emitter模块不一定只收到block块数据,也会收到其他类型的、其他频道ID的消息(比如channel模块会通过它的适配器往emitter中Add消息),而因为我们的关注点在block块消息,所以我们这里只是假设emitter只是清一色的接收到了11-20这10个属于chainID的block块消息。sendGossipBatch(...)
,简单的将接收的10条消息数组重新置换成SignedGossipMessage格式的消息数组msgs2Gossip,然后直接调用gossipBatch(msgs2Gossip)
发送消息数组。gossipBatch(msgs[])
可处理多种类型数据,这里我们还是只关注block块消息。调用partitionMessages(isABlock, msgs)
函数,将消息数组中的DataMessage类型SignedGossipMessage消息过滤出来(对看第3点)放入blocks,然后将blocks和一个过滤器filter传入gossipInChan(...)
,这里的这个过滤器filter将在下文单独谈论。gossipInChan(...)
中,(1)调用extractChannels(messages)
将消息中所有的频道ID抽取出来放入totalChannels,然后用第一层for循环遍历totalChannels,针对每个不同频道将属于各自频道的消息在频道内传播,这里10条消息中包含频道消息都是chainID,因此第一层for循环唯一一次循环就是针对chainID的。(2)在第一层循环for中,再次用partitionMessages
来过滤选出属于chainID的消息放入messagesOfChannel(这里是10条消息都是属于chainID的),这个是最终要发送的消息清单。(3)接着获取chanState模块管理的对应chainID的channel模块gc供下两步使用。(4)再利用discovery模块获取当前chainID频道中所有活着结点membership(这里就是nodeB,nodeC,nodeD三个结点)。(5)接着调用过滤器filter模块的SelectPeers(...)
从membership中随机筛选出3个结点集合peers2Send,这里原代码的逻辑会把nodeB,nodeC,nodeD三个结点都会被筛选出来,但为了体现gossip散播的过程,虽然三个结点都满足条件但只随机选出一个结点,假设为nodeB,这个是最终要发送的结点清单。清单的原型是RemotePeer,包含一个结点的Endpoint和PKIID(6)使用第二层for循环遍历消息清单中的所有消息(即序列号11-20的block块消息),依次调用comm模块的g.comm.Send(msg, peers2Send...)
,向结点清单发送M11,M12,…,M20。Send()
中,for循环遍历结点清单中的每个结点,这里只有nodeB,针对nodeB启动一个goroutine来调用c.sendToEndpoint(peer, msg)
。c.sendToEndpoint(peer, msg)
中,参数peer值为nodeB时,nodeA先调用connStore模块的connStore.getConnection(peer)
获取当前nodeB的grpc连接conn,当这个连接不存在时会进行创建,创建的时候会同时运行这个连接的读写函数。conn即nodeA与nodeB间的连接,然后调用conn的conn.send(msg, disConnectOnErr)
将消息封装成msgSending类型后经conn的发送通道outBuff从nodeA发给nodeB。经过第6步中(6)的第二层循环,将M11,M12,…,M20最终都会发送给nodeB。以下步骤只有M11为例。GossipStream()
函数中接收到nodeA发来的M11。也即从这一步起,就要站在nodeB的角度来看代码。nodeB此刻作为服务器角色,调用connStore.onConnected()
获取服务端流与nodeA的连接conn后,启动了conn的serviceConnection()
,即新启了goroutine来用readFromStream
接收nodeA发来的M11,然后抽取M11中的SignedGossipMessage类型内容作为msg后经由msgChan发给conn.handler(msg)
这个conn中的“倒钩”成员来处理。handler()
是在上一步的GossipStream()
中获取conn后被赋值的,该“倒钩”成员所做的是:把接收的SignedGossipMessage类型的M11封装成ReceivedMessageImpl消息,交由msgPublisher模块的DeMultiplex()
进行出版。start()
中调用incMsgs := g.comm.Accept(msgSelector)
在msgPublisher模块中注册订阅了专用于接收ReceivedMessageImpl类型消息的频道incMsgs,然后又调用了go g.acceptMessages(incMsgs)
新启了一个goroutine来接收incMsgs这个通道的消息。第10步中DeMultiplex()
出版的M11会通过incMsgs这个通道发送到了acceptMessages(incMsgs)
中,一旦收到M11,会交由g.handleMessage(msg)
处理。g.handleMessage(m)
中,先重新抽取ReceivedMessageImpl类型的M11中的SignedGossipMessage作为消息msg进行一系列if判断,调用g.chanState.lookupChannelForMsg(m)
获取chanState模块中chainID对应的channel对象gc,经过一系列判断,会调用gc.HandleMessage(m)
对接收的原消息M11进行处理。HandleMessage()
中,先从ReceivedMessageImpl类型的M11中抽取出SignedGossipMessage作为消息m,在m是DataMessage的前提下,m会进入if m.IsDataMsg()
分支,进行如下处理:(1)gc.blockMsgStore.Add()
,将M11存储到blockMsgStore中,如果添加成功,则继续,否则直接退出。(2)gc.Gossip()
,从nodeB出发,从第1步开始,继续在网络中散播M11。(3)gc.DeMultiplex(m),出版M11,供本地订阅这接收。(4)blocksPuller.Add()
,添加到blocksPuller,供bocksPuller在pull机制中运作(blocksPuller中有了M11,就不必再向其他结点索要了)。listen()
,接收gossipChan中来的消息。因此当第13步(3)中出版M11时,state模块会通过listen()
从gossipChan通道中接收到M11并执行go s.queueNewMessage(msg)
处理。queueNewMessage(msg)
中,先M11中抽取出payload,然后Push进payload。然后会被payloads弹出来后交由commitBlock
,同样,也是提交到nodeB自身的账本中后,更新nodeB自身的channel对象的stateInfoMsg,再次触发nodeB的channel的publishStateInfo
,向其他结点,包括nodeA,发送StateInfo消息(用于向这些结点报备自己的身份,可结合下文理解报备的意思)。我们做过比喻,gossip算法类似于办公室八卦和疫情传染,因此一个结点向另一些结点散播消息,这个另一些在选择上有两个特征:(1)数量不定。(2)随机并就近选择。
gossip在选择结点时使用的是filter/filter.go,即filter模块。主力函数是SelectPeers
,辅助函数是CombineRoutingFilters
,和util下的GetRandomIndices
。而上述的两个特征,会由SelectPeers
和GetRandomIndices
体现。
传播过程第5步中所提及的过滤器filter,指下面这个调用g.gossipInChan
的第二个参数:
//在gossip/gossip_impl.go中的gossipBatch函数中
g.gossipInChan(
blocks,
func(gc channel.GossipChannel) filter.RoutingFilter {
return filter.CombineRoutingFilters(
gc.EligibleForChannel,
gc.IsMemberInChan,
g.isInMyorg)
}
)
这个过滤器filter是在gossipInChan
中散播block时,作为所调用的filter.SelectPeers
的第3个参数,被用于筛选适合的结点集合peers2Send。上面代码中展示的筛选条件很清晰,有三条:即一个NetworkMember形式的结点身份传入gc.EligibleForChannel
,gc.IsMemberInChan
,g.isInMyorg
验证后都返回为true,那么这个结点才会被选中。撇开后两个条件不谈,单说gc.EligibleForChannel
,该条件验证了一个结点的PKIID是否存在于nodeA的channel对象(指chanState模块所管理的对应chainID的channel对象,下同)成员stateInfoMsgStore中。这个成员存储结点间发送的StateInfo消息和消息中携带的结点身份信息。也就是说,nodeA此刻传播block时,只有stateInfoMsgStore中存在的结点才会被筛选出来。而一个结点的身份信息想出现在nodeA的stateInfoMsgStore中,必须在nodeA开始筛选结点之前,就把自己的StateInfo消息通过publishStateInfo
发送给nodeA(publishStateInfo
是channel模块周期性执行的函数),而publishStateInfo
也是像上述过程那样一步步散播StateInfo消息。说到底,散播使用的是comm模块实现的grpc网络传输服务,而只有与nodeA近的结点,才能更快的通过网络将自己的StateInfo消息散播给nodeA,如此的话,当nodeA在散播block时,能筛选出的都是离自己特别近或者传输效率特别高的结点。另外,stateInfoMsgStore存储的身份会定时的清理,实际上是每隔400s,即清理MessageStore,也用callback同步清理MembershipStore,这点可以从NewMessageStoreExpirable
的实现看出。这么做是因为,一个结点,如nodeA,若不实施定时清除其他结点发来的StateInfo消息,经过一段时间后,nodeA也会接收到距离较远的结点发来的StateInfo消息并记录在stateInfoMsgStore中,这样当nodeA筛选结点发送block时,这些较远的结点也有可能被选中,这就违法了就近的原则。
每个结点,如nodeB,其state模块在初始化时都会调用UpdateChannelMetadata
来更新(虚假更新,因为给的StateInfo消息中的高度是现有高度-1)一下自己channel对象成员stateInfoMsg,并置shouldGossipStateInfo为1,目的就在于驱动publishStateInfo
传播一次这条虚假的stateInfoMsg,以让距自己比较近的结点,如nodeA的stateInfoMsgStore存储到自己的身份信息,进而在nodeA传播block时自己能在传播的范围之内。
关于虚假更新,这个对看文章14中在讲state模块处所遗留的高度-1的疑问,这里解释为:可能就是进行一个虚假更新,只是为了让距自己比较近的结点存储到自己的身份信息,在之后传播消息的时候能算自己一份。其实只要这个虚假更新所更新的高度比现有高度低就行,即-2,-3其实也行,但是因为一个结点最低的高度就是1(即genesis块),也因为高度没有负数一说,所有这里给的是-1。不过这个好像也不太对,因为既然是虚假更新,那为什么不直接给当前的高度呢?
以上就是gossip算法就近特征的叙述。
随机特征比较好理解,由GetRandomIndices
实现,在filter.SelectPeers
中被调用。
数量不定特征在gossip中不明显,因为指定数量的是filter.SelectPeers
的第1个参数,而这个参数在实现中又是由配置指定的。但是当符合条件的结点少于指定的个数时,则数量不定,比如配置指定的是每次向10个结点散播,但筛选出来的只有5个,那只能向这5个结点散播,如果筛选出来8个,则只向这8个结点散播。
state模块订阅了DataMessage类型数据,而且是gossipMessage或signedgossipmessage类型的
通过上述所讲的散播步骤的第14、15步,可知,消息在散播进一个结点后,一方面会存储到自己的结点的账本和一些本地模块中,另一方面会继续在网络中散播。至于散播何时停止,且往下看。
如图,A,B,C,D四个结点,属于同一频道同一组织。这里更粗线条的模拟一下传播的过程,初始条件:
此刻从0s开始计时,A为leader,开始接收deliverservice来的消息并开始传播。此前已花费了100s时间供四个结点同时初始化,每个结点此刻中只存在一个单位距离范围的结点名单,即:A中有S(B,1,0);B中有S(A,1,0),S(C,1,0),S(D,1,0);C中有S(B,1,0),S(D,1,0);D中有S(B,1,0),S(C,1,0)。
从这里可以看出用文字描述散播过程还是相当费力的,本想画一个类似state模块那样的通信流程图的,但发现很难画的清晰。上述过程还是最简单最简单的情况,但是也是可以看出,对stateInfoMsgStore清理名单的周期进行一定量的设置,每个结点所持有的散播名单都会保持在一定范围内。而且目前总感觉自己对gossip这个模块还隔着一层,还有一些深层的东西没挖掘,理解出来。由于字符化的表达过多,要是有错误,还请指出。