- 写在前面的话
年前工作的间歇期计划学一些优秀的开源代码充充电,侧面打听到微信团队开源的Paxos源码在微信内部有大量的线上应用加之自己对Paxos之前的理解并不算深刻,所以想借着这个机会彻底搞懂它。年后回来趁着还“热乎"整理成系列文章巩固一下记忆。
为什么选择PhxPaxos作为学习Paxos的源码,这就涉及到优秀开源项目的标准。个人理解优秀的开源项目需要具备以下几点:
- 经过大量的生产环境验证。
- 高质量的文档。
- 活跃的社区。
- 高质量的代码,以及良好的代码风格。
经过大量的生产环境验证代表着源码在稳定性、可运维性、吞吐性能上得到了充分验证,很多开源的项目PR文章写得很吸引眼球但是用起来却差强人意,正是没有大规模线上运行的原因,这也是PhxPaxos最吸引我的原因,其他因素并不是选择PhxPaxos的主要因素,因此不在这里展开了。
本系列文章主要分为以下六部分进行介绍:
- PhxPaxos源码解析之(1)概述篇
- PhxPaxos源码分析之(2)提案申请篇
- PhxPaxos源码分析之(3)提案发起篇(Paxos协议核心)
- PhxPaxos源码分析之(4)Learner篇
- PhxPaxos源码分析之(5)CheckPoint篇
- PhxPaxos原发分析之(6)工程优化篇
本系列文章在介绍相关源码部分会有类图、时序图、流程图方便理解,所有截图都是第一遍阅读源码时自己画给自己看的,方便自己理解记忆,基本都是不符合UML模型的规范在此声明一下。
- 协议概述
开始之前先对Paxos协议做一个简单的回顾,Paxos协议论文、详细的推理证明以及相关介绍网上已经很多了不在此重复。
1. 协议过程
ph1.a:Proposer 向集群中所有节点发送Prepare请求。请求中只包括Proposalid,Proposalid全局唯一且递增。
ph1.b:Acceptor应答Prepare请求。如果Acceptor接收Prepare请求,则
a. 不再接受 <= Proposalid 的Prepare请求。
b. 不再接受 < Proposalid 的Accept请求。
同样,如果不满足上述要求,则拒绝prepare请求。接收请求的同时,返回Accept过的最大的Proposalid以及内容,没有则 返回空。
ph2.a:Acceptor收集Prepare的应答。如果收到多数派的回答,比较所有应答中Proposalid最大的内容作为提案内容发起 Accept请求,如果内容为空则用Proposer的内容进行提案。
ph2.b:Acceptor应答Accept请求。满足ph1.b中的条件,则持久化Proposalid以及内容(用于Learn)。Proposer收到多数的应答 后形成最终决议。
2. 理解
下面通过一些问题进一步的理解Paxos协议:
a.在什么情况下可以认为提案(值)被确定不再更改?
多数Acceptor接受,即可以任务提案被接受了。
b.Paxos两个阶段都在做什么?
第一阶段,协商Proposalid以及Proposalid代表的轮次应该提交的内容。
第二阶段,提交提案,达到半数以上认为提交成功。
c.一个Proposalid是否会有多个Proposer在运行第二阶段?
不会,根据ph1.b中的规则,只会有一个Proposer获取多数的Prepare应答。
d.Proposer在什么情况下可以将自己提案作为内容发起提议?
Proposer收到的PrepareReply中Proposalid最大的内容为空时,即上一轮提案(多数派返回则最大的Proposalid一定是上一轮提 案)Learned(学习之后的内容会被删除)。
e.第二阶段如果获取的内容为空,为何保证旧的Proposalid无法形成新的提案?
第二阶段如果获取的内容为空表示当前Proposalid已经被Accepted,旧的Proposalid根据ph1.b中的规则会被拒绝。
f.新的Proposalid获取成功,旧的Proposer如何工作?
如果处于第一阶段,正常发起prepare请求,但是不会获取多数投票。由于新的Proposalid获取成功,旧的Proposer也不会工作在 第二阶段。
g.如何保证新的Proposalid不会破坏旧的提案?
ph1.b中投票规则,不再接受 <= Proposalid 的Prepare请求。
h.为什么在第二阶段,只需要考虑最大Proposalid的accpted的提案?
表示上一轮(距离当前Proposalid最近的Proposalid)提议的结果,如果不为空表示还没有被learned但是被多数accept,需要重 新提交。
i.提案确定之后,如何保证在任意半数以下Acceptor故障时,提案不被更改?
半数以上以上已经accepted,如果已经Learned,那么其他节点也会发现并Learn,如果还没有Learn,下次提议时会提交。
j.如果Proposer运行过程中半数以下Acceptor故障时,如何运行?
Prepare完成之后恰好半数Acceptor故障全是接收了Prepare的,仍能进行Accept请求,此时Accept由于没有被多数Acceptor同意, 提案被拒绝。
k.正在运行的Proposer和半数以下Acceptor故障时,提案内容会怎样?为何后面新的Proposer可以完成新的提案?
第一阶段期间故障,提案失败。第二阶段故障,提案成功。因为第一阶段会协商Proposalid以及提案内容。
3. 活锁
高并发时,多个Proposer同时发起Prepare请求,导致没有人格一个Proposer获得多数派的投票进而重试,循环往复的现象称为活锁。解决方案,第一是控制Proposer发起提案的频率,第二是Proposer重试加一个随机避让时间。
4. Muti-Paxos
如果一个Proposer连续发起n个提案,要进行n次prepare + n次Accept,显然n-1次prepare是浪费的。Muti-Paxos就是解决这个问题提升效率的。Muti-Paxos的解决办法是,Proposalid复用如果Proposer没有变换沿用之前的Proposalid,直到其他Proposer发起提案时进行Proposalid = Instance+1打破之前Proposalid的局面。从效果上看相当于多个提案被压缩成一个大的提案。
很多网上资料介绍Muti-Paxos需要一个Leader,参考资料中有说明,Leader不是必须的。
- 核心代码架构
协议的核心代码路径:phxpaxos/src/algorithm,包括了Proposer、Acceptor、Leaner类以及协议的两阶段逻辑。
Group:Paxos集群,奇数个节点(PNode)。PNode:代表一个Paxos节点。Instance:PNode的具体实现。
Instance:聚合了Proposer、Acceptor、Leaner,他们三个在一个线程上。
下图是核心类的关系图,通过类名和函数可以大概了解其作用。
Base是Proposer、Acceptor、Leaner的基类,维护InstanceID变量对应协议中提案的编号,注意每个节点上Proposer、Acceptor、Leaner各有一个InstanceID单独维护的,每次提案被确认之后都会调用Base的NewInstance进行+1。
Commiter负责提案的生成,IOLoop线程主循环负责事件的注册与回调。
- 参考资料
https://www.bilibili.com/video/av36134550/
https://github.com/brpc/braft/blob/master/docs/cn/paxos_protocol.md