CAP
分布式系统,尤其是分布式存储系统,在进行设计时有许多方面需要考虑,其中绕不开的一个问题就是CAP,CAP定理描述如下:
C(Consistency)一致性:也就是所有用户看到的数据是一样的。
A(Availability)可用性:总是能够在合理的时间内返回合理的响应。
P(Partition tolerance)分区容错性:当出现网络分区后,系统能够继续工作。
C、A、P三者之间不不可兼得,分布式系统的设计是就要思考对他们如果取舍。
在分布式系统中,网络无法100%可靠,网络分区其实是一个必然现象。如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构。
对于CP来说,放弃可用性,追求一致性和分区容错性,ZooKeeper 其实就是追求的强一致。
对于AP来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面要说的BASE也是根据AP来扩展。
另外,如果CAP中选择两个,比如你选择了CP,并不是叫你放弃A。因为P出现的概率实在是太小了,大部分的时间你仍然需要保证CA。就算分区出现了你也要为后来的 A 做准备,比如通过一些日志的手段,是其他机器回复至可用。
对于Consistency有着诸多分类,常见如:
Eventual Consistency
最终一致
Linearizability Consistency线性一致
Causal Consistency因果一致
以上这些分类却没有明确相对应的Availability。这也是分布式系设计时需要思考的一个问题:我们降低了Consistency后,是否真的能够提高Availability?Paxos算法可以容忍系统中少数集合中的节点失效,根据描述我们认为Paxos算法在系统级别提供高可用服务,同时提供了Consistency服务。这似乎与CAP理论相违背。考虑CAP理论对于Availability的定义,要求对任意节点的请求都能立刻(read-time)得到回应。假设由于网络分区将系统分为了一个多数集和一个少数集,对于Paxos算法,尽管多数集中的节点仍然可以正确且立即回复请求,但是少数集中的节点不能。CAP理论这样定义有一定道理,因为在网络分区发生时,有可能客户端并不能访问多数集中的节点。
ACID
ACID性质指的是并行执行多个事务(Transaction)时需要保证的性质:
A(Transaction Atomicity)原子性:组成事务的多个事件要么都成功要么都失败(all-or-nothing)
C(Database Consistency)一致性:执行事务的前后,数据库的状态保持一致
I(Isolation)独立性:并发执行的事务之间不互相影响
D(Durability)持久性:已经提交的事务中的事件不会丢失
ACID中的Consistency和CAP定理中的Consistency意义完全不同,为了区分这一点我将ACID中的C称为Database Consistency。同样的,ACID中的Atomicity和CAP中的Availability也完全不同。
BASE
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写,是对 CAP 中 AP 的一个扩展。
基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是 CAP 中的不一致。
最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。
BASE 解决了 CAP 中理论没有网络延迟,在 BASE 中用软状态和最终一致,保证了延迟后的一致性。
BASE 不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。
CAP和ACID的区别
CAP理论更多的关注于分布式共享模型下对象的一致性问题和可用性问题,可以称之为外部一致性。例如:在分布式系统中,写操作后再读,就必须返回写入的值。比如分布式数据库A
、B、C
,A
中写入数据 hello
,写完马上读 B
和 C
,就一定要读出 hello
,读出来我们就称之为符合一致性。
ACID性质是数据库系统中并发执行多个事务时的问题,是数据库领域的传统问题,可以称之为内部一致性。例如:事务开始前和结束后,数据库的完整性约束没有被破坏 。比如 A
向 B
转账,不可能 A
扣了钱,B
却没收到。
内部一致性注重于事务前后数据的完整性,而外部一致性则注重于读写数据的一致性。
强一致性约束下的分布式系统
线性一致性的实现方法
我们的目标是实现一个线性一致性的存储系统。由于线性具有本地属性,我们可以将问题进一步简化。
总的来说实现一个Linearizability有如下几种方法:
- Single-leader replication (potentially linearizable)
- Consensus algorithms (linearizable)
- Multi-leader replication (not linearizable)
- Leaderless replication (probably not linearizable)
分为这样几种思路:
- Total Order Broadcast
- Quorum read/write
- Write-invalidate & Read-through
- Write-through
Total Order Broadcast:让每个replication以相同的顺序接收到全序排列的消息,显然,这符合Linearizability Consistency的定义。使用Consensus算法,例如Raft、ZAB等,相当于先在Leader节点上定序,然后再将消息和这一顺序本身传播给所有的replication,实际上等同于Total Order Broadcast。
Quorum:实现Linearizability之前需要在每次Read/Write操作的之前都进行一次Repair,需要注意的是,只有Read/Write操作能够以这种方式实现,Compare-And-Set之类的操作不能以这种形式实现,而必须使用分布式共识(Consensus)算法。
Read-through/Write-through:主要适用于Cache场景,在分布式存储场景下使用这样的方法有较大的丢失数据的风险。
达成Linearizability的代价为Client到Leader的延迟加上Leader到多数集中最慢的节点的延迟。
串行隔离事务的实现方法
常见的串行隔离事务的方法主要有:
- 单线程Serialize执行
- Strict 2 Phase Locking
- Serializable Snapshot Isolation Algorithm
单并发Serialize和Strict 2PL(两阶段提交)属于悲观并发,SSI(Serializable Snapshot Isolation)属于乐观并发。SSI的原理大致是使用快照隔离每个人物都认为自己是唯一的,在处理过程中不检查是否冲突,但是在提交前检查是否与其他操作冲突,如果有的话就Abort,没有的话就可以提交。具体的乐观并发控制会出一篇详细介绍。
实现分布式事务所需要解决的主要问题是Atomic Commitment,即多个节点数据一致的问题。Atomic Commitment分为阻塞和非阻塞两种。阻塞的一个典型的实现方法是两阶段提交。非阻塞式提交一个典型的实现方法是3PC(3 阶段提交),但是由于3PC不能在节点失效时保证正确性,所以几乎没有人在实际环境中使用3PC。
对于一个Linearizability Consistency的系统,对于跨越多个机器的事务的实现方法,一个可行的方案是使用Paxos协调多个Replication,每个Replication使用Strict 2PL。这个方案基本上和传统数据库的事务处理一致。SSI的实现需要解决一个重要问题,即如何跨越多个Replication生成一致的快照。
强一致性系统的问题
强一致性虽然使得我们可以像是对待只有一份副本系统一样使用这一系统,但是代价不仅是增大了时延,还有扩展性的下降,因为我们不能够通过增加副本来提高系统的性能。因此,我们应该只在必要的时候提供强一致性服务,例如使用额外的系统(Zookeeper)。
Strict 2PL的并发度也比较低,带来的问题是事务处理的性能低。SSI的并发度尽管比Strict 2PL要高,但是在高并发场景下Abort的概率也比较高。
高可用性约束下的分布式系统
从前面的CAP定理的结论可知若想实现一个100% 高可用的系统,最高只能支持Causal Consistency。这里没说Eventual Consistency,是因为目前还没有统一的对于Eventual Consistency的形式化的定义,分歧主要集中在是什么程度才是最终和怎样才算一致两方面。
因果一致的实现方法
Single Object(或者说Per-Key)的因果一致性是容易实现的,使用MVCC等技术可以同时存储对象的多个版本,在写入时只需指定其依赖于哪一个版本,即可实现Single Object的因果一致性。需要注意的是,为了维持数据访问的一致性在会话期间只与同一个副本进行通信。一般情况下系统应该提供Single Object的因果一致性。
Serial number因果一致性是为每个操作分配一个表示顺序的序列号,以序列号来表明操作之间的因果关系。常见的大规模序列号生成手段有:
时间戳:使用高精度的时间戳作为序列号
提前规划:使用取模的方式,按照生成器个数来划分可用序列号,部署多个序列号生成服务(例如:两个节点可以分别使用奇偶序列号的生成器)
批量生成:生成器以批量分配的方式,每次向节点分配一个连续的区间。
以上这些生成序列号的方法问题在于,这些序列号不能保证全局有序:
系统时钟会有偏差,多个节点间时钟之间不一定同步,时间戳不一定能表示操作的先后顺序。
如果服务节点的负载不均,则旧的序列号可能会被应用到新的操作上面,先后顺序也无法保证。
为此,我们需要一个能够保证全局顺序的序列号生成机制。
最终一致的实现方法
在某个时间点左右数据库系统中的各个副本间的状态很可能是不一致的。但是经过一段任意长的时间后,数据库中的所有副本最终都能收敛convergence
到相同的状态。
这种极弱的一致性保证,就是我们常说的 最终一致性Eventual Consistency。
这种弱一致性保证使得系统设计较为灵活,从而能够达到较高的性能。但是这种最终一致性的系统设计要求比较高,当系统设计中涉及到最终一致性时,应用层需要十分关注复制滞后对系统的影响。并且需要根据业务所需的一致性保证来设计系统,变相增加了应用开发者的工作量。此外,某些问题在网络错误或者高并发等特定场景下时才会暴露出来。