欢迎关注存储老小伙的博客。
本文之前的版本全是代码和注释,没有讲解,本次做了大量调整。
上篇是Leader 选举部分。这篇主要是Ceph的Paxos协议的Phase1(Prepare),其目的是就PN达成一致。
1. 几个要点说明
1.1 Epoch
每次选举产生新的leader,也会产生新的epoch。不选举则不会修改epoch。
一个leader当选期间,发送的所有消息,都会带有这个epoch。
如果由于网络分割等现象,有新的选举发生,则根据epoch就发现leader已经变了。
注意,按照paxos论文描述,没有Leader也是可以正常运行的,只是可能降低效率。
没有leader则不需要epoch
1.2 PN (Proposal Number)
Leader当选后,会首先执行一次phase 1过程,以确定PN。 在其为leader期间,
所有的phase 2操作都共用一个PN。所以省略了大量的phase 1操作,这也是
paxos能够减小网络开销的原因。 “Paxos made simple”文中说:
“A newly chosen leader executes phase 1 for infinitely many
instances of the consensus algorithm”。
PN是必须的,无论是否有leader,都必须有PN
1.3 Version
可以理解成Paxos 的instance ID,或者raft的logID。
1.4 持久化
对比Raft,虽然ceph的复制也可以看成一个个log的追加,
但是所有信息都写在k/v中,而不是写log文件, 比如,instanceID为X的log,
在k/v存储中,其key是X,value是log内容。
其他各种需要持久化的值,都写在k/v存储中。
1.5其他需要持久化的数据结构
除了log以外,每个paxos成员,都维护以下几个需要持久化的变量。
大家可以跟raft的paper做些简单对比。
名称 | 含义 | 其他 |
---|---|---|
last_pn | 上次当选leader后生成的PN | get_new_proposal_number()使用,下次当选后,接着生成 |
accepted_pn | 我接受过的PN,可能是别的leader提议的PN | peon根据这个值拒绝较小的PN |
first_committed | 本节点记录的第一个被commit的版本 | 更早的版本(日志),本节点没有了 |
last_committed | 本节点记录的最后一次被commit的版本 | 往后的版本,未被commit,可能有一个 |
uncommitted_v | 本节点记录的未commit的版本,如果有,只能等于last_commit+1 | ceph只允许有一个未commit的版本 |
uncommitted_pn | 未commit的版本对应的PN | 与uncommitted_v,uncommitted_value在一个事务中记录 |
uncommitted_value | 为commit的版本的内容 | 见上面 |
注意,上述三个”uncommitted”开头的值,可能压根就不存在,比如正常关机,全部都commit了。
1.6 Phase 1交互过程简介
Phase 1就是 paxos协议的Propose阶段,包括三个步骤,如下表:
步骤 | Leader | Peons | 备注 |
---|---|---|---|
1 | collect() => | Leader给quorum中各个peon发送PN以及其他附带信息 | |
2 | <=handle_collect() | Peon同意或者拒绝PN。并中间可能分享已经commit的数据 | |
3 | handle_last() | Quorum中peon全部同意leader的PN,才算成功 |
2. 代码
2.1 初始化
void Paxos::init()
{ //几个持久化的变量,加载时即从从kv读出。
// load paxos variables from stable storage
//上次产生的PN
last_pn = get_store()->get(get_name(), "last_pn");
//上次接受的pn
accepted_pn = get_store()->get(get_name(), "accepted_pn");
//最近或最后一个被commit的verion,实际上是paxos 的instance ID。
last_committed = get_store()->get(get_name(), "last_committed");
//保存的最早被commit的版本(log)。更早的log可能已经被truncate掉了
first_committed = get_store()->get(get_name(), "first_committed");
//paxos的 first_committed,并不是某个monitor的first_committed,各个monitor
//对应值可能都是不一样的。
assert(is_consistent());
}
2.2 Leader发起的collect
// PHASE 1: collect和handle_collect基本能对应paxos的phase 1
//这是leader的当选后执行函数,用于确定新的PN。
//collect过程,相当于完成当选期间所有提议的phase 1。
//在其当选期间,会一直使用这个PN
void Paxos::collect(version_t oldpn)
{
// we're recoverying, it seems!
state = STATE_RECOVERING;
assert(mon->is_leader());
// reset the number of lasts received
uncommitted_v = 0; //新当选,初始化
uncommitted_pn = 0;
uncommitted_value.clear();
peer_first_committed.clear();
peer_last_committed.clear();
//ceph的实现中,只允许有一个proposal处于pending状态(跟raft相同)。
//如果新leader当选后发现有pending的提议,那么其instanceID/version
//只能是last_committed+1
if (get_store()->exists(get_name(), last_committed+1)) {
/*pending_v, pending_pn和last_committed+1是一个事务写的。
所以一起检查 */
version_t v = get_store()->get(get_name(), "pending_v");
version_t pn = get_store()->get(get_name(), "pending_pn");
if (v && pn && v == last_committed + 1){
//这个是正常分支
uncommitted_pn = pn;
} else {
dout(10) << "WARNING: no pending_pn on disk, using previous accepted_pn " << accepted_pn << " and crossing our fingers" << dendl;
uncommitted_pn = accepted_pn;
}
uncommitted_v = last_committed+1;
//找到uncommitted_v (这个key)对应的value
get_store()->get(get_name(), last_committed+1, uncommitted_value);
//uncommitted_v存在,要求uncommitted_value必须存在。
assert(uncommitted_value.length());
logger->inc(l_paxos_collect_uncommitted);
}
//生成一个新的更大的PN,并自己先accept
accepted_pn = get_new_proposal_number(MAX(accepted_pn, oldpn));
accepted_pn_from = last_committed;
num_las