fabric源码解析15——peer的gossip服务之散播

fabric源码解析15——peer的gossip服务之一条消息的散播旅行

旅行的前提

要理清一条消息在频道中的各个结点中如何散播的,重点在于三点:

  • 消息从何而来
  • 消息如何散播
  • 消息去往何方

需要说明的是,首先,这里消息指的是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命令搜索。

  • /fabric/core/deliverservice
  • /fabric/gossip
  • /fabric/protos/gossip

消息从何而来

一块块block消息由ordering服务序列化后,使用deliverservice服务的分发客户端发送给gossip服务器。即,消息直接来自于deliverservice模块。这里假设deliverservice模块会从ordering服务依次接收序号在11-20之间共10块的block,在传播过程中,这些数据在各个模块间传送,会变化或被封装成不同的消息类型,但均用M11,M12,…,M20表示。

deliverservice模块的初始化

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()启动了模块。

deliverservice模块的启动

StartDeliverForChannel()中,步骤如下:

  1. 在blockProviders中若属于chainID的BlocksProvider存在,则表明当前nodeA的deliverservice模块已经在运行(对看StopDeliverForChannel(),一旦nodeA停止模块,则把BlocksProvider从blockProviders中删除),则直接返回。
  2. 若不存在,调用newClient新建一个grpc客户端client。这个客户端原型为broadcastClient,在client.go中定义,实现的是/fabirc/protos/orderer/ab.pb.go中定义的AtomicBroadcast_DeliverClient这个Deliver服务的grpc流客户端接口。这里不直接使用ab.pb.go中的atomicBroadcastDeliverClient,自然是因为这个自动生成的结构不能满足功能的需要。而且可以臆测一下,Deliver服务的grpc流服务端应该实现在ordering服务中。
  3. 调用NewBlocksProvider(...)新建一个属于chainID的BlocksProvider对象,并把2中新建的client、deliverservice模块Config中的gossip服务器,签名对象CryptoSvc传给这个对象的各个成员,然后把这个对象放入blockProviders中。
  4. 新启一个goroutine,go d.blockProviders[chainID].DeliverBlocks(),执行3中新建的BlocksProvider对象的DeliverBlocks()服务。
  5. 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本地的账本中。

消息如何散播

  1. 序列号在11-20共10个gossipMsg进入nodeA的gossip服务器的Gossip(gossipMsg)中,先把gossipMsg包装成SignedGossipMessage类型的sMsg,然后使用MessageCryptoService对sMsg签名,使sMsg包含了nodeA的身份信息。
  2. 检查gossipMsg的Tag,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中存了但是压根没找有在哪里使用。
  3. 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块消息。
  4. 倒钩发送函数cb即为gossip服务的sendGossipBatch(...),简单的将接收的10条消息数组重新置换成SignedGossipMessage格式的消息数组msgs2Gossip,然后直接调用gossipBatch(msgs2Gossip)发送消息数组。
  5. gossipBatch(msgs[])可处理多种类型数据,这里我们还是只关注block块消息。调用partitionMessages(isABlock, msgs)函数,将消息数组中的DataMessage类型SignedGossipMessage消息过滤出来(对看第3点)放入blocks,然后将blocks和一个过滤器filter传入gossipInChan(...),这里的这个过滤器filter将在下文单独谈论。
  6. 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。
  7. comm模块就是nodeA向其他结点传播消息的grpc通信的模块了,既是grpc流客户端,也是grpc流服务端。站在nodeA的角度,在发送消息时,使用的是流客户端。在Send()中,for循环遍历结点清单中的每个结点,这里只有nodeB,针对nodeB启动一个goroutine来调用c.sendToEndpoint(peer, msg)
  8. 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为例
  9. nodeB的comm模块作为grpc服务器端,在GossipStream()函数中接收到nodeA发来的M11。也即从这一步起,就要站在nodeB的角度来看代码。nodeB此刻作为服务器角色,调用connStore.onConnected()获取服务端流与nodeA的连接conn后,启动了conn的serviceConnection(),即新启了goroutine来用readFromStream接收nodeA发来的M11,然后抽取M11中的SignedGossipMessage类型内容作为msg后经由msgChan发给conn.handler(msg)这个conn中的“倒钩”成员来处理。
  10. conn的handler()是在上一步的GossipStream()中获取conn后被赋值的,该“倒钩”成员所做的是:把接收的SignedGossipMessage类型的M11封装成ReceivedMessageImpl消息,交由msgPublisher模块的DeMultiplex()进行出版
  11. 在nodeB初始化自己的gossip服务对象实例时,已经在gossip/gossip_impl.go的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)处理。
  12. g.handleMessage(m)中,先重新抽取ReceivedMessageImpl类型的M11中的SignedGossipMessage作为消息msg进行一系列if判断,调用g.chanState.lookupChannelForMsg(m)获取chanState模块中chainID对应的channel对象gc,经过一系列判断,会调用gc.HandleMessage(m)对接收的原消息M11进行处理。
  13. ReceivedMessageImpl格式的M11再次进入了channel模块,在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,就不必再向其他结点索要了)。
  14. state模块在初始化时,就向msgPublisher模块注册订阅了专用于接收以DataMessage为Content的SignedGossipMessage消息的通道gossipChan,并启动一个goroutine来执行listen(),接收gossipChan中来的消息。因此当第13步(3)中出版M11时,state模块会通过listen()从gossipChan通道中接收到M11并执行go s.queueNewMessage(msg)处理。
  15. 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。而上述的两个特征,会由SelectPeersGetRandomIndices体现。

传播过程第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.EligibleForChannelgc.IsMemberInChang.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步,可知,消息在散播进一个结点后,一方面会存储到自己的结点的账本和一些本地模块中,另一方面会继续在网络中散播。至于散播何时停止,且往下看。

旅行足迹图

abcd.png

如图,A,B,C,D四个结点,属于同一频道同一组织。这里更粗线条的模拟一下传播的过程,初始条件:

  • A-B,B-C,B-D,C-D均为一个单位距离,A-C,A-D均为两个单位距离。
  • 四个结点处理消息效率一致,每100s,才能向一个单位距离范围的结点传播一条消息。
  • 每隔400s,每个结点的stateInfoMsgStore清理一次名单。对于存在时间>=400s的名单,将废止。
  • 只传播一条消息M,是高度为2的block。时间用Tn表示,如T100表示时间在100s时,T110表示时间在110s时。每个结点发送自身的状态信息用S(i,h,t)表示,i代表身份,h代表当前账本的高度,t代表时间戳。每个结点的账本高度当前均为1。每个结点需花费10s,才能将M存储到自身的账本中。
  • 此刻从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)。

    1. T10时,A将M提交到自己的账本中,发送了S(A,2,10)给B,随即将M传播给B。
    2. T110时,B收到S(A,2,10)并更新自己的名单,同时收到M,立即传播给A,C,D。T120时,B将M提交到自己的账本中,向A,C,D发送S(B,2,120)。
    3. T200时,在T0前100s就开始传播S的A-C,A-D两条2个单位距离线路的双方也都收到了各自的S,即A收到S(C,1,-100),S(D,1,-100);C收到S(A,1,-100);D收到S(A,1,-100),此刻每个结点的名单中都包含另外三个结点。T210时,A,C,D收到B发送的M,A已有M,未作进一步处理,C和D立即向其余三个结点传播M。T220时,C和D将M提交到自己的账本中,向其余三个结点分别发送了S(C,2,220),S(D,2,220),同时,A,C,D也收到B发送来的S(B,2,120)。 T220时,名单情况为:A中有S(B,2,120),S(C,1,-100),S(D,1,-100);B中有S(A,2,10),S(C,1,0),S(D,1,0);C中有S(A,1,-100),S(B,2,120),S(D,1,0);D中有S(A,1,-100),S(B,2,120),S(C,1,0)。
    4. T310时,A,B,D收到C发来的M,因为都有了,未作进一步处理;A,B,C都收到D发来的M,因为都有了,未作进一步处理。T320时,A,B,D收到C发来的S(C,2,220);A,B,C收到D发来的S(D,2,220),各自更新自己的名单。T320时,名单情况为:A中有S(B,2,120),S(C,2,220),S(D,2,220);B中有S(A,2,10),S(C,2,220),S(D,2,220);C中有S(A,1,-100),S(B,2,120),S(D,2,220);D中有S(A,1,-100),S(B,2,120),S(C,2,220)。
    5. T400时,每个结点的stateInfoMsgStore清理一次名单,C,D中的S(A,1,-100)将被清除。至此M在四个结点间散播过程终止。

从这里可以看出用文字描述散播过程还是相当费力的,本想画一个类似state模块那样的通信流程图的,但发现很难画的清晰。上述过程还是最简单最简单的情况,但是也是可以看出,对stateInfoMsgStore清理名单的周期进行一定量的设置,每个结点所持有的散播名单都会保持在一定范围内。而且目前总感觉自己对gossip这个模块还隔着一层,还有一些深层的东西没挖掘,理解出来。由于字符化的表达过多,要是有错误,还请指出。

猜你喜欢

转载自blog.csdn.net/idsuf698987/article/details/77948977
今日推荐