一、gossip协议介绍
基本原理
Gossip protocol 也叫 Epidemic Protocol (流行病协议),实际上它还有很多别名,比如:“流言算法”、“疫情传播算法”等。
这个协议的基本思想就是:一个节点同步信息到整个集群中的所有节点时,每次只向集群中的几个随机节点发送消息,收到消息的节点也会发送消息给其他随机几个节点,直至整个集群中的所有节点都收到了这个消息。
优缺点
优点:
1.扩展性
网络可以允许节点的任意增加和减少,新增加的节点的状态最终会和其他节点一致
2.容错
网络中任何节点的宕机和重启都不会影响Gossip消息的传播,Gossip协议
3.去中心化
无需中心节点,所有节点都是对等的,任意节点无需知道整个网络状况,只要网络连通,任意节点可把消息散播到全网。
4.一致性收敛
消息会以“一传十的指数级速度”在网络中传播,因此系统状态的不一致可以在很快的时间内收敛到一致。消息传播速度达到了 logN。
缺点:
1.消息延迟
节点随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网;不可避免的造成消息延迟。
2.消息冗余
节点定期随机选择周围节点发送消息,而收到消息的节点也会重复该步骤;不可避免的引起同一节点消息多次接收,增加消息处理压力。
当然,优缺点都是相对的,我们还是需要了解其原理,以便和其他分布式协议进行比较。如果从CAP的角度出发,gossip协议的特点就是低一致性、高分区容错性、高可用性。
gossip类型
传播策略的分类:
1.Anti-Entropy(逆熵,大致意思就是降低混乱程度,也就是使所有节点的状态一致)
每个节点周期性的随机选择其他节点,然后通过互相交换自己的所有数据来消除两者之间的差异。这种方法比较可靠,但开销较大。
2.Rumor-Mongering
每个节点只在有了新的信息后,再周期性的通知其他节点,且只发送新信息,直到所有的节点都知道该信息。这种方法开销小,但可能有小概率不能将信息同步到所有节点。
通信方式的分类:
1.Push:每个节点只对外发送信息,收到信息的节点更新状态
2.Pull:每个节点只从其他节点拉取信息,拉取后更新本节点的状态
3.Push&Pull:每个节点对外发送信息,并从收到信息的节点拉取该节点的信息(也就是收到信息的节点会返回信息给发送方)
scylladb中的gossip方案
scylladb中使用Anti-Entropy + Push&Pull方案。
每个节点定期(1s)执行以下过程:
1.挑选一些节点
a)随机一个活节点
b)随机一个死节点,主要用于拉起死节点
c)随机一个seed节点
挑选方式可能有变化,大致是这样子。记gossip的发起者为A节点,接收者为B节点
2.A节点向B节点发送gossip_digest_syn消息。消息主要内容是A节点中保存的节点摘要列表(digests),digest包括IP地址以及版本
3.B节点收到gossip_digest_syn后,回复gossip_digest_ack消息给A节点。B节点会通过A节点的digests和本地的比较结果,发送差异信息以及节点摘要列表给A节点。
4.A节点收到gossip_digest_ack消息后,回复gossip_digest_ack2消息给B节点。A节点收到gossip_digest_ack消息后,同步信息到本地,并返回差异信息给B节点。
二、源码分析
gossip的相关代码在gms/gossiper.cc中,初始化在main.cc中调用相关接口进行。
初始化过程
main函数中的流程,scylladb初始化过程比较复杂,这里只整理了gossip的相关部分:
第一个只是获取了一下gossiper,没有进行什么操作,下一步gossiper.start内容比较多,创建了gossiper实例:
上面的gossiper::run是gossiper的主体方法,start执行完后只是设置了run方法到定时器中,还没有运行起来。
第三步init_gossiper主要是给gossiper传入了cluster_name、seed、配置等信息。
第四步开启了定时器,run开始运行。这是在storage_service中进行了,主要应该是需要先读取一些系统相关的表中的信息后,再运行gossip,所以需要先对storage_service进行一些初始化。
另外,第四步中还有对gossip的消息注册了handler,在gossiper::init_messaging_service_handler方法中,内容如下,前三个对应3各gossip消息的处理,其他的还没有研究在哪里使用:
注册 | handler |
_messaging.register_gossip_digest_syn | ([] (const rpc::client_info& cinfo, gossip_digest_syn syn_msg) { auto from = netw::messaging_service::get_source(cinfo); // In a new fiber. (void)smp::submit_to(0, [from, syn_msg = std::move(syn_msg)] () mutable { auto& gossiper = gms::get_local_gossiper(); return gossiper.handle_syn_msg(from, std::move(syn_msg)); }).handle_exception([] (auto ep) { logger.warn("Fail to handle GOSSIP_DIGEST_SYN: {}", ep); }); return messaging_service::no_wait(); }) |
_messaging.register_gossip_digest_ack | ([] (const rpc::client_info& cinfo, gossip_digest_ack msg) { auto from = netw::messaging_service::get_source(cinfo); // In a new fiber. (void)smp::submit_to(0, [from, msg = std::move(msg)] () mutable { auto& gossiper = gms::get_local_gossiper(); return gossiper.handle_ack_msg(from, std::move(msg)); }).handle_exception([] (auto ep) { logger.warn("Fail to handle GOSSIP_DIGEST_ACK: {}", ep); }); return messaging_service::no_wait(); }) |
_messaging.register_gossip_digest_ack2 | ([] (gossip_digest_ack2 msg) { // In a new fiber. (void)smp::submit_to(0, [msg = std::move(msg)] () mutable { return gms::get_local_gossiper().handle_ack2_msg(std::move(msg)); }).handle_exception([] (auto ep) { logger.warn("Fail to handle GOSSIP_DIGEST_ACK2: {}", ep); }); return messaging_service::no_wait(); }) |
_messaging.register_gossip_echo | ([] { return gms::get_local_gossiper().handle_echo_msg(); }) |
_messaging.register_gossip_shutdown | ([] (inet_address from) { // In a new fiber. (void)smp::submit_to(0, [from] { return gms::get_local_gossiper().handle_shutdown_msg(from); }).handle_exception([] (auto ep) { logger.warn("Fail to handle GOSSIP_SHUTDOWN: {}", ep); }); return messaging_service::no_wait(); }) |
_messaging.register_gossip_get_endpoint_states | ([] (const rpc::client_info& cinfo, gossip_get_endpoint_states_request request) { return smp::submit_to(0, [request = std::move(request)] () mutable { return gms::get_local_gossiper().handle_get_endpoint_states_msg(std::move(request)); }); }) |
gossiper.handle_syn_msg
上面对初始化的过程做了介绍,流程已经到gossiper::run方法中发送gossip_digest_syn消息了。
另一个节点收到gossip_digest_syn消息后,就要执行该消息的handler了,也就是gossiper.handle_syn_msg方法。
可以看到,这个方法的主要内容就是要判断哪些信息需要在后面的ack和ack2消息中进行pull/push。
gossiper.handle_ack_msg
ack2的handle就不细讲了,流程是类似的。
消息内容说明
- endpoint
标识一个节点,实际就是IP地址
- heart_beat_state
generation+version,generation指节点启动的时间,version指节点的当前版本,每执行一轮gossip本地节点的version会加1
- digests
节点摘要,就是endpoint+heart_beat_state
- versioned_value
value+version,value记录了一些值,值是有对应的version的,用于判断哪个节点上的信息是最新的,以便判断是否需要更新
- endpoint_state
heart_beat_state+map<application_state, versioned_value>,这个保存了各种状态的信息内容
application_state表示类型,versioned_value保存数据
application_state的取值如下,STATUS应该用于表示节点的在线/下线等状态,LOAD表示节点的负载信息。。。这些信息的更新在其他模块中进行,还没有研究,暂不详细介绍了。更新通过add_local_application_state方法进行,可以搜索该方法来查找代码中使用到的相关模块。
enum class application_state {
STATUS = 0,
LOAD,
SCHEMA,
DC,
RACK,
RELEASE_VERSION,
REMOVAL_COORDINATOR,
INTERNAL_IP,
RPC_ADDRESS,
X_11_PADDING, // padding specifically for 1.1
SEVERITY,
NET_VERSION,
HOST_ID,
TOKENS,
SUPPORTED_FEATURES,
CACHE_HITRATES,
SCHEMA_TABLES_VERSION,
RPC_READY,
VIEW_BACKLOG,
SHARD_COUNT,
IGNORE_MSB_BITS,
CDC_STREAMS_TIMESTAMP,
// pad to allow adding new states to existing cluster
X9,
X10,
};
syn消息主要就是digests
ack和ack2主要就是digests+endpoint_state,state内容视场景,可能只更新一部分内容,也可能发送全部信息。
故障检测
故障检测模块在类failure_detector中,相关的流程主要是:
1.gossiper的构造函数
调用构造函数进行初始化,设置了cfg中的参数:_fd(cfg.phi_convict_threshold(), std::chrono::milliseconds(cfg.fd_initial_value_ms()), std::chrono::milliseconds(cfg.fd_max_interval_ms()))
注册了event_listerner:fd().register_failure_detection_event_listener(this);
2.failure_detector::report 更新心跳信息
处理ack和ack2消息时,会调用notify_failure_detector,最终调用到failure_detector::report。
操作内容是更新failure_detector::_arrival_samples,保存的内容是所有节点的历史心跳数据。
3.failure_detector::interpret 获取故障判断结果
根据failure_detector::_arrival_samples中的内容计算phi,并与phi_convict_threshold比较,如果超过了就认为节点下线,会调用listener的convict方法。
phi_convict_threshold是配置的一个阈值,默认是8。
phi的计算方法:
phi = t / m
t是最近一次心跳和上一次心跳的间隔,心跳就是在report中更新的。m是过去n次的间隔平均值,n取的是SAMPLE_SIZE,定义的是1000。
故障检测的是Accrual Failure Detector算法(增量故障检测),但做了简化,计算方法简化了很多。