Cassandra集群没有中心节点,各个节点的地位完全相同,它们通过一种叫做gossip的协议维护集群的状态。通过gossip,每个节点都能知道集群中包含哪些节点,以及这些节点的状态,这使得Cassandra集群中的任何一个节点都可以完成任意key的路由,任意一个节点不可用都不会造成灾难性的后果。
一、Gossip算法背景
Gossip算法如其名,灵感来自办公室八卦,只要一个人八卦一下,在有限的时间内所有的人都会知道该八卦的信息,这种方式也与病毒传播类似,因此Gossip有众多的别名“闲话算法”、“疫情传播算法”、“病毒感染算法”、“谣言传播算法”。但Gossip并不是一个新东西,之前的泛洪查找、路由算法都归属于这个范畴,不同的是Gossip给这类算法提供了明确的语义、具体实施方法及收敛性证明。
二、Gossip算法特点
Gossip算法又被称为反熵(Anti-Entropy),熵是物理学上的一个概念,代表杂乱无章,而反熵就是在杂乱无章中寻求一致,这充分说明了Gossip的特点:在一个有界网络中,每个节点都随机地与其他节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致(很神奇)。每个节点可能知道所有其他节点,也可能仅知道几个邻居节点,只要这些节点可以通过网络连通,最终他们的状态都是一致的。
三、Gossip本质
Gossip是一个带冗余的容错算法,更进一步,Gossip是一个最终一致性算法。虽然无法保证在某个时刻所有节点状态一致,但可以保证在“最终”所有节点一致,“最终”是一个现实中存在,但理论上无法证明的时间点。因此Gossip适合没有很高一致性要求的场景。
因为Gossip不要求节点知道所有其他节点,因此又具有去中心化的特点,节点之间完全对等,不需要任何的中心节点。实际上Gossip可以用于众多能接受“最终一致性”的领域:失败检测、路由同步、Pub/Sub、动态负载均衡。
但Gossip的缺点也很明显,冗余通信会对网路带宽、CUP资源造成很大的负载,而这些负载又受限于通信频率,该频率又影响着算法收敛的速度。
四、Gossip节点的通信方式及收敛性
Gossip中的每个节点维护一组状态,状态可以用一个key/value对表示,还附带一个版本号,版本号大的状态比版本号小的新。两个节点(A、B)之间存在以下三种通信方式:
通信方式 | 含义 |
push | A节点将数据(key,value,version)及对应的版本号推送给B节点,B节点更新A发过来的数据中比自己新的数据 |
pull | A不发送数据的value,仅发送数据的摘要key和version给B。B根据版本比较数据,将本地比A新的数据(key,value,version)推送给A,A更新自己的本地数据 |
push/pull | 与pull类似,A仅发送摘要给B。不同之处在于,B比较版本后,不仅将比A新的数据发送给A,同时还向A请求A的摘要中比自己新的数据 |
如果把两个节点数据同步一次定义为一个周期,则在一个周期内,push需通信1次,pull需2次,push/pull则需3次,从效果上来讲,push/pull最好,理论上一个周期内可以使两个节点完全一致。直观上也感觉,push/pull的收敛速度是最快的。
假设每个节点通信周期都能选择(感染)一个新节点,则Gossip算法退化为一个二分查找过程,每个周期构成一个平衡二叉树,收敛速度为O(n2),对应的时间开销则为O(logn)。这也是Gossip理论上最优的收敛速度。但在实际情况中最优收敛速度是很难达到的。
显然pull的收敛速度大于push,而每个节点在每个周期被感染的概率都是固定的p(0<p<1),因此Gossip算法是基于p的平方收敛,也成为概率收敛,这在众多的一致性算法中是非常独特的。
Gossip的节点的工作方式又分为以下两种:
Anti-Entropy(反熵):以固定的概率传播所有的数据
Rumor-Mongering(谣言传播):仅传播新到达的数据
Anti-Entropy模式有完全的容错性,但有较大的网络、CPU负载;Rumor-Mongering模式有较小的网络、CPU负载,但必须为数据定义”最新“的边界,并且难以保证完全容错,对失败重启且超过”最新“期限的节点,无法保证最终一致性,或需要引入额外的机制处理不一致性。
五、cassandra中Gossip的节点同步规则
当一个节点启动时,获取配置文件中的seeds配置。cassandra作为一个去中心化的分布式系统,没有中心节点的存在。但为了让节点启动时能与集群通信,仍然需要为它配置最少一个seed节点。
Cassandra内部有一个Gossiper,每隔一秒运行一次(在Gossiper.java的start方法中),按照以下规则向其他节点发送同步消息:
1.随机取一个当前活着的节点,并向它发送同步请求
2.随机取一个不可达的节点,并向它们发送同步请求
3.如果第一步中所选择的节点不是seed,或者当前活着的节点数少于seed数,则向随意一台seed发送同步请求
第一步的目的是和目前活着的节点同步状态,第二步的目的是尽快发现已下线的节点重新上线了。第三步中的第一个条件,是因为seed理论上总是有较多的节点状态信息,若第一次同步的节点不是seed,则应该再和seed同步一下。第三步中的第二个条件则是为了避免出现seed孤岛。
如果没有这个判断,考虑这样一种场景,有4台机器,{A,B,C,D},并且配置了它们都是seed,如果它们同时启动,可能会出现这样的情形:
A节点起来,发现没有活着的节点,走到第三步,和任意一个种子同步,假设选择了B。B节点和A完成同步,则认为A活着,它将和A同步,由于A是种子,B将不再和其他种子同步。C节点起来,发现没有活着的节点,同样走到第三步,和任意一个种子同步,假设这次选择了D。C节点和D完成同步,认为D活着,则它将和D同步,由于D也是种子,所以C也不再和其他种子同步。
这时就形成了两个孤岛,A和B互相同步,C和D之间互相同步,但是{A,B}和{C,D}之间将不再互相同步,它们也就不知道对方的存在了。
加入第二个判断后,A和B同步完,发现只有一个节点活着,但是seed有4个,这时会再和任意一个seed通信,从而打破这个孤岛。
六、cassandra中Gossip的实现
Cassandra采用的通信方式是push/pull,如前文所述,push/pull有三个阶段。在每个阶段,节点之间都需要传递一些自己的状态信息。状态信息的传递是通过封装在一种特定的消息里传递,每个阶段传递的消息格式均不同,如下表:
消息名 | 含义 |
GossipDigitsSynMessage | A向B请求同步 |
GossipDigitsAckMessage | B返回自己拥有的比A新的数据给A |
GossipDigitsAck2Message | A再返回自己拥有的比B新的数据给B |
Gossip互相之间通信,通过上表的消息封装需要传递的信息。节点间互相交换的状态信息主要有以下3种:
状态信息 | 含义 |
HeartBeat | 心跳信息,由generation和version组成。generation每次系统启动都加1,用于区分重启前后的状态 |
ApplicationState | 用于表示系统状态,存储系统的负载信息等 |
EndPointState | 维护节点自身数据的全局version,并封装HeartBeat和ApplicationState |
Cassandra的每个节点都实现了IEndPointStateChangeSubscriber接口的,它负责处理接收到的消息,该接口包含以下方法:
方法名 | 含义 |
onjoin | 有机器加入到集群中 |
onChange | 有状态发生变更了 |
onAlive | 机器可用 |
onDead | 机器不可用 |
这里假设192.168.1.1(源节点)决定和192.168.1.2(目标节点)同步,首先源节点向目标节点发送GossipDigestSynMessage包,这个包有本机维护的所有节点的状态信息的最新版本摘要,摘要只包含key和version,不包含具体的value,这样可以减小同步的带宽消耗。
当目标节点收到GossipDigestSynMessage包时,它需要做两件事:
1.找出收到的消息中比本地版本新的状态,按照版本号差异大小排序,将这些状态的摘要放入GossipDigestAckMessage中。
2.找出本地比源节点版本更新的状态,放入GossipDigestAckMessage中,并将它发送回源节点。
这里按照版本号差异大小排序的原因是每个Message允许发送的状态数量是有限的(参见Gossip.java中的MAX_GOSSIP_PACKET_SIZE定义),这样可以保证比较老的状态(版本号差异大的)可以优先得到更新。
源机器接收到GossipDigestAckMessage后,同样也做两件事:
1.使用目标节点发送过来的比自己新的状态更新本地的状态,源节点获取到了目标节点上比自己更新的状态。
2.源节点把包含在GossipDigestAckMessage中,目标节点向自己请求更新的摘要对应的状态信息通过GossipDigestAck2Message发送到目标服务器。
目标服务器更新本地的状态,这样目标服务器也获取到了源节点上比自己更新的状态。完成这样一次同步后,源节点和目标节点上的状态都得到了同步。