Serf使用Gossip Protocol来广播消息到集群中。本文介绍这个内部协议的细节。gossip协议基于“SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol”,有一写小的适配,很大程度上增加了传播速度和收敛速率。
SWIM Protocol Overview
Serf以加入一个已存在的集群或者启动一个新集群开始。如果启动一个新集群,其他节点则会加入它。为了加入现有集群,新节点必须至少分配一个已经存在的节点的地址。新的成员与现有成员通过TCP做一个完整的状态同步并且开始gossip它的存在到集群中。
Gossip基于UDP并且具有一个可配但是固定的扇出(fanout)和间隔。这保证网络资源使用是固定的,不论节点数有多少。通过TCP随机的与一个节点周期的进行完整状态交换,但是远远少于gossip消息。全状态交换和合并会增加成员关系列表收敛的可能性。全状态交换的间隔是可配的或者完全停止。
故障检测通过可配间隔的周期性的随机探测实现。如果节点在一个响应时间内(通常是RTT时间的倍数)没有进行ack,会尝试间接探测。间接探测要求可配数量的节点来探测相同的节点,以防网络问题导致我们的节点探测失败。如果我们的探测和间接探测都在响应时间内失败,则该节点被标记为“suspicious”并且会被gossip到集群中。一个可疑节点仍然视为集群的成员。如果集群的可以节点在一个可配的周期时间内对怀疑没有争论,则节点最终被认为死亡,这个状态被gossip到集群中。
这是对协议的一个简短和不完整的描述。更好的方式是完整阅读 SWIM论文和Serf的源代码。
SWIM Modifications
如前所述,gossip协议基于SWIM,但是包含小的变化,很大程度上增加了传播速度和收敛速率。
SWIM的变化都记录在这:
- Serf通过TCP定期的做完整状态同步。SWIM只通过gossip传递消息。虽然最终都是一致的,但是Serf可以更快速的收敛,以及优雅的从网络分区中恢复。
- Serf有一个从故障检测协议分离出来的gossip层。SWIM只通过在probe/ack消息上附带gossip消息。Serf基于专用的gossip消息传递。这个特征允许你有一个更高的gossip速率(例如200ms一次)和更低的故障检测速率(例如每秒一次),使得整体更快的收敛速率和数据传播速度。
- Serf保持死亡节点设置为死亡的时间,所以当完整的同步请求时,请求者也接收死亡节点的信息。因为SWIM不做完整同步,并且SWIM一旦知道节点死亡则立即删除节点状态。这个改变再一次帮助集群收敛更快。
Lifeguard Enhancements
SWIM假设本地节点是健康的,也就是说可以软实时处理数据包是可能的。然而,在本地节点正在经历CPU或者网络资源耗尽的情况下,这个假设就不成立了。结果是会导致节点健康偶尔振动,导致错误的监控报警,增加遥测噪声,并且直接导致整个集群浪费CPU和网络资源来诊断一个可能并不存在的故障。
Serf 0.8版本添加了Lifeguard,它完全解决了这个问题通过增强SWIM。
第一个扩展是引入了“nack”消息来探测查询。 If the probing node realizes it is missing "nack" messages then it becomes aware that it may be degraded and slows down its failure detector. As nack messages begin arriving, the failure detector is sped back up.
第二个变化是引入了在声明另一个结点故障之前动态改变怀疑超时时间的功能。探测结点初始化时有一个很长的怀疑超时。只有集群中的其他节点确认一个节点是可以的,计时器加速。在正常操作期间检测时间实际上与早期版本的Serf一样。然而,如果一个节点被退化,并且没有得到确认,则会有一个很长的超时时间来允许被怀疑的节点来反驳它的状态并且保持健康。
这两个机制联合使得Serf对于集群中退化的节点更加健壮,同时保持故障检测性能不变。Lifeguard没有额外的配置,它自动调节。
Serf-Specific Messages
在基于SWIM的gossip层上,Serf发送一些自定义的消息类型。
Serf大量使用Lamport clocks来维护消息的顺序,虽然最终是一致的。每个由Serf发出的消息都包含一个Lamport clock时间。
当一个节点优雅的离开集群时,Serf通过gossip层发送一个leave intent。因为gossip层以下不区分节点离开集群和一个节点被检测为故障的,这允许更高级别的Serf层来检测故障与优雅的离开。
当一个节点加入集群时,Serf发送一个join intent。这个意图的目的仅仅是绑定一个Lamport clock时间到join上使得在leave乱序来临时,join可以被正确的排序。
对于自定义的事件和查询,Serf发送user event或者user query消息。这个消息包含Lamport时间,事件名称和事件负载。因为user event是沿着使用UDP的gossip层发送,负载和整个消息帧必须满足在单个UDP包内