fabric源码解析14——peer的gossip服务之初始化

fabric源码解析14——peer的gossip服务之初始化

自《fabirc源码解析11——peer的Admin和Endorser服务》之后,饶了一个圈,去讲了peer的msp和bccsp服务,现在我们再回到peer的start.go中的serve函数,继续往下讲。在11中讲了Admin和Endorser两个服务的注册,接着,serve开始初始化gossip服务service.InitGossipService(...)

gossip本意是绯闻,流言蜚语,闲谈聊天的意思,上大学的时候看过一部美剧叫《gossip girls》。而在这里,gossip代表了一种可最终达到一致的算法,其灵感来源于办公室八卦:当一个八卦在办公室出现时,在一定阶段内通过散播(dissemination),所有人最终都会知道这个八卦。这样就比较容易理解了,比如peer经过背书签名,将一个有效的交易最终提交。这份交易的写集合是A减100,B加100,因为网络中所有的结点都存有一份账本,因此该交易提交后,在有限的时间内,每个分布在网络中的结点中的账本都会应用这个交易,将自己的账本中的A减去100,B加上100。或者,有新结点加入网络中后,经过一定的时间,该新结点也会存储和其他结点一样的账本数据。这里需要注意是,最终一致的另外的含义就是,不保证同时达到一致,也就是在某一指定的时刻,每个结点的账本(也就是状态)不保证一致。同时,gossip不要求节点知道所有其他节点,因此具有去中心化的特性,节点之间完全对等,不需要任何的中心节点,这点也是区域链的显著特征。gossip是区域链相当核心的模块了,值得好好学习一下。

gossip中有三种基本的操作,如下:

  • push - A节点将数据(key,value,version)及对应的版本号推送给B节点,B节点更新A中比自己新的数据
  • pull - A仅将数据key,version推送给B,B将本地比A新的数据(Key,value,version)推送给A,A更新本地
  • push/pull - 与pull类似,只是多了一步,A再将本地比B新的数据推送给B,B更新本地

gossip数据传播协议

该章节内容翻译子官方文档

Fabric通过划分各个执行交易的(背书和提交)peer和ordering结点之间的工作负载来优化区域链网络的执行,安全和可测量性。网络操作的解耦要求一个安全的,可信赖的,可测量的数据传播协议,以保证数据的完整性和机密性。为了达到这些要求,Fabric实现了一个gossip数据传播协议。

gossip协议

peer结点“撬动”gossip以可测量的方式去广播(broadcast)账本和频道数据。gossip消息传送是是持续的,而且在频道中的每个peer不间断的从其它的peer那里接收当前的和一贯的(也就是格式等前后一致)账本数据(ledger data)。每个传播的消息都被签名过,因此“拜占庭的参与者”发送虚假的消息会很容易被识别,把消息发到消息不想到达的目标的分发行为会被阻止。peer会被延迟,网络参与者或者其他造成block丢失的原因所影响,但这些丢失block的peer最终将通过联系持有这些丢失block的peer异步更新到当前账本状态。

以gossip为基础的数据传播协议在Fabric网络上执行三个基础的功能:

  1. 通过持续性的识别有效成员peer和检测那些已经下线的peer,管理peer的发现(discovery)和频道成员关系。
  2. 在频道上所有的peer之间传播账本数据。任何持有与频道其他peer结点不同步的数据的peer识别丢失的block并通过拷贝正确的数据来同步自身。
  3. 通过允许账本数据以peer点对peer点(peer-to-peer)状态传输更新的方式,提高新加入网络的peer结点的同步速度。

以gossip为基础的广播操作是通过peer从频道中其他peer中接收消息,然后把这些消息传送到频道上一定数量随机选择的peer结点,这个数量是可配置常量。peer结点也能运用一个pull机制,而不是等待一个消息的投递。这个循环重复着,伴随着频道成员关系的结果,账本和状态信息持续保持最新且同步。对于新block的传播,在频道中的领导peer(the leader peer)从ordering服务pull数据并初始化gossip到各个peer的传播。

gossip消息传送

在线的peer结点通过持续的广播“alive”信息来(向leader或其他结点)指示其自身的有效性,每条消息中都包含PKI_ID(the public key infrastructure ID)和发送者的签名。每个peer结点也通过收集这些“alive”消息,来维护自身的频道成员关系(channel membership)。如果没有任何一个peer接收到某一特定的peer的“alive”消息,则这个“dead”peer最终会从频道成员关系中被清除。因为“alive”消息都是加密签名了的,所以恶意的peer会因缺少由root CA认证的签名匙(signing key)而不可能冒充其他正确的peer。

除了自动传输接收的消息(即散播dissemination),一个状态调节进程(state reconciliation process)会通过每个频道上的众多peer结点来与世界状态(world state)同步。每个peer持续性的从频道上的其他peer那里pull来block数据,目的在于,如果(通过与自己的block数据对比)存在差异则修复自身的状态。因为固定的连接(fixed connectivity)不被要求去维护以gossip为基础的数据散播,因此这个进程会可靠的提供私密的和完整的数据到共享的账本,同时包括了对错误结点的容错度。(这里的固定的连接应该这么理解:在网络中没有发生变化的结点集合,比如A,B,C,D四个结点一直没有发生变化,因而四个结点之间的关系也不会发生变化,因此这四个结点之间就不需要去进行gossip散播消息数据。比如一个新加入的E点是与D点发生关系,则只需要D去向E散播消息,而A,B,C,D四者之间仍是不需要互相进行gossip散播的。)

因为多个频道之间是被相互隔离的,所有在一个频道上的peer点不能向其他频道发送消息或分享信息。虽然任一peer都可以属于多个频道,但是依照应用的消息线路选择策略(message routing policies),分配的消息传送禁止把block数据散播到其他频道的peer结点,这里的消息线路选择策略是以peer的频道订阅为基础的。(关于频道订阅,参考出版-订阅消息系统,即一个peer能够接收一个频道中的消息,必须先订阅这个频道的消息。)

注意:

  1. 点对点(point-to-point)消息的安全性由peer的TLS层来处理,不需要签名。peer结点凭借它们自身的证书获得认证,这些证书由一个CA分配。虽然TLS证书也被使用,但是在gossip层是该peer点的证书被验证授权(而不是TLS的证书)。账本的block由ordering服务签名,然后投递到频道中的leader peer。
  2. 认证是由peer的MSP对象管理的。当一个peer第一次连接到频道上,TLS会话(session)同成员身份绑定。这主要是使用在网络和频道中的成员关系去认证每个与新的peer发生的连接。

gossip源码结构

  • /fabric/gossip/
    • api - 消息加密服务接口,与peer衔接的接口
      • crypto.go - MessageCryptoService接口定义,用于消息加密
      • channel.go - SecurityAdvisor接口定义,用于安全辅助
    • comm - Comm模块,供gsssip调用,实现点对点的通信
    • common - 公用函数,定义,结构体
    • filter - 过滤模块,用于选择一个信息是否应该发送给一个网络成员
    • discovery - 发现模块,用于发现网络中的有效结点,供gossip调用
    • identity - 身份映射模块,用于PKI-ID与identity之间的映射,供service调用
    • election - 选举模块,用于选举领导peer,供service调用
    • state - state模块,供service调用
    • util - 公用工具文件夹,提供工具函数
    • gossip - 定义了gossip接口,实现goosip服务,供integration调用
      • algo - 算法,PullEngine对象,供pull调用
      • pull - Mediator对象,供channel调用
      • msgstore - MessageStore对象,供channel调用
      • channel - GossipChannel对象,供channelState实例管理
      • chanstate.go - channelState对象,管理GossipChannel,供gossip实例调用
      • gossip.go - 接口定义
      • gossipimpl.go - 接口实现
    • integration - 整合实现在获取配置基础上的生成连入grpc的gossip实例的功能,供service调用
    • service - gossip服务器,封装了gossip服务,状态,分发模块等,与核心代码衔接
  • —————————————————————–
  • /fabric/peer/gossip/
    • mcs.go - MessageCryptoService接口实现
    • sa.go - SecurityAdvisor接口的实现
  • —————————————————————–
  • /fabric/protos/gossip/ - 原型
    • message.pb.go/message.proto - 定义gossip处理的消息的原型,grpc服务原型
    • extensions.go - gossip原型方法的拓展,用于辅助gossip服务处理消息
  • —————————————————————–
  • /fabric/core/deliverservice/ - 与gossip服务衔接的核心分发消息的代码

由上述结构可知:

  1. gossip实例的专用生成函数供integration调用,integration供service调用,service供peer node start命令调用。

  2. peer程序 –> proto实现的MessageCryptoService接口 –> gossip –> service模块实例 –> fabric核心代码 –> fabric其他模块。

  3. fabric的gossip服务有多个主体和模块组成,如MessageCryptoService,discovery,filter,Gossip,GossipService。

  4. deliverservice中是与gossip服务相关的核心代码,提供核心的消息分发客户端等供ordering等服务,从而也就衔接了gossip服务与ordering等服务。但是消息的散播,最终还是回归到gossip下,使用gossip服务的功能代码。

fabric中不直接使用gossip服务(/fabric/gossip/gossip/gossip_impl.go中定义的gossipServiceImpl),而是使用gossip服务器(/fabric/gossip/service/gossip_service.go中定义的gossipServiceImpl),注意gossip服务和gossip服务器是两个不同的对象,后者管理并拥有前者,并为peer提供调用的窗口。在peer node start命令的serve函数中,service.InitGossipService(...)初始化了gossip服务器,初始化后这个实例被存储在/fabric/gossip/service/gossip_service.go中的gossipServiceInstance变量中。同时,gossip服务器管理着一个gossip服务作为其成员,此成员也被初始化。接下来分别详述gossip服务和gossip服务器两个对象的初始化。

gossip服务的初始化

//gossip接口,在fabric/gossip/gossip/gossip.go中定义
type Gossip interface {
    // 发送一个消息到远程的各个peer
    Send(msg *proto.GossipMessage, peers ...*comm.RemotePeer)
    //返回所有被认为是活的网络成员
    Peers() []discovery.NetworkMember
    //返回所有被认为是活的且订阅了ChainID频道的网络成员
    PeersOfChannel(common.ChainID) []discovery.NetworkMember
    //更新自身发现层的metadata
    UpdateMetadata(metadata []byte)
    //更新自身metadata,该metadata是peer出版给其他peer的自身频道相关的状态数据
    UpdateChannelMetadata(metadata []byte, chainID common.ChainID)
    发送一个消息给网络中的其他peer
    Gossip(msg *proto.GossipMessage)
    //返回一个针对由其他匹配一个明确断言的结点发送的消息的专用只读频道
    Accept(...) (<-chan *proto.GossipMessage, <-chan proto.ReceivedMessage)
    //使一个绯闻实例加入频道
    JoinChan(joinMsg api.JoinChannelMessage, chainID common.ChainID)
    //验证可疑peer点的身份,若发现该可疑peer是无效的,则关闭与此peer的连接
    SuspectPeers(s api.PeerSuspector)
    //停止这个gossip实例
    Stop()
}
//gossip实例,在/fabric/gossip/gossip/gossipimpl.go中定义
type gossipServiceImpl struct {
    selfIdentity          api.PeerIdentityType //自身身份标识
    includeIdentityPeriod time.Time
    certStore             *certStore           //certStore模块
    idMapper              identity.Mapper      //idMapper模块
    presumedDead          chan common.PKIidType
    disc                  discovery.Discovery  //discovery模块
    comm                  comm.Comm            //comm模块
    incTime               time.Time
    selfOrg               api.OrgIdentityType  //自身组织标识
    *comm.ChannelDeMultiplexer                 //多路分配器
    logger            *logging.Logger
    stopSignal        *sync.WaitGroup          //等待组
    conf              *Config                  //配置
    toDieChan         chan struct{}            //停止通道
    stopFlag          int32                    //停止标识
    emitter           batchingEmitter          //emitter模块
    discAdapter       *discoveryAdapter        //discovery适配器
    secAdvisor        api.SecurityAdvisor      //安全辅助
    chanState         *channelState            //chanState模块
    disSecAdap        *discoverySecurityAdapter//discovery安全辅助适配器
    mcs               api.MessageCryptoService //消息加密服务
    stateInfoMsgStore msgstore.MessageStore    //消息存储模块
}

1. gossip服务初始化的前提元素

这些前提元素都是在peer start的serve函数中,既供gossip服务使用,也供gossip服务器使用,在此一并列出:

1.1 签名者serializedIdentity

在fabirc源码解析12——peer的MSP服务中最后部分提及的localMsp的初始化(自然也包括了其成员signer的初始化),而serializedIdentity,err = mgmt.GetLocalSigningIdentityOrPanic().Serialize()则是获取localMsp中的签名者signer。localMsp对象的初始化的数据来源于MSPConfig配置数据,该MSPConfig是随着InitCrypto->LoadLocalMsp->GetLocalMSP().Setup(conf)一步步包装出来的,最后形成conf传入Setup进行初始化。使用到的是core.yaml配置文件中的BCCSP,mspConfigPath,localMspId项,分别作为bccsp服务的配置,msp配置文件(指各种代表身份的证书路径等)路径,localMsp的ID。其中mspConfigPath中就包含用于初始化signer的身份证书。这个签名者,一方面提供了签名的能力,另一方面代表着peer的身份。

1.2 消息加密者服务messageCryptoService

MessageCryptoService接口实例,接口在/fabric/gossip/api/crypto.go,实现在/fabric/peer/gossip/mcs.go。该实例是gossip模块与peer的加密层之间的“合约”,被gossip模块用于确认和认证远程peer身份和这些peer发送的数据(加密解密),也被用于验证由ordering服务发来的block。本身包含频道策略管理者获取器,本地签名者,反序列化工具管理者,三者分别用peer.NewChannelPolicyManagerGetter()localmsp.NewSigner()mgmt.NewDeserializersManager()初始化,分别用于获取指定频道ID的策略管理者,本地签名者(假设本地MSP已被初始化)进行签名,提供各种反序列化peer发送过来的序列化数据的工具,分别在/fabric/core/peer/peer.go,/fabric/common/localmsp/signer.go,/fabric/msp/mgmt/deserializer.go中实现。该元素生成后,通过参数渗透到了gossip服务自身的多个模块中,如成员idMapper,disSecAdap。

1.3 安全顾问secAdv

SecurityAdvisor接口实例,接口在/fabric/gossip/api/channel.go,实现在/fabric/peer/gossip/sa.go中。为了系统安全,让MSP能够更新是十分基础的,通过由ordering服务分配的配置交易,可以更新频道的MSP。安全顾问接口就是为了这点,提供安全和身份相关(如身份验证识别等)的能力。该元素也和messageCryptoService和idMapper模块(参看下文章节3.4)一样,生成后通过参数渗透到了gossip服务自身的多个模块中。

1.4 其他元素

bootstrap,gossip服务引导的地址。secureDialOpts,grpc服务拨号的标准选项。peerEndpoint.Address,使用的是core.yaml中的peer.address项,peer本地的IP地址。peerServer,peer结点的grpc服务器对象。

InitGossipService中,除了用上述传递进来的参数用于给自身成员赋值,也使用其他模块中的函数给剩余的成员赋值,如使用integration.NewGossipComponent给重要的成员gossipSvc赋值,而这个函数是专门用来整合gossip实例和gossip在core.yaml中的配置的函数。

2. 消息类型分类

说到底,gossip服务是处理消息的,每种类型的消息有不同的用途,gossip服务也使用不同模块处理不同类型的消息。gossip服务所处理的消息类型原型在/fabric/protos/gossip/message.proto中定义,对应生成message.pb.go。gossip中传播(发送)的消息以GossipMessage形式传递,而不同类型的具体的消息数据存放在GossipMessage中的Content成员中。

2.1 辅助类消息

之所以叫辅助类消息,是这类消息不承担具体传送传播数据的任务,而是辅助性的:

  • Empty - 空消息,用于结点间的Ping(来测试结点是否连通)和测试。
  • ConnEstablish - 用于gossip之间的握手,即任何时候一个peer想与另一个peer连接通信,都需要先发送这个消息以证明其身份。

2.2 pull机制消息

用于pull机制中处理的消息类型。所谓pull机制,就是一个结点主动向其他结点索要自己需要的消息(数据),pull进(即拉进,拽进的意思)自己的结点中处理或存储,整个pull过程需要四步,可参看下文章节3.5。从内容上可以分为未定义消息,块消息,身份消息(即pull进来的可以是以块数据为内容的消息,也可以是身份数据为内容的消息,下述4种消息中都有一个成员MsgType来表明这个消息中所携带的数据内容),但从pull步骤上分为四种:

  • GossipHello - hello消息
  • DataDigest - 消息摘要
  • DataRequest - 摘要请求
  • DataUpdate - 摘要应答

2.3 state消息

状态有关的消息。这里的状态指的是chain的数据状态,如一个结点所存储的块数据是否一致,可以说这里所谓的状态接近数据的意思。

  • DataMessage - 数据消息
  • StateInfo - 状态消息
  • StateInfoSnapshot - 状态快照消息(即一组StateInfo消息)
  • StateInfoPullRequest - 状态请求消息(索要StateInfoSnapshot消息)
  • RemoteStateRequest - 远程单点状态请求消息
  • RemoteStateResponse - 远程单点状态应答消息

2.4 关系消息

与频道成员身份,关系,存续相关的消息类型。

  • AliveMessage - alive消息
  • MembershipRequest - 成员关系请求消息
  • MembershipResponse -成员关系应答消息
  • LeadershipMessage - leader消息
  • PeerIdentity - 身份消息

以上只是为了方便理解和辨识,所以依照消息的作用和处理所依赖的模块的不同进行人为的分类。在实际的代码中,模块处理的消息类型有所交叉,如关系消息,前3种主要由discovery模块处理,PeerIdentity由certStore模块处理,LeadershipMessage却又由chanState模块处理。而且,消息的分类其实有多个标准或维度,以上是按照GossipMessage中的Content所包含的消息类型进行分类的,也可以按GossipMessage中Tag成员对消息进行分类,Tag成员的值决定了一个消息可以在哪种范围内传播,有四种值:

  • UNDEFINED - 未定义
  • EMPTY - 空,对应上文章节2.1中的EMPTY消息
  • ORG_ONLY - 只在同一组织内传播
  • CHAN_ONLY - 只在同一频道内传播
  • CHAN_AND_ORG - 同时在同一频道和同一组织内传播
  • CHAN_OR_ORG - 既可以在同一频道内,也可以在统一组织内传播

图示举例CHAN和ORG范围如下,一个网络中,有A,B两个(或更多)频道(也可以说是chainA,chainB),每个频道包含两个(或更多)组织:组织I,组织II。每个组织I又各包含三个(或更多)结点。

这里写图片描述

3. gossip服务中主要模块的初始化

gossip服务的实现个人感觉还是挺复杂的,这里面牵涉到了利用grpc通信,chan通道控制,类“出版-订阅”消息系统,各个模块,模块的“适配器”,这些元素相互配合,共同实现gossip服务。该章节文章中目录除绝对路径外,都以/fabric/gossip/为基础。gossip服务的各个模块都作为成员存在于gossipServiceImpl实例中,并在gossipServiceImpl专用初始化函数NewGossipService中初始化。

在初始化的过程中,要注意“倒钩”的使用。这也算fabric中常用的一招了,宿主将倒钩以参数的形式传入一个成员对象,成员对象在实现功能A的时候调用了这个倒钩,然后宿主使用这个成员对象的A功能时,实质还是使用自身的功能,但又可以让成员对象发挥一些作用来帮助宿主,这些作用一般都是宿主自身无暇顾及或者不屑于干的“小事儿”。

3.1 discovery模块

在NewGossipService中使用g.disc = discovery.NewDiscoveryService(...)初始化discovery模块,代码集中在discovery目录。可以简单的将discovery模块当作一个扫描器,扫描并辨别管理频道或网络中存在的活着的(alive)结点和死掉的(即不再存在于网络或频道中,掉线)结点。为了实现这样的功能,discovery模块使用handleMsgFromComm处理三类信息(对应三个if分支,很清晰):alive消息AliveMessage,成员关系请求消息MembershipRequest,成员关系应答消息MembershipResponse。discovery模块中有一个来自gossip实例的“适配器”discoveryAdapter(在gossip/gossip_impl.go中定义)的成员,这个适配器成员封装了comm模块(参看下文章节2.3),使得discovery模块利用这个适配器可以处理这三类消息。

discovery模块判断一个结点是否活着的机制是:根据最后一次收到一个结点的alive消息的时间与当前时间的差值,当这个差值大于某个值,则判定该结点已经死掉。因此discovery模块判定的死掉的结点,只是这个模块自认为这个结点死掉了,也有可能这个结点其实并没有死,只是alive消息没有及时发送而已。discovery实例中有成员aliveLastTS,该映射即存储了每个peer结点最后一次所发送的alive消息的时间。对于判定死掉的peer点,discovery则将该peer点死掉的时间存储到deadLastTS成员中,并定期发送复活请求去尝试复活这些死掉的结点。另外,相应的,discovery把活着的和死掉的peer点信息分别存储在成员aliveMembership,deadMembership中。

3.2 emitter模块

emitter模块的原型是batchingEmitter,在gossip/batcher.go中定义,可以把emitter模块理解为批量发射器,即在gossip服务传播的过程中,为了提高效率,不是来一条信息就传播一条,而是累计到一定条数,然后一批一批的发送。一批消息的个数由batchingEmitter的成员burstSize决定,gossip服务中默认的是10条为一批,存储在成员buff中。成员iterations指定了每条消息发送的次数,默认是1次。成员delay指定了每批消息发送的时间间隔,默认是10ms。在NewGossipService中使用g.emitter = newBatchingEmitter(...)对此模块进行初始化。

emitter模块在发送信息过程中,自身主要起到一个控制批次的作用,而发送的功能还是使用其宿主gossip实例投放的“倒钩”cb,这个emitter成员在初始化时被赋值为g.sendGossipBatch,即gossip实例的函数sendGossipBatch

3.3 comm模块

comm模块代码集中在comm目录中,实现了结点与结点之间的通信,即实现了grpc通信服务。比较特殊的是comm既是grpc的客户端,也是服务器端。首先comm实现了/fabric/protos/gossip/中定义的rpc服务GossipStreamPing。其次,comm也实现了用于客户端发送消息的功能函数Send。comm模块的专用初始化函数有两个:NewCommInstanceWithServerNewCommInstance,前者是comm自己创建一个grpc.Server,后者是由参数传入一个grpc.Server作为自己的自己的服务器成员gSrv,gossip服务默认使用的是后者,传入的是peer node start命令中serve函数中初始化的peer结点的peerServer。在专用初始化函数NewCommInstance中用proto.RegisterGossipServer(s, commInst.(*commImpl))完成了comm模块服务器的grpc注册。comm模块中包含两个主要的子模块:connStore模块和msgPublisher模块,都在NewCommInstance中初始化。

3.3.1 connStore模块

connStore模块原型为connectionStore,在comm/conn.go中定义,可以将其理解为peer点间的连接管理器,管理的连接原型是connection,也在同一文件中定义。所有的connection都存储在connectionStore的成员pki2Conn中。既然是管理连接,自然的,主要的方法有创建connection,获取connection。而创建连接的方法又是comm模块所放的“倒钩”:在connectionStore的专用生成函数newConnStore中,用于创建新connection的成员connFactory被赋值为comm实例本身,而comm实例所实现的createConnection函数就是用于生成一个*定制的新的**connection。

connection代表了一个peer与另一个peer之间的grpc连接,这个连接可能是客户端流连接,也可能是服务端流连接,具体要看connection专用生成函数newConnection中对成员clientStream赋值,还是对成员serverStream赋值。因为connection封装了grpc的流接口,因此在gossip服务散播消息的过程中,消息经comm模块,最终是使用使用serviceConnection收发处理消息。收发消息方面,收消息用readFromStream,发消息用writeToStream,connection的send函数也是用于发送消息,但是这个函数只是辅助writeToStream的:send将消息直接丢到成员outBuff中,outBuff是一个通道,所发送的消息经这个通道到达writeToStream中,进而使用stream.Send发送消息(即用grpc的流对象发送消息)。处理消息方面(即,处理接收到的消息),使用connection成员handler,这又是comm模块放的“倒钩”:在定制connection的createConnection函数(参看上一段)中,handler被赋值为使用msgPublisher模块的DeMultiplex函数(参看下文章节2.3.2)去处理消息,这里将接收到的消息封装进ReceivedMessageImpl对象(接口原型在/fabric/protos/gossip/extensions.go中定义,在comm/msg.go中实现),同时也封装了所建的connection信息,以供之后向发送者回复消息所用。最后需要注意的是,connection成员pkiID代表的是对方peer点的信息,一个peer点的客户端流connection和服务端流connection是两个单独的连接存在于connStore中并接受其管理。

3.3.2 msgPublisher模块

msgPublisher模块原型为ChannelDeMultiplexer,在comm/demux.go中定义,可以将其理解为是一个以频道chan为基础的类似“出版-订阅”的模块,所有的频道channel存储在成员channels中。主要有两个方法:添加chan函数AddChannel,广播消息函数DeMultiplex。AddChannel很容易理解,DeMultiplex做的是遍历channels,按每个channel所定的条件(这个条件就是消息选择器)发送消息。

频道channel定义在同一个文件中,成员包含一个go语言的chan和一个消息选择器。这个消息选择器的原型为common.MessageAcceptor,用于甄选消息,即msgPublisher在通过成员channels中的一个个channel广播消息时,channel中的消息选择器会判断是否对这个消息感兴趣,如果感兴趣,则通过这个channel发送消息,如果不感兴趣,则忽略这个消息。这个消息选择器是gossip实例通过comm模块所放的“倒钩”:在gossip/gossip_impl.go中的start函数中,首先,调用incMsgs := g.comm.Accept(msgSelector),既新建了一个channel并将消息选择器赋值为msgSelector,又把这个新的channel注册到msgPublisher中的channels并返回这个频道(即incMsgs)。接着,调用go g.acceptMessages(incMsgs),新启一个goroutine接收incMsgs这个频道所进来的消息(acceptMessages就是gossip服务接收消息的函数,参看下文章节3.2,go g.acceptMessages)。对照章节2.3.1中connection中处理接收到的消息的handler所描述的:使用msgPublisher模块的DeMultiplex函数去处理消息,即遍历所有频道,把消息传递给消息选择器感兴趣的频道,最后又通过频道把消息交给了gossip服务的acceptMessages函数去处理。

3.4 idMapper模块

idMapper模块原型为identityMapperImpl,在identity/identity.go中定义,可以将其理解为一个网络中peer点的身份管理者。在gossip服务中,peer点有两种表示自身身份的类型:PeerIdentityType和PKIidType。PeerIdentityType是peer点的所持有的证书,值来自于serializedIdentity(参看上文章节1.1),PKIidType可以简单的理解为是从PeerIdentityType中摘取出一部分摘要,然后把摘要经哈希处理后生成的叫做PKI-ID的身份,具体可参看/fabric/peer/gossip/mcs.go中messageCryptoService的实现中的GetPKIidOfCert函数。idMapper模块所管理的就是PKIidType与PeerIdentityType之间的映射,即成员pkiID2Cert,同时也就为gossip服务中身份和消息的认证,签名提供了基础。如idMapper被封装在gossip实例成员discoverySecurityAdapter(也是一种安全辅助适配器,在gossip/gossip_impl.go中定义)中,该成员实现了ValidateAliveMsg函数。在初始化discovery模块时,成员crypt被赋值为discoverySecurityAdapter,在discovery模块处理接收到的alive消息时(即handleMsgFromComm,参看上文章节2.1),就调用了crypt的ValidateAliveMsg函数,即提供了验证接收到的alive消息(和消息中包含的身份)的功能。

idMapper还被封装在其他模块中,如comm模块,certStore模块,但是包括gossip实例中的idMapper在内,这些idMappper均来自于gossip服务器。gossip服务器实例初始化时,创建了一个idMapper对象,并将这个对象以参数的形式传入gossip服务实例,进而传入gossip服务的各个模块。因此,在各个模块中对idMappper中数据的增减,都会体现到gossip服务器实例中的那个成员idMapper,而gossip服务器实例对成员idMapper的操作,也会渗透体现到各个模块中的idMapper。

3.5 Mediator模块

Mediator模块原型为pullMediatorImpl,在gossip/pullstore.go中定义,可以将其理解为一个调解媒介,在每个peer结点中,有些存储的信息(如成员身份,block等信息)是存在呼应关系的,比如网络中有一个C结点,那么A和B存储的成员信息中肯定都应该有C的身份信息,但是有时候由于一些原因会造成B中有C而A中没有C的差异情况,这个时候可以通过Mediator模块调解“矛盾”,通过A向B索要关于C的身份信息而弥补差异。该模块所处理的是pull机制消息,身份消息由certStore模块(参看下文章节2.6)处理,块消息由chanState模块处理(参看下文章节2.7);hello消息,消息摘要,摘要请求,摘要应答分别对应Mediator模块处理消息的函数HandleMessage中的四个if分支。

Mediator模块是certStore模块和chanState模块的成员,在初始化这两个宿主时,Mediator模块同时也被初始化。以certStore模块为例,在初始化certStore模块时,在gossip/gossip_impl.go中,调用createCertStorePuller()创建了一个pullMediatorImpl实例:创建了配置项conf和适配器adapter后传入pullMediatorImpl的专用生成函数NewPullMediator中。注意这里的conf中的MsgType被赋值为PullMsgType_IDENTITY_MSG,即身份消息类型,也即表明这里所生成的Mediator对象是用于pull进身份类型消息的,适配器adapter的作用是封装一些gossip模块和处理身份消息的函数供Mediator模块使用。在NewPullMediator中,又调用了p.engine = algo.NewPullEngineWithFilter(...)生成了一个Mediator模块的一个重要成员engine

engine是Mediator模块的pull引擎,周期性的做了具体的pull工作,原型为PullEngine,在gossip/algo/pull.go中定义。这个引擎伴随着engine的初始化启动起来:在NewPullEngineWithFilter(…)中,新启了一个goroutine周期性的运行engine.initiatePull(),周期间隔为4s,由core.yaml中的peer.gossip.pullInterval项指定。这里要注意,engine初始化时,把宿主pullMediatorImpl作为自己成员PullAdapter的值,以便利用宿主pullMediatorImpl所包含的适配器(即上一段所提的adapter)的能力发送消息(如下文所提的Hello()函数)。PullAdpter也是engine的一个适配器,在Mediator中,包括其他模块中,都有较多的适配器,适配器的作用就如它的名字,起到适配的作用,比如这里的Mediator模块,certStore模块也想用它,chanState模块也想用它,但是在实现细节上和所处理的消息类型上都有区别,因此Mediator的实现是固定的,Mediator拥有一个适配器成员(虽然是成员,但是我觉得一个对象的适配器和这个对象应该是平级的关系),通过这个适配器,既可以实现Mediator使用certStore,chanState两个不同模块各自所拥有的功能,也可以实现certStore,chanState两个不同模块使用Mediator处理不同数据的功能。

engine引擎的执行有四步(假设有A,B两个结点,请对看gossip/algo/pull.go中开始处的注释+码图):

  1. A的engine调用Hello()发送hello消息给B,B的engine接收到了hello消息后调用OnHello()处理。
  2. B在OnHello()中处理hello消息后调用SendDigest()返回消息摘要给A,A接收消息摘要调用OnDigest()处理。
  3. A发送hello消息后等待一段时间接着调用processIncomingDigests(),进而调用SendReq()发送摘要请求给B,B接收后调用OnReq()处理。
  4. B在OnReq()中处理消息后调用SendRes()返回摘要应答给A,A接收消息调用OnRes()处理。
  5. 在3中A调用SendReq()发送摘要请求后等待一段时间接着调用endPull()结束整个pull过程,等待下一次调用initiatePull()

engine引擎执行的过程中有两个关键词,一个是摘要,一个是NONCE摘要指从消息中择取出的足以代表一条消息的关键信息,身份消息的摘要即为PKI-ID块消息的摘要则为块序号,在接收到摘要应答后,调用OnRes()将摘要最终存储到engine成员state中,这些摘要来自于Mediator模块处理消息的函数HandleMessage中处理摘要应答的分支中itemIDs[i] = p.IdExtractor(msg),即使用的是Mediator模块的适配器中封装的IdExtractor函数,也因此,由于certStore模块和chanState模块所实现的适配器中的IdExtractor不一样,所有才能从消息中分离出两种不同的摘要。NONCE是通信安全中的一个概念,指的是用一次即废弃的一个整数,目的在于防止replay attack,但是在这里只是简单使用NONCE一次性的性质,在engine引擎周期性执行pull的过程中,每个周期中的步骤之间不产生交叉混淆。

3.6 certStore模块

certStore模块原型为certStore,在gossip/certstore.go中定义,可以将其理解为一个身份存储器,辅助性的维护idMapper模块,即通过处理身份消息,进而维护idMapper模块中存储的身份数据。certStore模块自身不提供什么功能,主要是提供窗口供gossip服务实例调用,其实现的功能基本依赖其封装的其他模块,如验证身份消息的函数validateIdentityMsg使用MessageCryptoService,返回废弃结点的函数listRevokedPeers使用idMapper,处理消息handleMessage则使用Mediator模块。certStore模块只处理pull机制消息中以身份数据为内容的消息,这点可以从gossip/gossip_impl.go中handleMessage函数的if msg.IsPullMsg() && msg.GetPullMsgType() == proto.PullMsgType_IDENTITY_MSG分支看出。

3.7 chanState模块

chanState模块原型为channelState,在gossip/chanstate.go中定义,和certStore模块一样,chanState本身作为管理角色并向gossip服务实例提供操作窗口,同时为管理的channel模块提供适配器gossipAdapterImpl,而任务性、功能性部分都由其管理的channel模块实现。这里的状态可以理解为数据的意思,即chanState模块主要按频道处理数据类型的消息(参看上文章节2.3)。chanState模块将管理的channel对象存储在成员channels中,映射的key为字符串格式的chainID,即频道的ID。chanState模块的专用生成函数是在gossip/gossip_impl.go中的newChannelState(...),而且既然是管理channel模块的,自然有增加获取功能:增加joinChannel函数,获取lookupChannelForMsg函数。

channel模块原型为gossipChannel,在gossip/channel/channel.go中定义,是具体处理chanState模块任务的对象。channel模块中的成员Adapter原型为gossipAdapterImpl,即由chanState提供的适配器,适配器中封装了gossip服务实例和discovery模块,为channel提供了收发消息的能力。成员blockMsgStore,stateInfoMsgStore,leaderMsgStore三个存储成员为channel提供了存储能力。成员blocksPuller为channel提供了pull块消息的能力(参看上文章节3.5)。在channel专用生成函数中NewGossipChannel中,启动了两个go gc.periodicalInvocation(...)goroutine,周期性的执行gc.publishStateInfogc.requestStateInfo函数:

  • gc.publishStateInfo - 周期性的传播频道的状态消息,该状态消息即为channel的成员stateInfoMsg,代表着频道当前的数据状态。周期间隔为4s,由core.yaml的peer.gossip.publishStateInfoInterval指定。这个散播的过程由成员shouldGossipStateInfo的值和函数UpdateStateInfo相互配合,每隔4s调用publishStateInfo函数,在其中检查shouldGossipStateInfo的值,如果是0,则表示频道当前的stateInfoMsg没有更新(之前的已经散播过),不用散播。当调用UpdateStateInfo函数更新stateInfoMsg后,也将shouldGossipStateInfo的值赋为1,当下一次调用publishStateInfo函数检查shouldGossipStateInfo的值时,发现为1,则调用适配器Adapter的gc.Gossip(stateInfoMsg)将stateInfoMsg散播出去,并将shouldGossipStateInfo的值赋为0,等待下次UpdateStateInfo函数的更新。
  • gc.requestStateInfo - 周期性的发送StateInfoPullRequest类型消息,向远程peer结点索要StateInfoSnapshot消息。在requestStateInfo中,每隔4s(由core.yaml中的peer.gossip.requestStateInfoInterval指定),由filter模块随机筛选出来(参看下文章节3.8)3个(由core.yaml中的peer.gossip.pullPeerNum指定)peer点,然后调用适配器Adapter的gc.Send(req, endpoints...)向这些peer点发送StateInfoPullRequest消息。等这些peer点返回StateInfoSnapshot消息时,则放入HandleMessage函数中处理。

channel模块最重要的一个函数就是处理接收消息的HandleMessage(msg proto.ReceivedMessage),其余的基本都是配合该函数处理消息:要么是用于验证消息和消息发送者身份,要么是生成回复消息,要么是处理指定类型消息。HandleMessage函数十分清晰,一个个if分支对应处理6种类型的消息:DataMessage,StateInfo,StateInfoSnapshot,StateInfoPullRequest,pull机制消息中以块数据为内容的消息,LeadershipMessage。基本的过程就是验证消息和消息的发送者,如果验证后消息合法,则该存储的存储,该相应回复的回复,该转发的转发。if m.IsStateInfoPullRequestMsg()分支是处理StateInfoPullRequest类型消息,处理过程就是调用createStateInfoSnapshot函数生成此结点的StateInfoSnapshot消息然后使用ReceivedMessage的接口函数msg.Respond原路回复。在createStateInfoSnapshot函数中还体现了一个传播策略问题,即传播范围选择问题(对看上文对于Tag成员的图例解释),对于发送StateInfoPullRequest类型消息的结点A,接收到该请求的结点B需要判断A是否与自己在同一个组织内,如果A是同一个组织内的结点,则B会把存储的StateInfo消息都回给A,如果A不是同一个组织的,则B只把存储的非组织内消息或者既是组织内也是频道内的消息回给A。再如if m.IsDataMsg() || m.IsStateInfoMsg()分支是处理DataMessage,StateInfo消息,处理过程就是将数据存储到blockMsgStore或stateInfoMsgStore中,然后继续调用gc.Gossipgc.DeMultiplexgc.blocksPuller.Add转发,广播,pull进此消息。

3.7.1 msgstore模块

msgstore模块为消息存储模块,为每一个channel模块提供存储功能,原型有两种:(1)MessageStore,在goosip/msgstore/msgs.go中定义,用于存储3种类型消息:DataMessage、pull机制消息中以块数据为内容的消息、LeadershipMessage,channel中的成员blockMsgStore就用于存储前两种,成员leaderMsgStore存储后一种。(2)stateInfoCache,在gossip/channel/channel.go中定义,用于存储StateInfo类型消息,channel中的成员stateInfoMsgStore就用于存储这种消息。

MessageStore把消息存储在成员messages中,撇开存储中的增加和获取的功能不讲,MessageStore在存储消息的过程中有一个消息有效期的概念:在MessageStore专用生成函数NewMessageStoreExpirable中,go store.expirationRoutine()新启了一个goroutine周期性的调用expireMessages()来检查messages中的消息并废止(即用以新换旧的方式删除)已经失效的消息。blockMsgStore的周期间隔为4s,leaderMsgStore的周期间隔为1s,分别由core.yaml中的peer.gossip.pullInterval*100/100,peer.gossip.election.leaderAliveThreshold*10/100计算指定(见MessageStore的成员msgTTL和函数expirationCheckInterval())。关于消息的有效性,涉及到两点,即有效性的判断标准判断函数,判断函数中体现判断标准。MessageStore的成员pol就是判断函数,原型MessageReplacingPolicy在common/common.go中定义,在/fabric/protos/gossip/extensions.go中实现,为msgComparator对象的invalidationPolicy(this,that)函数,这个函数接收两个参数:新消息this和已存在的消息that,返回三种验证结果(在common/common.go中定义):MessageNoAction,MessageInvalidates,MessageInvalidated。MessageNoAction表示this与that相安无事,MessageInvalidates表示this使that无效,MessageInvalidated表示this被that无效(即that使this无效)。这三个结果的名字也起的挺有意思的,NoAction,主动动词Invalidates,被动动词Invalidated,巧妙的表示出了谁是无效的。在invalidationPolicy(this,that)函数中,每个if分支中判断一类消息,每种类型的消息的判断标准不尽相同,如if thisMsg.IsDataMsg() && thatMsg.IsDataMsg()分支调用dataInvalidationPolicy函数判断DataMessage类型的this与that,判断标准是:依据this的消息序号this_N和that的消息序号that_N,如果this_N == that_N,说明已经存储过一个同样的消息,则this无效;如果this_N-that_N的值的绝对值 <= 100,则this与that相安无事;如果this_N-that_N的值的绝对值 > 100且this_N > that_N,则that无效;其余情况this无效。这里为什么是100,涉及到ordering服务,将在ordering服务主题文章中详述。

stateInfoCache封装了MembershipStore和MessageStore(参见上一段),StateInfo消息在这两个成员中各存一份,MembershipStore存储所有接收到的消息,MessageStore则只存储有效期内的消息。MembershipStore在util/msgs.go中定义,正常的进行增,删,获取。

3.8 filter模块

filter模块相对简单,只实现了几个筛选函数和筛选策略原型,在filter/filter.go中定义,可以将其理解为一个成员过滤器,即按照给定的条件筛选出符合条件的成员。基础过滤策略的原型,其实就是定义的筛选函数,为type RoutingFilter func(discovery.NetworkMember) bool。筛选函数为func SelectPeers(...)[]*comm.RemotePeer,接收三个参数:筛选个数k,成员集合peerPool,筛选策略filter,所做的就是在peerPool中随机筛选出k个符合filter的成员,放入数组中返回(如果筛选出来的成员个数<=k,则将筛选出了的成员全部返回,这时候可能返回的不足的成员不足k个)。辅助筛选函数CombineRoutingFilters(...),接收多个RoutingFilter,然后通过for循环返回一个“合并”的RoutingFilter,比如现在要筛选出满足条件1,条件2的结点,相应实现的RoutingFilter是R1,R2,将R1和R2传入CombineRoutingFilters(...),返回一个合并的RoutingFilter为R,R在循环中依次使用R1,R2判断传入的结点是否满足条件,然后给出“综合”结果。

4. gossip服务初始化中所起的goroutine

gossip服务用于传播消息,但是这只是gossip服务明面上的功能,为了实现(或者说维护)这个功能,gossip服务初始时需要在背后启动一系列goroutine做一些工作,有些是准备性的工作,有些是周期性的工作,有些是循环性(即在服务存在期间,不间断)工作。初始时,指的就是一个gossip服务实例创建的时候,即在gossip/gossip_impl.go中的gossip实例专用生成函数NewGossipService执行时。

gossip_goroutines.png

4.1 discovery模块goroutine

在创建discovery实例时,启动了6个goroutine,用于服务discovery模块的扫描处理工作。这6个goroutine所做的工作可与上文章节2.1内容对看。

  • go d.periodicalSendAlive() - 周期性的发送alive消息,以告之网络中的其他结点:我还没死。间隔时间默认为5s,由core.ymal中的peer.gossip.aliveTimeInterval指定。
  • go d.periodicalCheckAlive() - 周期性的检查aliveLastTS中的时间戳,判断结点是死是活,然后相应调整deadLastTS,aliveLastTS,aliveMembership,deadMembership中的信息。
  • go d.handleMessages() - 循环处理来自discoveryAdapter成员incChan频道的消息,这个通道是gossip实例专门用来处理GossipMessage_AliveMsg,GossipMessage_MemReq,GossipMessage_MemRes三类消息。incChan频道的消息又来源自gossip专用于处理接收消息的handleMessage函数(参看下文章节3.2,go g.acceptMessages)的if selectOnlyDiscoveryMessages(m)分支,这个分支中调用了g.forwardDiscoveryMsg(m),将接收到的消息m发送到了incChan频道。
  • go d.periodicalReconnectToDead() - 周期性的查新尝试连接死掉的结点,通过尝试向死掉的结点发送GossipMessage_MemReq类型的消息来实现。
  • go d.handlePresumedDeadPeers() - 循环处理来自discoveryAdapter成员presumedDead频道的消息,presumedDead频道在newDiscoveryAdapter()中初始化时即被赋值为gossip实例的成员g.presumedDead,而g.presumedDead是gossip专用于处理被认为是死掉的peer结点的频道(参看下文章节3.2,go g.handlePresumedDead)。
  • go d.connect2BootstrapPeers(bootstrapPeers) - 这个属于准备性工作,整体只调用一次,即在discovery模块初始化时,向bootstrapPeers发送GossipMessage_MemReq类型消息,索要成员关系信息。对于新加入的peer结点X实现的gossip服务,无论是散播消息,还是其他工作,都是基于现有网络成员信息的,也就是说我要先知道有哪些结点,才能与这些结点通信,收发消息。bootstrapPeers就是X指定的要去索要成员信息的地址列表,由core.yaml中的peer.gossip.bootstrap指定,默认只有127.0.0.1:7051一个值。这个过程最多会尝试索要120次,每隔25秒索要一次,分别由discovery/discovery_impl.go中的maxConnectionAttempts常量和core.yaml中的peer.gossip.reconnectInterval指定,但一旦获取到了成员信息(即成员aliveLastTS中存在信息)则停止这个goroutine。

4.2 start函数goroutine

go g.start()新启了一个goroutine去开始gossip实例的服务。在start函数中分别启动了三个goroutine:

  • go g.syncDiscovery() - 周期性的在discovery模块的成员aliveMembership中随机选择n个peer结点,向其发送GossipMessage_MemReq类型消息,索要成员关系信息,即在网络存在的结点之间进行成员关系信息的同步。时间间隔默认为4秒,n默认为3,分别由core.yaml中的peer.gossip.pullInterval,peer.gossip.pullPeerNum指定。这个goroutine主要调用了discovery/discovery_impl.go中定义的InitiateSync函数,其中所选出的peer点的随机性由定义在util/misc.go中的GetRandomIndices函数实现,该函数接收两个参数c1和c2,随机选出c1个小于c2的数。选出peer后,即依次发送GossipMessage_MemReq请求消息索要成员关系信息。
  • go g.handlePresumedDead() - 循环接收从comm模块的deadEndpoints频道来的消息,然后转发到gossip实例的presumedDead频道(参看上文章节3.1,go d.handlePresumedDeadPeers)。即,这个goroutine专门用gossip实例来转发被认为死掉了的结点的身份信息,从comm模块接收后经适配器discoveryAdapter转交给discovery模块来处理。
  • go g.acceptMessages(incMsgs) - 循环接收从msgPublisher模块订阅的频道incMsgs来的消息,交由g.handleMessage(msg)处理。可与上文章节2.3.2对看。这个goroutine就是用于gossip实例处理接受到的消息的。

4.3 身份验证终止goroutine

连续启动两个goroutine,用A和B表示,都执行g.periodicalIdentityValidation(...)函数。该函数接收两个参数:猜想函数(suspectFunc),周期间隔。也因这两个参数,A和B有两点区别:(1)根据猜想函数判断是否使用MessageCryptoService(参看上文章节1.2)去验证一个身份是否被废止。(2)周期间隔不同。A和B的任务是:周期性的检查idMapper模块中所存储的身份,将无效身份删除(包括idMapper模块中和Mediator模块中的),并停止这些无效身份所代表的peer结点的连接。这里的无效身份长时间未使用(即不活动的,Inactivity)或死亡(即无效,被废止,被终止,Expiration)的身份。

  • go g.periodicalIdentityValidation(…) - A周期间隔为24小时,由identityExpirationCheckInterval常量指定,猜想函数是永远返回true。B周期间隔为10分钟,由identityInactivityCheckInterval常量指定,猜想函数是永远返回false。A和B周期性的调用g.SuspectPeers(suspectFunc),追溯SuspectPeers这个函数,经certStore模块(gossip/certstore.go中的listRevokedPeers函数),最终调用了idMapper模块,即identity/identity.go中的validateIdentities函数来获取无效身份列表:遍历idMapper模块成员pkiID2Cert中存储的所有身份,如果一个身份的最后访问时间(由该身份的成员lastAccessTime记录)距当前时间的时间差 >= 1小时,则说明该身份长时间未活动,这个1小时由同文件中的identityUsageThreshold常量指定;如果这个时间差小于1个小时,且猜想函数返回为true(只有A符合),则进一步使用MessageCryptoService验证这个身份,若验证失败,则说明该身份无效,被废或被终止。在遍历pkiID2Cert中存储所有的身份结束后,将无效身份列表revokedIds原路返回,在certStore模块中将这些无效的身份从Mediator模块中删除,最后在SuspectPeers函数中把这些无效身份所代表的peer结点的连接关闭。

gossip服务器的初始化

gossip服务器原型为gossipServiceImpl,在service/gossip_service.go中定义,同目录中还有一个eventer.go定义了gossip服务器配置升级相关的代码。gossip服务器管理的主要有:一个gossip服务实例gossipSvc,各个频道的状态模块chains和选举模块leaderElection,核心数据分发客户端deliveryService等等。除了gossipSvc提供的消息散播功能,直接管理的election模块与state模块(在下文详述),gossip服务器主要向外界提供两个功能(其余功能较简单或已在其他模块讲述,此处略):配置初始化或更新,频道初始化。

(1)配置更新 - gossip服务器也是需要通过配置而被控制的,除了像core.yaml这样的静态配置文件,还需要可以动态升级gossip服务器的配置。这样的实现涉及到gossip服务器的NewConfigEventer()接口,configUpdated()函数和eventer.go文件。NewConfigEventer()返回一个可更新gossip服务器配置的对象,这个对象在eventer.go中实现为configEventer。configEventer封装了存储最新配置的成员lastConfig和接收配置的成员receiver(即谁接收配置并应用到自己身上)。这里receiver有一个configUpdated(config)接口,就是让接收者用来升级自身的配置的,由gossip服务器实现,即在NewConfigEventer()中初始化configEventer对象实例时,receiver被赋值为gossip服务器实例本身。configEventer对象有一个ProcessConfigUpdate(config)接口实现,将传入的最新的配置数据config转换成适当的形式存储在lastConfig中,最后让receiver,也即gossip服务器,调用自身的configUpdated(config)升级自己的配置。追溯configUpdated,最终调用了channel模块的ConfigureChannel()将配置数据存储在对应的channel实例里面,channel实例再据此配置数据来验证结点和结点传送来的消息等,如IsOrgInChannel这个验证函数在HandleMessage这个处理消息函数中被调用。如此,调用者从gossip服务器实例的NewConfigEventer()获取该gossip服务器的配置对象,然后调用这个对象的ProcessConfigUpdate()传入配置数据,即可完成对gossip服务器的配置更新。这里补充两点,第一,配置数据有哪些:lastConfig的原型为configStore,封装了成员anchorPeers和成员orgMap,前者存储了频道中所有组织中的所有结点的anchor(锚点,即包含一个结点的地址,端口),后者存储了频道中所有组织的config.ApplicationOrg类型的配置,这个类型是configtx工具使用的配置对象,在eventer.go中实现为appGrp,包含了一个组织的名字,MSP服务ID,组织内所有结点的anchor。也即,配置的中数据项就包含这些而已。第二,ProcessConfigUpdate()所接收的配置config的数据有哪些:config原型是eventer.go中的Config,这个是一个兼容接口,/fabirc/common/config/application.go中的ApplicationConfig和/fabric/common/configtx/manager.go中的configManager各实现了Config的一部分,而/fabirc/core/peer/peer.go中的chainSupport则综合了两者,最终传入ProcessConfigUpdate()的其实也就是chainSupport实例,这点可以从peer.go的createChain函数中看出来,fabric中只有这个函数中有调用ProcessConfigUpdate()

(2)初始化频道 - 这里的初始化频道主要指为gossip服务器初始化一个所在频道的state模块,然后根据配置决定是使用election模块还是直接使用分发客户端deliveryService的服务。这个过程体现在InitializeChannel()函数,state模块的初始化参看下文,这里主要说根据配置二选一的问题。gossip服务器分发数据的工作有两种模式,一种随着结点在网络中的变动,动态的选举leader后,由选举出leader结点启动deliveryService服务开始负责分发数据,这就要启用electioin模块;另一种是直接有配置文件静态的的指定某一个peer作为leader,由其直接启动deliveryService服务开始负责分发数据。两者由core.yaml中的peer.gossip.useLeaderElection和peer.gossip.orgLeader指定,这两项是互斥的,最多只能有一项是true值。即:若使用election模块动态选举leader,所有结点的useLeaderElection必须为true,orgLeader必须为false;若直接静态指定leader,所有结点的useLeaderElection必须为false,leader结点的orgLeader必须为true,其他结点的orgLeader必须为false。fabric中默认使用第二种,即指定的leader结点直接调用StartDeliverForChannel()以启动deliveryService服务。election模块的初始化参看下文,在每次选举后每个结点根据自身的角色通过onStatusChangeFactory()函数进行开启或停止自己的deliveryService服务。

election模块

election模块原型为leaderElectionSvcImpl,在election/election.go中定义,相关代码都在election目录中,可以将其理解为一个leadership管理模块,leadership可以翻译成领导关系,引申一下还包含了谁是领导,谁是群众,如何选出领导的意思。这里我们可以学习一下大神如何写The Algorithm in pseudo code的,即用“半code半语言”的方式描述一下算法,在election.go的开头注释中,详细描述了整个election模块的算法过程,十分清晰准确。election模块把网络中的所有结点分为leader(领导者)和follower(追随者,也可以说成群众)两种角色,处理的消息类型为LeadershipMessage(参看上文章节2.4),但LeadershipMessage消息成员IsDeclaration将消息分为proposal(申请)和declaration(声明)两种,proposal用于一个结点向其他结点自荐成为leader,declaration用于告知结点谁是leader。election模块有两大功能分区:(1)选举的算法,选举状态,领导者和群众的管理,(2)辅助选举而需要进行消息的收发,election模块自身专注于(1),而把(2)交给了一个gossip适配器,即成员adapter,adapter的接口原型为LeaderElectionAdapter,在election/adapter.go中实现。其实在gossip服务器的service/gossip_service.go中,有一个专门生成election模块的函数newLeaderElectionComponent,分别调用了适配器专用生成函数NewAdapter和election模块专用生成函数NewLeaderElectionService,在这里,NewAdapter(g,...)传入了gossip服务器实例自身,自然这个适配器拥有收发消息的能力。

一个election模块初始化时,在专用生成函数NewLeaderElectionService中,初始化leaderElectionSvcImpl对象后,新启了一个goroutine执行start()函数开始运行这个election模块的服务,共做了三件事:

  • go le.handleMessages() - 开启处理消息服务。首先,由适配器adapter获取接收消息的频道,msgChan := le.adapter.Accept(),msgChan是专门处理election模块中特定消息类型msgImpl的,所以Accept()中新启了一个goroutine去把gossip服务中获取的proto.GossipMessage类型的数据转成msgImpl类型的消息并转发到了msgChan中。然后,在for循环中持续接收msgChan中到来的一条条消息,交由handleMessage函数处理,处理的逻辑是:(1)如果消息是proposal,则记录到election模块成员proposals中,即把所有其他结点送来的想要成为leader的“自荐信”先放起来。(2)如果消息是declaration,则把已经存在leader的标识leaderExists置为1,然后若此时模块正在休眠且interruptChan是空的,则向interruptChan发空命令唤醒模块,若自己当前是leader而declaration中的新leader又不是自己,则调用le.stopBeingLeader()停止当leader。这里的interruptChan可以理解为模块的中断休眠频道,模块成员sleeping标识着模块当前是否在休眠,而waitForInterrupt(time)函数即通过select{...}让模块进入休眠,等待time时间后或declaration消息一旦到来,再置sleeping为false,即唤醒模块。

  • waitForMembershipStabilization - 等待成员关系固定化。这里的成员关系指的是网络中存在的所有活着的结点的数量,固化指成员关系是稳定的。这个等待的过程是:确定当前时间为N后开始判断,使用适配器获取当前成员数量viewSize(追溯适配器可知,获取成员数量使用的还是discovery模块),然后进入for循环不断地判断成员关系是否稳定,判断的标准是,每隔一秒重新获取最新的成员数量newSize,若newSize == viewSize,则称当前成员关系是稳定的,否则,更新viewSize为最新发现的成员数量,即newSize = viewSize,进行下一轮判断。若整个判断过程超过N+15s的时间点(15s是由core.yaml中的peer.gossip.election.startupGracePeriod指定),或leader已经出现(即接收到declaration消息),则停止判断。这里需要理解,在一个新初始化的chain中,新的结点陆续加入到网络中,被gossip服务的discovery模块发现并记录,这个过程还是很快的,可能几毫秒短的时间内就会发现若干个新结点,所以在判断成员关系是否稳定时,若在等待1s后新获取的成员数量还是等于原来的成员数量,那么election模块就认为在1s“这么长”的一段时间内都没有新结点加入,则就“自认为”当前网络中所有活的结点都已经被发现了,即成员关系固定了。另外,这个等待成员关系固定化的过程是一个辅助election模块进行选举leader的,不能无限期的等待下去而耽误了选举的正事儿,所有给规定了15s,要是15s还没固定,那就不等了。自然的,要是这期间已经有了leader,那也就没有选举的必要了,没选举的必要,更没再等待固定化的必要了,所以这种情况下会停止判断。

  • go le.run() - 在上两步的基础上,这一步进行election模块所应该做的本职的正事儿。(1)如果现在还没有leader,则调用le.leaderElection()进行选举,这里与其说是选举,不如说是进行自荐,在leaderElection()中,调用le.propose()向其他结点传播自己的“自荐信”(即包含自己身份信息的proposal消息,其他结点收到后会在它们的handleMessage中存储这封信)。然后调用waitForInterrupt(time)让模块进入休眠,这里的time为5s,由core.yaml中peer.gossip.election.leaderElectionDuration指定,在这个休眠的过程中既接收其他结点发来的它们的“自荐信”并存储在proposals中,也等待可能出现的declaration消息(参看上文第一步)。在模块被唤醒后,判断此时是否有leader存在,若存在(说明在休眠期间接收到了declaration消息),则自己放弃成为leader,若不存在,则拿自己的身份与proposals中已经收到“自荐信”中的其他结点的身份一一进行对比,看看自己是否比他们都更有资格当leader。这里还是比较有趣的,也算是无厘头的,这里所说的身份是一个结点的PKI-ID,而判断谁更有资格当leader的标准是bytes.Compare(peerID(id), le.id),即谁的身份的二进制值更小,谁更有资格。嗯,怎么说呢,这样的标准好像没什么现实中的意义,不过gossip里面当leader是干更多活的,也没有什么其他特权和优待,所以呢,随便选选就好啦。若proposals中所有的身份都没有自己的身份小,那么自己就当仁不让当leader,通过调用le.beLeader(),真正成为一名leader:把已经有leader和自己是leader的标识,即成员leaderExists和isLeader,都置为1,然后开始调用le.callback(true)这个“倒钩”函数做leader在gossip中该做的事情。(2)选举后,如果自己是leader,则执行le.leader(),如果自己是follower,则执行le.follower(),都是自己所成为的角色该做的事情。leader要做的事儿:循环地每休眠5s后(因为自身是leader,所有不可能再从其他结点中接收到declaration消息而使休眠中断)调用le.adapter.Gossip(...)向其他结点传播发送declaration消息来宣告自己是leader;follower要做的事儿:清空“信箱”proposals以备下一轮选举,置leaderExists为0(用以等接收到leader发来的declaration消息后再置为1),然后用select{...}等待10s,这个10s由core.yaml中的peer.gossip.election.leaderAliveThreshold指定,10s结束后如果还没收到leader发来的declaration消息,即leaderExists还为0,则该结点将再发起新一轮的选举。这里仍然是,follower认为10s这么长的时间足够leader发送一条declaration消息给自己,若10s过后还没有收到,则follower“自认为”这个leader已死,则自己将发起新一轮的选举。(3)第(1)和第(2)在run()中循环进行的。

当一个结点调用le.beLeader()真正成为一名leader时(参看上文第三步),调用了le.callback(true),当一个结点调用le.stopBeingLeader()停止作leader时,调用了le.callback(false),两者只是参数不同。“倒钩”函数callback由election模块初始化时由参数传入,具体的赋给的值是service/gossip_service.go中的onStatusChangeFactory(...)返回的函数func(isLeader bool) {...},即根据是否是leader而分别执行g.deliveryService.StartDeliverForChannelg.deliveryService.StopDeliverForChannel,从这点就可以看出如上文所说的,leader多干了DeliverForChannel向其他结点分发数据相关的一些事情,这个涉及到ordering服务的数据分发客户端,会在相关主题文章中详述。

state模块

state模块原型为GossipStateProviderImpl,在state/state.go中定义,相关代码都在state目录中,可以将其理解为gossip传播和提交状态消息的管理模块,这里的状态的意思与账本数据,block数据的意思(参看上文章节2.3)一致。state模块也封装了一个适配器,election模块类似,也是被赋值为gossip服务器实例,在此不讲,而把笔墨集中在消息处理流程上。在state模块中,消息处理流程如下图:

state_module.png

参看流程图,蓝色虚线以上,以黑色箭头为流的为结点A的state模块state_A,蓝色虚线以下,以红色箭头为流的为结点B的state模块state_B。state模块处理三种消息类型:DataMessage,StateResponse,StateRequest。在state模块专用初始化函数NewGossipStateProvider中,初始化GossipStateProviderImpl对象后新启动了4个goroutine来开始运行state模块的服务。这些均在流程图中有所反映:

(1)payloads既是一个存储器,也是一个消息排序输出器,原型为PayloadsBufferImpl,在state/payloads_buffer.go中定义。state_A把接收的所有消息Push进payloads,一旦所接收的消息的序号SeqNum == next(next指所期望的下一个消息序号,消息序号即是一个频道的状态高度,也即一个频道的block高度),则向readyChan通道发送ready命令以指示所期望的消息已经收到,即当前阶段的数据已经准备好了。这里有一个前提Q是:ordering服务所输出的消息都是排序过并依次输出给state模块的。比如,在Push()函数中,若next为2,则说明序号为1的消息之前肯定已经被接收并处理过,当前所想要的是接收序列号为2的消息,在前提Q下,state模块再收到消息的序号也只可能是1或2,因为ordering服务可能因某些原因重发已经发过的消息1,但绝不可能跳过序号为2的消息而去发序号为3的消息。因此,若Push进的消息的序号小于2,则直接返回,若等于2,则存储成员buf发送ready命令。此外,Pop()函数从payloads中弹出一个消息给state模块处理,同时将next**增1**。比如一旦序号为2的消息被弹出交由state模块处理,则next变为3,即此时序号为2的消息已在处理且payloads所期望的消息变为了序号为3的消息。如此,由前提Q,Push()Pop()三者相互配合,实现payloads的消息排序输出功能

(2)启动的listen()循环接收来自gossipChan,commChan通道的消息,接收后分别交由queueNewMessage()directMessage()处理。两个通道均在state模块初始化时由gossip服务的Accept()函数生成,且均在msgPublisher模块(参看上文章节3.3.2)中完成订阅,即gossipChan接收出版的标准的GossipMessage类型消息,实质上只处理DataMessage类型的消息,commChan则接收由comm模块转发来的ReceivedMessage类型的消息,实质上只处理StateRequest,StateResponse两种类型的消息。当state_A接收到DataMessage消息时直接Push进payloads;state_B在初始化时(也)启动了processStateRequests()循环监听stateRequestCh通道,当state_B接收到来自state_A的StateRequest消息时,说明A在向B索要**一定序号范围内的**block数据,state_B则将StateRequest消息经由stateRequestCh通道发给handleStateRequest()函数,handleStateRequest()函数将自身存储的这些数据封装成StateResponse类型的消息,然后直接使用从A处接收的StateRequest消息中所携带的A的地址信息和方法Respond()把StateResponse消息回复给state_A。

(3)state_A在初始化时(也)启动了antiEntropy()这个反熵函数,每间隔10s执行一次去尝试调用requestBlocksInRange(),这个10s由state/state.go中的defAntiEntropyInterval常量指定。反熵的本意就是降低一个事物的絮乱程度,在这里所做的就是尝试降低自己结点与其他结点之间状态的差异,即所存储的数据高度的差异。由于消息在网络中传播快慢有所不同,自然,每个结点间当前所存储处理的数据也不同,可能state_B已经在处理完了序列号为10的消息,而state_A才刚刚处理完序列号为7的消息。这时10s的周期到了,state_A调用committer.LedgerHeight()获取当前自身账本中的block高度current(假设为7),然后调用maxAvailableLedgerHeight()获取频道中其他结点中最高的高度max(假设为10),一看,current与max差了8-10三个数据块,则调用requestBlocksInRange()来弥补这个差异,差异弥补了,也就降低了整个频道状态的絮乱程度,也即反熵。在requestBlocksInRange()函数中,会尝试3次分批索要。3次由defAntiEntropyMaxRetries常量指定,分批则是在差异较大时,如几十块数据时,以10块(由defAntiEntropyBatchSize常量指定)为一批,一次次的索要,当然这里我们只差3块,只索要一次即可。索要时,先将stateTransferActive置为1以表明自己发生了索要行为,然后调用stateRequestMessage()生成StateRequest消息gossipMsg,再然后调用selectPeerToRequestFrom()随机从满足条件(即拥有8-10这三块数据)的结点中选出一个索要对象peer(假设peer为结点B),接着向调用gossip.Send(gossipMsg, peer)向state_B发送StateRequest消息后,进入stateResponseCh通道阻塞,以**等待**state_B的回复(对看上一点(2))。当state_A的listen()监听到state_B回复的StateResponse消息时,把消息交给directMessage()分流,此时若stateTransferActive==1,说明自己索要过StateResponse消息且当前正在等待这个回复(否则就说明state_A之前不存在索要行为也没有在等,也即当前state_A与其他结点状态暂时不存在差异,即便别人主动给state_A发了,state_A也不会处理),则再将StateResponse消息分流至stateResponseCh通道,此时阻塞等待结束,StateResponse消息被交由给handleStateResponse()处理,处理的过程无非就是把收到的消息中包含的自己索要的自己没有的块数据Push到payloads中以供进一步的处理(对看下一点(4))。antiEntropy()解决的是防止一个结点永远也跟不上其他结点的节奏或越来越落后于其他结点的状态。比如state_A所在环境导致处理消息的效率本身就很慢,state_B所在环境导致处理消息的效率较快,两者的环境一直处于不变的状态,则若没有这个反熵函数,则state_A与state_B的差距会越来越大。

(4)state_A在初始化时(也)启动了deliverPayloads()deliverPayloads()函数循环等待来自readyChan通道的ready命令。在以上(2)(3)两点state_A所接收的包含数据的消息,最后都Push进payloads中,当payloads处于准备就绪的状态时,会发送ready命令(参看第(1)点),deliverPayloads()函数一旦收到这个命令,就会将payloads中所存储的所有消息(一般只有一条)Pop出来,Unmarshal()后将数据封装成common.Block类型的rawBlock,最后调用commitBlock(rawBlock),一方面将数据提交至自己的核心账册:committer.Commit(block),另一方面更新自己的状态(高度):UpdateChannelMetadata(...)。这里的committer是核心代码中的数据提交模块,作用就是将数据提交至账本记录下来,将在相关主题文章中详述。

AddPayloard()也可以向payloads中Push消息,但这个函数是经由gossip服务器的AddPayloard()供外界手工调用以加入一条消息的。关于state模块初始化时有两个遗留问题,还是比较关键的,自己没有想明白,若有大神知道,还望赐教:

  1. 调用committer.LedgerHeight()获取当前自己结点的账本状况高度height,并将height作为next初始化了payloards,按理说从Ledger中获取的序号height,说明序号为height的消息已经处理并提交至账本了,为什么还要作为next的值,即payloards初始化时期望接收的是序号为height的消息,但这个消息不是已经处理过并提交至账本中了么,那此时state模块初始化后,ordering服务再发序号为height的消息给state模块吗?这点可能要在探究ordering服务的时候寻找相应的答案了。
  2. 在GossipStateProviderImpl对象初始化后,就调用了UpdateChannelMetadata(...)升级了自己的状态,但这里用的高度是height-1,这是为什么,是否和第1点相关?

gossip服务的停止

gossip服务,包括它控制的各个模块的停止,都是通过一个停止通道来实现。以gossip服务对象gossipServiceImpl为例,它的停止通道是成员toDieChan。在停止服务函数Stop()中,按顺序依次停止各个模块中,g.toDieChan <- struct{}{}语句就是向停止通道发送一个空命令,而正在服务的periodicalIdentityValidationhandlePresumedDeadacceptMessagesAccept函数中所起的goroutine,select接收到toDieChan频道发来的停止服务的空命令,随机return退出循环而结束goroutine,即停止了服务。gossip服务中的其他模块也遵循这种停止自身服务的模式。

猜你喜欢

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