COORDINATION AND AGREEMENT http://www.cdk5.net/wp/
背景知识点:Reliable failure detector
实际系统中没有reliable failure detector。这里reliable是指监控程序没法精确的知道被监控进程的真实状态,比如在进程超过最大timeout时间时没法精确知道是什么错误,有可能是网络分裂或者运行缓慢等
一般实现方式:本地监控+中心监控
本地监控代理,方法一通过子进程退出信号捕捉进程crash。 (PMS LOADER code)
方法二通过定时心跳来获取进程或者线程处于活动状态而非僵死中等 (All KERNEL processes as a logical ring for heartbeat)。
中心监控从各个本地监控代理获取各个节点上进程的运行信息
在具有额外辅助管理的分布式系统中, 比如一个机架内部除了IP网络外,还存在硬件连接通道和管理模块(比如ATCA机架的IPMB总线)。 通过辅助功能可以将怀疑节点或者进程状态转换成确定的错误状态(hardware reset)之后再进行后续的恢复操作。这种方法可以认为使得reliable failure detector具有接近完全准确的检测能力。
|
第二节:
分布式系统共享资源Critical section(CS)的互斥访问
ME1: (safety) 同时只有一个进程能够访问CS区域
ME3: ( -> ordering) 如果一个进程的访问请求在另一个进程的访问请求之间,则进入CS区域的顺序也是如此
We
evaluate the
performanceof algorithms for mutual exclusion according to the . 在一个进程集合中选举某个进程具有特殊角色的算法称为选举算法。比如上述CS互斥访问,在N个进程中选举一个具有center server功能的进程来协调互斥访问。
部分进程参与选举(并非需要所有进程都进行竞选,根据reliable failure detector发现master不存在的报告,多个进程可能同时发起竞选)
多个进程并发发出竞选消息
我们可以定义选举成功的标准是具有某一全局最大值的进程获胜:
P
E2: (liveness) All processesp
participate and eventually either set
�C or crash.
j
elected
the identifier of the previous elected process.
1> ring上的部分进程同时参与竞选,发送竞选消息和自己的标示符给相邻进程。
把竞选消息中的标示符替换成本地标示符,再转发。 如果本地标示符大于竞选消息标示符并且自己是另一个竞选者则drop消息而不继续发送。
4> 如果进程最终收到自己发出的标示符的竞选消息,则说明自己获胜。之后将自己设置为非竞选进程并发送elected消息给邻居进程。 进程收到elected 消息后更新本地master标示符为elected 消息中设置的标示符。
不允许有进程失败,需要最小(N-1)或者最大(3N-1)条消息才能决策出elected process
算法2:The bully algorithm
2T
+T)并且具有可靠的消息传递机制, 每个进程知道其他进程标示符(更大或者更小的标示符,每个标示符唯一)
进程设置master
operation, as follows:
On receive(m) at p: B-deliver(m) at p.
|
1> Integrity, deliver(m)到某个进程只出现一次
2> Validity,一个进程如果没有失败并且multicasts 消息m, 则最终它会delivery m
3> Agreement/原子性,如果一个进程成功的delivery m了,所有其他没有失败的进程都最终delivery m
简单的说,每个收到m消息的进程同时也广播m给其他组内所有进程。
et al.1990] and scalable reliable multicast protocol [Floydprovide further delivery ordering guarantees.
then all correct processes in
m.
顺序多播
then every correct process that delivers
m‘ .
happened-before relation induced only by messages sent between the members ofthen any correct process that delivers
m' .
<span style=\"\\"color:#010101;font-family:'times\">CO是针对有因果关系的两个消息而言,比如C1和C3,使得任何其他进程deliver消息的时候确保C1在C3之前。
Total ordering: If a correct process delivers messagembefore it deliversm' , then ' will deliver
m<span style=\"\\"color:#010101;font-family:'times\">' .
FIFO和CO是偏序关系,可以有多个进程同时并行多播
上述的TO与FIFO可以不同,因此也可以定义同时遵循FIFO 和 TO的混合顺序 以及 遵循 CO和TO的混合顺序。
<span style=\"\\"color:#010101;font-family:'times\">TO并不意味着reliable delivery, 比如进程1 deliver m,然后deliver m” 而进程2 deliver m 然后lost m'
synchronous distributed 前面的都是基于异步分布式网络,上面一段没看明白。。。
Dolev and Strong’s algorithm again takesf+ 1 rounds, but the
.以上两种算法是基于同步分布式系统,也就是说每个回合使用timeout并且假设没有在timeout范围内的进程为failed 进程。
Fischer 证明没有算法可以在异步分布式系统中保证guarantee. 同样可知也没有算法在异步分布式系统中可以保证BG, IC以及totally ordered and
et al.2> Consensus using detectors, 通过failure detector来移除failed process或者超时的proceess,从而剩下的进程子集类似于一个同步分布式进程集合来达到concensus。 问题是timeout的设置为影响consensus的时长。 另外第十八章的另一个问题是network partition
showed
N/
A .
|
然后发现consensus 也非终极武器,全宇宙最终极武器paxos姗姗来迟。
!!!Paxos Made Simple by
Leslie Lamport!!!
大家觉得Lamport的Paxos算法原文太难理解,于是Lamport用英文白话文重新解释了Paxos算法。
Lamport认为Paxos是一种consensus算法(多数人一致)。作者以一种特别的方式来阐述Paxos算法,即从结果倒推需要达到一致性结果所需要的属性, 然后根据这些属性或者特性来设计算法,从而保证分布式结果的一致性。赞这思维!
问题
假设多个进程可以提交自己的请求值,consensus算法保证最终其中一个进程的请求值被选择。
如果没有请求值被提交,就没有请求值会被选择出来。
如果一个请求值已经被选出来,那么最终这些进程都会知道这个请求值被选出来。
因此确保consensus正确的条件是:
1) 只有一个提交的请求值可能会被选出
2) 最多只有一个请求值被选择
3) 只有一个请求值被选择以后,其他进程才能知道这个请求/值被选择到。
作者定义了consensus算法中的三种角色:proposer, acceptors, learners 一个进程可以充当其中一个或者多个角色。
这里假设我们使用一般异步模型, 进程只通过发送消息通信,非Byzantine(没有叛徒或者假冒的消息?)模型:
a) 即进程可以以任何速度运行,失败或者重启,可能在一个值被选定后,失败,再重启 ==》因此要确保选出的值被进程知道,则需要存储这个信息。
b) 消息传送的速度可能丢失或者具有任意的延时,或者重复,但是数据不会被破坏 ==》可以通过checksum来保证这点。
对于选出一个请求值,最简单的是只有一个acceptor时,它收到的第一个请求值即为被选出来的的值。但是一个acceptor会存在单点失败的故障。
当有多个acceptors时, 要确保唯一一个请求值被选出,则需要大多数acceptors都接受这个值,这样唯一性就可以得到保证。
在假设没有进程失败和消息丢失的情况下,如果只有一个请求值被提交,那么这个请求值将会被接受
需求P1:
一个acceptor必须接受它收到的第一个请求值。
但是如果多个proposer同时提交不同的值,会导致没有一个请求值会被大多数acceptors最终选择。
因此为了确保某一个请求值可以被大多数acceptors接受,那么一个acceptor则需要可以接受多个请求值(即覆盖最开始的值,使得其他后来某个值可以被大多数acceptors接受;既然acceptor可以接受多个值,则我们可以分配id(比如说自然数)来标示这些请求值。 为了防止混淆,我们以某种数字顺序方式来保证这个id具有不同值,即相互不重复。)
虽然我们允许多个请求值被接受,但是我们必须保证这些被接受的值都是相同的某一个请求值。
基于以上推理:
需求P2:
如果一个请求值v被选择,那么所有具有更高数字的请求值被选择出来的话都具有请求值v。
要满足P2,即某个请求值被选择需要它至少被一个acceptor接受。因此通过P2可以得出:
需求P2-a:
如果一个请求值v被选择,那么所有具有更高数字的请求值被任何acceptor接受的话,请求值为v。
因为通信是异步的,假设某个新的proposer可能在很久后以更高数字的请求某个之前没有接受过任何请求值的proposer,即
需求P1,那么acceptor可能会接受一个请求值不为v的请求。这样就违反了
P2-a。
因此为了满足P1和P2-a,我们对P2-a进行加强,得到P2-b:
需求P2-b:
如果一个请求值v被选择,那么所有具有更高数字的请求值被提出的话,请求值都是v。
由P2-b可以推出,如果P2-b被满足的话,将会存在一个大多数acceptor集合,这个集合中的每一个acceptor都接受请求值v
因此得到需求P2-C
需求P2-c:
对于任意的v和请求n, 如果带有v和n的请求被提出,那么存在一个集合S由大多数acceptors组成,
这些acceptors要么(1)没有一个S集合中的acceptor接受过任何比n小的任何请求值,要么(2)请求值v应该选择为S中所有acceptors接受的请求值中具有最大的请求id的请求值(id < n)。
如果我们一直遵循P2-C的原则,那么就可以保证P2-b的成立。对(1)来说,为初始化条件,任何S中的acceptor没有接受过请求值,为空。 那么提交一个值v =x , n = 1,如果被选择(因为此前acceptor都为空,根据P1 acceptor必须接受第一个请求值), 即存在一个大多数集合S中的所有acceptor都接受v = x. 当下一个proposer开始请求的时候,利用(2)可以设置自己的v = x, n = 2来进行提交, 这样请求值v = x 被选择,而n = 2 > 1的请求值为x。使得维持P2-c 可以保证 P2-b的成立。
P2-b和P2-c从proposer和acceptor两个方面来探讨满足consensus一致性结论的特性。
根据以上原则,通过学习之前被选择的最大值来保证这个请求值v在任何时候都处于一个被大多数acceptors接受的状态中。 算法可以设计为:
(1) 一个proposer 选择一个id = n 然后发送请求给一个大多数acceptor集合并请求acceptor (a) 不要接受比n小的请求值 (b) 返回当前具有的最大请求id的请求值。 (prepare request)
(2) 如果proposer 接受到大多数acceptor的回复,则提交一个请求值 id = n , 如果回复中包含v为最大的请求值,则proposer设置自身的请求值为v;否则则可以选取任意值进行提交。 (accept request)
需求P1-a:
对于acceptor: 如果之前没有收到大于n的请求值,则可以接受带有id = n的prepare请求。
可以推断出P1-a可以满足需求P1.
综上,在假设每个proposer都具有唯一proposal id值的条件下,分布式一致性可以得到保证。
从算法实现角度再对acceptor做一个优化,如果acceptor已经接受了比n大的请求,因此如果acceptor收到比n小的请求,就直接忽略而不需要reply。同时如果再收到id = n的请求也忽略掉。
因此acceptor需要记住接受的最大proposal id和具有这个id的已回复的prepare request。 这意味着在实际系统中,
需要持久存储来保证即使acceptor进程 crash 再重启也具有相同的状态信息。 相比之下,proposer不需要此类状态保存并且可以随时随意丢弃或者终止提交的proposer。
算法如下:
阶段1:
(1) 一个进程选择一个唯一的proposer number (id) 然后发送带有id的prepare请求给acceptor集合中的大多数acceptor进程。
(2) 如果acceptor 收到一个带有id为n的prepare请求并且n大于它之前收到并回复的任何prepare消息,则acceptor回复一个promise,承诺不接受任何小于n的prepare请求, 并且如果之前接受过propsal则将最大的proposal返回。
阶段2:
(1) 如果proposer从大多数acceptors收到回复,它将检查acceptors回复中具有最大id的propsal是否带有请求值v,如果带有v,则发送带有请求值v和id=n的accept请求给每个acceptor进程。如果不带v',则任意选择本地的请求值,发送带有请求值v'和id=n的acceptor请求给每个acceptor
(2) 如果acceptor收到一个accept请求,如果当前没有任何已经接受的大于n的prepare请求,则接受proposal和请求值v.
从实现上也可以主动在拒绝接受一个prepare或者acceptor请求值,主动发送通知给proposer。不过这不会影响算法的正确性而属于实现优化的范畴。
learner
在一个proposer被大多数acceptor接受以后,需要一种机制来让相关进程(proposer和其他没有参与这次consensus算法执行的其他acceptor等)知道请求值v被选择。因此需要一个新的角色,learner来学习或者检测到请求值v被选择。直观上,每个acceptor在选择了一个请求值v以后可以发送消息给所有的learner(在分布式环境下,需要多个learner来避免单点故障)。但是这样会产生很多消息消耗带宽。当然也可以让acceptors回复给某一个子集的learner,然后这些learner再通知所有的learner某个值被选择。同时假设消息可以会丢失,也可以让learner去主动询问acceptor或者通过发送proposer(prepare消息)来获知当前被选择的值。
Progress
根据以上算法,可以很容易的构造出两个proposer,每一个以递增的方式来提交自己的proposer,这样的话会造成两个proposer循环提交。比如proposer p1和p2, 各自通过n1和n2来提交请求。n2 > n1,当p1和p2都提交prepare消息后,p1的accept消息会失败,因此再次增加n1, 使得n1 > n2重新提交prepare消息,这样也会导致p2的acceptor消息失败,然后再次增加n2, n2 > n1提交prepare消息,如此循环下去。为了避免这种死锁,通过选举唯一一个proposer来保证算法可正常结束。如果这个proposer crash,则可以重新选举一个proposer进行提交而不影响算法的正确性。
Implementation
从实现角度,我们需要实现算法的假设:
1> 保证任何两个proposal都具有不同的proposal id (number), 通常可以通过给每个proposal进程配置一个唯一id = 1, 2, 3, ..., N。每次提交proposal时,id = id + N 来保证不同的proposal id唯一性。
2> 保证acceptor在回复promise消息前,将接受的最大proposal id 和请求值v保持在持久存储中。并在crash后restart的时候恢复到之前的状态。
3> 动态确保只有一个proposer被选举出来进行proposer提交。
状态机
在分布式系统中,
状态机(在当前状态,接受一个输入命令执行一步操作然后产生一个输出)具有确定性, 即如果多个状态机以相同的序列接受相同的client命令,那么结果是一致的。
因此通过paxos算法,执行一个序列的多个paxos的实例, 将会保证状态机的一致性 (见下一篇 paxos made live 论文)
!!!Paxos Made Live by
Tushar Chandra, Robert Griesemer, Joshua Redstone!!!
在理解了上面paxos算法之后,这篇论文通过侧重从工程实践的角度,对实现一个基于paxos算法的分布式数据库进行了描述,可以看做为best pratics之内的东东。因为paxos算法需要结合实际系统在编码上存在复杂性以及分布式fault-tolerate系统难以测试的特性, 这篇论文具有相当的指导意义,另外具有指导意义的开源实现见zookeeper。
fault-toerance通常通过在各个副本机器上执行相同的identical log来确保每个副本机器上数据结构和状态的一致性。log可以包括各种执行命令,比如说一系列的数据库操作可以被执行到多个副本机器的本地数据库上,从而使得多个数据库具有相同内容。因此文章基于paxos算法和identical log来实现一个fault-tolerant分布式数据库。建立这样一个系统的具有以下的实际意义:
1> 虽然paxos算法可以通过一张纸的长度将伪代码描述清楚,但是实际实现中,比如用C++来实现,则需要上千行代码。将算法转化为实际生产级别的代码,包括实际应用中需要的一些特性和优化,很多这些方面的内容在学术论文中并没有被提到。
2> fault-tolerant算法领域一般倾向于证明短小简洁的伪代码的正确性。这种方式缺乏通过真实系统来获得算法正确性上的“信心”,因此通过实现系统来验证算法正确性是必要的。
3> fault-tolerant算法通常存在一些假设条件并且只能容忍某些精确定义的错误和失败。但是对于实际系统,必须处理各种更广泛类型的错误,比如说算法实现中的bug,维护人员的操作错误。我们只能通过软件和设计操作流程来处理这类更广泛的错误。
4> 实际系统很少被精确详细的定义说明。而且需求经常在实现阶段发生变化以及修改。造成的结果是,系统可能由于对规范或者定义的误解造成被错误的实现。
Chubby 是在Google使用的一个fault-tolerant 系统, 它提供分布式锁机制和提供小文件存储的服务。通常一个数据中心有一个Chubby实例。多个Google的应用系统,比如GFS和Bigtable 使用Chubby服务进行分布式协商和存储元数据。 Chubby通过replication实现fault-tolerant功能,一个Chubby实例一般由5个独立的副本机器组成,运行着相同的代码。每个Chubby对象(比如一个Chubby lock或者 文件)都作为数据库的一项被存储着。数据库进行副本机器间的同步,其中一个副本被选为“master”。
Chubby客户端(GFS或者Bigtable)请求Chubby实例进行服务。“master” 负责处理所有请求。如果Client请求的不是“master"副本,则该副本会将当前”master“地址返回给client,比如IP。然后client在联系”master“。 如果”master“失败,新的”master“会被自动选举出来。Chubby的最初版本选用一个第三方的商用fault-tolerant数据库, 取名为”3DB“。但是这个3DB有一些复制同步间的bug,而且复制协议不是基于被证明的复制算法上的。鉴于有这些bug以及Chubby的重要性,有了重新实现3DB功能的需求。
架构
Chubby架构图
1> Fault-tolerant log 复制基于Paxos 算法处理协议栈的最底层。每个副本机器维护一个本地的log副本。Paxos算法的运行保证每个副本上拥有相同的本地log操作。副本直接通过Paxos算法描述的协议进行通信。
2> 第二层是一个fault-tolerant 数据库,每个副本机器上包括一个本地的数据库拷贝, 数据库由一个本地镜像(snapshot 或者成为当前状态)和 可重放(重新执行)的数据库操作组成, 新的数据库操作通过将消息提交给本地的副本log来执行。当一个数据库操作作为副本内容出现在本地log上,则将这个log中的具体操作应用到本地数据库中。 即 local DB --> local log --> Paxos --> local log (marked as replica) --> local DB.
3> 最后Chubby使用fault-tolerant数据库来保持它自己的状态。 Chubby clients通过Chubby自定义的协议来访问某一个Chubby副本机器。
通过这种设计,我们可以定义清楚的结构来区别底层的Paxos framework,数据库和 Chubby服务。 而且Paxos framework可以被重用到其他的应用中去。这种基于Paxos算法的fault-tolerant log在很多分布式系统中具有重要的作用。
当一个value被提交到Paxos framework的local log中,系统通过回调来通知每个副本机器中的回调应用并传递value。系统通过多线程来保证多个值可以并发被多个线程提交。replicated log本身并不创建自己的线程,但是可以被其他多个线程并发调用。
这种线程模式可以帮助测试系统,在后面中提到 (how??)。
Paxos相关
介绍Paxos算法以及描述如何将多个paxos执行实例连接在一起。
1> Paxos基础 (细节见前面的 Paxos made simple)
主要过程
a) 选举一个副本机器作为协调者(一个proposer保证没有两个或者多个同时提交导致循环下去)
b) 协调者选择一个值v 然后广播给所有的副本机器(包括自己),即 accept 消息。 其他副本消息要么接受消息发送acknowledge 消息或者发送reject拒绝消息
c) 当大多数副本机器回复acknowledge消息给协调者后,说明达到大多数一致,协调者广播commit消息给所有的副本机器。
当少数副本机器失败时,算法仍然可以运行(liveness)和保持正确性(safety)。Paxos算法可以允许多个协调者同时存在,但是在实际系统中,当协调者失败时,会避免多个机器副本成为协调者,因为这样会导致达成一致的时延增加。
2> 多个Paxos实例
实际系统使用Paxos来完成一个序列多个请求值的一致性,比如replicate log。最简单的方法是多次执行Paxos算法,每个执行称为一个Paxos实例。
在多个Paxos实例中,某些副本机器可能因为机器运行缓慢而没有参与最近的多次Paxos实例。作者设计了一个
catch-up机制来确保这些缓慢的副本机器可以赶上领先的副本机器。每个副本机器维护一个本地的持久log来记录全部的Paxos实例执行。当一个机器副本crash然后被恢复后,它可以重放在crash之前的持久log来重构之前的状态。副本机器也使用这个log来帮助缓慢的副本机器赶上最近的Paxos实例状态。
Paxos算法要求在发送消息之前记录自己的状态。因此算法要求先后5次写入(for each of the propose, promise, accept, acknowledgment, and commit messages) 到磁盘上。所有的写入必须先flushed到磁盘上,之后才能进行其他步骤的执行。如果副本机器都处于同一个高速以太网环境中, 磁盘flush的实际将主导整个实现的延时。
一个广为人知的优化是减少多个Paxos实例的消息次数,方法是通过选取唯一一个proposer/协调者长期地作为master来进行消息的提交。这样
每次Paxos实例只需要一条写入消息(Paxos实例之间并行执行):master在发送accept消息之后写入立刻写入磁盘而其他副本机器先
写入磁盘
再发送acknowledge消息。
为了增加并发系统的吞吐量,可以在一个Paxos实例中,提交一批请求值集合。
算法的挑战
虽然Paxos算法被很好的描述,但是实现fault-tolerant log的复杂性还是存在的,比如说磁盘失败,有限的资源等, 还有一些因为额外的需求或者优化, 比如说 ”master leases“ 可以增加read吞吐量和减少时延。 许多这种实际方案和Paxos算法联系在一起。
1> 处理磁盘损坏
磁盘可能因为物理介质损坏或者操作员误操作(删除)。当磁盘损坏时,它就丢失了自己的持久状态,比如忘记之前的promise 请求,这导致破坏了Paxos算法的一个重要假设。磁盘损坏的两种表现形式:文件内容改变 或者 文件不可访问。 对前一种,可以通过校验和(checksum)来检测 (注意:checksum没法检测文件和对应checksum值一起回滚的问题,即这两个值相同但是不是最新的,这个问题可以通过后面介绍的分布式checksum机制来检测)。
当文件不可用时,比如从一块新的硬盘初始化,副本机器在这种情况下被表示为不参与投票的成员。通过catch-up机制来更新自己的状态但是不能回复promise或者acknowledge消息。通过catch-up机制重新构建自己的状态并且直到它观察过一个完整的Paxos实例后, 才能回复到正常的Paxos状态,即参与回复消息。(通过这个额外的观察一个完整的Paxos实例执行,确保副本机器不会破坏之前的promise,如果有的话)
2>master租约
当基本的Paxos算法用来实现复制的数据结构时,读取数据结构也需要执行Paxos实例 (比如说local DB read -> local log -> Paxos -> local log -> local DB read)。这种方式确保读和更新可以序列化并且保证当前状态被读取。 特别,读操作不能只通过读取”master“的本地拷贝来实现,因为此刻可能有其他副本竞选”master“并且提交更新而没有通知旧的master。 这种情况下,只读取旧的master上的值可能返回的是陈旧的状态。 但是通常实际应用中,读操作占整个操作的比重是相当大的,通过Paxos序列号读操作会非常耗时。
一种workaround是实现master leases(master租约):只要master还持有租约,它就保证其他副本不能成功的提交请求给Paxos实例。因此一个master的本地log就具有了当前最新的状态信息,可以用来提供读操作服务。在绝大部分时间中,可以在租约超期之前重续租约。(从Google的系统运行表面,一个master一次可以维持多天的master 租约)。在实现中,所有的副本机器在执行Paxos实例之前都会授予一个master租约从而在租约期间拒绝来自己其他副本机器提交的请求值。master维持一个比其他副本机器更短的租约超时时间(因为各个副本机器的时间可能会发生漂移)。master 周期性的提交一个”heartbeat“ 请求值来更新租约。
在network partition(网络分裂)的时候能够为了保持系统的稳定性,当一个master短暂地失去联系,Paxos将选举一个新的master。 新的master维持一个跨多个Paxos实例的固定的序列号,同时当失去连接的旧的master试图提交一个Paxos请求时(它还能连上其他的机器副本),它可能会增加自己的序列号。当它恢复连接后,它可能有比新的master有更高的序列号,然后取代新的master。之后它可能因为网络不稳定,再次市区联系,然后重复上面的现象。这种行为会导致快速的master切换。 因此在实现中,master在运行完一系列Paxos实例后周期性的增加它的序列号(id)。通过这种周期性的增加可以避免大多数的master连续切换。
3> Epoch numbers
Chubby client 提交的请求会被路由到当前的Chubby master副本机器,master在接收到请求时会更新下一层的本地数据库,如果在此刻,master 副本机器失去了master状态,甚至之后又恢复了master状态,在这种条件下,Chubby要求客户端终止请求。因此需要一种机制来检测master 翻转然后终止请求。 在实现中,通过使用global epoch number来支持这种机制:如果一个master持续在一个时间段内为两个请求服务,则将收到相同的epoch number。epoch number 存在数据库中,
5> Snapshots镜像
不停的运行Paxos算法将导致replicated log不停的增长。这会带来两个问题:磁盘容量的持续增加(vs bounded space)和 更坏情况下更长时间的副本机器的恢复。因为log通常是应用在某个数据结构上的操作序列来更新数据结构的内容(通过重放序列操作),因此一种简单的方法是使用snapshot镜像,即直接的数据结构内容值。因此不需要之前的操作序列。
Paxos框架本身并不知道我们要复制的数据结构,它只关心replicated log的一致性。通过一个特殊的应用来使用Paxos实例获得replicated 数据结构。因此该应用负责产生镜像。 在Google的Paxos framework中提供一种机制来告诉Paxos framework一个snapshot被产生。它将截断和删除这个snapshot之前的日志记录。 在可能导致正在catch-up的副本恢复会失败,它可以简单的重新获得最新的镜像再重放和重构自己的状态。镜像并不是在多个副本机器上同步产生,每个副本机器自主决定何时创建一个镜像。 这种机制在Lamport的论文中也被提到。但是在论文中使用一致性的snapshot和log增加了复杂性。replicated log是由Paxos framework来提供的,但是snapshot的用途却是应用相关的,上面的这种机制由专门应用来产生镜像可能更加灵活。
数据库事务
Chubby实现的数据库需求比较简单,数据库存储key-value 键值对(key和value都是任意的字符串),支持通用的操作,比如插入,删除,查找和原子操作CAS (compare and swap) 和迭代所有行。 论文实现了一个基于整个数据库的snapshot和应用于这个snapshot之上的log 数据库操作。数据库log 由Paxos replicated log实现。系统周期性的产生snapshot并截断log。
CAS操作需要支持原子性。
这可以通过将所有CAS相关数据提交为一个Paxos请求来实现。这种实现具有通用性,而不需要像一般数据库事务一样被实现, 称为MultiOp. 除了迭代以外,所以其他的数据库操作都被实现为一个单独的MultiOp。一个MultiOp可以被原子性的使用。
MultiOp由三部分组成:
1) 一个序列的测试条件,称为
guard。guard中的每个测试都检测数据库中的一行值。比如检查存在与否,或者比较特定的一个值。 guard中两个不同的测试可能应用在相同或者不同的行上。guard中所有的测试被检测,如果所有测试返回true,则MultiOp 执行t_op, 否则执行f_op
2) 一组数据库操作序列被称为t_op。 每个序列中的操作可以是插入,删除或者查找,应用给单独一个数据库行。两个不同的操作可能被应用到相同或者不同的数据库行上。这些操作在guard返回成功后执行。
3) 一组数据库操作序列被称为f_op。通t_op,只是在guard执行失败后才执行。
在采用epoch numbers规则之后, 在guard中增加对epoch number的检测。
相关的软件工程经验
为了确保Chubby可靠的运行,采用了如下的软件工程方法:
1> 清晰的实现算法
将fault-tolerant算法用代码精确的表示是比较困难的事情。特别是该算法和其他代码混合在一起的时候,代码难于阅读,推理和调试。另外根据新的需求更改代码也比较困难。
解决方法是通过两个显式的状态机来进行编码。设计了一个简单的状态机规范语言并创建了一个编译器将规范转化成C++ 语言。状态机语言设计的比较简洁使得可以在一页上显式整个算法。另外一个好处是,状态机编译器可以自动产生代码来记录状态变化和在调试和测试中检测代码覆盖率。 使用这种方法而非直接编码并将其他代码混合在一起更加方便维护和修改。文章通过描述在实现过程中对状态变化的修改只需要几天的时间来说明其优越性。
BSC的SWR状态机也是使用了这种方法,来简化因为需求或者流程变化带来的状态机修改。
2> 运行式一致性检查
随着项目的变化,代码量会增加,人员会变动,不一致的概率会增加。论文通过多种主动自我检测机制来保证一致性,比如使用assert, 显示验证代码对于数据结构的一致性。
比如,master周期性的提交checksum 请求给database log。在副本机器接受到这个请求后,每个副本机器自己计算本地的数据库,因为Paxos log 是序列化所有的操作,因此所有副本应该计算得到相同的checksum。master完成checksum计算以后,它发送自己的checksum给所有副本。 通过这个机制,检测到几个错误。
测试
基于以上的实现,不可能对一个实际系统进行证明是否完全正确。为了提高可靠性, 最佳解决方案是进行系统测试。论文中的系统从最开始就被设计成易于测试并通过增加一系列测试用例来检测bug。作者设计了两类检测,safety mode, 检测系统具有一致性。 但不需要系统可以持续正确运行。比如一个操作失败,或者系统不可用都是可接受的。
liveness mode, 检测系统具有一致性和可继续运行。所有的操作被期望可以完成并且保持一致。
从safety mode开始,注入随机错误到系统中。在运行一段时间后,停止错误注入等待系统恢复。之后再切换到liveness mode。 通过liveness mode来检测是否在一系列失败后存在死锁。
一种测试是验证 fault-tolerant replicated log. 模拟一个由随机数量的副本机器的分布式系统,并通过一序列随机的网络失败,消息延时,超时,进程失败和恢复,文件损坏,以及交叉调度等。 为了使得这种测试可以重复执行以便于调试,使用一个随机数生成器来决定错误情况的调度。随机数的种子在测试之前给出,具有相同测试随机数种子的测试过程应该是相同的,使用单线程来移除由多线程引入的非决定性。这就是以上所说的fault-tolerant replicated log不创建自己的线程,而是跑在单线程环境中。
另一种测试是模拟底层系统和硬件失败。论文在fault-tolerant replicated log中实现了多个钩子来注入失败。测试随机调用这些钩子和验证上层系统能够处理这些失败。钩子包括:crash副本机器,断开网络连接一段时间,master副本放弃master角色。
并发性测试
之前为了保证代码的可测试性,之前的实现是单线程的,多线程放在核心实现之外的区域,比如网络监听请求。随着项目需求的变化,多线程和并发特征需要被满足,因此需要修改代码来支持并发性。对于测试,通过只能通过对并发性的约束来进行。
其他异常
1> 最开始使用了相对原系统10倍的worker 线程以期望提高吞吐量,但是发现线程为处于饥饿状态(未被调度打)而导致经常timeout。这会导致快速的master failover,再接着伴随着大量请求的切换,从而继续master 切换。
2> 升级失败(在各种分布式系统中都非常难以克服和测试,BSC的SWR也是如此)
3> 增加epoch number 来保证两个或者多个操作由同一个master提交完成。
4> 还是升级相关的失败
5> OS问头,在执行snapshot写入后,log小文件写入flush变的更慢。 W.A是将大文件分成小chunks来写,并且flush是在每个small chunks被写完之后。 这虽然延长了snapshot的写入,但是保证了critical patch,即fault-tolerant log的写入不超过非指定的延时。
|
因为字数的限制,剩下介绍请见 “分布式系统概念和设计 第十五章 (2)” 并附件完整版。