[go]gossip协议之memberlist

19 篇文章 2 订阅


Gossip是最终一致性协议,是目前性能最好、容错性最好的分布式协议。

Gossip协议

Gossip协议又称epidemic协议(epidemic protocol),是基于流行病传播方式的节点或者进程之间信息交换的协议,在P2P网络和分布式系统中应用广泛:

在一个处于有界网络的集群里,如果每个节点都随机与其他节点交换特定信息,经过足够长的时间后,集群各个节点对该份信息的认知终将收敛到一致。

节点会周期性地选择一些节点(一般是N,即fanout系数),把要交互的信息传递出去。

特点

Gossip协议的主要用途就是信息传播和扩散;也被用于数据库复制、信息扩散、集群成员身份确认、故障探测等。其有以下特点:

  • 扩展性:允许意增加和减少节点任,新增加的节点的状态最终会与其他节点一致。
  • 容错性:任何节点的宕机和重启都不会影响消息的传播,Gossip协议具有天然的分布式系统容错特性。
  • 去中心化:所有节点都可以是对等的,任何一个节点无需知道整个网络状况,只要网络是连通的,任意一个节点就可以把消息散播到全网。
  • 一致性收敛:消息会以一传十、十传百一样的指数级速度在网络中快速传播(传播速度达到了 logN),因此系统状态的不一致可以在很快的时间内收敛到一致。
  • 简单:协议的过程极其简单。

缺陷

分布式网络中,没有一种完美的解决方案;Gossip协议也存在不可避免地缺陷:

  • 消息延迟(不适合用于实时性要求较高的场景):节点只会随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网的,因此使用 Gossip 协议会造成不可避免的消息延迟。
  • 消息冗余:节点会定期随机选择周围节点发送消息,而收到消息的节点也会重复该步骤,因此就不可避免的存在消息重复发送给同一节点的情况,造成了消息的冗余,同时也增加了收到消息的节点的处理压力。

memberlist

memberlist是gossip协议的go实现库:
import "github.com/hashicorp/memberlist"

节点故障检测

所有节点分为三种状态:

  • StateAlive:活动节点
  • StateSuspect:可疑节点
  • StateDead:死亡节点

memberlist利用点对点随机探测机制实现成员的故障检测:

  • 随机探测:节点每隔一定间隔,会随机选取一个其他节点发送PING消息。
  • 重试与间隔探测请求:PING消息失败后,会随机选取N(由config中IndirectChecks设置)个节点发起PING请求,并对刚才失败的节点再发起一个TCP PING消息(可能UDP不通,但TCP通)。
  • 探测超时标识可疑:若节点在指定时间内没有收到要探测节点的ACK消息,则标记要探测的节点状态为suspect。
  • 可疑节点广播:启动一个定时器用于发出一个suspect广播
    • 此期间内如果收到其他节点发来的相同的suspect信息时,将本地suspect的确认数+1;
    • 当定时器超时后,仍然收到不到探测节点的alive消息,且确认数达到要求,则将该节点标记为dead。
  • 可疑消除:当节点收到标记本节点为suspect的消息时,会发送alive广播,以消除suspect标记。
  • 死亡通知:当本节点离开集群时或者探测的有其他节点被标记为死亡,会向集群发送节点dead广播。
  • 死亡消除:当节点收到标记本节点为dead广播消息时(可能本节点相对于其他节点网络分区了),此时会发起一个alive广播,以消除dead标记。

消息

Memberlist在整个生命周期内,有两种类型的消息:

  • UDP协议消息:传输PING消息、ACK消息、NACK消息、Suspect消息、Alive消息、Dead消息、广播消息;
  • TCP协议消息:用户数据同步、节点状态同步、PUSH-PULL消息

push/pull协程周期性的从已知的alive的集群节点中选1个节点进行push/pull交换信息。交换的信息包含2种

  • 集群信息:节点数据;
  • 用户自定义的信息:Delegate接口LocalState返回的数据。

方法

memberlist中一些方法说明:

  • func DefaultLANConfig() *Config:返回一个适合LAN的默认配置,在create之前可根据需要修改。
  • func Create(conf *Config) (*Memberlist, error):使用给定的配置创建一个新的成员节点。不会连接到任何其他节点(需通过Join连接);但会启动侦听器,以允许其他节点连接加入。
  • func (m *Memberlist) GetHealthScore() int:数字越低越好,0意味着“完全健康”。
  • func (m *Memberlist) Join(existing []string) (int, error):尝试通过连接给定主机并执行状态同步来加入集群。返回成功连接的主机数量,如果无法连接到主机,则返回错误。
  • func (m *Memberlist) Leave(timeout time.Duration) error:广播一条Leave消息,但不会关闭后台侦听器,这意味着节点将继续参与gossip和状态更新。
  • func (m *Memberlist) LocalNode() *Node:返回本地节点信息。
  • func (m *Memberlist) Members() []*Node:返回所有活着的节点列表。
  • func (m *Memberlist) NumMembers() (alive int):返回活着的节点的数量。
  • func (m *Memberlist) Ping(node string, addr net.Addr) (time.Duration, error):向指定名称的节点发起Ping。
  • func (m *Memberlist) ProtocolVersion() uint8:返回当前使用的协议版本。
  • func (m *Memberlist) SendBestEffort(to *Node, msg []byte) error:使用UDP向指定节点发送消息。消息的最大值取决与配置UDPBufferSize。
  • func (m *Memberlist) SendReliable(to *Node, msg []byte) error:使用TCP向指定节点发送消息。
  • func (m *Memberlist) Shutdown() error:关闭本节点。如果希望更优雅地退出集群,请在关闭之前调用Leave。
  • func (m *Memberlist) UpdateNode(timeout time.Duration) error:更新节点数据(metaData)。

集群示例

要通过memberlist创建集群,需要:

  • 创建节点
  • 设定消息队列
  • 加入集群

客户端代理

所有客户端节点,都需要实现以下接口:

type Delegate interface {
    // 节点meta数据,可用于存放节点相关的客户数据;可通过Node结构体的Meta获取
    NodeMeta(limit int) []byte

    // 节点接收到的所有用户数据,都会触发此消息(不能阻塞)
    // 数据可以是其他节点通过broadcast广播,或类似SendReliable主动发送的
    NotifyMsg([]byte)

    // 获取要广播的消息
    GetBroadcasts(overhead, limit int) [][]byte

    // 要push的本地数据
    LocalState(join bool) []byte

    // 接收到的其他节点的数据
    MergeRemoteState(buf []byte, join bool)
}

LocalState与MergeRemoteState会定时周期调用,不适合做大量数据的收发。

type messageData struct {
    Action    int 		 `json:"action"`
    SeqId     uint64     `json:"seqId"`
    Data      any        `json:"data"`
}

type NodeClient struct {
    Cluster   *memberlist.Memberlist
    DataQueue *memberlist.TransmitLimitedQueue

    metaData string
}


func (cs *NodeClient) NodeMeta(limit int) []byte {
    return []byte(cs.metaData)
}

func (cs *NodeClient) NotifyMsg(msg []byte) {
    if len(msg) == 0 {
        return
    }

    data := &messageData{}
    if err := json.Unmarshal(msg, &data); err != nil {
        log.Printf("[ERROR] unmarshal fail: %v", err)
        return
    }
    log.Printf("[DEBUG] received notify message %v", data)

    go func() { // handle data
    }()
}

func (cs *NodeClient) GetBroadcasts(overhead, limit int) [][]byte {
    return cs.DataQueue.GetBroadcasts(overhead, limit)
}

func (cs *NodeClient) LocalState(join bool) []byte {
    return []byte{}
    // if has data to push, return the data
}

func (cs *NodeClient) MergeRemoteState(buf []byte, join bool) {
    log.Printf("Remote state: %v, join: %v", string(buf), join)
    if len(buf) > 0 {
        // merge the data from other node
    }
}

func (cs *NodeClient) addBroadcastData(action int, data any) error {
    if cs.Cluster.NumMembers() > 1 { // only broadcast when more than one node in cluster
        msg := &messageData{
            Action: action,
            SeqId:  GetSequenceId(),
            Data:   data,
        }

        out, err := msg.marshal()
        if err != nil {
            return err
        }

        broadcast := &messageBroadcast{out}
        cs.DataQueue.QueueBroadcast(broadcast)
    }

    return nil
}

事件代理

当节点加入或离开时会触发节点事件:在以下事件中,不能再去操作节点(如获取节点列表中),否则会引起死锁。

type NodeEvent struct {
    client *NodeClient
}

func (ce *NodeEvent) NotifyJoin(node *memberlist.Node) {
    log.Printf("[INFO] node %v joined: %v", node, node.Address())
}

func (ce *NodeEvent) NotifyLeave(node *memberlist.Node) {
    log.Printf("[INFO] node %v leaved: %v", node, node.Address())
}

func (ce *NodeEvent) NotifyUpdate(node *memberlist.Node) {
    log.Printf("[INFO] node %v updated: %v", node, node.Address())
}

对于标识为dead的节点,将不会再尝试连接;所以,若本节点发现所有其他节点都dead后,将会永久脱离集群,除非有其他节点主动连接。为了避免此类问题,可在NotifyLeave中(发现所有其他节点都dead时)通过后台线程主动尝试连接其他节点。

创建节点

以创建一个Lan节点,并加入集群为例:

  • 若节点需要映射对外端口,则需要设定AdvertiseAddr与AdvertisePort;
  • 通过设定Logger,可控制节点的日志输出;
  • Name是节点的名称,在集群中要保证唯一;
client = &NodeClient{}

lanConf := memberlist.DefaultLANConfig()
lanConf.BindPort = bindPort	// port to bind
lanConf.Name = nodeName
lanConf.Logger = log.Default()
lanConf.Events = &NodeEvent{client}
lanConf.Delegate = client

outAddr := natAddress // if need nat address(ex. it is in docker)
if len(outAddr) > 0 {
    log.Printf("[INFO] node %v use nat address %v", idConfig.NodeId, outAddr)
    if h, p, err := net.SplitHostPort(outAddr); err == nil {
        lanConf.AdvertiseAddr = h
        lanConf.AdvertisePort, _ = strconv.Atoi(p)
    } else {
        log.Printf("[ERROR] invalid nat address: %v", err)
    }
}

cluster, err := memberlist.Create(lanConf)
if err != nil {
    log.Printf("[ERROR] creat node %v on port %v fail: %v", idConfig.NodeId, options.BindPort, err)
    return nil, err
}
local := cluster.LocalNode()
log.Printf("[INFO] self %v start on %v ok", local.Name, local.Address())

client.Cluster = cluster
client.DataQueue = &memberlist.TransmitLimitedQueue{
    NumNodes: func() int {
        return cluster.NumMembers()
    },
    RetransmitMult: 3,
}

count, _ := cluster.Join(members) // members is the address({IP}:{Port}) of other node in the cluster
log.Printf("[INFO] joined %v members", count)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值