浅谈分布式一致性协议 | 青训营
这是我参与「第四届青训营 」笔记创作活动的的第3篇笔记*
一、分布式系统
1.分布式系统面临的挑战
- 数据规模越来越大
- 服务的可用性要求越来越高
- 快速迭代的业务要求系统足够易用
- 各种各样的错误
- 网络
- 磁盘
- CPU
2.理想中的分布式系统
- 高性能:可拓展、低时延、高吞吐
- 正确:—致性、易于理解
- 可靠:容错、高可用
1.容错: 通常指一种软件架构,架构可以容许一定数目的节点失效,同时保证系统正常或降级运行。
2.高可用: 指系统可以达到较高的可用时间,如系统保证一年中只有若干分钟不可用,通常以 SLA(Service-level agreement) 进行描述,容错架构是实现高可用系统的一种方式。
3.远程过程调用(RPC):分布式系统中通常将不同组件,或者不同节点的交互使用 RPC 的方式进行封装,在调用方的视角一次远程过程调用不需要关心如何对请求和响应进行编码,也不用关心具体的网络传输。
4.状态机(State Machine);一种编程架构,状态机只取决于当前的状态与的输入,确定下一个状态
5.并发/并行:
- 并发:多个任务可以在重叠的实践中启动、运行、完成,并发不一定意味着并行,可以通过切换的方式做到在一个执行单元上实现并发任务
- 并行:真正的多个任务同时运行,如多核CPU
二、一致性与共识算法
1.什么是一致性
1)从复制开始
- 主副本定期拷贝全量数据到从副本,造成问题:刚写完主副本,还没来得及拷贝到从副本,主副本就挂了
- 主副本拷贝操作到从副本: 满足需求
写操作
- 主副本把所有的操作打包成Log。所有的Log 写入都是持久化的,保存在磁盘上。
- 应用包装成状态机,只接收Log作为Input
- 主副本确认Log已经成功写入到副本机器上,当状态机 apply后,返回客户端
读操作
- 方案一:直接读状态机,要求写操作进入状态机后再返回client
- 方案二∶写操作复制完成后直接返回,读操作 Block 等待所有pending log进入状态机
- 如果不遵循上述两周方案呢? 可能存在刚刚写入的值读不到的情况(在Log中)
2)什么是一致性
- 对于我们的KV:像操作一台机器一样,要读到最近写入的值
- 一致性是一种模型(或语义):来约定一个分布式系统如何向外界(应用)提供服务
KV中常见的一致性模型:
最终一致性:读取可能暂时读不到但是总会读到
线性一致性:最严格,线性执行
- 一致性的分类:经常与应用本身有关
- Linearizability线性一致性是最理想、自然的
3)一致性协议的限制
-
对于分布式系统
- 拓展性:写入性能不能水平拓展
- 性能:强 Leader 的一致性协议跨地域部署时带来的额外网络开销
-
对于 KV 系统,解决方案一般是通过分片的方式水平拆组
- 引入了事务的概念
- 常见二阶段提交
- 2phase commit protocol
- propose阶段向各个raft group发送对应的操作,log apply中尝试加锁写入数据等,返回结果
- coordinator获得commit许可后,再次向所有raft group写入提交的log
- coordinator也需要保证自身的高可用
- 否则voter (raft组) 就一直不能获得 commit的执行结果
3)复制协议-当主副本失效
- 手动切换
- 容错?不,我们的服务还是停了
- 高可用?也许,取决于我们从发现到切换的过程的有多快
- 正确?
- 操作只从一台机器上发起
- 所有操作返回前都已经复制到另一台机器了
4)复制协议-小结
1.当主副本失效时,为了使得算法简单
- 我们人肉切换,只要足够快,我们还是可以保证较高的可用性。
2.但是如何保证主副本是真的失效了呢?
- 在切换的过程中,主副本又开始接收client端的请求
- 两个主副本显然是不正确的,log会被覆盖写掉
- 我们希望算法能在这种场景下仍然保持正确
3.要是增加到三个节点呢?
- 每次都等其他节点操落盘性能较差
- 能不能允许少数节点挂了的情况下,仍然可以工作
- fault-tolerance
2.共识算法
1)什么是共识算法
简而言之一个值一旦确定,所有人都认同。
2)面临的问题
- 错误总是发生
- Non-Byzantine fault(有进程突然停了或挂了,可以正确运行算法步骤)
- 错误类型很多
- 网络断开,分区,缓慢,重传,乱序
- CPU、IO都机会停住
- 容错(falute-tolerance )
- 有文章证明是一个不可能的任务(FLP impossibility ),这篇文章证明在异步的系统中,甚至无法容忍一个进程挂掉,设计不出来正确的共识算法。
"In this paper, we show the surprising result that no completely asynchronous consensus protocol cantolerate even a single unannounced process death. We do not consider Byzantine failures, and we assume that the message system is reliableit delivers all messages correctly and exactly once." -- JACM 85[1]
3)共识算法
- 共识协议不等于一致性
- 应用层面不同的一致性,都可以用共识协议来实现。比如可以故意返回旧的值
- 简单的复制协议也可以提供线性─致性
- 一般讨论共识协议时提到的一致性,都指线性一致性。
- 因为弱一致性往往可以使用相对简单的复制算法实现
三、一致性协议
1. Paxos
- The Part-Time Parliamen by Lamport 1989
- 也就是人们提到的Paxos
- 基本上就是一致性协议的的同义词
- 该算法的正确性是经过证明的
- 那么问题解决了吗?
- Paxos是出了名的难以理解,Lamport本人在01年又写了一篇Paxos Made Simple: “The Paxos Algorighm, when presented in plain English, is very simple."
- 算法整体是以比较抽象的形式描述,工程实现时需要做一些修改
- Google在实现Chubby的时候是这样描述的:here are significant gaps between the description of the Paxos algorithm and the needs of areal-world system. . . . the final system will be based on an unproven protocol .
2.Raft
-
2014年发表
-
易于理解作为算法的设计目标
- 使用了RSM、Log、RPC的概念
- 直接使用RPC 对算法进行了描述
- Strong Leader-based
- 使用了随机的方法减少约束
-
正确性
- 形式化验证
- 拥有大量成熟系统
-
Raft 是在 paxos 的基础上的基础上,着重于易于理解
- 协议接口直接暴露 Log 给用户
- 强 Leader 简化 Paxos 中的二阶段
- 使用随机事件简化选主逻辑
-
Raft 的基本工作原理
- 各个节点的角色
- Log 同步
- 失效节点如何恢复 Log
- 如何选举新 Leader
- Term 以及安全性的保证
-
Raft 在工程中的优化
- Prevote 的应用,防止离群 Leader 重加入时引发的切主
3.复制状态机(RSM)
- RSM (replicated state machine)
- Raft中所有的consensus都是直接使用Log作为载体
- Commited Index
- 一旦Raft更新Commited Index,意味着这个Index前的所有Log都可以提交给状态机了
- Commited lndex是不持久化的,状态机也是volatile的,重启后从第一条Log开始
4.Raft角色
- Leader:所有操作的发起者,把log同步给Follower,确定log已经commit
- Candidate:候选者,向其他人发起请求是否能投票给它,多数派同意则成为Leader
- Follower:发现不存在Leader,申请成为Leader,自己变成Candidate
5.Raft整体流程
1)有Client
客户端Client向Leader发起请求,Leader把请求转为Log,分发给所有Follow。Follow收到信息落盘之后告诉自己复制完成。当多数派写完把状态写入状态机,返回给用户操作已经收到了。
2)没有客户端
在没有客户端时,Leader向其他人定期发送心跳,确认Leader存在。如果Leader挂了,Follower向其他人发起请求是否能投票给它,多数派同意则成为Leader。
6.Raft Term
- 每个Leader 服务于一个term
- 每个term至多只有一个leader
- 每个节点存储当前的term
- 每个节点term从一开始,只增不减
- 所有rpc的 request reponse都携带term
- 只commit本term内的log
7.Raft主节点失效
- Leader定期的发送AppendEntries RPCs 给其余所有节点
- 如果Follower有一段时间没有收到Leader的AppendEntries,则转换身份成为Candidate
- Candidate自增自己的term,同时使用RequestVote RPCs向剩余节点请求投票
- raft在检查是否可以投票时,会检查log是否outdated,至少不比本身旧才会投给对应的Candidate
- 如果多数派节点投给它,则成为该term的leader
8.Raft安全性
1)Raft安全性-同Term
- 对于Term内的安全性
- 目标:对于所有已经的commited 的<term, index>位置上至多只有一条log
- 由于Raft的多数派选举,我们可以保证在一个term中只有一个leader
- 我们可以证明一条更严格的声明:在任何<term,index>位置上,至多只有一条log
2)Raft安全性–跨Term
- 对于跨Term的安全性
- 目标: 如果一个log被标记commited,那这个log一定会在未来所有的leader中出现Leader completeness
- 可以证明这个property
- Raft选举时会检查Log 的是否outdated,只有最新的才能当选Leader
- 选举需要多数派投票,而commited log也已经在多数派中(必有overlap)
- 新Leader 一定持有commited log,如果其他人有就不同意别人当选Leader,且 Leader永远不会overwrite log,只会append不会修改之前的log
- 真的安全吗? Raft使用 TLA+进行了验证
- 形式验证(Formal Method)以数学的形式对算法进行表达,由计算机程序对算法所有的状态进行遍历
四、实现细节以及未来
1.使用Raft构建我们的KV系统
1)案例-KV
1.回顾一下一致性读写的定义
- 方案一:
- 写log被commit 了。返回客户端成功,
- 读操作也写入一条log,状态机apply时返回client
- 增加Log量
- 方案二:
- 写log被commit 了。返回客户端成功
- 读操作先等待所有commited log apply,再读取状态机
- 优化写时延
- 方案三:
- 写Log被状态机 apply,返回给client
- 读操作直接读状态机
- 优化读时延
- Raft不保证一直有一个leader
- 只保证一个term至多有一个 leader
- 可能存在多个term 的leader
既然可能存在多个term 的leader,如何确定只有一个Leader呢?
- 确定合法的Leadership
- 方案一: 通过一轮 Heartbeat 确认 Leadership(获取多数派的响应)
- 方案二:
- 通过上一次 Heartbeat时间来保证接下来的有段时间内follower不会timeout
- 如果follower上一次收到心跳,那么follower在这段时间内不进行投票
- 如果多数follower满足条件,那么在这段时间内则保证不会有新的Leader产生
3.结合实际情况(写多或读多)选择读写方案二或方案三,只不过读之前要采用上面方案确认是否是合法的Leadership。
- 取决于raft 的实现程度以及读写的情况
4.出现问题及方案
- 多个副本只有单个副本可以提供服务,一个Leader忙不过来。
- 服务无法水平拓展
- 增加更多Raft组
- 操作跨Raft组。每个Raft组负责一个range的Key,这样读写就找对应的Raft组,这样就把集群的Leader打散并且可以水平拓展
2.回到共识算法
-
Raft :关于Log
- 论文中就给出的方案,当过多的Log占用后,启动snapshot,替换掉Log,相当于从0开始到snapshot包含的这条Log都扔掉而被snapshot替换掉。
- 如果对于持久化的状态机,如何快速的产生Snapshot?需要优化。
- 多组 Raft的应用中,Log 如何合流
-
关于configuration change(比如说现在是三节点,其中一个节点挂了之后,将三个节点扩容到4个节点后再踢掉一个节点的过程)
-
论文中给出的joint-consensus以及单一节点变更两种方案
-
Raft是正确的,但是在工程世界呢?
- 真实世界中不是所有的错误都是完美fail-stop的
- cloudflare 的case,etcd在partial network 下,outage了6个小时[1]
[1]AByzantine failure in the real world. blog.cloudflare.com/a-byzantine…
3.共识算法的未来
- 高性能
- 数据中心网络100G,时延约为几个us
- RDMA 网卡以及programable switch的应用。
- 我们想要的是:us级别的共识,以及us级别的容错,下面是两种出名方案。
- HovercRaft(原来状态机都是在CPU里面跑,现在通过可编程交换机把raft协议用了IP multicast做了网络层,这样用户从这边就不感知)
- P4 programable switch:IP multicast
- Mu(Leader通过one-sided RDMA(不通过rpc)去直接往Follower内存里面去写,这样可以达到超高性能的共识)
- Use one-sided RDMA
- pull based heartbeat instead of push-based.
- HovercRaft(原来状态机都是在CPU里面跑,现在通过可编程交换机把raft协议用了IP multicast做了网络层,这样用户从这边就不感知)
- 多节点提交(Leaderless)
- 节点跨地域,导致节点间的RTT(Round Trip Time)很大
- EPaxos[1]
- 使用了冲突图的方式来允许并行Commit
- 不冲突的情况下1RTT 提交时间
[1] Moraru, lulian, David G. Andersen, and Michael Kaminsky. "Egalitarian paxos." ACM Symposium on Operating Systems Principles. 2012.
- Raft Paxos相互移植
- Raft有很多成熟的实现
- 研究主要关注在Paxos 上
- 如何关联两种算法
- On the Parallels between Paxos and Raft, and how to Port Optimizations[1]
- Paxos vs Raft: Have we reached consensus on distributed consensus?[2]
[1] Wang, Zhaoguo, et al. "On the Parallels between Paxos and Raft, and how to Port Optimizations." Proceedings of the 2019 ACM Symposium on Principles of Distributed Computing.2019.
[2] Howard, Heidi, and Richard Mortier. "Paxos vs Raft: Have we reached consensus on distributed consensus?." Proceedings of the 7th Workshop on Principles and Practice of Consistency for Distributed Data.2020.
- 共识算法作为一个系统
- 多数分布式系统都选择共识算法作为底座
- 不同一致性协议有不同的特性
- Virtual consensus in delos[1]
- 对外暴露一致性的LOG作为借口
- 内部对于LoG可以选择不同的实现
Raft相关
-
Raft paper raft.github.io/
-
开源 raft 实现
- etcd:
- braft: