系统的可扩展性是推动NoSQL运动发展的的主要理由,包含了分布式系统协调,故障转移,资源管理和许多其他特性。这么讲使得NoSQL听起来像是一个大筐,什么都能塞进去。尽管NoSQL运动并没有给分布式数据处理带来根本性的技术变革,但是依然引发了铺天盖地的关于各种协议和算法的研究以及实践。正是通过这些尝试逐渐总结出了一些行之有效的数据库构建方法。在这篇文章里,我将针对NoSQL数据库的分布式特点进行一些系统化的描述。
接下来我们将研究一些分布式策略,比如故障检测中的复制,这些策略用黑体字标出,被分为三段:
数据一致性。NoSQL需要在分布式系统的一致性,容错性和性能,低延迟及高可用之间作出权衡,一般来说,数据一致性是一个必选项,所以这一节主要是关于数据复制和数据恢复。
数据放置。一个数据库产品应该能够应对不同的数据分布,集群拓扑和硬件配置。在这一节我们将讨论如何分布以及调整数据分布才能够能够及时解决故障,提供持久化保证,高效查询和保证集群中的资源(如内存和硬盘空间)得到均衡使用。
对等系统。像ladrlction这样的的技术已经被用于多个数据库产品以实现容错和数据强一致性。然而,即使是分散的的数据库(无中心)也要跟踪它们的全局状态,检测故障和拓扑变化。这一节将介绍几种使系统保持一致状态的技术。
数据一致性
众所周知,分布式系统经常会遇到网络隔离或是延迟的情况,在这种情况下隔离的部分是不可用的,因此要保持高可用性而不牺牲一致性是不可能的。这一事实通常被称作“CAP理论”。然而,一致性在分布式系统中是一个非常昂贵的东西,所以经常需要在这上面做一些让步,不只是针对可用性,还有多种权衡。为了研究这些权衡,我们注意到分布式系统的一致性问题是由数据隔离和复制引起的,所以我们将从研究复制的特点开始:
可用性。在网络隔离的情况下剩余部分仍然可以应对读写请求。
读写延迟。读写请求能够在短时间内处理。
读写延展性。读写的压力可由多个节点均衡分担。
容错性。对于读写请求的处理不依赖于任何一个特定节点。
数据持久性。特定条件下的节点故障不会造成数据丢失。
一致性。一致性比前面几个特性都要复杂得多,我们需要详细讨论一下几种不同的观点。但是我们不会涉及过多的一致性理论和并发模型,因为这已经超出了本文的范畴,我只会使用一些简单特点构成的精简体系。
读写一致性。从读写的观点来看,数据库的基本目标是使副本趋同的时间尽可能短(即更新传递到所有副本的时间),保证最终一致性。除了这个较弱的保证,还有一些更强的一致性特点:
写后读一致性。在数据项X上写操作的效果总是能够被后续的X上的读操作看见。
读后读一致性。在一次对数据项X的读操作之后,后续对X的读操作应该返回与第一次的返回值相同或是更加新的值。
写一致性。分区的数据库经常会发生写冲突。数据库应当能处理这种冲突并保证多个写请求不会被不同的分区所处理。这方面数据库提供了几种不同的一致性模型:
原子写。假如数据库提供了API,一次写操作只能是一个单独的原子性的赋值,避免写冲突的办法是找出每个数据的“最新版本”。这使得所有的节点都能够在更新结束时获得同一版本,而与更新的顺序无关,网络故障和延迟经常造成各节点更新顺序不一致。数据版本可以用时间戳或是用户指定的值来表示。Cassandra用的就是这种方法。
原子化的读-改-写。应用有时候需要进行读-改-写序列操作而非单独的原子写操作。假如有两个客户端读取了同一版本的数据,修改并且把修改后的数据写回,按照原子写模型,时间上比较靠后的那一次更新将会覆盖前一次。这种行为在某些情况下是不正确的(例如,两个客户端往同一个列表值中添加新值)。数据库提供了至少两种解决方法:
冲突预防。读-改-写可以被认为是一种特殊情况下的事务,所以分布式锁或是PAXOS这样的一致协议都可以解决这种问题。这种技术支持原子读改写语义和任意隔离级别的事务。另一种方法是避免分布式的并发写操作,将对特定数据项的所有写操作路由到单个节点上(可以是全局主节点或者分区主节点)。为了避免冲突,数据库必须牺牲网络隔离情况下的可用性。这种方法常用于许多提供强一致性保证的系统(例如大多数关系数据库,HBas,MongoDB)。
冲突检测。数据库跟踪并发更新的冲突,并选择回滚其中之一或是维持两个版本交由客户端解决。并发更新通常用向量时钟(这是一种乐观锁)来跟踪,或者维护一个完整的版本历史。这个方法用于Riak,Voldmort,CouchDB.
现在让我们仔细看看常用的复制技术,并按照描述的特点给他们分一下类。第一幅图描绘了不同技术之间的逻辑关系和不同技术在系统的一致性、扩展性、可用性、延迟性之间的权衡坐标。第二张图详细描绘了每个技术。
复本因子是4。读写协调者可以是一个外部客户端或是一个内部代理节点。
我们会依据一致性从弱到强把所有的技术过一遍:
(A,反熵)一致性最弱,基于策略如下。写操作的时候选择任意一个节点更新,在读的时候如果新数据还没有通过后台的反熵协议传递到读的那个节点,那么读到的仍然是旧数据。(下一节会详细介绍反熵协议)。这种方法的主要特点是:
过高的传播延迟使它在数据同步方面不太好用,所以比较典型的用法是只作为辅助性的功能来检测和修复计划外的不一致。Cassandra就使用了反熵算法来在各节点之间传递数据库拓扑和其他一些元数据信息。
一致性保证较弱:即使在没有发生故障的情况下,也会出现写冲突与读写不一致。
在网络隔离下的高可用和健壮性。用异步的批处理替代了逐个更新,这使得性能表现优异。
持久性保障较弱因为新的数据最初只有单个副本。
(B)对上面模式的一个改进是在任意一个节点收到更新数据请求的同时异步的发送更新给所有可用节点。这也被认为是定向的反熵。
与纯粹的反熵相比,这种做法只用一点小小的性能牺牲就极大地提高了一致性。然而,正式一致性和持久性保持不变。
假如某些节点因为网络故障或是节点失效在当时是不可用的,更新最终也会通过反熵传播过程来传递到该节点。
(C)在前一个模式中,使用提示移交技术可以更好地处理某个节点的操作失败。对于失效节点的预期更新被记录在额外的代理节点上,并且标明一旦特点节点可用就要将更新传递给该节点。这样做提高了一致性,降低了复制收敛时间。
(D,一次性读写)因为提示移交的责任节点也有可能在将更新传递出去之前就已经失效,在这种情况下就有必要通过所谓的读修复来保证一致性。每个读操作都会启动一个异步过程,向存储这条数据的所有节点请求一份数据摘要(像签名或者hash),如果发现各节点返回的摘要不一致则统一各节点上的数据版本。我们用一次性读写来命名组合了A、B、C、D的技术-他们都没有提供严格的一致性保证,但是作为一个自备的方法已经可以用于实践了。
(E,读若干写若干)上面的策略是降低了复制收敛时间的启发式增强。为了保证更强的一致性,必须牺牲可用性来保证一定的读写重叠。通常的做法是同时写入W个副本而不是一个,读的时候也要读R个副本。
首先,可以配置写副本数W1。
其次,因为R+WN,写入的节点和读取的节点之间必然会有重叠,所以读取的多个数据副本里至少会有一个是比较新的数据(上面的图中W=2,R=3,N=4)。这样在读写请求依序进行的时候(写执行完再读)能够保证一致性(对于单个用户的读写一致性),但是不能保障全局的读一致性。用下面图示里的例子来看,R=2,W=2,N=3,因为写操作对于两个副本的更新是非事务的,在更新没有完成的时候读就可能读到两个都是旧值或者一新一旧:
对于某种读延迟的要求,设置R和W的不同值可以调整写延迟与持久性,反之亦然。
如果W=N/2,并发的多个写入会写到不同的若干节点(如,写操作A写前N/2个,B写后N/2个)。设置WN/2可以保证在符合回滚模型的原子读改写时及时检测到冲突。
严格来讲,这种模式虽然可以容忍个别节点的失效,但是对于网络隔离的容错性并不好。在实践中,常使用”近似数量通过“这样的方法,通过牺牲一致性来提高某些情景下的可用性。
(F,读全部写若干)读一致性问题可以通过在读数据的时候访问所有副本(读数据或者检查摘要)来减轻。这确保了只要有至少一个节点上的数据更新新的数据就能被读取者看到。但是在网络隔离的情况下这种保证就不能起到作用了。
(G,主从)这种技术常被用来提供原子写或者冲突检测持久级别的读改写。为了实现冲突预防级别,必须要用一种集中管理方式或者是锁。最简单的策略是用主从异步复制。对于特定数据项的写操作全部被路由到一个中心节点,并在上面顺序执行。这种情况下主节点会成为瓶颈,所以必须要将数据划分成一个个独立的片区(不同片有不同的mastr),这样才能提供扩展性。
(H,TransactionalRadQuorumWritQuorumandRadOnWritAll)更新多个副本的方法可以通过使用事务控制技术来避免写冲突。众所周知的方法是使用两阶段提交协议。但两阶段提交并不是完全可靠的,因为协调者失效可能会造成资源阻塞。PAXOS提交协议是更可靠的选择,但会损失一点性能。在这个基础上再向前一小步就是读一个副本写所有副本,这种方法把所有副本的更新放在一个事务中,它提供了强容错一致性但会损失掉一些性能和可用性。
上面分析中的一些权衡有必要再强调一下
一致性与可用性。严密的权衡已经由CAP理论给出了。在网络隔离的情况下,数据库要么将数据集中,要么既要接受数据丢失的风险。
一致性与扩展性。看得出即使读写一致性保证降低了副本集的扩展性,只有在原子写模型中才可以以一种相对可扩展的方式处理写冲突。原子读改写模型通过给数据加上临时性的全局锁来避免冲突。这表明,数据或操作之间的依赖,即使是很小范围内或很短时间的,也会损害扩展性。所以精心设计数据模型,将数据分片分开存放对于扩展性非常重要。
一致性与延迟。如上所述,当数据库需要提供强一致性或者持久性的时候应该偏向于读写所有副本技术。但是很明显一致性与请求延迟成反比,所以使用若干副本技术会是比较中允的办法。
故障转移与一致性/扩展性/延迟。有趣的是容错性与一致性、扩展性、延迟的取舍冲突并不剧烈。通过合理的放弃一些性能与一致性,集群可以容忍多达upto的节点失效。这种折中在两阶段提交与PAXOS协议的区别里体现得很明显。这种折中的另一个例子是增加特定的一致性保障,比如使用严格会话进程的“读己所写”,但这又增加了故障转移的复杂性。
反熵协议,谣言传播算法
让我们从以下场景开始:
有许多节点,每条数据会在其中的若干的节点上面存有副本。每个节点都可以单独处理更新请求,每个节点定期和其他节点同步状态,如此一段时间之后所有的副本都会趋向一致。同步过程是怎样进行的?同步何时开始?怎样选择同步的对象?怎么交换数据?我们假定两个节点总是用较新版本的数据覆盖旧的数据或者两个版本都保留以待应用层处理。
这个问题常见于数据一致性维护和集群状态同步(如集群成员信息传播)等场景。虽然引入一个监控数据库并制定同步计划的协调者可以解决这个问题,但是去中心化的数据库能够提供更好的容错性。去中心化的主要做法是利用精心设计的传染协议,这种协议相对简单,但是提供了很好的收敛时间,而且能够容忍任何节点的失效和网络隔离。尽管有许多类型的传染算法,我们只