从CAP理论到Raft算法

发布时间 2023-10-31 16:11:38作者: AlenYang

什么是分布式系统

分布式系统是支持分布式处理的软件系统,是由通信网络互联的多处理机体系结构上执行任务的系统。
一个业务拆分为多个子业务,落地成不同的服务,将各个服务部署在不同的容器上。各个服务之间通过某种协议通信交互。
好处是有更好的可靠性,可扩展性,但也带来了一致性问题。所以碰到分布式系统,主要就是分析他的一致性。
一致性问题产生的原因有很多,有网络问题,有节点响应不一致,分析一致性最常用的就是CAP理论。

CAP理论

  • C - Consistency - 一致性
  • A - Availabibity - 可用性
  • P - Partiton Tolerence - 分区容错性
    image

Partition Tolerence分区容错性

分区容错性要求即使由于部分节点宕机或者网络不通仍能对外提供服务。对于平行扩容的分布式系统来说天然用用这种特性,因为每一个节点都可以独立的对外提供完备的服务,也就是说既支持单机部署也支持分布式集群模式的应用都是有分区容错性的。如nacos,redis,kafka都属于这种情况,如Apache Druid就不是这种集群,它里面有协调节点,有数据节点,有负责查询的节点,哪至少得有这3个节点才能提供完整的服务。

Consistency一致性

一致性要求集群中各个节点的数据时刻都保持一致,这是理想情况,实际上我们只要保证一个写操作成功后,之后的读操作在每个节点上都能读到这个最新的数据即可。这种特性也被叫作强一致性。如zk。

Availability可用性

可用性要求系统一直可以提供服务,且要有正常的响应,这里的正常响应指的是不能有超时不能有错误,但不对返回的结果做要求。如nacos,kafka,redis。

CAP取舍

根据CAP理论,系统无法同时满足三种特性,所以需要根据使用场景进行取舍,
首先P是一定要满足的,如果一个分布式系统的部分节点不可达,导致无法提供完整的服务,我们做分布式的意义就没有了,初衷希望的是通过分布式来提高可靠性的,所以一般都是在CP和AP之间取舍。

CP中间件:Zookpeer,Redis主从,Mysql主从

AP中间件:Eureka,Nacos,Redis集群

【注】Nacos默认为AP模型,可以通过下面这个请求改为CP。

curl -X PUT http://localhost:8848/nacos/v1/ns/operator/switches?entry=ServerMode&value=CP

Kafka

Kafka提供了一些配置,用户可以根据具体的业务需求,进行不同的配置,使得Kafka满足AP或者CP,或者它们之间的一种平衡。
定制配置
比如下面这种配置,就保证强一致性,使得Kafka满足CP。任意写入一条数据,都需要等到replicate到所有节点之后才返回ack;接下来,在任意节点都可以消费到这条数据,即是在有节点宕机的情况下,包括主节点。

# 分区副本数
replication.factor = 3
# 最小写入数据份数
min.insync.replicas = 3
# acks = all/-1 : 表示kafka isr列表中所有的副本同步数据成功,才返回消息给客户端
# acks = 0 :表示客户端只管发送数据,不管服务端接收数据的任何情况
# acks = 1 :表示客户端发送数据后,需要在服务端 leader 副本写入数据成功后,返回响应
acks = all

而下面的配置,就主要保证可用性,使得Kafka满足AP。对于任意写入一条数据,当主节点commmit了之后就返回ack;如果主节点在数据被replicate到从节点之前就宕机,这时,重新选举之后,消费端就读不到这条数据。这种配置,保证了availability,但是损失了consistency。

replication.factor = 3
min.insync.replicas = 3
acks = 1

还有一种配置是公认比较推荐的一种配置,基于这种配置,损失了一定的consistency和availability,使得Kafka满足的是一种介于AP和CP之间的一种平衡状态。因为,在这种配置下,可以在容忍一个节点(包括主节点)宕机的情况下,任然保证数据强一致性和整体可用性;但是,有两个节点宕机的情况,就整体不可用了。

replication.factor = 3
min.insync.replicas = 2
acks = all

原文链接:https://blog.csdn.net/weixin_43469680/article/details/115204832

一致性算法

分类

  • 强一致性
  • 弱一致性
    • 最终一致性
    • 读写一致性
    • 单调读
    • 因果一致性

https://zhuanlan.zhihu.com/p/67949045/

强一致性模型

  • 主从同步,对于写操作主节点需要等待所有从节点的成功响应,如果一个节点失败就回滚。
  • 多数派,每次写都保证写入一半以上节点,每次读都保证从一半以上节点读。
    • Paxos
    • Raft
    • ZAB
2PC(two phase commit) - AP

两段式提交。第一段:主发起事务,向所有从节点发送事务内容,从节点响应成功,第二段:主发起commit,所用从节点提交事务。因为任意节点响应失败或超时,第二段:主都会发起rollback。

原理简单,但同步阻塞效率低,主在阶段一二间发生故障,所有节点都会锁定,第二段commit消息失败会导致数据不一致,如果网络波动大,几乎不可用。

3PC(three phase commit) - AP

三段式提交,依次为can commit,pre commit,do commit。

正常流程

image

can commit阶段从库只做资源的检查,比如是否有锁,并未加锁。

pre commit阶段从执行事务,加锁,并开启定时器。

do commit阶段从提交事务,解锁,删除定时器。

can commit响应失败或超时

主发起abort,从库未加锁,即使未收到abort也不会锁库。

pre commit响应失败或超时

主发起rollback,从库执行回滚,删除定时器。此时若从库未收到rollback则会等待超时并提交,而收到rollback的则会回滚,数据不一致发生。

NWR协议模型 - AP/CP

NWR协议是一种分布式存储系统中,用于控制一致性级别的一种策略。Kafka集群,亚马逊的云存储中,就是用NWR来控制一致性。

N:在分布式存储系统中标识有多少份备份数据。可以理解为节点
W:代表一次成功的更新操作至少成功写入的数据份数
R:代表一次成功读取数据,至少要求读取的数份数

NWR值的不同组合可以达到不同的一致性效果。

当W+R>N 系统数据一致性级别达到强一致性,也就是说一次成功读取数据一定会读取到最新的数据节点。因为R>N-W,N-W代表未写入更新的节点数量。
当W+R≤N 系统无法达到强一致性,因为R无法完全覆盖未更新数据的节点。
Gossip协议模型 - AP

核心思想是集群中任意连个节点间比较数据的版本,数据版本低的节点更新到更高的哪个节点。Redis Cluster就用的这种方式。

Gossip是一种去中心化的分布式协议,数据通过节点像病毒一样逐个传播。因为是指数传播,整体速度非常快。
优点:

拓展性:允许节点的任意增加和减少,新增节点的状态最终会与其他节点一致。
容错性:任意节点的宕机和重启都不会影响Gossip消息的传播,具有天然的分布式系统容错性特性。
最终一致性:Gossip协议实现消息指数级的快速传播,因此在有新消息需要传播时,消息可以快速的传播到全局节点。

缺点:

消息延迟:节点随机向少数几个节点发送消息,消息最终通过多个轮次的散播而达到全网,不可避免的造成消息延迟。
消息冗余:节点定期随机选择周围节点发送消息,收到的消息节点也会重复改步骤,不可避免的引起同意节点消息多次接收,增大消息压力。
Paxos协议模型 - CP

Paxos协议其实说的就是Paxos算法, Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。

分布式系统中常见的问题,例如:机器宕机、网络异常(消息延迟、丢失、重复、乱序、网络分区)等情况。
Paxos算法就是解决在可能发生上述异常的分布式系统中,快速且正确的在集群内部对某个数据的值达成一致,并且保证不论发生以上任何一场,都不会破坏系统的一致性。

原理

角色:

  • client - 客户端,请求的发起者
  • proposer - 提案者
  • accepter - 议员
  • learner - 记录者

流程:

正常流程

image

少数Acceptor失败流程

Acceptor失败时的basic Paxos,在下图中,多数派中的一个Acceptor发生故障,因此多数派大小变为2。在这种情况下,Basic Paxos协议仍然成功。

image

Promise消息不可达

Proposer失败时的basic Paxos Proposer在提出提案之后但在达成协议之前失败。具体来说,传递到Acceptor的时候失败了,这个时候需要选出新的Proposer(提案人),那么 Basic Paxos协议仍然成功。

并且提案编号要+1,回到请求准备阶段重来。

image

多提案者冲突

当多个提议者发生冲突时的basic Paxos 最复杂的情况是多个Proposer都进行提案,导致Paxos的活锁问题。只需要在每个Proposer 重复提案时加上一个随机等待时间即可解决。

image

Promise阶段发现提案号已过时,这时重新发起提案并且提案号+1,当并发场景下会导致多个提案只增加提案号,不执行提案的问题,并且无限循环,这种情况称为活锁。

Multi-Paxos协议模型 - CP

与Base-Paxos最大的区别在于有主节点概念,只要一个主节点接收提案即可,其他的节点通过复制主节点的数据。

正常流程

未确定Leader 先确定Leader再复制

image

已确定Leader 直接由Leader确定,并复制

image

Multi-Paxos 实施时会将Proposer、Acceptor、Learner合并,统称为服务器,这样到最后只有 客户端和服务器交互

image

zookpeer的ZAB协议实现了Paxos。

Raft协议 - CP

Paxos是分布式一致性协议的理论模型,实际工程要解决一致性问题也要兼顾可用性,经过工程实践最终都优化为了类似于Raft的协议。

原理

角色:

  • Follower - 从节点
  • Candidate - 竞选者
  • Leader - 主节点

概念:

  • term - 任期,自增
  • log - 预写日志,write ahead log,简称 wal
  • logIndex - 日志id,自增

竞选流程:

可以将每个节点的角色转换理解为一个状态机。

初态为Follower

Follower: 定时到期 -> Candidate

Candidate: 投票超时-> Candidate

Candidate: 获得半数以上投票 -> Leader

Leader: 发现更高任期的Leader -> Follower

Candidate: 收到原Leader或者新的Leader发起的心跳 -> Leader

image

单个节点按照上述的方式工作,如果集群中每个节点都这样工作就构成了Raft集群,并有如下Raft集群的特征:

  • 强Leader的设计
  • 某1任期内只能有1个Leader
  • Leader主动向所有节点发心跳,这里的所有节点包括宕机或网络不可达的节点
  • 任意节点都可以成为主节点,只要获得了半数以上的投票就可以成为Leader
  • 由于脑裂的修复,出现多个Leader,以任期高且日志多的Leader为准
  • 只有Leader节点可以写,其他节点只读,并只响应Leader的写操作
  • 某1任期内所有节点存储的term应与Leader同步
  • 某1任期内所有节点存储日志应与Leader同步

Q:1个集群中只有1个Leader如何保证?

A:开机的场景,所有的集群从Follower开始,经过一段随机延时时间内未收到Leader的心跳,Follower变为Candidate并像其他节点发起term=1的选举,其他节点本地未存储term则回复选票。

如果恰好两个Follower同时变为Candidate并发起选举,Candidate收到相同自己任期的的选举请求,就知道有节点竞争Leader,则会再随机一个时间重新发起选举并且term+1。

一个集群出现多个Leader时,term较小的Leader将变为Follower并记录更高的term以及同步新的主的数据。

日志同步流程:

这里的日志指的是预写日志(write ahead log,简称 wal),日志由内容和index组成。

正常流程:

同步的流程类似于2PC,client写请求发起给Leader节点,Leader写日志到本地后将预写日志发送给其他节点,其他节点记录日志并返回成功,Leader节点收到半数以上的成功响应后,对响应成功的节点发起提交,并对client回成功响应,Follower收到提交后将刚刚的日志提交给状态机。

Follower宕机后恢复流程:

对失败和未响应的节点会重复发起心跳,心跳即AppendEntries,其中包含enties(日志本体)和prevLogIndex(当前日志的前一条日志的index),如果一个Follower恢复了之后收到了这个心跳,发现prevLogIndex就是本地日志最新的index,则说明只缺失了最后一条日志,响应给Leader成功后,Leader会发起提交请求,Follower最将其提交给状态机。

Follower长时间宕机后恢复流程:

如果说发现prevLogIndex不是本地日志最新的index,则回复Leader失败,Leader随即会再发起一次AppendEntries,并携带上个失败的请求中的prevLogIndex对应的日志作为entries,和这条日志的前一个index作为prevLogIndex,直到找到和Leader节点最新的一个已同步了的日志,这样Follower就得到了一个日志链,响应给Leader成功后,Leader会发起提交请求,Follower最将这个日志链提交给状态机。

网络分区的恢复流程:

网络分区后会有多个主,由前面的选主流程中可知,term较高的Leader将成为新的主,那么就要以新的Leader的数据为准,由于网络分区的发生虽然有多个主,但是只可能有一个主可以响应给客户端成功,因为最多只可能有一个分区的Leader能响应成功,而其余的Leader虽然可以将日志发给可访问的Follower但是不够半数以上,则不能对client响应成功,所以对外看来,这些请求是未被执行的,只有多数派的Leader可以响应成功,并且多数派的Leader在恢复后依然是Leader(因为少数派的节点不足以选出新Leader,所以多数派的Leader节点的term一定是最高的),这样少数派即使有一些日志,也是未提交状态的,将会全部回滚,然后从新的Leader节点获取日志,并提交。

投票请求

image

心跳和日志请求

image

学习Raft算法最好的方式则是阅读论文:https://raft.github.io/raft.pdf

关于Raft协议步骤动画:http://thesecretlivesofdata.com/raft/

关于Raft算法的动画:https://raft.github.io/

参考:https://zhuanlan.zhihu.com/p/600147978?utm_id=0