我们每个人都不能抱怨自己的出身,没有好的家世,那就去创造好的家世。要知道,那些在雨里奔跑的,从来都是没有伞的孩子。
回顾
gossip
先理解一下gossip协议:在一个有界网络中,每个节点都随机地与其他节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。每个节点可能知道所有其他节点,也可能仅知道几个邻居节点,只要这些节可以通过网络连通,最终他们的状态都是一致的,当然这也是疫情传播的特点。
简单的描述下这个协议,首先要传播谣言就要有种子节点。种子节点每秒都会随机向其他节点发送自己所拥有的节点列表,以及需要传播的消息。任何新加入的节点,就在这种传播方式下很快地被全网所知道。这个协议的神奇就在于它从设计开始就没想到信息一定要传递给所有的节点,但是随着时间的增长,在最终的某一时刻,全网会得到相同的信息。当然这个时刻可能仅仅存在于理论,永远不可达。
memberlist
回忆一下memberlist的总体流程:
项目在memberlist.go 函数Create启动,调用sate.go中函数schedule
Schedule函数开启probe协程、pushpull协程、gossip协程
probe协程:进行节点状态维护
push/pull协程:进行节点状态、用户数据同步
gossip协程:进行udp广播发送消息。
memberlist利用点对点随机探测机制实现成员的故障检测,因此将节点的状态分为3种:
- StateAlive:活动节点
- StateSuspect:可疑节点
- StateDead:死亡节点
probe协程通过点对点随机探测实现成员的故障检测,强化系统的高可用。整体流程如下:
- 随机探测:节点启动后,每隔一定时间间隔,会选取一个节点对其发送PING消息。
- 重试与间隔探测请求:PING消息失败后,会随机选取N(由config中IndirectChecks设置)个节点发起间接PING请求和再发起一个TCP PING消息。
- 间隔探测:收到间接PING请求的节点会根据请求中的地址发起一个PING消息,将PING的结果返回给间接请求的源节点。
- 探测超时标识可疑:如果探测超时之间内,本节点没有收到任何一个要探测节点的ACK消息,则标记要探测的节点状态为suspect。
- 可疑节点广播:启动一个定时器用于发出一个suspect广播,此期间内如果收到其他节点发来的相同的suspect信息时,将本地suspect的 确认数+1,当定时器超时后,该节点信息仍然不是alive的,且确认数达到要求,会将该节点标记为dead。
- 可疑消除:当本节点收到别的节点发来的suspect消息时,会发送alive广播,从而清除其他节点上的suspect标记。。
- 死亡通知:当本节点离开集群时或者本地探测的其他节点超时被标记死亡,会向集群发送本节点dead广播
- 死亡消除:如果从其他节点收到自身的dead广播消息时,说明本节点相对于其他节点网络分区,此时会发起一个alive广播以修正其他节点上存储的本节点数据。
Memberlist在整个生命周期内,总的有两种类型的消息:
- udp**协议消息:**传输PING消息、间接PING消息、ACK消息、NACK消息、Suspect消息、 Alive消息、Dead消息、消息广播;
- tcp协议消息:用户数据同步、节点状态同步、PUSH-PULL消息。
push/pull协程周期性的从已知的alive的集群节点中选1个节点进行push/pull交换信息。交换的信息包含2种:
- 集群信息:节点数据
- 用户自定义的信息:实现Delegate接口的struct。
push/pull协程可以加速集群内信息的收敛速度,整体流程为:
- 建立TCP链接:每隔一个时间间隔,随机选取一个节点,跟它建立tcp连接,
- 将本地的全部节点 状态、用户数据发送过去,
- 对端将其掌握的全部节点状态、用户数据发送回来,然后完成2份数据的合并。
Gossip协程通过udp协议向K个节点发送消息,节点从广播队列里面获取消息,广播队列里的消息发送失败超过一定次数后,消息就会被丢弃。
使用示例
1 | package main |
这里通过一个简单的http服务查询和插入数据,找两台机器,第一台执行
1 | memberlist |
会生成gossip监听的服务ip和端口
使用上面的ip和端口在第二台执行
1 | memberlist --members=xxx.xxx.xxx.xxx:xxxx |
那么一个gossip的网络就搭建完成了。
1 | # add |
alertmanager 高可用实现
上文我们说到,alertmanager在初始化时调用了memberlist的create方法,返回了Peer结构体:
1 | // Peer is a single peer in a gossip cluster. |
然后加入集群和初始化状态
1 | // 集群peer的状态监听器已经进行注册成功,现在可以进行加入集群和初始化状态。 |
1 | // Join is used to take an existing Memberlist and attempt to join a cluster |
join的注释很详细了,最后起了一个协程 go peer.Settle(ctx, *gossipInterval*10)
,用于同步集群状态,如果同步完成就关闭 channel p.readyc
,后面判断集群状态是否OK,都是根据该 channel
判断的。
1 | // Settle waits until the mesh is ready (and sets the appropriate internal state when it is). |
可以看到上面这里是个死循环,实现了一个类似心跳机制,定时检测集群是否已经同步完成,接着往下看
1 | waitFunc := func() time.Duration { return 0 } |
1 | pipeline := pipelineBuilder.New( |
这里很重要,在pipeline中创建gossip协议检查就绪阶段,ms := NewGossipSettleStage(peer)
,和其他一些需要处理message的阶段
1 | // NewGossipSettleStage returns a new GossipSettleStage. |
那么这个就绪阶段是干嘛的呢?往下看
1 | go disp.Run() |
看下这个方法 d.run(d.alerts.Subscribe())
,其中 d.alerts.Subscribe()
返回一个告警遍历器接口。遍历器会返回还没有解决和还没有被成功通知出来的告警。遍历器所返回的告警,并不能保证是按照时间顺序来进行排序的。
看下 run
中的 d.processAlert(alert, r)
1 | func (d *Dispatcher) processAlert(alert *types.Alert, route *Route) { |
主要看下这个方法 _, _, err := d.stage.Exec(ctx, d.logger, alerts...)
Exec 循环执行 MultiStage 里面的每一个阶段。MultiStage 主要使用两个场景。
场景一: RoutingStage 的map[receiver name] MultiStage。
里面有集群Gossip阶段,静默Mute阶段,抑制Mute阶段,Receiver阶段。
场景二: FanoutStage 的切片,里面每个元素是一个 MultiStage。
里面有分组等待阶段,去重阶段,重试阶段,设置通知阶段。
也就是说每次有新告警过来的时候都会经历同步集群状态的阶段,保证当前集群状态是OK的。
同时当有多个prometheus往alertmanager发送消息的时候,可能发生告警重复的情况,在alertmanager中有个去重阶段(DedupStage)是处理这样的情况:
DedupStage用于管理告警的去重,传递的参数中包含了一个NotificationLog,用来保存告警的发送记录。当有多个机器组成集群的时候,NotificationLog会通过协议去进行通信,传递彼此的记录信息,加入集群中的A如果发送了告警,该记录会传递给B机器,并进行merge操作,这样B机器在发送告警的时候如果查询已经发送,则不再进行告警发送。关于NotificationLog的实现nflog可以查看nflog/nflog.go文件。
1 | // DedupStage filters alerts. |
具体的处理逻辑如下:
1 | func (n *DedupStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) { |
其中的nflog.Query将根据接收和group key进行查询,一旦查找到,则不再返回对应的alerts. nflog设置了GC用来删除过期的日志记录。防止一直存在log中导致告警无法继续发送.
这里有个疑问?上面我们看到判断集群状态是否OK的是用的 p.readyc
这个channel,这个channel在 go peer.Settle(ctx, *gossipInterval*10)
同步完成之后就关闭了,也就是说后面读取这个channel的话,都是同步成功的状态,如果中途有个alertmanager实例挂掉了,这个还会是集群组建成功的状态吗?
参考: