本文主要介绍Consul中的重要核心库——Serf,解释其概念及作用,能够解决的问题,以及其内部通信原理。
Consul是用于服务发现和配置的工具。它提供了一系列高级功能,例如服务发现、健康检查和键/值存储。它使用一组高度一致的服务器来管理数据中心。其内部使用的gossip协议是由Serf库提供支持。Consul使用了Serf的成员管理和故障检测的机制,并在此基础上进行高级功能的构建。
而Serf主要用于集群成员管理、故障检测和编排的工具,它是分布式、高容错和高可用的。Serf能够运行在所有主要平台上:Linux、MacOSX和Windows。它本身是轻量级的,仅使用5-10 MB的常驻内存,并使用UDP消息进行通信。
Serf使用gossip协议来解决以下三个问题:
集群成员管理:Serf维护一个集群成员列表,并能够在成员更改时执行自定义程序脚本。例如,Serf可以维护负载均衡器的web服务器列表,并在节点联机或脱机时通知负载均衡器。
故障检测及恢复:Serf能在几秒钟内自动检测失败的节点,通知集群内部其他节点,并执行处理这些事件的程序脚本。它会通过定期重新连接失败的节点来恢复它们。
自定义事件传播:Serf可以向集群广播自定义事件,可以用来触发部署、传播配置等。Serf在面对离线节点或网络分区时采取最大努力传递消息策略。
Serf还可以用于服务发现和编排,但它是建立在最终一致性的gossip模型之上的,没有集中式服务器。但Serf并没有提供任何与Consul耦合的高级功能。Serf提供的成员是节点级别的,而Consul则更侧重于服务级别的抽象。Consul还使用了强一致的Catalog,而Serf只是最终一致。
Consul还提供了一个键/值存储和对多个数据中心的支持。它利用多个gossip池,保留Serf在LAN上的性能,同时仍然在WAN上使用它来连接多个数据中心。
Consul在使用方式上相对固定,而Serf则是一种更加灵活和通用的工具。Consul侧重于CP(一致性与分区支持)体系结构,更强调一致性而非可用性。Serf则是一个AP(可用性与分区支持)系统,它牺牲了一致性而强调可用性。这意味着如果中央服务器不能形成有效仲裁,Consul将无法运行,而Serf将在几乎所有情况下良好运行。
Gossip 协议
Serf使用gossip协议向集群广播消息。gossip协议是基于“可伸缩的弱一致的感染式进程组成员协议(SWIM协议)”并进行了一些小的修改,主要是为了提高传播速度和收敛速度。
SWIM协议概述
Serf从加入现有集群或启动新集群开始。如果启动一个新的集群,则需要其他节点加入它。要加入集群,必须向现有集群中的新节点提供至少一个现有成员的地址。新成员通过TCP与现有成员进行完全的状态同步,并开始向集群同步其自身的存在。
gossip是通过UDP完成的,具有可配置但固定的扇出和间隔。这确保了网络的使用相对于节点数量是恒定的。与随机节点的完整状态交换定期通过TCP完成,但比gossip消息要少得多。这增加了成员列表适当收敛的可能性,因为交换和合并了完整状态。完全状态交换之间的间隔是可配置的,也可以完全禁用。
故障检测是通过使用可配置的间隔进行周期性随机探测来完成的。如果节点未能在合理的时间内ack(通常是RTT的若干倍),则尝试间接探测。间接探测要求可配置数量的随机节点探测同一个节点,以防止由于网络问题而导致我们自己的节点探测失败。
如果探测在合理时间内失败,那么节点将被标记为“可疑”,并且这些信息将被传播到集群。可疑节点仍然被认为是集群的成员。如果集群中的可疑成员在一段可配置的时间内没有对怀疑提出异议,则最终认为节点已死,然后将此状态传播给集群。
SWIM协议变更
如前所述,gossip协议是基于SWIM的,但是包含了一些小的变化,主要是为了提高传播和收敛速度。
Serf定期在TCP上执行完全的状态同步,而SWIM只通过gossip传播更改。虽然两者最终都是一致的,但是Serf能够更快地达到收敛性,并从网络分区中优雅地恢复。
Serf有一个独立于故障检测协议的专用gossip层,而SWIM只在探针/ack消息之上集成gossip消息。Serf使用集成方式捎带专用的gossip消息。该特性拥有更高的传输率(例如每200ms一次)和更慢的故障检测率(例如每秒一次),从而导致总体更快的收敛速度和数据传播速度。
Serf会将离线节点的状态保留一段时间,这样当请求完全同步时,请求者也会收到关于离线节点的信息。而SWIM不做完全同步,所以在得知节点已离线,SWIM会立即删除离线节点及其状态信息。所以这一变更将再次帮助集群更快地收敛。
Lifeguard机制
SWIM假设本地节点是健康的,但是假设本地节点正处于CPU或网络耗尽的情况下,可能会导致节点的健康状况会出现问题,导致错误的监视警报,并进一步导致整个集群浪费CPU和网络资源,从而诊断出可能并不真正存在的故障。Serf 0.8增加了Lifeguard机制来解决这个问题。
第一个扩展是引入了一个“nack”消息来探测查询。如果探测节点意识到它丢失了“nack”消息,那么它就会意识到它可能会降级并降低故障检测器的速度。当nack消息开始到达时,故障检测器速率则还原。
第二个变更在于将一个节点声明为故障节点之前引入动态更改怀疑超时。探测节点将从一个非常长的怀疑超时开始。当集群中的其他节点都确认某个节点可疑,计时器就会加速。在正常操作期间,检测时间实际上与以前版本的Serf相同。但是,如果一个节点降级并且没有得到确认,则会有一个很长的超时,允许可疑节点反驳其状态。
这两种机制结合在一起,使得Serf对集群中降级节点的处理更加健壮,同时保持故障检测性能不变。
Serf-Specific消息
在基于SWIM的gossip层的顶部,Serf发送一些自定义消息类型。Serf大量使用Lamport时钟来维护消息排序的概念,尽管它们最终是一致的。Serf发送的每条消息都包含一个Lamport时钟时间。当节点优雅地离开集群时,Serf通过gossip层发送一个leave意图。由于底层gossip层不区分离开集群的节点和被检测为失败的节点,因此允许高级Serf层检测节点失败与优雅离开的场景。
当节点加入集群时,Serf发送连接意图。这个意图的目的仅仅是将一个Lamport时钟时间附加到一个连接上,以便在节点离开发生故障时可以正确地对其进行排序。对于自定义事件和查询,Serf要么发送用户事件,要么发送用户查询消息。此消息包含Lamport时间、事件名称和事件负载。因为用户事件是使用UDP的gossip层发送的,所以负载和整个消息帧必须依附于一个UDP包。
参考资料:
Serf内部机制介绍:https://www.serf.io/docs/internals/index.html