由于blog的字数限制,将本章分为两个部分,参见分布式系统概念和设计 第十五章 (1)。

附件为完整版。




A simple totally ordered broadcast protocol
by Benjamin Reed, Flavio P.Junqueira from Yahoo (Zookeeper)


文章概要介绍了一个全序广播协议Zab,并且基于Zab实现了一个高可用性的一致协商服务(coordination service), ZooKeeper。 ZooKeeper可以支持大规模应用程序执行协商, 比如master选举,状态广播,配置(rendezvous??)等。
Zookeeper以树状层级结构的方式来表示逻辑资源(类似一个文件系统的结构),每个节点称为znodes, Client程序利用znodes来实现协商。
基于这种设计架构的Zookeeper可以为大量(web-scale),重要(mission critical)的应用程序服务。

Zookeeper放弃锁机制而是实现了在共享数据对象上的无等待(wait-free),并基于操作顺序来保证对这些共享对象的有序访问。对于Zookeeper来说相比锁机制,它更侧重于对顺序访问的保证。

有序广播是实现顺序访问的保证,并且确保每个副本机器上状态的一致性。

ZooKeeper通常由3到7台机器组成,当然更多的机器也可以支持,不过3到7台可以满足一个足够的性能和弹性要求。Client连接任意一个提供服务的副本机器。 如果ZooKeeper集群配置为2f + 1 个 副本机器,那么ZooKeeper服务能够容忍f个副本机器的失败而不中断服务。


ZooKeeper支持数以千记的客户端并发访问。 ZooKeeper设计的工作负载假设为读写比为2:1, 但是ZooKeeper对写操作的高吞吐量支持使得它可以被用在即使写占主要比例的工作负载下。ZooKeeper通过每台副本都可以提供从本地副本中支持读服务来提供高吞吐量的读操作支持。 因此也可以通过增加机器来提供fault-tolerant 和 更高的读操作吞吐量的要求。 但是写操作的吞吐量受限于广播协议本身。




ZooKeeper的读写操作的逻辑结构
1> 读操作直接从本地的复制数据库来获得服务。
2> 写操作被转化为Zab协议的幂等性事务请求(idempotent transactions, 见下面的介绍),然后通过根据Zab协议提交后,写入本地复制数据库再返回给客户端。
(多次调用而执行相同的操作, 比如参考链接中的使用http来取款http://www.cnblogs.com/weidagang2046/archive/2011/06/04/2063696.html, 如果取款消息在返回的过程中包丢失,则用户可能重新提交,此时会再次扣款,
通过增加一个本地的ticketID,使得多次提交带有ticketID的请求,即<ticketID, accountID, withDrawNum> 的多次调用返回相同结果,web client和server端执行相同的操作和结果)。

许多写请求是基于某些条件的。比如说一个znode只能在不包含任何子znode的时候才能被删除。一个znode可以根据一个名字和序列号来创建,更改只能被应用在被期望的版本上。当然非条件型的写操作,比如修改版本数据也是被支持的(需要吗??

从单独一个master机器上广播所有的更新, 并将非幂等性请求转换为幂等性事务。论文中使用事务来表示请求的幂等性版本。因为master具有最新的副本状态,因此它可以执行这个请求转换。


Zab广播需要具备的需求和特性

1> Reliable delivery
如果消息m被某一个副本机器接受,则它将最终会被所有工作正常的副本机器接受。
2> Total order
如果消息a在消息b之前被接受,则对于每台副本,消息a都在消息b之前被接受。
3> Causal order
如果消息a引入消息b,并且a和b都被接受的话,则消息a必须在消息b之前。
存在两种因果顺序:a) 两个消息a和b被同一个机器发出,a在b之前,则a在因果关系上也在b之前; b) 一个master在一个时间段提交请求,如果master角色切换,那么由之前master提交的消息都在新的master提交消息之前。
4> Prefix property
如果消息m是被master广播并被其他副本接受的最后一个消息,那么所有在m之前被该master发送并接受的消息都应该被所有正常工作的副本机器接受。
一个副本机器可以被多次选为master,但是每次都做为一个不同的master出现。

<1>和<2> 保证每个副本都具有一致性的状态
<3>确保从客户端应用的角度,因果顺序被正确的排序反映。



Zookeeper的fault-tolerant模型是master失败后进行状态恢复。使用timeout来检测失败, 进程失败后通过保持在持久存储上的状态信息进行恢复 (对于之前没有deliver的请求则需要忽略)。
为了从失败中恢复,需要在失败之前,将消息记录在大多数副本机器的磁盘上。
虽然假设没有Byzantine错误(没有伪造的消息),但是在实际系统中需要对消息进行完整性检测(在消息中增加元数据),对于数据被损坏的消息将被检测并且终止服务。

Zookeeper使用内存数据库来存储事务日志(transaction logs)并且周期性的备份镜像到磁盘。事务日志在内存数据库和磁盘各有一份,但只写磁盘一次(Zab's transaction log doubles as the database write-ahead transaction log so that a transaction will only be written to the disk once??)。 一般情况下,使用Gbps网络,因此瓶颈在磁盘写操作上。 为了抑制磁盘写操作造成的瓶颈,论文中使用批量写事务到磁盘,即一次写操作记录多个事务(我的理解这里多个事务应该是同一时段并发的多个没有相互关联的事务,需要阅读参考文献中的4,5,6来搞清楚??

节点失败后的恢复通过读取镜像数据以及重放镜像之后的事务日志来完成。在恢复期间,原子广播协议不需要保证只广播一次。因为使用了幂等事务,则可以多次提交相同的请求。这使得全序消息发送变的更容易一些。 比如消息a在消息b之前被接受,那么某次失败之后,消息a会被重新发送,消息b也会在消息a被发送后再被发送。

Zab广播的其他需求
1> Low latency
应用希望有较低的响应时延
2> Burst high throughput
虽然应用通常使用ZooKeeper时,读操作占大部分请求比例,但是偶尔会因为重新配置导致同时大量写操作请求的产生
3> Smooth failure handling
如果一个副本机器失败并且不是master,则服务不应该被中断


为什么还需要额外的多播协议Zab,特别是Paxos发明之后(对Paxos的改进)
1> 增加对FIFO的支持

这是因为在Paxos协议中没有指定FIFO顺序,消息可以异步发送。利用实际系统中的一些特性(比如TCP的FIFO)Zab可以支持FIFO,类似”Paxos made live“中增加的epoch number来检测违反保证FIFO并返回失败,可以简化Paxos协议并支持高吞吐量需求。
2> 将master选举引入到协议本身,避免多proposer竞争的问题
另外Paxos存在多个proposer竞争的问题,Zab通过将master选举也转化为某次请求以获得大多数机器副本的认可。
3> 增加epoch number来简化master切换和recovery



fix sequencer的使用会降低协议复杂度从而获得高吞吐量(why?? 见参考文献8)但是传统的fix sequencer会在失败发送的时候造成更大的时延, failure detector的实现也比较困难,需要监控集群中多个机器)而基于单独master的方式(why?? 见参考文献8),failure detector就简单许多,只需要监控一个master即可。

因此Zab使用一个fix sequencer,master(称为leader)选举出来后产生一个fix sequencer并且广播给大多数副本机器(称为follower),实现这种方法的原因:
1) 客户端可以连接任何副本机器,副本机器可以基于本地日志响应读请求并维护和客户端的连接。写请求则转发给拥有fix sequencer的leader。这样可以分担系统负载从单独一个master到多个副本机器。
2) 参与Zab集群的机器数通常为3-7个,因此由fix sequencer带来的消息数不会很多。
3) 不需要实现更复杂的方法,因为这种简单方法以及具有足够的性能要求。


移动sequencer会使实现变的更加复杂,比如token ring协议,需要处理token丢失的情况。

对于leader失败,我们通过view changes来反应这种变化。这里为了避免view changes随着单个副本机器的加入和离开而变化,只通过view changes来反应leader的切换(细节见参考论文10??



Zab协议实现
Zab协议有两部分组成:恢复模式 和 广播模式。

当某个leader失败,Zab转变为恢复模式。当新的leader被选举出来并且和大多数副本机器同步以后(确保leader和大多数副本机器具有一直的状态,同时也把上一个leader最后的deliver完成?),Zab结束恢复模式,变为广播模式。

在Zab中,Leader负责处理请求,其他副本如果收到请求,则转给leader。leader从恢复模式中选举出来,负责初期写操作(转化为Zab的幂等写请求)和协调广播协议。这样做就避免了将应用程序写操作先转换成Zab的写请求再发送给广播leader的延时。

当一个副本机器加入到Zab中,此时有leader在广播消息,这个副本机器将进入recovery 模式, 发现和从leader同步状态。
整个Zookeeper服务持续运行处于广播模式直到一个leader失败或者没有大多数的follower存在。 任何数据的大多数follower都可以保持leader和整个服务可以持续运行。比如说3台机器组成的ZooKeeper,1个leader,2个followers。当1个follower crash,系统继续运行。 当它recovery之后,另一个follower crash也不影响系统继续运行。

1> 广播
广播分为两个阶段:
leader 提出一个请求(propose),收集投票(collect votes),再提交(commit)。 简化流程如下,不支持abort(没有abort 那么合适一个proposal才算结束,timeout??timeout之后呢?)。followers或者acknowledge leader with vote 或者 abandon the request。 当leader收集到大多数投票后,就可以立刻commit 之前的请求。 广播协议使用FIFO (TCP) 来进行通信。这样可以确保消息之前的顺序。

在广播消息之前,leader会分配一个单调递增的唯一id,称为zxid。 因为Zab保持因果顺序,因此发出的消息通过zxid来排序。 广播将请求放到每个follower的输出队列。当一个follower收到请求后,它写到磁盘(如果有多个请求,则可以批量写),之后发送应答给leader。当leader收到大多数ACks时,则广播COMMIT消息,包括发给自己。 Followers 当接收到commit消息后发送消息给应用。

2> 恢复
当leader失败或者丢失大多数followers之前,以上简单的广播机制可以很好的工作。 为了保证服务可持续运行,需要一个恢复过程来选举新的leader 并且将所有副本机器同步到正确的状态。对于Leader选举,需要一个算法可以保证具有较大成功概率从而保持服务的可持续性。leader选举不仅让某个节点知道它将变成新的leader,也要让大多数followers知道新的leader被选举出来。 如果选举阶段失败,副本机器将在timeout之后重新进行选举。

论文中提到两种不同的选举实现。快速leader选举只需要数百毫秒级来选出leader。

sheer number of proposal (?? 不清楚,得看zookeeper源码??)
除了leader选举, 有两个特别的保证需要实现:
(1) 部分deliver的消息需要完成deliver

如果一个消息在失败之前,至少deliver给了一台机器,则需要之后完成deliver。 比如leader commit了一个消息,然后在commit到达其他server之前失败。如下图,因为至少一个副本机器具有更新的信息,因此事务必须在其他机器上也被更新,使得应用程序具有一致的view。

分布式系统概念和设计 第十五章 (2)_Byzantine


问题,zookeeper说在2f+1个节点的时候可以容忍f个节点失败,假设有5个节点,如果leader在把commit消息发送给server2之后, server1和server2都失败了,那么server3,server4和server5都没有收到commit消息,如果知道commit??像 Paxos一样,一个回合只会保证某一个请求值被提交吗?即至少还有一个节点发了对应的ack(大多数ack了,leader才会commit),再新选举的leader中会把之前ack的请求再次执行吗? 假设server1, server2, server3 ack了。server4 和 server 5没有,那么server1和server2失败后,只剩下server3是ack的,该如何处理?? 继续提交请求也可能导致server4和server5不ack吧?? 难道需要client知道失败以后,重新提交吗?  参考ZooKeeper代码吧

(2) 没有deliver而失败的消息需要忽略
leader接受到来自客户端的请求,但在其他followers都没有收到广播之前,leader就失败了。 从图3到图4 (注意P1...P3 和 C1...C10..01是两个并行的实例,可以同时执行propose->ack->commit),对于P3而言,在图3, server 1 失败时,server 2和server 3不知道P3的存在,当server 1 重新变成leader时,它将丢弃P3。(应该在server 1重新加入集群,同步完状态就丢弃P3了吧?
分布式系统概念和设计 第十五章 (2)_chubby_02
记住delivered消息可以通过leader选举算法来处理。当一个leader被选举时,算法保证新的leader具有当前最高的proposal number, 它也将包含当前所有committed过的消息(how??)。在提交新的消息之前,新当选的leader将确保本地事务日志中所有的消息都(包括回复给前lead回复ack的消息吗?)被提议和通过大多数follower是的commit。 通过这种方式选举出来的新leader不需要去询问follower,谁具有最高的zxid并先获取自己本地没有的事务日志。

leader确保follower具有所有已经提交过的和committed过的请求。如果follower没有某些已经committed过多的请求,leader则将这些proposal放到队列中,将这些proposal的commit发给follower。


对于需要忽略的消息,处理相对简单。在Zookeeper实现中,使用64bits 数字作为zxid, 其中低32bits做为简单的counter使用。每个proposal会使得低32位的counter加1. 高32位作为epoch number。每次在leader发送切换以后,它将从日志从找到最大的epoch number,然后递增后,将低32位置0,作为新的zxid并广播给大部分副本机器,被大部分副本机器认可后将称为新的leader。 通过这种方法避免了多个leader提交具有相同zxid的不同请求。 这种方法的优点是可以忽略失败的leader上的实例,加速和简化恢复过程。 如果一个机器重启后,提交一个之前没有出现过并且带有之前epoch number的zxid,则它将不会被接受,因为此时大多数副本节点都持有更新更大的zxid。 当机器作为一个follower连接时,新的leader将检查它拥有的最后的committed请求和最后的proposal并告诉它来截断没有commit成功的请求。



在实际产品中,zab协议具有高吞吐量和低延时(毫秒级,因为典型的提交需要4个回合的消息传递 request->broadcast->ack->commit)。Bursty load 突发的负载也可以被很好的处理,因为commit需要等待ack回应,但是缓慢的服务器并不会阻碍大多数ack的收集。 最后,leader在收到大多数ack后会立即广播commit消息,因此followers的失败不会影响到可用性或者zab的吞吐量。每秒钟可以支持数百K的请求操作(以2:1的读写比例的混合请求)。


另一篇介绍Zookeeper细节的论文, ZooKeeper: Wait-free coordination for Internet-scale systems
https://www.usenix.org/legacy/event/usenix10/tech/full_papers/Hunt.pdf
ZooKeeper视频
http://www.hakkalabs.co/articles/apache-zookeeper-introduction/

The Chubby lock service for loosely-coupled distributed systems
Mike Burrows, Google Inc.

Chubby锁服务,提供一个基于松散耦合的分布式系统上的粗粒度锁和可靠的低容量存储服务。

提供一个类似带有文件锁(advisory lock)的分布式文件系统API接口。

设计重点侧重于可用性和可靠性而非高性能(high performance)。

1 介绍
论文描述了Chubby锁服务,提供给由高速网络连接的多个节点(比如一个Chubby服务为有Gbps以太网连接的上万台4核节点服务)。 Chubby锁服务的目的是为了这些客户机活动提供同步和一致性服务。主要的目标是可靠性和可用性,以及容易使用的语法。吞吐量和存储容量并不是优先考虑的要点。 Chubby的客户端接口类似于文件系统,打开文件,读取和写入,带有advisory lock参数和不同事件的通知事件(比如文件修改)
Chubby希望可以帮助开发者提供粗粒度同步,特别是处理从一个集合节点中选举leader。GFS和Bigtable利用Chubby实现多种需求,比如选举master,允许master来发现可被控制的节点, 允许客户端找到master, 将元数据存在Chubby中,存储分布式数据结构,使用锁来对工作分区。

在Chubby被部署之前,Google的大多数分布式系统都使用自治的primary选举,或者需要操作人员手动接入。对于前者,Chubby可以通过集中提供选举服务来节约计算资源,对于后者,Chubby可以实现即使在出现错误的时候也无需操作人员介入。

对于分布式系统,从集合节点中选举一个primary属于分布式一致性问题,并且需要使用异步通讯来解决该问题(因为以太网或者互联网会发生丢包,延时以及乱序)。异步一致性可以被Paxos协议很好的解决,参考文献中列举了一些使用Paxos协议的文章,如12, 13, 19和14。 所有的异步一致性基本上都是以Paxos作为核心, Paxos在没有时间条件假设的条件下可以保证安全性,但是时钟需要被引入来确保可用性(liveness)。

设计和实现Chubby 属于工程范畴,它不算是研究(research),因为Chubby使用的是已知算法和技术。这篇论文的目的是介绍我们所做的工作和为什么要这么做的原因而非鼓吹它。在下面的章节,论文将描述Chubby的设计和实现,基于经验的修正,同时也描述Chubby被不正确使用的场景。我们忽略一些已经在其他论文中介绍过的细节,比如说Paxos一致性协议和RPC(远程过程调用)

2 设计
2.1 原理

有人可能会争论,实现一个Paxos库可能比实现一个中心化的锁服务更合适。Paxos链接库不需要依赖其他节点而且会提供一个标准的框架给开发者(假设这些服务能够被实现成状态机,Paxos使用状态机来简化一致性的实现)。
在Chubby系统中,客户端链接库也同时被提供。在作者看来,一个锁服务相比客户端链接库具有一些优势:
1> 开发者可能一开始并没有打算就实现高可用性,而是在从原型开始,负载和用户增加,系统成熟以后,可靠性才开始变的重要。复制和primary才被加入到代码中。而锁服务会使得维护现有程序结构和通讯模式更贱简单。比如选举一个master只需要增加额外两行代码和RPC参数,即获得锁来变成master,传递一个锁计数,然后服务器增加分支判断,如果锁计数小于当前值则拒绝服务。这种方式比将现有代码集成到paxos协议而且需要维持兼容性要简单许多。

2> 许多基于选举primary的服务需要一种机制来广播选举结果。这意味着需要让客户端存储和获取少量的数据,即读写文件。这种服务可以通过命名服务器来实现。但是作者发现Chubby锁服务本身就能很好的支持这种需求, 因为它即可以减少系统依赖的服务或者节点(锁服务和命名服务在一起),并且满足一致性要求。Chubby作为命名服务的成功要归功于使用客户端一致性缓存,而非是基于时间超时的缓存。因为基于时间的缓存,比如说DNS,较大的超时(TTL, time-to-live)会导致IP切换缓慢,而较短的超时则使得DNS服务器负载过重。

3> 锁服务接口对开发者来说更简单和易接受。Paxos状态机和基于锁保护的CS(critical section)区域都提供给开发者顺序编程。但是开发者之前具有使用锁的经验并且知道如何使用。意外的是,很多开发者错误的使用锁,特别是在分布式系统中,很少会考虑到在异步通讯下的单节点持有锁失败的情况。而对锁的熟悉使得开发者更倾向于使用可靠的机制来进行分布式决策。

4> 分布式一致性算法使用大多数成员一致性来进行决策,使用多个副本来达到高可用性。Chubby自身一个实例一般使用5个节点,其中至少3个节点需要提供服务。相对地,即使一个客户系统只有一个节点,也可以很安全的使用锁服务。因此一个锁服务减少客户系统为高可用性所需的节点数目。另外,Chubby锁服务可以被认为是提供了一种通用的选举策略,使得一个客户系统在即使只有少数节点工作的情况下(相对分布式一致性必须多数节点存在)也可以正确的执行。虽然一个提供一致性Paxos服务的实例也可以达到这种效果,但是它没法提供之上1>, 2>和3>中的解决方法。

以上的论述暗示了两个重要的设计决定
a> 选择实现一个锁服务而非链接库或者通用一致性服务
b> 选择实现小文件服务来允许选举出的primary广告它们自己和相关的参数,而不是在开发和运行另外一个服务(比如说命名服务)

另外的一些设计决定来自于内部希望使用的需求:
c> Primary通过小文件广告它的信息,它可能同时为数以千计的客户端服务。因此需要允许这些客户端同时观察监听这个文件而不需要多个服务器(Chubby一般为5个节点)
d> 客户端或者副本(replicas)节点希望知道primary服务的变化,这说明事件通知机制需要被用来避免轮询(polling)
e> 虽然客户端不需要周期性轮询,但是某些开发者可能倾向于这么做(比如获取信息),因此需要提供文件的缓存。
f> 开发者对于缓存语义可能会比较模糊,因此提供一个一致性缓存是一个比较好的方案(而不是基于timeout的缓存机制)
g> 为避免干扰和破坏,安全机制需要被支持,包括访问控制


可能令一些读者奇怪的是,作者并不期望Chubby锁服务被使用于细粒度的场景,在很短的时间内(秒级或者更低时间)持有和释放锁,而是提供一个粗粒度锁(比如说小时级或者按天来使用)。
这两种使用对一个锁服务的需求是不同的。 粗粒度锁意味着更小的负载,特别是锁的获取率通常比较低,和客户端应用的事务率相关。粗粒度锁不经常被使用,因此短暂的锁服务不可用会更少概率来使得客户应用受到影响或者时延。另一方面,将客户端锁从一个节点转移到另一个节点可能需要较长的恢复时间(比如说锁服务集群自身的primary切换),客户端不希望在这段时间内导致客户端锁的丢失,因此粗粒度锁会更好的节点失败的情况下运行良好。 而细粒度锁确会导致不同的结果,即使短时间锁服务不可用也会造成许多客户被拖延。性能和增肌新机器变的更加重要,因为事务率会随着客户端的事务率增加而增加。对于锁服务节点的切换来说,在节点切换时,不提供服务的方式可能更有利一些,因为锁是被短暂持有的(客户必须来应对这种比如因为网络分裂造成的锁丢失情况),这样锁服务就不需要额外的fail-over策略 (BSC使用相同的策略,即在fail-over的时候新的call不被接受,需要用户重新拨打)。

Chubby只实现粗粒度锁,对于客户系统来说,由客户应用来提供基于应用需求的细粒度锁更加直观一些。应用可能将自己的锁分组(类似数据库分表分区)然后使用Chubby的粗粒度所来选出应用自己的锁服务节点。这样只有很少的信息需要维护这些细粒度锁。这些应用节点只需要保存一个不丢失,单点递增且偶尔更新的的锁计数就可。客户系统可以在没有获得Chubby锁的时间段类主动丢失应用自身的细粒度锁,并且可以使用一个固定时间的租约,这种会使得应用和协议简单高效。最重要的是,通过这种方法,客户应用开发者可以根据负载来配置客户服务器数量而不需要实现复杂的分布式一致××× (这一段感觉不是完全清楚,理解的对吗??)。


2.2 系统结构

Chubby有两个主要通信组件通过RPC来通信。服务器节点 和 客户端应用连接的链接库。如下图
分布式系统概念和设计 第十五章 (2)_Byzantine_03Chubby客户端和服务器之前的通信都是通过客户端Chubby library来协调的。另外一个Chubby代理服务器也是Chubby服务中可选的一个组件,在3.1中介绍。

一个Chubby单元由多个服务器组成(通常是5个),称为副本节点, 放置在多个机架中来减少关联错误 (比如说机架断电)。副本节点使用分布式一致性协议来选举一个master,master必须获得多数副本节点的投票,外加承诺在一个master lease租约时间内不会选举其他不同的master (类似于总统选举,多数议员投票选出总统,并承认四年内,该总统不会被替代。看到过一句话,许多算法都可以在自然界找到印证)。master lease会周期性的被选举获胜(获得多数副本节点同意)的master进行更新。

副本节点维护一个简单的本地数据库拷贝。但是只有master才能发起读写这个数据库。其他数据库副本都只是简单的根据master发出的消息(基于分布式一致性算法协议)来更新。

客户端通过DNS Chubby节点列表来找到master。其他副本节点收到这类消息会将当前master地址返回给客户端。 当客户端连接到master之后,就可以直接发送请求给master,直到该master失败或者它不再是master。写请求会通过分布式一致性算法广播给所有的副本节点,当请求到达大多数的副本节点时,这些节点会回复ack表示收到。 读请求只需被master处理即可。假设master lease没有到期,那么就不会有其他的master存在。如果一个master失败了,其他的副本节点会进行执行选举协议, 通常一个新的master会在几秒钟内被选出,作者观察到最近的两次选举分别用时4秒和6秒,最长的一次是30秒(见4.1)。

如果一个副本失败并且在几个小时内没有被恢复,一个简单替换系统将选择一个全新的机器并将Chubby lock服务软件置于其上。之后更新DNS表,替换失败的副本节点IP为新的节点IP。当前master会周期性的轮询DNS,最终会发现这个变化。 然后它会在数据库中更新Chubby cell成员列表。这个列表通过分布式一致性算法来报纸各个副本看到的是一致性的列表。同时,新的副本节点会获取最近一个数据库的副本以及从处于工作中的副本机器获得最新的更新。当新的副本处理过当前master正等待的commit请求后,副本节点则被允许进行投票来参与选举新的master。(以上可以看到使用域名作为机器地址的好处,当然也可以使用相同IP地址,发送ARP广播来更新


2.3 文件,目录和句柄

Chubby对外呈现一个类似类似UNIX文件系统的接口和结构。它有一个树形目录和文件组成,通过斜线来分割,比如:
/ls/foo/wombat/pouch

ls前缀表示lock service
第二层foo表示Chubby cell的名字,它可以被DNS查询返回一个或者多个副本节点。 有两个特殊cell名字,local是指本地的Chubby cell,应该是最新的Chubby服务,可能在同一个建筑物中; global是指全局的Chubby cell(跨机房,见后面的章节)。
/wombat/pouch 是一个Chubby内部的名字。

和UNIX类似,每个目录包含多个子文件和子目录,每个文件包含一个序列的字节流(不可被文件系统解释)

因为Chubby的命名结构类似于文件系统,因此可以简化使用和开发者的学习难度。
和UNIX文件系统不同的是因为需要允许不同目录被不同的Chubby master服务,因此不提供操作来支持文件从一个目录移动到另一个目录,另外也避免路径依赖的权限语义(文件的权限被文件自己控制而非基于父目录)。为了简化缓存元数据,系统不提供最后访问时间。

系统只包含文件盒目录,称为nodes(节点)。每个节点都只有唯一一个名字;没有软连接和硬连接。

节点可能是持久的或者是临时的。任何节点可以被显示地删除,但是临时节点会在没有客户端打开是被自动删除。临时文件可以被用来指示该客户端应用处于活动状体(alive)。每个节点都可以作为advisory reader/writer锁,这些锁在2.4节描述。

每个节点有多个元数据,包含三个访问控制列表(ACLs)名称来控制读,写和改变节点的ACL名称。 除非是覆盖重写,一个节点集成它的父目录的ACL名称。ACLs自己也是文件,处于ACL目录中,属于Chubby cell本地命名空间的一部分。三个ACL文件包括一个简单的名字列表。 比如假设文件F's的写ACL名字是foo, ACL目录下包含一个foo文件,该文件包含一个bar记录,则用户bar可以允许写文件F。 用户通过一个RPC系统来验证。 因为Chubby的ACL是简单文件,因此他们可以自动的被希望使用简单访问控制机制的其他服务所使用。

每个节点元数据包括4个单调递增的64位数字,使得用户可以轻易的检测出变化:
1> 实例号(an instance number),大于之前相同名字节点的数字 (比如说临时文件,昨天和今天创建的相同名字)
2> 内容序列号(a content generation number)(限于文件),当文件内容被写入时,递增。
3> 一个锁序列号(a lock generation number),当节点的锁由free 变成 held时,递增
4> ACL序列号(an ACL generation number),当节点的ACL被写入时,递增
另外,Chubby还展示一个64位的文件内容checksum来告知文件是否是不同的

客户端打开节点来或者句柄,类似于UNIX文件描述符:
1> 校验数位(check digits),阻止客户端创建和猜测句柄,使得访问控制检测只有在句柄被创建是才需要检查。 (fd=0, 1, 2, .. 在进程PCB中再映射到具体内核打开文件nodes)
2> 序列号(sequence number), 区分句柄是是否由之前的master产生
3> mode information 在打开文件时提供的,允许被旧的master创建的旧句柄被新的master用来重新创建句柄状态。


2.4 锁和序列

每个Chubby文件和目录可以作为一个读写锁,要么一个客户端可持有写锁具有排他性,要么多个客户端持有读锁。 类似于mutexes,锁时协商式的(advisory)。 当多个企图获取锁时才发生冲突,持有锁并不一定必须访问文件,也不能阻止其他客户端这么做(这句表达的不好?可以参考之上的advisory来理解)。 作者并不使用强制锁(mandatory lock,见背景文件锁),使得被锁的对象不能被其他没有持有锁的客户端看见。
1> Chubby锁经常被其他服务用来保护资源,而不是带有锁的文件。而强制锁会迫使对系统进行更多的修改。
2> 作者不期望用户在对持有锁的文件进行调试和管理时必须要关闭持有锁的应用 (mandatory lock使得只有持有锁的客户才能看见内容)。
3> 开发者对于advisory锁已经比较熟悉,从编程角度来说advisory这种显示的持有锁方式更安全

对Chubby来说,获取锁需要具有写权限,所以没有权限的reader不能阻止写作者进行。

在分布式系统中,锁操作是复杂的,因为通信是不确定的,进程可能独立地失败。 因此一个持有锁L的进程可能发出一个请求R,之后进程失败。另一个进程在之后获取锁L,然后在请求R到达之前进行了操作。如果R之后到达,它可能被执行,从而造成不一致性。 这类乱序接受消息已经被研究过,方法有virtual time 和virtual synchrony,见参考文献11和1,通过保证消息有序的到达。 但是在所有的交互中实现序列号会比较困难或者代价比较大。

Chubby提供了一个方法只在锁被使用的交互过程中才引入序列号。在任何时候,锁的持有者可以请求一个sequencer,一个字符串来描述获得锁的状态,它包括锁的名字,锁请求模式(排他还是共享,即写锁还是读锁)和上面 提到的锁序列号。客户端将这个sequencer传给客户应用服务器(比如说文件服务器),请求被锁被包含的资源进行操作。 服务器接收到sequencer之后,可以测试是否合法以及是否具有相应的模式,如果没有则拒绝。sequencer的合法性可以根据Chubby的一致性cache来验证,或者如果服务器不想和Chubby一直保持会话连接,则和最近观察到的sequencer进行对比。sequencer机制仅仅只需要附加额外的字符串给受影响的消息(已有项目),因此可以比较容易的解释给开发者。

虽然sequencer比较简单,但是某些核心协议修改比较缓慢。因此Chubby提供一个虽然不完美但是更简单的机制来降低延时和乱序请求。如果一个客户端以正常的方式释放锁,它将立即被其他客户端所获取。但是如果锁变成free是因为持有者失败或者不可达,lock服务器需要在一段时间内阻止其他客户端声称持有锁,称为lock-delay。 客户端可以指定任何lock-delay上限,当前系统是1分钟。这种限制阻止一个错误的客户端持有锁。(持有者失败或者不可达导致的锁变成free,应该需要通知给服务器吧,如果丢失呢??,如何实现? 即使自身有机制和持有者保持心跳,但可以持有者和Chubby不可达,但是可以与服务器可达。

以上可以发现一个锁访问模式:
1) 多个客户端申请锁
2) 某个客户端成功持有锁并得到sequencer
3) 将sequencer和请求发给资源服务器
4) 资源服务器验证sequencer的合法性和权限,如果合法则响应请求,否则则拒绝请求。


2.5 事件

Chubby客户端在创建文件句柄的时候可以订阅多个消息通知。这些事件会被异步的发送给客户端。事件包括:
1> 文件内容修改, 常用来通过文件来监听某个服务地址的改变。
2> 子节点添加,删除或者修改, 用来实现镜像(见2.12, Chubby cell之间的镜像和备份), 此外返回子节点事件可以在不影响临时文件计数的情况下监控该临时文件。
3> Chubby master fail-over, 告诉客户端其他事件可能丢失,数据必须被重新扫描。
4> 文件句柄和锁并的不合法,经常用来暗示存在通信问题
5> 锁被获取, 可以用来决定primary选举
6> 从其他客户端发出的冲突的锁请求

事件在某个动过发生之后发送个客户端。因此如果一个客户端被通知文件内容发生变化,则如果再次读取文件的话,它保证会看到新的数据。 最后两个事件5>和6> 很少被使用,而且根据经验可以忽略。一般在primary选举之后,客户端会与新的primary通信,而非只是简单知道一个primary存在,因此他们等到文件修改通知来指示新的primary已经在文件中写了它的地址。 锁冲突事件6> 理论上,允许客户端在其他服务器上持有缓存数据, 使用Chubby锁来保证缓存一致性。 一个锁请求冲突事件告诉一个客户端来结束和锁关联的数据,结束pending的操作,flush修改到home location,丢弃缓存并释放。 到现在为止,没有人采用这样的方式使用。


2.6 API

客户端将Chubby 句柄作为一个指向一个隐蔽结构的指针来支持多种操作。句柄只在通过open函数被创建,通过close函数被销毁。

open函数打开一个文件名或者目录并创建一个句柄,类似UNIX文件描述符。 只有这个函数使用节点名,其他函数都使用句柄。

名字可以是相对路径,即某个已存在句柄的为当前路径。链接库提供”/“句柄,并且永远是合法的。目录句柄避免使用当前目录,见参考文献18。

客户端可以指定多个选项:
1> 句柄如何被使用,读,写,锁或者修改ACL;句柄只在具有适当权限时被创建。
2> 指定需要被发送的事件
3> lock-delay
4> 是否一个新文件或者目录被创建。如果文件被创建,调用者可能提供初始的文件内容和初始ACL名字。返回值只是文件是否已经被创建。

close函数关闭一个文件句柄。之后再使用句柄是不被允许的,相应的调用会失败。Poison函数会导致后续基于句柄的操作失败而不需要调用close函数。它允许一个客户端在其他线程中取消Chubby 调用而不需要考虑释放被访问的内存。

和句柄一起工作的主要函数如下:
GetContentsAndStat() 函数返回一个文件的内容和元数据。文件内容被整体原子性的读取,论文避免避免部分读和写大文件操作。相关的函数GetStat()返回元数据,而ReadDir返回一个目录中所有子节点的名字和元数据

SetContents() 函数写内容到文件中。可选地,客户端可以提供一个上面描述的内容序列号(a content generation number) 来实现CAS(compare-and-swap)原子操作。只有在内容序列号为当前序列号时才进行修改。文件内容会整个的被原子性的写入。 相关的SetACL()函数执行类似的对节点相关的ACL文件进行写操作。

Delete()函数在节点没有子节点的时候删除该节点。

Acquire(), TryAcquire(), Release()用来获取和释放锁。

GetSequencer()返回上述介绍的锁持有者对应的sequencer。

CheckSequencer()验证sequencer是否合法。

如果节点被删除,则函数调用失败, 即使节点在被删除之后再次创建。 这是因为文件句柄是和一个文件名的实例关联(这是一种好的设计方法)Chubby可能在任意一个函数调用上检查访问权限,但是open()函数肯定是需要被检查的。

以上所有调用除了调用本身需要的参数外,都可以额外指定过一个operation参数,operation参数持有函数调用需要的数据和控制信息。特别的,通过operation参数,客户端可以
1> 传递一个回调函数来异步执行
2> 等待某个调用的完成
3> 获得额外的错误和诊断信息

客户端可以使用这个API来进行Primary选举:所有潜在的节点打开一个锁文件并且试图获取锁,其中之一会成功获得锁并变成primary,而其他将变成副本节点。Primary 通过SetContents来写入自己的标识到锁文件中,使得客户端和其他副本节点能够在一个文件修改事件中调用GetContentsAndStat()来获取。 理想状态,primary通过GetSequencer()获取sequencer ,然后传递给服务器,这个sequencer可以通过CheckSequencer()来验证。一个lock-delay机制可以被不能实现sequencer验证的服务所使用。


2.7 缓存

为了减少读请求所带来的消息流量消耗,Chubby客户端在一致性的条件下缓存文件数据和节点元数据在内存中。缓存通过一个租约(lease)机制维护并且通过master发送的invalidation消息来保持一致性,master会保持一个客户端列表,包含当前正在使用缓存的客户端。 这个协议确保客户端可以看到和Chubby一致的视图或者一个错误。

当文件内容或者元数据修改时,修改会被阻塞直到master发送invalidation给每个可能缓存数据的客户端, 这个机制是基于KeepAlive RPC之上的,将在下一节讨论。 在收到invalidation消息之后,客户端设计invalidation状态并且在下一次KeepAlive消息中发送ack。修改操作只有在master知道每个客户端都invalidate它自身的cache后才进行处理,即要么客户端发送ACK,要么客户端使得缓存lease超时。

只需要一轮invalidation消息,因为master会在cache invalidation还没有ack时设置节点为uncacheable状态,这使得读操作不会有延时,因为读操作的数量比写操作要多。另外在invalidation期间,将阻塞新的调用和请求。这将使得某些客户端频繁请求master该节点的概率降低, 但会造成偶尔的延时(因为写操作很少)。

缓存协议比较简单,它在缓存数据变化是invalidation,但是不去更新它。因为更新比invalidation要复杂,比如说客户端可能不停的更新访问,导致更多数量的不必要的更新消息。

虽然提供这种严格一致性会有上述的代价,这里避免使用比这种一致性弱的模型,因为作者认为那样的话用户会难以使用。同样的,类似于virtual synchrony机制,需要在所有客户端之间交换序列号可能是一种并不合适的方式。

除了缓存文件数据和元数据,Chubby客户端缓存文件句柄(handle)。 因此一个客户端打开之前打开过的文件,只有第一个Open函数会调用RPC到master。这种缓存被严格的限制,所以它不会影响到客户端的语义:临时文件的句柄不能再应用关闭之后再使用;持有持久锁的句柄可被重复使用,但是不能被多线程并发使用。因为其他线程可能调用Close()或者Poison()来取消其他线程的Acquire()。

Chubby协议允许客户端缓存锁,即比持有锁的实际时间更长的时段来使得被同一个客户端重发使用。如果另一个客户端请求的锁(conflicting lock??),则会通知锁持有者,允许持有者释放锁。

2.8 会话和KeepAlives

一个Chubby会话是一个Chubby cell和Chubby客户端之间的连接(注意不是Chubby master)。它存在一段时间,并且被周期性的握手消息所维护,称为KeepAlives。除非Chubby客户端通知master,客户端的句柄,锁和缓存数据在session期间是合法的(但是会话协议要求客户端需要ACK 由master发出的cache invalidation消息)。

一个客户端在第一次连接Chubby cell是请求一个新的会话。会话可以被显示的结束,或者超过一段时间而没有操作(在一分钟内没有调用Open函数或者其他调用)

每个会话有一个会话租约lease,一个时间段保证在将来的一段时间内,master不会中断会话。时间段结束时,会话超时。master可以自由向前挑战超时时间,而不能向后挑战(NTP中向后调整时间会造成timer混乱)

master在三种情况下向前调整lease timeout(缩短时间段):创建会话时,master failover 和客户端回应一个KeepAlive RPC时。 在接受到KeepAlive消息后,master 阻塞 RPC (不允许返回)知道客户端之前的session lease快要到期,之后master再允许RPC 返回给客户端,并通知新的超期时间。master可能扩展timeout为任意值。默认是12s,但是一个负载很高的master可能使用一个更高的值来减少KeepAlive调用次数。客户端在收到之前的回复后,初始化一个新的KeepAlive。因此客户端确保一直最多只有一个KeepAlive消息阻塞在master端。  

出了客户端lease,KeepAlive回复也被用来传送消息和cache invalidation给客户端。master允许当invalidation或者事件发生是提早返回keepAlive消息。这种回带事件确保客户端不会在没有ack cache invalidation的情况下维持一个会话。 这种方式简化客户端并且允许协议在某些处于防火墙后只允许一个方向上的初始化连接的可操作性。
这种设计,使得服务器端具有灵活性,而客户端则被动接受,值得借鉴

客户端维持一个本地lease 超时,和master lease类似,但是要考虑到KeepAlive网络传输的实际, 为了保持一致性,需要服务器的时钟不快于某个常量。如果客户端本地lease超时,它会不确定是否master终止会话。客户端清空和disable缓存,将状态置为jeopardy(危险),客户端再等待一个时间段,默认是45s, 称为grace period。如果在这个grace period超时之前,客户端和master成功交换了KeepAlive消息,则客户端再次enable本地缓存。否则,客户端假设session超期。这样可以保证如果一个Chubby cell不可达是,函数可以返回失败。

当会话进入grace period时,Chubby链接库通知应用jeopardy事件。当session在grace period期间被恢复的话,一个safe事件会通知给客户端。如果会话超时,则发送expired超时消息。这个信息允许应用在会话状态不确定的情况下可处于静默状态,如果状态能够恢复则不需要终止或者重启服务。

如果客户端持有一个节点的句柄并且因为相关联的的会话超时,则任何基于这个句柄的操作都会失败,除了Close()和Poison()其他的操作也都会失败。客户端使用这种方式来保证当网络或者服务器失败时只会造成一段子序列的操作失败而非任意子序列,因此能够允许复杂操作最终被提交。


2.9 Fail-overs

当master失败或者丢失多数副本节点时,它会丢弃包括会话,句柄和锁的内存状态。一个authoritative timer 在master上运行来作为session lease, 只到新的master被选举出来,session lease才停止,这相当于扩展客户端的租约(问题: 旧的master可能因为网络原因不会知道新的master被选举出来的,当然这样可以保证会话在旧的master上一直不会超时而等新的master工作后超时,这样会拒绝旧的session)。如果master选举很快的话,客户端就可以在本地session超时前连接到新的master。如果选举时间很长,客户端在grace period期间试图找到新的master。因此grace period 允许会话在master fail-over的时候维持会话状态。 见下图,
分布式系统概念和设计 第十五章 (2)_Byzantine_04


在master fail-over期间,grace period可以保存它的会话。时间从左向右发生。粗线条表示会话租约时间,master上的会话租约(M1到M3),客户端上的会话租约(C1到C3),方向向上的细线表示KeepAlive消息请求,方向向下的细线表示KeepAlive的回复。 最开始,旧master设置客户端的会话租约为M1,客户端构建一个大致的租约为C1。旧master在租约M1快结束之前,回复之前C1的请求并同时把新的租约M2发给C1。这样客户端可以延长租约。之后旧的master在回复下一条KeepAlive之前失败,因此客户端没法收到KeepAlive的回复,则在C2超期后,进入jeopardy状态和开始grace period 阶段的timer。在grace period阶段,客户端没法知道它的会话是否在服务器端也超时(如果是fail-over,只会在新的master被选出后才超时),但是它阻塞当前的应用调用API从而可能造成的不一致情况。在grace period阶段,Chubby链接库 发送jeopardy事件给应用使得它可以处于静默状态。

之后新的master选择出来,新的master开始初始化之前旧master保持的会话。从客户端发给新的master的第一条KeepAlive request被新的master拒绝, 因为它持有的是旧的epoch number。客户端再次发出KeepAlive请求。之后新的master发送新的租约给客户端,客户端收到后可以延长它的租约,并可以通知应用会话不再处于jeopardy状态而变成safe状态。 因此有一段比租约要长的grace period将保证会话可以在fail-over期间保留。如果grace period较短,客户端则会丢弃会话并告知应用程序 (下面会详细介绍消息4, 5, 6, 7阶段的新master上的状态和行为变化)。

一旦客户端连接到新的master,客户端链接库和master会一起合作,使得从应用程序角度来看,没有错误发生(mask failure)。新的master必须重现之前旧的master在内存中保持的状态信息。一部分是通过从磁盘上读取的数据(副本同步通过底层数据库复制协议,paxos), 一部分是通过从客户端获取的状态,以及 部分从conservative assumptions保存假设?)。数据库记录了每个session, 持有锁和临时文件。
一个新的master会执行下面操作:
1> 它首先从客户端消息中提取epoch number (epoch number会放在每个客户端消息中),如果客户端仍然使用旧master的epoch number, 则拒绝请求并返回新的epoch number。这保证新的master不会处理由旧的master发出的网络包。
2> master会应答请求新master 网络地址的消息,但是不会处理会话相关的操作
3> master根据数据库来创建关于会话,锁的内存数据结构。前master使用的最大会话lease可能被用来延长现在的会话lease
4> master 允许客户端发送KeepAlive消息,但不处理其他会话相关的操作(和2>的区别是可以处理KeepAlive消息了)
5> master发送fail-over消息给每个会话。这导致客户端会flush它们的缓存(因为客户端可能没有收到invalidation消息),然后警告应用程序,其他的消息可能会丢失
6> master等待每个会话的acknowledge fail-over消息或者会话超时(在master上的会话租约到期也没有收到ack)
7> master允许所有的请求操作
8> 如果客户端使用fail-over之前的句柄(通过句柄中额sequence number来区别新旧master),则master在内存中重新创建并发给客户端(之前创建的是会话和锁状态)。如果重新创建的句柄已经关闭,master则在内存中记录并使得在这个epoch(master)上不能再被创建。这保证一个延时或者被复制的网络消息报不能重新创建一个已经关闭的句柄。一个错误的客户端可能在之后的epoch/master上再重建一个已经关闭的句柄,但是这不会造成太大影响,因为客户端已经处于错误的状态中(最后一句如果发生,会出现什么问题??
9> 在某个时间段之后(比如说1分钟),master会删除没有客户端打开的临时文件。客户端需要在fail-over之后但在这个时间段超时之前refresh 临时文件句柄。这个机制可能会临时文件产生不好的效果,比如说临时文件没有在最后一个客户端会话断开之后就马上被删除。

fail-over在实际中不常见,但是过程复杂,因此会存在很多bug (BSC的split brain改了好几年)。

2.10 Database 实现

Chubby第一个版本使用Berkeley DB作为它的数据库, Berkeley数据库使用B-trees来映射字符串key到任意的字符串值。作者设置了一个key比较函数来目录名进行排序,它使得节点以路径名为key并且保持邻居节点的有序性。
因为Chubby不使用基于目录的权限设置,因此需要一个额外的查找来找到每个文件的访问权限。

Berkeley DB使用分布式一致性协议来复制数据库log。当一个master lease被添加,这个就比较符合Chubby的设计了。但是Berkeley DB的代码使用比较广泛,而复制则使用的很少,开发者需要区分产品的优先级。因此作者决定重写一个简单数据库来代替Berkeley DB。 细节见之上的“Paxos made live”论文来描述如何通过Paxos来实现database的。


2.11 Backup

每隔几个小时,Chubby cell的master就会将它的数据库镜像写到GFS不同楼的GFS服务器上。因为同楼的服务器可能会产生相互依赖,即local GFS depends on local Chubby

备份同时也提供灾难备份和初始化一个新的备份节点。

2.12 Mirroring

Chubby 允许一个Chubby cell的一个文件集合可以被镜像到另外一个Chubby cell上。镜像的速度比较快,因为文件很少并且如果有文件被添加,删除或者修改后事件机制可以通知镜像。假设没有网络问题,文件修改可以在秒级复制到全球范围内的多个镜像上。如果镜像不可达,则在它恢复以后再进行恢复。修改的文件通过checksum进行比较。

镜像最常见的用途是复制配置文件到全球的多个计算集群。一个特殊的Chubby cell, 称为global, 包括一个子树 /ls/global/master 被镜像到其他每个Chubby cell的/ls/cellX/slave 上。这个global cell比较特殊,因为它的五个副本节点分散在全球各地,因此可以被其他大多数系统所访问。

Chubby自身的访问控制列表也放在这些镜像的文件中, Chubby cell中的多个文件和其他系统广播他们的有效性给我们的监控服务, 指向允许客户端定位大量的数据集,比如说Bigtable cell 和其他系统的许多配置文件。

(高大上!!)


3 Mechanisms for scaling

Chubby的客户端是单个的进程,因此Chubby会处理很多的客户端进程,曾经观察到同时有90k个客户端连接一个Chubby master, 比实际的机器数要大很多。因为一个Chubby cell中,只有一个master,而且master的硬件配置和客户端的硬件配置类似,因此客户端可能会使得master过载。因此减少通信是提高scaling(scaling up)的有效方法。作者使用了如下方法来×××能和并发:
1> 可以创建任意数量的Chubby cell,客户一般都根据DNS使用较近的cell。典型的部署是在一个数据中心为几千个机器使用一个Chubby cell
2> master可以在负载较大的时候增加会话lease时间从默认12s 到 60s,这样会处理更少的KeepAlives RPC (在后面的章节,可以通过数据知道KeepAlives占所有消息中的绝大多数)
3> Chubby客户端缓存文件数据,元数据,文件不在和打开的句柄来减少调用master的次数。
4> 使用协议转换服务将Chubby协议转换成更简单的协议,比如说DNS和其他。

下面介绍两种通用的机制:代理和分区,可以使得Chubby cell有更好的扩展性。

3.1 Proxies
Chubby 协议可以被代理(两边使用相同的协议)将请求从某些客户端发给一个Chubby cell。 代理可以通过处理KeepAlives和读请求还减少master的负载,但是写请求只能发给master。不过因为写请求小于Chubby 负载中的1%,因此写请求不会对负载造成太大问题, 这样通过Proxy,会允许更多的客户端间接连接到Chubby cell。如果一个Proxy可以处理N个客户端,那么KeepAlive traffic 将被减少到某个比例C/N。Proxy代理缓存也可以将read请求降低很多。但是因为读请求占整个Chubby 负载的比例不到10%,因此其贡献也有限。

Proxies在写和第一次读的时候增加了额外的RPC。另外也增加了Chubby cell服务不可用的概率。

3.2 分区
Chubby类文件系统的接口选择,可以使得一个cell的命名空间被分区到多个servers上。 虽然暂时不需要,但是代码支持根据目录名称来分区(类似于在不同的目录点上mount 不同的物理存储,因此存储容量被分配到不同的介质中)。 如果被激活,一个Chubby cell可以由N个分区组成,每一个有多个副本节点和一个master组成。每个节点将存储hash(D) mode N 对应的数据, 元数据可以被存储由另一个hash(D')mod N对应的节点上。

如果分区之间通信量较少,分区可以使得Chubby cell变成更大的规模。虽然Chubby缺少硬连接,目录修改时间和跨目录重命名操作,一些操作仍然需要跨分区通信:
1> ACLs 自身也是文件,所以一个分区可能使用另一个来做权限检查。但是,ACL文件一般都已经放在缓存中,并且 只有open和delete需要ACL检查,大多数客户端读取文件并不需要ACL
2> 当一个目录被删除,一个跨分区调用可能需要被触发来保证目录已被删除。

因为每个分区基本上是独立处理本cell的绝大多数请求,cell间的通信对性能或者可用性的影响较小。

除非分区的数量N很大,每个客户端可能连接大多数分区。因此分区以类似C/N的方式减少了读和写消息的数量并没有减少KeepAlive的流量。因此为了支持更多的客户端,需要将proxies和partition结合使用。



后续试验结果和总结

第一个java客户端的需求,通过增加一个网关来提供支持,这样会保持Chubby的稳定性 (也是一个蛮好的思路,特别是对于这种高可用性系统,需要服务器和客户端链接库的共同支持)。

----------------------------------------------------------------------------------------------------------------

实际运行中发现把Chubby 作为name service占了几乎一半的应用,因此专门实现了一个Chubby DNS server.

----------------------------------------------------------------------------------------------------------------

fail-over的问题
为了在fail-over切换的时候保留当前的session, 需要在创建session的时候来写入session到数据库,这会在多个进程同时开始时,造成写数据库过载。因此将写数据库的动作修改为当第一次提交修改请求(获取锁,修改文件,打开一个临时文件)时写数据库而不是在session建立的时候或者读操作的时候。 这样对占大量操作的读就比较实时, 并且可以避免过载。但会存在一个问题,对比较新的会话来说,在fail-over的时候可能会被丢掉。
if all the recordedsessions were to check in with the new master before theleases of discarded sessions expired, the discarded sessions could then read stale data for a while.?? 没明白为啥

在之后的设计,作者避免把session放入数据库,而是在新的master重建。新的master会等到最大的会话超期时间之后再处理请求(how,不放数据库如何同步状态? 4.4 还需要再理解 )。

----------------------------------------------------------------------------------------------------------------


错误的客户端使用Chubby的方式。
1) 最开始没有缓存不存在的文件(absence of files, 客户端周期性检查某一个文件是否被创建)和重用打开文件句柄。 开发者习惯在文件不存在的时候写重试来探测。这会导致有很多的open()调用。
2) 缺乏容量控制, Chubby从最开始就没有设计成一个提供大量存储容易的系统。因此客户端会错误的提交多个大文件。 通过限制文件大小为256KB和鼓励将大文件放到其他存储系统中来解决问题
3) 将Chubby的通知机制作为 publish/subscribe 来使用。Chubby底层的分布式一致性算法和对一致性cache的维护 使得它无法提供高效的publish/subscribe 服务。因此需要被禁止这种模式的使用。

----------------------------------------------------------------------------------------------------------------
学到的教训

1> 开发者很少考虑可靠性,比如认为Chubby一直可用。需要考虑Chubby在失败或者不可达的时候,尽量避免或者免受Chubby不在的影响。使用本地Chubby cell比global Chubby cell相对具有更高的可靠性。 fail-over期间的事件通知会使得开发者比较迷惑。 ==》在使用Chubby服务时候进行交流并设计机制考虑Chubby不在时的处理,提供链接库来隔离CHubby outage对应用程序的影响, 事后review bug和流程
2> 不支持细粒度锁
3> close()和Poision()用来取消长时间的call,会导致server的状态被丢失。这会阻止句柄来获取锁。增加cancel()来允许中止call调用而允许共享文件句柄
4> RPC 所使用的底层网络协议,因此TCP的中断策略并没有考虑上层应用的超时,比如说Chubby会话租约,因此给予TCP的KeepAlive会导致在网络拥塞的时候同时会有很多会话丢失(网络拥塞以后TCP会延时发包造成上层应用超时)。(TCP’s back off policiespay no attention to higher-level timeouts such as Chubby leases, so TCP-based KeepAlives led to many lost sessions at times of high network congestion.) 因此不得不改用UDP方式来实现KeepAlives RPC协议。UDP没有拥塞避免机制,因此使用UDP对应用的时间要求更好一些。 另外使用额外基于TCP的GetEvent() RPC 来传输事件和invalidation消息。


----------------------------------------------------------------------------------------------------------------
和相关工作的比较
Chubby的设计基于一些已知的思想。 Chubby的cache 设计基于参考文件10的一个分布式文件系统。session和cache token 和参考文献17中的Echo类似。session reduce the overhead of leases 从参考文件9 V system而来。
建立一个通用的锁服务基于参考文献23的VMS。设计成类似文件系统的API是基于参考文献18,21,22。

Chubby和Boxwood锁服务(见参考文件16)在很多方面是类似的。 BoxWood 提供三个服务, 锁服务,Paxos 服务和一个失败检测服务, Boxwood系统本身使用这三个服务, 其他应用则可以单独使用其中之一。
Chubby相比Boxwood来说,提高更简单更高级的API。
Chubby和Boxwood有很多不同的默认配置参数。另外Boxwood缺少Chubby的grace period。而Boxwood的“grace period” 类似于Chubby的“session lease”。 这个对scale和failure处理具有不同期望。
Chubby和Boxwood对锁的用途也不一致。Chubby倾向于粗粒度锁,而Boxwood倾向于轻量级,主要提高给内部使用。


扩展阅读:
Boxwood: Abstractions as the Foundation for Storage Infrastructure (John MacCormick, Nick Murphy, Marc Najork, Chandramohan A. Thekkath, and Lidong Zhou, Microsoft Research Silicon Valley)

Paxos made parallel

Building Reliable Large-Scale Distributed System: When Theory Meets Practice. by Lidong Zhou



CAP 理论(http://www.cnblogs.com/mmjx/)

CAP理论被很多人拿来作为分布式系统设计的金律,然而感觉大家对CAP这三个属性的认识却存在不少误区。从CAP的证明中可以看出来,这个理论的成立是需要很明确的对C、A、P三个概念进行界定的前提下的。在本文中笔者希望可以对论文和一些参考资料进行总结并附带一些思考。


一、什么是CAP理论

CAP原本是一个猜想,2000年PODC大会的时候大牛Brewer提出的,他认为在设计一个大规模可扩放的网络服务时候会遇到三个特性:一致性(consistency)、可用性(Availability)、分区容错(partition-tolerance)都需要的情景,然而这是不可能都实现的。之后在2003年的时候,Mit的Gilbert和Lynch就正式的证明了这三个特征确实是不可以兼得的。


二、CAP的概念

Consistency、Availability、Partition-tolerance的提法是由Brewer提出的,而Gilbert和Lynch在证明的过程中改变了Consistency的概念,将其转化为Atomic。Gilbert认为这里所说的Consistency其实就是数据库系统中提到的ACID的另一种表述:一个用户请求要么成功、要么失败,不能处于中间状态(Atomic);一旦一个事务完成,将来的所有事务都必须基于这个完成后的状态(Consistent);未完成的事务不会互相影响(Isolated);一旦一个事务完成,就是持久的(Durable)。

对于Availability,其概念没有变化,指的是对于一个系统而言,所有的请求都应该‘成功’并且收到‘返回’。

对于Partition-tolerance,所指就是分布式系统的容错性。节点crash或者网络分片都不应该导致一个分布式系统停止服务。


三、基本CAP的证明思路

CAP的证明基于异步网络,异步网络也是反映了真实网络中情况的模型。真实的网络系统中,节点之间不可能保持同步,即便是时钟也不可能保持同步,所有的节点依靠获得的消息来进行本地计算和通讯。这个概念其实是相当强的,意味着任何超时判断也是不可能的,因为没有共同的时间标准。之后我们会扩展CAP的证明到弱一点的异步网络中,这个网络中时钟不完全一致,但是时钟运行的步调是一致的,这种系统是允许节点做超时判断的。

CAP的证明很简单,假设两个节点集{G1, G2},由于网络分片导致G1和G2之间所有的通讯都断开了,如果在G1中写,在G2中读刚写的数据, G2中返回的值不可能G1中的写值。由于A的要求,G2一定要返回这次读请求,由于P的存在,导致C一定是不可满足的。


四、CAP的扩展

CAP的证明使用了一些很强的假设,比如纯粹的异步网络,强的C、A、P要求。事实上,我们可以放松某些条件,从而达到妥协。

首先 —— 弱异步网络模型

弱异步网络模型中所有的节点都有一个时钟,并且这些时钟走的步调是一致的,虽然其绝对时间不一定相同,但是彼此的相对时间是固定的,这样系统中的节点可以不仅仅根据收到的消息来决定自己的状态,还可以使用时间来判断状态,比如超时什么的。

在这种场景下,CAP假设依旧是成立的,证明跟上面很相似。


不可能的尝试 —— 放松Availability或者Partition-tolerance

放弃Partition-tolerance意味着把所有的机器搬到一台机器内部,或者放到一个要死大家一起死的机架上(当然机架也可能出现部分失效),这明显违背了我们希望的scalability。

放弃Availability意味着,一旦系统中出现partition这样的错误,系统直接停止服务,这是不能容忍的。


最后的选择 —— 放松一致性

我们可以看出,证明CAP的关键在于对于一致性的强要求。在降低一致性的前提下,可以达到CAP的和谐共处,这也是现在大部分的分布式存储系统所采用的方式:Cassandra、Dynamo等。“Scalability is a bussness concern”是我们降低一致性而不是A和P的关键原因。

Brewer后来提出了BASE (Basically Available, Soft-state, Eventually consistent),作为ACID的替代和补充。


五、战胜CAP

1,2008年9月CTO of atomikos写了一篇文章“A CAP Solution (Proving Brewer Wrong)”,试图达到CAP都得的效果。

这篇文章的核心内容就是放松Gilbert和Lynch证明中的限制:“系统必须同时达到CAP三个属性”,放松到“系统可以不同时达到CAP,而是分时达到”。

Rules Beat CAP:

1) 尽量从数据库中读取数据,如果数据库不能访问,读取缓存中的数据

2) 所有读都必须有版本号

3) 来自客户端的更新操作排队等候执行,update必须包括导致这次更新的读操作的版本信息

4) 分区数量足够低的时候排队等待的update操作开始执行,附带的版本信息用来验证update是否应该执行

5) 不管是确认还是取消更新,所有的结构都异步的发送给请求方

证明:

1) 系统保证了consistency,因为所有的读操作都是基于snapshot的,而不正确的update操作将被拒绝,不会导致错误执行

2) 系统保证了availability,因为所有的读一定会返回,而写也一样,虽然可能会因为排队而返回的比较慢

3) 系统允许节点失效

缺点:

1) 读数据可能会不一致,因为之前的写还在排队

2) partition必须在有限的时间内解决

3) update操作必须在所有的节点上保持同样的顺序


2, 2011年11月Twitter的首席工程师Nathan Marz写了一篇文章,描述了他是如何试图打败CAP定理的: How to beat the CAP theorem

本文中,作者还是非常尊重CAP定律,并表示不是要“击败”CAP,而是尝试对数据存储系统进行重新设计,以可控的复杂度来实现CAP。

Marz认为一个分布式系统面临CAP难题的两大问题就是:在数据库中如何使用不断变化的数据,如何使用算法来更新数据库中的数据。Marz提出了几个由于云计算的兴起而改变的传统概念:

1) 数据不存在update,只存在append操作。这样就把对数据的处理由CRUD变为CR

2) 所有的数据的操作就只剩下Create和Read。把Read作为一个Query来处理,而一个Query就是一个对整个数据集执行一个函数操作。

在这样的模型下,我们使用最终一致性的模型来处理数据,可以保证在P的情况下保证A。而所有的不一致性都可以通过重复进行Query去除掉。Martz认为就是因为要不断的更新数据库中的数据,再加上CAP,才导致那些即便使用最终一致性的系统也会变得无比复杂,需要用到向量时钟、读修复这种技术,而如果系统中不存在会改变的数据,所有的更新都作为创建新数据的方式存在,读数据转化为一次请求,这样就可以避免最终一致性的复杂性,转而拥抱CAP。

具体的做法这里略过。


总结:

其实对于大规模分布式系统来说,CAP是非常稳固的,可以扩展的地方也不多。

它很大程度上限制了大规模计算的能力,通过一些设计方式来绕过CAP管辖的区域或许是下一步大规模系统设计的关键。


问题:
Paxos协议算是解决CAP了吗?

--------------- 参考文献 -----------------------

[1] Seth Gilbert and Nancy Lynch. 2002. Brewer's conjecture and the feasibility of consistent, available, partition-tolerant web services. SIGACT News 33, 2 (June 2002), 51-59. DOI=10.1145/564585.564601 http://doi.acm.org/10.1145/564585.564601

[2] Guy Pardon, 2008, A CAP Solution (Proving Brewer Wrong), http://blog.atomikos.com/2008/09/a-cap-solution-proving-brewer-wrong/

[3] Werner Vogels, 2007, Availability & Consistency, http://www.infoq.com/presentations/availability-consistency

[4] Nathan Marz, 2011, How to beat the CAP theorem, http://nathanmarz.com/blog/how-to-beat-the-cap-theorem.html



The Byzantine Generals Problem (TBR, to be read)

上面介绍过一小部分Byzantine问题,在n个节点中,可以容忍有f个节点传递错误消息而整个系统还是可以达成一致的条件是:n >=3 * f + 1。 在之上的分布式系统中,特别是组播算法中,都是假设没有拜占庭问题的,通常使用checksum 和 安全认证 来避免错误消息或者伪造消息。