前言
单机系统存在什么问题呢?
- 容错能力差,单机如果故障,那么相关数据、服务均无保证,恢复困难
- 性能受限,单机提供的吞吐量无法水平扩展,而垂直扩展存在瓶颈且代价巨大
因此,考虑将单机系统转换为集群系统,发挥团结的力量来提高容错、性能,达到高可用、高并发的要求。具体应用场景,比如新闻网站(新浪、qq…),购物网站(淘宝、京东…),12306等。
那么,问题来了,多机之间如何正确、高效的协同呢?如果没有一个协议能保证多个节点的数据一致性(先不考虑最终一致性、强一致性等),那么集群就没有了意义。
举个例子,试想你妈让你帮她买火车票,她在家里查看余票信息是有的,然后让你给她买票。结果你登录之后,发现并没有余票,但是你妈一直说有余票。分布式情况下,如果你和你妈的查询请求被转发到不同机器,而这两个机器的信息没有保持一致,上述场景是很有可能出现的。
为此,Lamport发表了著名的Paxos协议。Paxos解决了集群一致性的问题,并且理论证明了正确性。但是,存在协议复杂、难以工程化等缺点。后人也发表了相当多的解释Paxos的论文,也有一些系统声称是基于Paxos实现的(GFS、HDFS、Spanner…?)。有人这么讲,因为Paxos算法和实践的巨大鸿沟,最终实现的系统将基于未证明的协议。
总而言之,虽然Paxos理论上解决了多节点的共识问题,但工程上有巨大的改进空间。
为了解决难以理解+难于实现两大难题,诞生了Raft算法。
一、解决之道
1、分解
面对复杂的大问题,将其拆解为若干稍简单的稍小问题,从而逐个击破是一个好思路。Raft就将整个协议分解成了三大块:
- leader选举
- log复制
- 安全性
2、降低状态空间大小
减少不确定性的程度,减少服务器间不一致程度。
比如,log不允许有洞(即不连续)
3、平衡
在有多种解决方案的情况下,倾向于选择易理解的方案,这可能在某些场景下牺牲性能。
与已有算法的区别
- 强leader,只能主节点(leader)更新从节点log,从节点不能更新主节点
- leader选举,随机定时器来选举leader
- 成员更新
优点
- 简单、易理解;
- 易于工程化实现;
- 存在多个开源实现并被多个公司使用;
- 安全性被形式化证明;
- 足够高效。
复制状态机(SMR)
复制状态机通常用复制log实现,当然可以用复制状态实现。复制log,要求每个节点的log以相同的顺序包括相同的命令。状态机是确定性的,那所有节点只要执行log,既可以获得相同状态。
共识算法的作用就是保持节点log的一致性。
共识算法的基本要求:
- safety(安全性),任何非拜占庭情况(网络延迟、分区、丢包、重复发、乱序等)下,不会有错误发生
- 可用性(活性?),只要大部分节点正常,服务就可用
- 一致性不依赖时机,错误的时钟、极端的消息延迟最多带来可用性问题,其实还是安全性
- 整体性能取决于大部分节点,少部分低性能节点不影响整体响应速度,还是可用性
基本概念
节点状态
- leader(主节点),任一任期leader数不大于1,处理所有命令并负责更新所有从节点
- follower(从节点),接收正确leader的命令,投票
- candidate(候选节点),尝试成为新leader
term 任期
全部时间被分割为严格递增的任期,每个新任期都需要一个选举阶段,一个生效阶段。与生活中的选举不同,Raft的选举阶段其实是无政府的混乱状态,现实中的老政府还在工作。
每个任期最多一个leader,可能由于选票被均摊而无法选出leader。因为不同于美国的2选1,Raft是N选1,而且规定必须获得半数以上选票才能当选,不存在联合执政。
另外一个点,Raft的leader是终身制。
任期类似逻辑时钟,标记信息是否落后。
leader选举
- 启动即进入从节点角色
- 等待leader的心跳,如果收到并合法,不变;否则等待超时并进入候选状态
- 进入候选状态,给自己投票,并向其它节点发送拉票请求
- 其它节点收到拉票请求后,合法则投票
- 候选节点拉到半数选票后,进入leader状态,并向其它节点周期性广播心跳
- 其它候选节点收到新leader广播后,合法则进入从状态
- 如果任期超时后,还没有选出新leader,则进入下一任期选举
candidate合法的条件
- 任期不落后本节点任期,其实这里要求大于也可以,因为等于的情况肯定不满足后面条件
相等意味着两个节点都是候选节点? - 本节点在新任期未投票,
- 候选节点log不落后于本地
log复制
leader负责将所有新log同步给所有从节点。如果当前term的log被大部分节点复制,则leader可以提交,并将消息更新给从节点,保证提交的log最终会被所有工作节点提交。
大致流程,leader维护一个数组nextIndex[],记录每次同步log的时候,每个从节点log的起始高度,初始为leader log最高高度。
理论上,只要match的高度与本地log最高高度不一致,leader就发送send[i]到本地最新高度的log。
如果[nextIndex[I], highest] 返回ok,那么nextIndex[I] = highest + 1;如果返回失败,则send[I]–,leader下次发送高度更低的log。
安全性(safety not security)
底线&红线:log只要在任意节点commit,就必须在所有节点正常之后commit!
选举限制
从节点收到拉票请求后,处理校验任期外,还必须校验log新鲜度?
新鲜度定义:
- 最新log term更大的,更新鲜(term是逻辑时钟!!)
- 最新log term相同的,log越长越新鲜
合法规则: 候选节点最新log term大于本地,或者term相同且log长度不短于本地,即本地不比候选节点更新鲜。当然,这个规则也可以变化!!
commit log的条件
commit的条件不是大部分节点都已经存储。关键原因是新鲜度的定义,如果新leader的term更高,即使log在大部分节点存储了,它还是可以覆盖的,只有他自身任期内的log,他自己不会覆盖。
反例:
假定3个节点,A,B,C,用<索引序列化-term>对代表一个log,比如0-0第一个0表示第一个log,第二0表示任期0。
- A是leader,log 0-0,term = 0
- A crash, B成为leader,log 0-1,term = 1
- B crash, A成为leader,C也有log 0-0了,此时,0-0已经在2个节点存储,term = 2
- A crash,B成为leader,因为term = 3,符合新鲜度规则,B的0-1将会覆盖A,C中0-0
当然,也可以改新鲜度规则,让B无法覆盖0-0,这个可以留作思考^^。
因此commit的条件是
- log在大部分节点存储
- log的term与当前leader的term相等
安全性证明
时机
整个协议保证,除非有拜占庭节点,否则任何情况下都满足安全性要求。
但是,系统的可用性依赖于时机。
broadcastTime ≪ electionTimeout ≪ MTBF