比特币源码分析--paxos算法

    P2P系统中一个最重要也是最复杂的问题就是共识,也就是对于分散在各处的网络节点,如何让他们对某件事情达成一致。因为网络的复杂性(网络传输有延迟、数据无序到达、节点可能宕机不响应,恶意节点伪造数据等等),节点之间达成共识非常复杂。比特币区块链作为一个分布式的账本,它的节点间如何形成共识,可以说是比特币以及任何区块链都需要解决的核心问题,理解共识算法也是区块链学习的重中之重。

    分布式系统的共识算法主要分为两类:CFT算法(Crash falut)和BFT(Byzantine fault)算法。CFT算法主要解决网络中节点可能出错(比如宕机),但是节点不会作恶(比如伪造数据)的情况下节点之间如何达成共识,而BFT算法则针对网络中可能存在节点作恶的情况下节点间达成共识的方法。

    从本文开始,将分多篇文章来了解常用的共识算法,包括CFT类算法的经典算法paxos,BFT类算法的PBFT算法,比特币中使用的PoW工作量证明算法,权益证明算法PoS等。本文先从paxos算法开始。

1 分布式系统共识问题的基本原理

    在了解分布式系统的共识算法前,先来熟悉几个和分布式系统共识有关的基本原理。

1.1 FLP不可能原理

    任何问题一般都存在一个理论上的下限(最坏的情况),那么对于分布式系统的共识问题,其理论上的解是什么呢?经过科学家的证明,异步分布式系统的共识问题的通用解决方法是:无解,也就是说即便是在网络通信可靠的情况下,可扩展的分布式系统的共识问题是无解的。这个定理由Fischer,Lynch和Patterson三位科学家于1985年发表的论文中给出了证明,也叫做FLP不可能原理。这个定理也告诉人们不要试图去设计一套在任意场景下都适用的共识算法,否则等同于浪费时间。

    关于FLP定理的证明,可以参考下面这篇文章:

    FLP证明

    定理的证明用到了图论的知识,对于像笔者这样数学已经忘的差不多的同学,可以先记住这个无解的定理。

1.2 CAP原理

    先说说CAP原理中的C/A/P是指什么:

    Consistency:一致性,是指分布式系统中的所有节点的数据备份,在同一时刻都具有同样的值(强一致性),换言之,所有用户任一时刻访问系统,都能得到相同的结果(不能是用户A得到1,用户B得到0);

    Availability:可用性,是指分布式系统中的一部分节点出现故障以后,系统依然能够响应用户的请求,换句话说就是用户在任何时候都能向系统发出请求并得到响应(不能出现不响应的情况);

    Partition:分区容忍性,如果分布式系统不能再一定的时限内达成数据的一致性,意味着系统产生了分区,一旦产生了分区,就需要在C和A之间做出选择:要么牺牲可用性,不响应用户;要么牺牲一致性,给用户响应不一致的结果;

    CAP原理是指分布式系统不可能同时满足一致性、可用性和分区容忍性,实际实现时只能在三者之间做出权衡。用一句古话说就是鱼与熊掌不可兼得。

    此定理也是由FLP定理证明的作者之一的Lynch给出证明。

1.3 BASE原理

    BASE是对CAP原理中一致性和可用性权衡以后的结果:

    Base Available:基本可用性,是指在系统的部分节点出现故障以后,允许损失一部分可用性;

    Soft state:软状态,允许系统中的数据存在中间状态,允许不同节点之间的数据副本的同步过程存在延迟;

    Eventually consistent:最终一致性,是指系统中的数据,在经过一定的时间以后会最终达到一致状态,也就是降低一致性要求,不要求实时的强一致性。比特币区块链就是一个很好的例子,在区块链延长的过程中,有可能会出现分叉,不同的节点上区块不一样(不一致),但是经过一定时间以后,随着所有节点都切换到最长的主链,分叉的情况就会消失(最终一致)。

2 Paxos共识算法

2.1 paxos算法介绍

    paxos算法是由Leslie Lamport于1990年提出的一种非拜占庭模型的分布式系统共识算法。

    为了描述算法,Lamport虚拟了一个希腊城邦paxos(这也是算法名字的由来),这个岛按照议会民主制的政治模式制订法律,但是没有人愿意将自己的全部时间和精力放在这种事情上。所以无论是议员,议长或者传递纸条的服务员都不能承诺别人需要时一定会出现,也无法承诺批准决议或者传递消息的时间。但是这里假设没有拜占庭将军问题(Byzantine failure,即虽然有可能一个消息被传递了两次,但是绝对不会出现错误的消息);只要等待足够的时间,消息就会被传到。另外,Paxos 岛上的议员是不会反对其他议员提出的决议。

    paxos算法的推导过程理解起来有一定困难,但是从操作层面讲,paxos解决共识的思路其实并不难。

    首先算法将网络中的节点划分为3种角色:提案者,接受者和学习者。

    提案者:负责提出提案,这里提案可以是任何可以达成共识的东西,比如某个要写入DB的值,一条日志等等;

    接受者:接收提案,确定接受还是拒绝提案;

    学习者:获取并同步最终选择的提案;

    提案是一个由提案编号和提案值组成的pair,即[proposalNo, proposalValue],一个提案被半数以上的接受者接受即认为达成共识。提案编号是最终能达成共识的关键,每个接受者都会记录已响应过的提案的最大编号,并且承诺不会接受比此提案号小的提案。

    具体操作时,paxos可以分为两个阶段:

    第一阶段:准备阶段,提案者选择一个提案号,发送提案给接受者,试探能否得到半数以上接受者的响应;

    第二阶段:如果第一阶段收到超过半数的接受者的响应,则提交提案,如果能够得到半数以上接受者的响应,则共识完成;

2.2 paxos算法推导过程

    paxos算法最初的论文被公认为是晦涩难懂,后来作者Lamport又发布了《paxos made simple》,用更加容易理解的方式对paxos做了阐述。

    可以从下面的链接下载原论文:

    paxos made simple

    论文中通过从简单到复杂逐步添加约束条件的方式来论证其共识算法。虽然作者已经做了简化,但毕竟还是比较学术化,理解论文中提出的几个约束条件还是有一定的困难,详细的过程读者可以自行阅读论文。

    paxos算法的论证过程虽然比较难理解,但是实际的操作过程却比较简单,网上有人用一个形象的例子来说明paxos达成共识的过程:

    假设有2个商人P1和P2想从政府买块地,商人想要最终拿下这块地,需要经过3个政府官员A1,A2和A3的审批,只有经过2个以上的官员同意,才能最终拿下地皮,现在的目标是:最终只能有一个商人拿到地。另外假设商人要想通过官员的审批,必须给一定数量的贿赂费。

    我们看看这样一个场景下,如何达成共识(选定一个商人把地批给他)。

    拿地是一件争分夺秒的事情,于是商人P1和P2开始准备了:

    (1) 假设P1的行动快一些,他很快找到了官员A1和A2,告诉两人只要批了地就给100W的感谢费,两个官员很高兴,告诉P1回去拿钱吧;

    注:这一步实际上是P1在进行paxos算法中的准备阶段;

    (2) 商人P2在P1之前先找了官员A3,告诉他只要批了地就给他200W,A3愉快的接受了;

    (3) P2又跑到官员A1和A2那,承诺只要批地,就给200W,因为这份费用比此前P1承诺的高,于是贪财的官员A1和A2变卦了;

    注:以上两步是P2在进行paxos的准备阶段;

    (4) 商人P1此前已经初步贿赂A1和A2成功,于是满心欢喜的拿着准备好的钱找到了A1和A2,结果却是A1和A2都告诉他:对不起,Mr XX,就在刚刚有人承诺了200W,你是个聪明人,你懂得该怎么做的。商人P1很是郁闷,告诉A1和A2:容我想想再给答复;

    注:以上P1拿钱给A1和A2,对应与paxos算法的提交阶段,因为此前P1已经得到了3位官员中2位的同意;

    (5) 就在P1还在犹豫要不要提高贿赂费的时候,商人P2按之前承诺的向A1和A2的账户分别打入200W,于是A1,A2拿钱办事,批准通过,因为超过半数的官员审批通过,于是在政府网站上向大众公布P2最终拿地成功,所有人都知道了这个结果。

    注意上面的过程中的一个问题:假设上面第(4)步中P1被拒绝以后,立刻向官员承诺一个更高的费用,那么当商人P2拿着钱到A1和A2时,同样也会被拒绝,于是P2又可能会抬价,这样交替下去就可能造成死循环,这就是paxos算法中的活锁问题。

2.3 paxos算法的实现

    libpaxos是一个开源的paxos算法的实现,我们可以结合这个开源库的代码学习paxos算法。这里只挑重点来分析,对算法有兴趣的同学可以自行荡一份代码下来学习之。

    前面提到paxos算法是将节点的角色分为了提案者:proposer,接受者acceptor和leaner,重点是proposer和acceptor,在libpaxos中这两个角色的实现分别位于evproposer.c和evacceptor.c文件中。

    以下是提案者的初始化:

struct evproposer*
evproposer_init_internal(int id, struct evpaxos_config* c, struct peers* peers)
{
	struct evproposer* p;
	int acceptor_count = evpaxos_acceptor_count(c);

	p = malloc(sizeof(struct evproposer));
	p->id = id;
	p->preexec_window = paxos_config.proposer_preexec_window;

	peers_subscribe(peers, PAXOS_PROMISE, evproposer_handle_promise, p);
	peers_subscribe(peers, PAXOS_ACCEPTED, evproposer_handle_accepted, p);
	peers_subscribe(peers, PAXOS_PREEMPTED, evproposer_handle_preempted, p);
	peers_subscribe(peers, PAXOS_CLIENT_VALUE, evproposer_handle_client_value, p);
	peers_subscribe(peers, PAXOS_ACCEPTOR_STATE,
		evproposer_handle_acceptor_state, p);

	// Setup timeout
	struct event_base* base = peers_get_event_base(peers);
	p->tv.tv_sec = paxos_config.proposer_timeout;
	p->tv.tv_usec = 0;
	p->timeout_ev = evtimer_new(base, evproposer_check_timeouts, p);
	event_add(p->timeout_ev, &p->tv);
	
	p->state = proposer_new(p->id, acceptor_count);
	p->peers = peers;
	
	event_base_once(base, 0, EV_TIMEOUT, evproposer_preexec_once, p, NULL);

	return p;
}

    首先提案者注册了处理接受者响应消息的回调:

    evproposer_handle_promise处理PAXOS_PROMISE消息,这个对应paxos的准备阶段:提案者提出一个提案,然后接受者初步接受了此提案,并承诺不会接受比提案者提案编号更小的提案;

    evproposer_handle_accepted处理PAXOS_ACCEPTED消息,这个对应paxos的提交阶段:提案者提交提案,接受者接受了提案;

    evproposer_handle_preemted处理PAXOS_PREEMTED消息:当接受者响应一个比提案者的提案更大的编号时,提案者知道有更新的提案了,此时提案者重新选择一个提案编号再次进行paxos的准备阶段。

2.3.1 准备阶段

    以下的代码关键地方都做了注释,读者看注释应该就能明白。

    准备阶段:向acceptor发送提案,如果超过半数的acceptor接受此提案,则进入提交阶段。

    (1) proposer发送提案:

static void
proposer_preexecute(struct evproposer* p)
{
	int i;
	paxos_prepare pr;
	int count = p->preexec_window - proposer_prepared_count(p->state);
	if (count <= 0) return;
	for (i = 0; i < count; i++) {
		//生成提案
		proposer_prepare(p->state, &pr);
		//向连接的每个接受者发送提案
		peers_foreach_acceptor(p->peers, peer_send_prepare, &pr);
	}
	paxos_log_debug("Opened %d new instances", count);
}

    生成的提案信息将会被保存到一个instance实例中:

void
proposer_prepare(struct proposer* p, paxos_prepare* out)
{
	int rv;
	iid_t iid = ++(p->next_prepare_iid);
	ballot_t bal = proposer_next_ballot(p, 0);
	struct instance* inst = instance_new(iid, bal, p->acceptors);
	khiter_t k = kh_put_instance(p->prepare_instances, iid, &rv);
	assert(rv > 0);
	kh_value(p->prepare_instances, k) = inst;
	*out = (paxos_prepare) {inst->iid, inst->ballot};
}

    (2) acceptor接受第一阶段提案

    下面的代码片段展示了acceptor接收到proposer发送的准备阶段的提案以后的处理:

static void 
evacceptor_handle_prepare(struct peer* p, paxos_message* msg, void* arg)
{
	paxos_message out;
	paxos_prepare* prepare = &msg->u.prepare;
	struct evacceptor* a = (struct evacceptor*)arg;
	paxos_log_debug("Handle prepare for iid %d ballot %d",
		prepare->iid, prepare->ballot);
	if (acceptor_receive_prepare(a->state, prepare, &out) != 0) {
		send_paxos_message(peer_get_buffer(p), &out);
		paxos_message_destroy(&out);
	}
}

    acceptor_receive_prepare的代码:

int
acceptor_receive_prepare(struct acceptor* a, paxos_prepare* req, paxos_message* out)
{
	paxos_accepted acc;
	if (req->iid <= a->trim_iid)
		return 0;
	memset(&acc, 0, sizeof(paxos_accepted));
	if (storage_tx_begin(&a->store) != 0)
		return 0;

	//检索本地已接受的提案
	int found = storage_get_record(&a->store, req->iid, &acc);

	//没有此提案或者提案者有编号更大的提案,更新本地的提案信息
	if (!found || acc.ballot <= req->ballot) {
		paxos_log_debug("Preparing iid: %u, ballot: %u", req->iid, req->ballot);
		acc.aid = a->id;
		acc.iid = req->iid;
		//提案号更新为收到的更大的提案的编号
		acc.ballot = req->ballot;
		if (storage_put_record(&a->store, &acc) != 0) {
			storage_tx_abort(&a->store);
			return 0;
		}
	}
	if (storage_tx_commit(&a->store) != 0)
		return 0;
	paxos_accepted_to_promise(&acc, out); //响应提案者
	return 1;
}

    (3) proposer处理第一阶段提案的响应

static void
evproposer_handle_promise(struct peer* p, paxos_message* msg, void* arg)
{
	struct evproposer* proposer = arg;
	paxos_prepare prepare;
	paxos_promise* pro = &msg->u.promise;
	int preempted = proposer_receive_promise(proposer->state, pro, &prepare);
	//如果接受者响应了更高的提案编号,则重新选择提案号发送提案
	if (preempted)  
		peers_foreach_acceptor(proposer->peers, peer_send_prepare, &prepare);
	try_accept(proposer);
}

    proposer_receive_promise的实现如下:

int
proposer_receive_promise(struct proposer* p, paxos_promise* ack,
	paxos_prepare* out)
{
	khiter_t k = kh_get_instance(p->prepare_instances, ack->iid);
	
	if (k == kh_end(p->prepare_instances)) {
		paxos_log_debug("Promise dropped, instance %u not pending", ack->iid);
		return 0;
	}
	struct instance* inst = kh_value(p->prepare_instances, k);
	
	//忽略编号更小的提案
	if (ack->ballot < inst->ballot) {
		paxos_log_debug("Promise dropped, too old");
		return 0;
	}
	
	//接收端的提案编号比发送的提案编号更大,重新选择一个提案号生成提案
	if (ack->ballot > inst->ballot) {
		paxos_log_debug("Instance %u preempted: ballot %d ack ballot %d",
			inst->iid, inst->ballot, ack->ballot);
		proposer_preempt(p, inst, out); //重新生成一个提案号
		return 1;
	}
	
	//如果此前指定的接受者已经接受了此提案,直接返回,否则提案被接受的数量加1
	if (quorum_add(&inst->quorum, ack->aid) == 0) { 
		paxos_log_debug("Duplicate promise dropped from: %d, iid: %u",
			ack->aid, inst->iid);
		return 0;
	}
		
	paxos_log_debug("Received valid promise from: %d, iid: %u",
		ack->aid, inst->iid);
		
	if (ack->value.paxos_value_len > 0) {
		paxos_log_debug("Promise has value");
		//使用接受者返回的提案值更新提案值
		if (ack->value_ballot > inst->value_ballot) {
			if (instance_has_promised_value(inst))
				paxos_value_free(inst->promised_value);
			inst->value_ballot = ack->value_ballot;
			inst->promised_value = paxos_value_new(ack->value.paxos_value_val,
				ack->value.paxos_value_len);
			paxos_log_debug("Value in promise saved, removed older value");
		} else
			paxos_log_debug("Value in promise ignored");
	}
	
	return 0;
}

    接下来提案者尝试第二阶段的请求。

2.3.2 提交阶段

    提案者将检查是否有经过半数以上acceptor接受的第一阶段的提案,如果有就开始提交阶段。

static void
try_accept(struct evproposer* p)
{
	paxos_accept accept;
	//检查是否有被半数以上的acceptor接受的第一阶段的提案,有就发起提交阶段请求
	while (proposer_accept(p->state, &accept)) 
		peers_foreach_acceptor(p->peers, peer_send_accept, &accept);


	//检查是否还有需要发起准备阶段的提案,有则发起准备阶段请求
	proposer_preexecute(p); 
}

    proposer_accept的实现请看下面的代码片段:

int
proposer_accept(struct proposer* p, paxos_accept* out)
{
	khiter_t k;
	struct instance* inst = NULL;
	khash_t(instance)* h = p->prepare_instances;
	
	// Find smallest inst->iid
	for (k = kh_begin(h); k != kh_end(h); ++k) {
		if (!kh_exist(h, k))
			continue;
		else if (inst == NULL || inst->iid > kh_value(h, k)->iid)
			inst = kh_value(h, k);
	}
	
	//没有提案信息或者第一阶段的提案没有被半数以上acceptor同意,直接返回
	if (inst == NULL || !quorum_reached(&inst->quorum)) 
		return 0;
		
	paxos_log_debug("Trying to accept iid %u", inst->iid);
	
	// Is there a value to accept?


	//如果没有提案值,提案者自己准备一个提案值
	if (!instance_has_value(inst)) 
		inst->value = carray_pop_front(p->values);
	//还是没有提案值,直接取消
	if (!instance_has_value(inst) && !instance_has_promised_value(inst)) { 
		paxos_log_debug("Proposer: No value to accept");
		return 0;
	}
	
	// We have both a prepared instance and a value
	//提案已经被半数acceptor接受,进入第二阶段,将第一阶段的提案信息移动到第二阶段提案信息中,计数清0
	proposer_move_instance(p->prepare_instances, p->accept_instances, inst); 
	
	//提案者生成提交阶段的请求
	instance_to_accept(inst, out); 


	return 1;
}

    acceptor收到提案者的提交请求后的处理参考下面代码片段:

static void 
evacceptor_handle_accept(struct peer* p, paxos_message* msg, void* arg)
{	
	paxos_message out;
	paxos_accept* accept = &msg->u.accept;
	struct evacceptor* a = (struct evacceptor*)arg;
	paxos_log_debug("Handle accept for iid %d bal %d", 
		accept->iid, accept->ballot);
	if (acceptor_receive_accept(a->state, accept, &out) != 0) {
		if (out.type == PAXOS_ACCEPTED) { //提案被接受者选定了
			peers_foreach_client(a->peers, peer_send_paxos_message, &out);
		} else if (out.type == PAXOS_PREEMPTED) {
		    //接受者接受了更大编号的提案,告诉提案者此提案已经被取代了,提案者需重新发起第一阶段
			send_paxos_message(peer_get_buffer(p), &out);
		}
		paxos_message_destroy(&out);
	}
}

    acceptor_receive_accept处理收到的请求,可能有两种情况:要么提案被acceptor选定,要么此前acceptor又收到了编号更高的提案,驳回,让提案者重新从第一阶段开始:

int
acceptor_receive_accept(struct acceptor* a,
	paxos_accept* req, paxos_message* out)
{
	paxos_accepted acc;
	if (req->iid <= a->trim_iid)
		return 0;
	memset(&acc, 0, sizeof(paxos_accepted));
	if (storage_tx_begin(&a->store) != 0)
		return 0;
	int found = storage_get_record(&a->store, req->iid, &acc);


	//检索本地提案,如果提案者提交的提案编号比本地的大,则选定
	if (!found || acc.ballot <= req->ballot) {
		paxos_log_debug("Accepting iid: %u, ballot: %u", req->iid, req->ballot);
		paxos_accept_to_accepted(a->id, req, out);
		if (storage_put_record(&a->store, &(out->u.accepted)) != 0) {
			storage_tx_abort(&a->store);
			return 0;
		}
	} else {
		//acceptor有比提案者提交的提案更大的提案号,通知提案者重新开始第一阶段
		paxos_accepted_to_preempted(a->id, &acc, out); 
	}
	if (storage_tx_commit(&a->store) != 0)
		return 0;
	paxos_accepted_destroy(&acc);
	return 1;
}

    我们继续跟进代码看看提案者收到响应后的处理:

    第一种情况:提案者第二阶段提交的提案被接受者接受,此时该提案将是最终被选定的提案,处理逻辑参考下面代码段:

static void
evproposer_handle_accepted(struct peer* p, paxos_message* msg, void* arg)
{
	struct evproposer* proposer = arg;
	paxos_accepted* acc = &msg->u.accepted;
	//检查提案是否被半数以上的接受者接受
	if (proposer_receive_accepted(proposer->state, acc))
	    //如果还有已经被半数以上接受者接受的第一阶段提案,发起第二阶段请求
		try_accept(proposer);
}

    proposer_receive_accepted的代码如下:

int
proposer_receive_accepted(struct proposer* p, paxos_accepted* ack)
{
	khiter_t k = kh_get_instance(p->accept_instances, ack->iid);
	
	if (k == kh_end(p->accept_instances)) {
		paxos_log_debug("Accept ack dropped, iid: %u not pending", ack->iid);
		return 0;
	}
	
	//找到已完成第一阶段的提案信息
	struct instance* inst = kh_value(p->accept_instances, k); 
	
	if (ack->ballot == inst->ballot) {
		//提案被接受者接受的数量加1
		if (!quorum_add(&inst->quorum, ack->aid)) {  
			paxos_log_debug("Duplicate accept dropped from: %d, iid: %u", 
				ack->aid, inst->iid);
			return 0;
		}
		
		//如果提案被半数的接受者接受,则此次共识完成,此提案的值为最终选定的值
		if (quorum_reached(&inst->quorum)) { 
			paxos_log_debug("Proposer: Quorum reached for instance %u", inst->iid);
			if (instance_has_promised_value(inst)) {
				if (inst->value != NULL && paxos_value_cmp(inst->value, inst->promised_value) != 0) {
					carray_push_back(p->values, inst->value);
					inst->value = NULL;
				}
			}
			kh_del_instance(p->accept_instances, k);
			instance_free(inst);
		}
		
		return 1;
	} else {
		return 0;
	}
}

    第二种情况:接受者告诉提案者有编号更高的提案,此时提案者需要生成新的提案号,重新进入第一阶段:

int
proposer_receive_preempted(struct proposer* p, paxos_preempted* ack,
	paxos_prepare* out)
{
	khiter_t k = kh_get_instance(p->accept_instances, ack->iid);
	
	if (k == kh_end(p->accept_instances)) {
		paxos_log_debug("Preempted dropped, iid: %u not pending", ack->iid);
		return 0;
	}
	
	//找到已完成第一阶段的提案信息
	struct instance* inst = kh_value(p->accept_instances, k); 
	
	//接受者有更大的提案号
	if (ack->ballot > inst->ballot) {    
		paxos_log_debug("Instance %u preempted: ballot %d ack ballot %d",
			inst->iid, inst->ballot, ack->ballot);


		//将之前接受者承诺的提案值清掉(因为此时提案已经被取代了)
		if (instance_has_promised_value(inst)) 
			paxos_value_free(inst->promised_value);
		
		//将提案信息重新移动到第一阶段的提案集合中
		proposer_move_instance(p->accept_instances, p->prepare_instances, inst); 


		//重新生成一个提案号,发起第一阶段请求
		proposer_preempt(p, inst, out); 
		return  1; 
	} else {
		return 0;
	}
}

    以上截取了libpaxos的关键代码,感觉libpaxos更大的作用应该是用来让开发人员了解paxos算法,很难直接拿来做产品级应用,另外这个库也没有实现leader角色来解决paxos算法的活锁问题。

2.3.3 paxos算法的交互图

    前面两小节截取了libpaxos的代码展示了paxos算法的具体实现,代码看起来还是略微有点繁琐,这一节画个图在来总结一下paxos算法的流程:

    

2.3.4 paxos算法的优化

    paxos算法有很多变种和优化算法,这里只说一下multi paxos算法。

    之前提到paxos算法存在活锁的问题:一个提案者提案被拒以后用更高的提案,另一个提案者发现被提案被拒以后也增加提案编号,从而形成死循环,造成活锁。

    multi paxos算法对paxos进行了优化,引入leader这个角色以避免活锁问题。首先选举出一个节点作为leader,之后所有的提案先提交给leder节点,因为通过leader可以控制提案提交进度,从而避免活锁发生。

    考虑前面商人买地的那个例子:此时官员们不直接和商人碰面,而是由官员指定一个总代理,啥事情都先跟代理说,再由代理统一汇报。于是P1跑到代理那承诺说:只要能批,咱就给领导100W酬劳,但是代理可以选择不立刻就去把这事给官员汇报,他可以等一等,结果不久后P2来承诺说事成之后200W,代理就可以选择报价高的拿给官员审批。

    可以参考paxos和multi paxos算法的流程图仔细体会一下:

    paxos算法:

    

    multi paxos算法:

    

    从图中可以看到最大的区别在于,multi paxos算法没有了第一阶段(prepare阶段),而是直接由leader发送提案(直接进行第二阶段),如果收到半数以上的acceptor的应答就达成共识。

    引入leader节点虽然可以解决活锁问题,但是又引出其他一些问题:比如leader应该如何选举产生等等。

2.3.5 paxos算法在区块链中的适用场景

    paxos主要用于解决非拜占庭模型的共识问题,一般适用于私链,因为私链仅在组织内部使用,因此可以不考虑集群中存在拜占庭节点问题,而公链或者联盟链则必须要考虑存在恶意节点的情况,因此不适合用paxos或者raft这类非拜占庭模型的共识算法。

3 小结

    本文学习了分布式系统经典的paxos共识算法。

    paxos是CFT类共识算法,不考虑拜占庭错误即节点可能作恶的情况;

    paxos算法将节点分成三个角色:提案者(proposer),接受者(acceptor)和学习者(learner)

    paxos算法分成两个阶段来完成共识:

    (1) 准备阶段:提案者发出提案,试探是否能得到半数以上acceptor的同意;

    (2) 提交阶段:如果提案在准备阶段得到半数以上的支持,则尝试提交此提案,如果响应的acceptor超过半数以上,则此提案被选定,完成共识;否则提案者需要新选定一个提案编号重新进入准备阶段;   

    

    


猜你喜欢

转载自blog.csdn.net/ztemt_sw2/article/details/80758286