libpaxos阅读笔记

来源:互联网 发布:linux cp命令详解 编辑:程序博客网 时间:2024/05/16 12:22

这篇阅读笔记是在大概1年前写的,是对libpaxos源码的一些注释吧。其中我的师弟yy和dm(不知道他们愿不愿意我署他们的名字,暂用缩写代替吧)对其做了一些修改,还有川大的hs老师,师兄(姐)yc,yz所做的工作,在此感谢。

鉴于个人学识有限,对paxos算法的理解也有限,有任何问题,请留言。废话就不说了,下面是笔记的全文:


        libpaxos阅读笔记

libpaxospaxos算法的一个库实现,它几乎完全是按照<<paxos made simple>>来写的,在阅读程序前,最好先看看<<paxosmade simple>>

  1. proposer.c

此文件实现proposer

    1. 在此文件中用到的数据结构

typedefstruct proposer_record_t {

intiid;/*实例id*/

intballot;/*proposal number*/

status_flagstatus;/*proposal的状态*/

intpromise_count;/*回应了preparerequestacceptor的个数*/

promise_msg*promises[N_OF_ACCEPTORS];/*保存所有acceptorpreparerequest的回应*/

promise_msg*reserved_promise;/*如果acceptor回应的preparerespond中已经有达成共识的value,这在voidcheck_ready(proposer_record*rec)中将其保存在这个结构中*/

}proposer_record; /*这个结构体用于保存所有proposer提出但还没有达成决议的提案*/


typedefstruct pending_promise_t {

intfrom_iid;/*一次发送的所有preparerequest的起始实例id*/

intto_iid;/*一次发送的所有preparerequest的结束实例id*/

longunsigned timer;/*为这些preparerequest设置的起始的时间,这是一个绝对时间用于超时判断*/

structpending_promise_t* next;

}pending_promise;

staticpending_promise*p1_pending_list_head;/*未决的proposalrequest链表*/

staticpending_promise*p1_pending_list_tail;


typedefstruct client_value_wrapper_t {

int value_size;

void* value;/*客户端发来的value*/

structclient_value_wrapper_t* next;

}client_value_wrapper;/*用于保存客户端发来的value,在paxos中即为提案的内容*/

使用一个有头结点和尾节点的链表来保存客户端发送的value:

staticclient_value_wrapper*client_list_head;

staticclient_value_wrapper*client_list_tail;

staticintclient_list_size=0;


staticclient_value_wrapper*p2_pending_accept;/*用来保存p2a提交的value*/

staticlongunsignedp2_pending_timer;/*p2a的时间*/


staticproposer_recordproposer_array[PROPOSER_ARRAY_SIZE];/*记录开始但还没有完成的paxos实例及相关信息*/


staticint highest_open; /*proposer当前所发起过的最高的实例id*/

staticint current_iid/*当前proposer所能看到的已经形成决议了的最高的实例id*/

staticint prop_id;/*用户指定,用于计算proposalnumber,由两个宏来计算

proposalnumber,它们是BALLOT_NEWBALLOT_NEXT通过简单的增加一个间隔MAX_PROPOSERS*/

    1. proposer执行过程简介

proposer的执行由proposer_init函数开始,此函数通过参数传入一个整数,用于赋值给prop_id,每个proposer传入的参数都必须不一样,以便能使每个proposer永远不会产生相同的proposalnumber。此函数将创建3个线程----1)、proposer_loop、(2)、timeout_checks_loop、(3)、delivery_check_loop。下面将分别介绍各个线程的作用。

1.2.1proposer_loop

此线程的作用是等待acceptorpreparerequest的回应,如果返回的不是preparerequest的回应,则直接返回不做处理,(实际上acceptor不会给peoposer发送对preparerequest回应之外的其它消息,libpaxosProposerLearner要做到同一个进程中【注:delivery_check_loop线程】,由于Acceptor不会对p2a的消息进行回应,Proposer判断p2a的决议是否达成只有通过Learner学习到的消息来判断)。回应的消息格式为:

proposer_loop函数通过handle_promise_batch函数分析上面收到的消息,并依次处理消息的内容(由handle_promise函数处理,并且以下所述都是对于一个promise_msg,一个实例id来说的),如果收到的某个promise_msg对象是本proposer正在等待的回应,则在proposer_array数组中记录此回应,并且判断是否达到大多数条件inthandle_promise(promise_msg*pmsg,intacceptor_id,proposer_record*rec)的返回值为1的时候就表示收到了大多数的回应了),如果大多数条件满足,就调用check_ready函数,此函数根据之前收到的所有promise_msg对象,为proposer_array数组的对应项设置一个标志,根据不同的情况,标志可能有两种:(1p1_ready---此标志表示收到的所有promise_msg中,没有包含任何value(如果value是字符串的话,value=“”),这样proposer在发送acceptrequest时从客户端发来的请求中选择一个value;(2)、p2_reserved----此标志表示收到的所有promise_msg中,至少包含一个value(①如果有多个value的话,那么这些value一定是一样的。这句话将在后面证明),这样的话proposer在发送acceptrequest时将选择这个值作为value。(参考<<paxosmadesimple>>Phase2)。这样在periodic_phase2_check函数中,根据这两个标志,就可以选择不同的值来发送acceptrequestacceptrequest的发送和preparerequest不太一样:acceptrequest是一个一个实例发送的,其它的acceptrequest将在其它两个线程中完成。并且根据条件:p1_ready_count+p1_pending_count<(PROPOSER_PREEXEC_WIN_SIZE/2)prepare_new_instances();

发送的消息格式如下所示:

+-------------------------+-----------------+-----------------+-------------------+

+prepare_batch_msg+prepare_msg+prepare_msg+……….+

+-------------------------+-----------------+-----------------+-------------------+

prepare_new_instances()中除了发送上面格式的消息,还会调用initialize_proposer_rec()函数在proposer_array数组中对每个实例进行记录和调用add_to_p1_pending_list()函数将发送的preparerequest保存到p1_pending_list_headp1_pending_list_tail形成的链表中。


1.2.2timeout_checks_loop

此线程的主要作用是:(1)、检查preparerequestacceptrequest是否超时,并对超时做相应的处理;(2)、检查preparerequest的发送窗口是否低于一定的限度值,如果是,就再发起一些preparerequest。算法开始阶段的preparerequest就是在这里完成的。

此线程依次调用3个函数:check_p1_pendingcheck_p2_pendingprepare_new_instances,他们的作用分别是:检查preparerequest是否超时,检查acceptrequest是否超时,发送新的preparerequest。在check_p1_pending函数中,依次检查p1_pending_list_head链表,如果某次preparerequest(s)没有超时,就不用再检查链表后面的项了,因为链表中所有的项都是以时间为顺序来排列的;如果某次preparerequest(s)超时了,则调用check_expired_p1_range函数来将这些preparerequest重新发送出去(发送之前要增加proposalnumber,调用BALLOT_NEXT宏来实现proposalnumber的增加)。在check_p1_pending函数完成之后,根据p2_pending_accept= NULL?来判断是否调用check_p2_pending函数,这样判断的原因如下:(1)、acceptrequest是一个一个发送的;(2)、acceptrequest发送成功之后,此acceptrequest所发送的value的信息会保存在p2_pending_accept变量所指的对象中;(3)、如果本proposer所提出的提议在规定的时间内最终成为决议的话,p2_pending_accept的值将被赋为NULL。在check_p2_pending函数中将首先判断正在等待的对acceptrequest的“回应”(姑且这样,实际上除了对preparerequest的回应之外,其它任何角色都不会对proposer所发出的任何消息做回应)是否超时,如果超时的话,check_p2_pending函数首先重新将acceptrequest所发出的value插入到client_list_head链表,以便在下次paxos的执行实例中再次对此value提出提议;之后再发起一次paxos实例,此次实例的preparerequest只有一个。最后在timeout_checks_loop中,还要根据preparerequest的窗口是否低于一定的限度来决定是否调用prepare_new_instances函数,从而发起新的paxos实例。

1.2.3delivery_check_loop

此线程的主要作用是根据learner中提供的信息,检查本proposer本次所提出的proposalacceptrequest)是否被大多数的节点所accept,成为决议。

此线程将主要依次调用learner_get_next_value_wrapperverify_accepted_valueperiodic_phase2_check3个函数。learner_get_next_value_wrapperlearner.c文件中,他将当前(1)形成了决议的,(2instanceid为最小的决议,返回的变量类型为lrn_value_wrapper,设其返回值为lmsglist_headlist_tail形成的链表中的头结点)。则lmsg->iid= current_id必然成立。verify_accepted_value函数首先检查lmsg->iid=current_id是否成立,如果不成立,则表示发生了致命错误,直接结束程序;接着函数增加current_id,原因如下:不管是不是本proposer所提出的提案成为决议,总之current_id所对应的实例id已经被人占用了,永远不可能再在此实例id上达成决议。然后又调用remove_from_pending_if_matches函数,此函数此决议的value在本机的client_list_head中是否存在如果不存在就直接使verify_accepted_value函数返回(超时操作线程会在合适的时候对此value重新发起paxos实例),如果存在就再判断此决议是不是本proposer正在等待的决议,如果是的话,在一些清理操作后就从verify_accepted_value函数返回;反之,调用resubmit_pending_accept函数将p2_pending_accept插入到client_list_head链表中。最后线程将调用periodic_phase2_check函数,根据rec->status的值决定是否发送acceptrequest

    1. 对前面的证明

1.3.1、①如果有多个value的话,那么这些value一定是一样的。

证明:

设这两个value分别是v1v2v1!=v2),acceptors中大多数acceptor的集合为A= {a1,a2,……}。即在之前已经有2个不同的proposer对同一个实例id发出了acceptrequest,且所发出的value分别为v1v2,由于proposer要发出acceptrequest,必须要满足回应的acceptor的集合a∈A,b∈A,且a!=b,而这显然不满足paxos

  1. acceptor.c

此文件实现了Acceptor

2.1、相关变量及数据结构

typedefstruct acceptor_record_t {

int iid; //实例id

int ballot;//proposal number

int value_ballot;//如果此acceptor在之前已经acceptor过一个value,则此成员为此valueproposalnumber

int value_size;//对同一个实例idaccept过的的value的大小

char* value;//对同一个实例idaccept过的的value

}acceptor_record;//保存每次的acceptrequest


staticint acceptor_id;//当前acceptorid,不同的acceptorid不同


staticacceptor_recordacceptor_array[ACCEPTOR_ARRAY_SIZE];//保存一定数量的实例id不同的acceptrequest

staticchar send_buffer[MC_MSG_MAX_DATA_SIZE];//发送对preparerequest的回应的buffer


2.2acceptor的执行过程

acceptor只有1个线程,通过一个switch语句来处理----处理的消息有PAXOS_PREPARE

PAXOS_ACCEPTPAXOS_LSYNC,他们分别对应与处理preparerequestacceptrequestlearner发起的“同步请求”,acceptor对于收到的PAXOS_PREPAREPAXOS_ACCEPT都要在diskBDB)上做记录,记录时以实例idkey

PAXOS_PREPARE:由于proposer可以同时发送多个preparerequest,所以acceptor在处理preparerequest时是以实例id来区分的(由handle_prepare函数处理):(1)、当收到的preparerequest的实例idacceptor记录的实例id(在acceptor_array数组中)大时,表示这是新的paxos实例,直接给proposer回应;(2)、当收到的preparerequest的实例idacceptor记录的实例id(在acceptor_array数组中)的小时,则说明此preparerequest的实例id在之前已经收到过,但是在acceptor_array数组中的记录已经被新的拥有不同实例idpreparerequest所覆盖,而先前的preparerequest则被保存在disk上,所以直接查询BDB,并比较proposalnumberballot),如果此次的proposalnumber大,就给proposer一个回应;(3)、当收到的preparerequest的实例idacceptor_array数组中对应项的实例id相同时,则直接比较proposalnumber,如果刚刚收到的preparerequest的实例id大的话,就回应,否则不做任何处理。

PAXOS_ACCEPT(由handle_accept函数处理):对于PAXOS_ACCEPT的处理与PAXOS_PREPARE的差不多,只是发送的是消息是向learner发送learn消息,而不是向proposer发送preparerequest的回应。

PAXOS_LSYNC(由handle_lsync函数处理):此消息是由learner发出的,消息格式如下:

若有一个新节点启动,当他发现其它节点的实例id比自己的current_id大时,即说明此节点需要追赶其它先启动的节点,此时learner就会发出此消息。对于此消息的处理如下:

设收到的PAXOS_LSYNC消息中的任意一个实例id的值为instance_idhandle_lsync函数首先根据instance_id查询acceptor_array数组,设查询的结果为rec。分两种情况:(1ifinstance_id =rec->iid,则说明这个决议在acceptor_array数组中还没有其它的paxos实例所覆盖,直接给learner发送learn消息;(2)、ifinstance_id <rec->iid,则说明实例idinstance_id的决议已经不在acceptor_array数组中,则从BDB中将其取出,然后给learner发送learn消息。

  1. learner.c

3.1、相关变量及数据结构

typedefstruct learner_record_t {

int iid;//实例id

learn_msg* learns[N_OF_ACCEPTORS];//接收到的由acceptor发送来的learn消息

int final_value_ballot;//如果达成了决议,则此值为发起此提案的proposalnumber

char* final_value;//如果达成了决议,则此值为决议的value

int final_value_size;//决议的value的大小

}learner_record;//对于同一个实例id,在达成决议之前(value=NULL)每收到一个learnmessage都要保存在这个结构体中。


staticlrn_value_wrapper*list_head;

staticlrn_value_wrapper*list_tail;

/*以上两个变量为一个链表的头和尾,这个链表的作用是保存所有的决议*/


staticint highest_enqueued = -1;//在达成决议的实例中,没有gap的最高的实例id

staticint highest_closed = -1;//达成了决议的最高的实例id

staticint highest_seen;//learner所看到过的最高的实例id

staticlearner_recordlearner_array[LEARNER_ARRAY_SIZE];//不同实例idlearner_record

//保存在这里

staticint majority = -1;//大多数的acceptor的个数。


3.2learner的执行过程

learner在启动后一共会初始化2个线程:learner_loopretransmission_loopleaner_loop用于等待acceptorlearnmessage并处理这些消息;retransmission_loop用于定时的检查gap并向acceptor发送重传这些gap的决议的请求。

3.2.1learner_loop

此线程阻塞等待learnmessage,当有learnmessage来时,调用handle_learn函数进行处理,handle_learn函数的根据实际情况将收到的learnmessage保存在learner_array的对应项中,在保存之前需要做一些判断以决定是否保存这个learnmessage,判断的条目有下面的一些:if(lmsg->iid>= highest_enqueued + LEARNER_ARRAY_SIZE)---此实例id是否过大;if(lmsg->iid<= highest_enqueued)---learnmessage中的实例id已经成为一个决议;if(rec->iid!= lmsg->iid)----learnmessage是否是此learner收到的此实例id的第一个learnmessage等等。在更新完learner_array后,调用check_majority函数检查是否收到了大多数的acceptor发来的learnmessage,如果是,则关闭此提案(设置对应的learner_record对象中所有final成员的值),并使check_majority函数返回1;反之不做任何处理,并返回0;最后,如果check_majority返回1learner_loop线程,将判断if(lmsg->iid ==highest_enqueued+1),若满足此条件,则调用enqueue_values函数将此决议的value插入到以list_head为头的链表的尾部。做以上判断的原因是防止插入链表中出现gap而出现错误。

proposer将定时的调用learner_get_next_value函数从此链表中取出值

3.2.2retransmission_loop

线程定时的检查决议中是否有gap产生(highest_seen>highest_enqueued?),如果有的话就调用ask_retransmission函数,请求acceptor重传产生gap的实例id和其对应的value