Cassandra集群没有中心节点,各个节点的地位完全相同,它们通过一种叫做gossip的协议维护集群的状态。通过gossip,每个节点都 能知道集群中包含哪些节点,以及这些节点的状态,这使得Cassandra集群中的任何一个节点都可以完成任意key的路由,任意一个节点不可用都不会造 成灾难性的后果。
gossip的学名叫做Anti-entropy(逆熵?),比较适合在没有很高一致性要求的场景中用作同步信息。信息达到同步的时间大概是 log(N),这里N表示节点的数量。
gossip有两种形式:anti-entropy和rumor-mongering。
gossip中的每个节点维护一组状态,状态可以用一个key/value对表示,还附带一个版本号,版本号大的为更新的状态。
消息的处理有3种方式,Cassandra采用第三种方式——Push-pull-gossip
Push-gossip | A节点将状态集合发送到B,B通过和本地的状态集合比较,返回 S(A)和S(B)的笛卡尔积 |
Pull-gossip | A发送一个摘要(digest,只包含key和version)给 B,B通过比较,仅仅返回A上需要更新的状态 |
Push-pull-gossip | 这种方式和pull-gossip一样,在B发送给A其需要更新的 状态的同时,会向A请求本地过期的状态 |
关于gossip协议本身更详细的信息可以参考这篇paper 。
当一个节点启动时,获取配置文件(storage-conf.xml)中的seeds配置,从而知道集群中所有的seed节点。
Cassandra内部有一个Gossiper,每隔一秒运行一次(在Gossiper.java的start方法中),按照以下规则向其他节点发 送同步消息:
第一和第二步好理解,通过第一步可以和当前活着的节点同步状态,以更新本地的状态,通过第二步可以尽早发现不可用的节点重新可用了。
第三步中的第一个条件,如果第一步中的节点不是seed,则向随意一台seed发送同步请求也比较好理解,因为seed理论上总是有较多的节点状态 信息。
第三步中第二个条件则有点难理解,当活着的节点数少于seed时,也需要向随机的seed发送同步消息。其实这里是为了避免出现seed孤岛。
如果没有这个判断,考虑这样一种场景,有4台机器,{A, B, C, D},并且配置了它们都是seed,如果它们同时启动,可能会出现这样的情形:
这时就形成了两个孤岛,A和B互相同步,C和D之间互相同步,但是{A,B}和{C,D}之间将不再互相同步,它们也就不知道对方的存在了。
加入第二个判断后,A和B同步完,发现只有一个节点活着,但是seed有4个,这时会再和任意一个seed通信,从而打破这个孤岛。
关于这个问题更详细的信息可以参考这里 。
Cassandra的每个节点都有一个实现了IEndPointStateChangeSubscriber接口的订阅者,它负责处理接收到的消 息,该接口包含以下方法:
方法名 | 含义 |
onjoin | 有机器加入到集群中 |
onChange | 有状态发生变更了 |
onAlive | 机器可用 |
onDead | 机器不可用 |
同时Gossiper本身实现了IEndPointStateChangePublisher,该接口包含register和unregister 两个方法,用于添加和删除订阅者。
gossip通信的状态信息主要有3种:
HeartBeatState由generation和version组成,generation每次启动都会变化,用于区分机器重启前后的状态
ApplicationState用于表示系统的状态,每个对象表示一种状态,比如表示当前load的状态大概是这样:(1.2, 20),含义为版本号为20时该节点的load是1.2
EndPointState封装了一个节点的ApplicationState和HeartBeatState
一个节点自身的状态只能由自己修改,其他节点的状态只能通过同步更新。
两个节点之间的一次同步过程可以用下图表示:
这里假设192.168.1.1(源节点)决定和192.168.1.2(目标节点)同步,首先源节点向目标节点发送 GossipDigestSynMessage包,这个包包括本机维护的所有节点的状态信息的最新版本摘要,摘要只包含key和version,不包含具 体的value,这样可以减小同步的带宽消耗。
当目标节点收到GossipDigestSynMessage包时,它需要做两件事:
当GossipDigestAckMessage构建完成后,会被发送给源节点
这里按照版本号差异大小排序的原因是每个Message允许发送的状态数量是有限的(参见Gossip.java中的 MAX_GOSSIP_PACKET_SIZE定义),这样可以保证比较老的状态(版本号差异大的)可以优先得到更新。
源机器接收到GossipDigestAckMessage后,使用发送过来的目标节点更新的状态更新本地的状态,这样源节点就获取到了目标节点上 比自己更新的状态。
同时源节点把包含在GossipDigestAckMessage中摘要对应的状态通过GossipDigestAck2Message发送到目标 服务器,目标服务器更新本地的状态,这样目标服务器也获取到了源节点上比自己更新的状态。
完成这样一次同步后,源节点和目标节点上的状态都得到了同步。这中工作方式还是比较优的