【分布式】 CAP 原理和最终一致性

简单描述一下 CAP 原理

CAP 原理

对一个分布式计算系统来说,CAP 是指:

  • 一致性(Consistency),所有节点在同一时间具有相同的数据
  • 可用性(Availability),保证每个请求不管成功或者失败都有响应
  • 分区容忍性(Partition tolerance),在出现网络分区的情况下,仍然能够满足一致性和可用性

而 CAP 原理是指,这三个要素最多只能同时实现两点,不可能三者兼顾。对于分布式数据系统来说,分区容忍性是基本要求,否则就失去了价值。因此设计分布式数据系统,就是在一致性可用性之间取一个平衡。

例如:

假设用一台服务器 A 对外提供存储服务,为了避免这台服务器宕机导致服务不可用,又在另外一台服务器 B 上运行了同样的存储服务。每次用户在往服务器 A 写入数据的时候,A 都往服务器 B 上写一份,然后再返回客户端。一切都运行得很好,用户的每份数据都存了两份,分别在 A 和 B 上,用户访问任意一台机器都能读取到最新的数据。

这时不幸的事情发生,A 和 B 之间的网络断了导致 A 和 B 无法通信,也就是说网络出现了分区,那么用户在往服务器 A 写入数据的时候,服务器 A 无法将该数据写入到服务器 B。这时,服务器 A 就必须要做出一个艰难的选择:

要么选择一致性(C)而牺牲可用性(A):为了保证服务器 A 和 B 上的数据是一致的,服务器 A 决定暂停对外提供数据写入服务,从而保证了服务器 A 和 B 上的数据是一致,但是牺牲了可用性。(指服务器虽然活着,但是却不能对外提供写入服务。)

要么选择可用性(A)而牺牲一致性(C):为了保证服务不中断,服务器 A 先把数据写入到了本地,然后返回客户端,从而让客户端感觉数据已经写入了。这导致了服务器 A 和 B 上的数据就不一致了。

最终一致性

对于一致性,可以分为从客户端和服务端两个不同的视角。

从客户端来看,一致性主要指的是多并发访问时更新过的数据如何获取的问题。

从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。

一致性是因为有并发读写才有的问题,因此在理解一致性的问题时,一定要注意结合考虑并发读写的场景。

从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。如果能容忍后续的部分或者全部访问不到,则是弱一致性。如果经过一段时间后要求能访问到更新后的数据,则是最终一致性。

最终一致性根据更新数据后各进程访问到数据的时间和方式的不同,又可以区分为:

  • 因果一致性。如果进程 A 通知进程 B 它已更新了一个数据项,那么进程 B 的后续访问将返回更新后的值,且一次写入将保证取代前一次写入。与进程 A 无因果关系的进程 C 的访问遵守一般的最终一致性规则。
  • “读己之所写(read-your-writes)”一致性。当进程A自己更新一个数据项之后,它总是访问到更新过的值,绝不会看到旧值。这是因果一致性模型的一个特例。
  • 会话(Session)一致性。这是上一个模型的实用版本,它把访问存储系统的进程放到会话的上下文中。只要会话还存在,系统就保证”读己之所写”一致性。如果由于某些失败情形令会话终止,就要建立新的会话,而且系统的保证不会延续到新的会话。
  • 单调(Monotonic)读一致性。如果进程已经看到过数据对象的某个值,那么任何后续访问都不会返回在那个值之前的值。
  • 单调写一致性。系统保证来自同一个进程的写操作顺序执行。要是系统不能保证这种程度的一致性,就非常难以编程了。

从服务端角度,如何尽快将更新后的数据分布到整个系统,降低达到最终一致性的时间窗口,是提高系统的可用度和用户体验非常重要的方面。对于分布式数据系统:

N — 数据复制的份数
W — 更新数据时需要保证写完成的节点数
R — 读取数据的时候需要读取的节点数

如果 W + R > N,写的节点和读的节点重叠,则是强一致性。例如对于典型的一主一备同步复制的关系型数据库,N=2,W=2,R=1,则不管读的是主库还是备库的数据,都是一致的。

如果 W + R <= N,则是弱一致性。例如对于一主一备异步复制的关系型数据库,N=2,W=1,R=1,则如果读的是备库,就可能无法读取主库已经更新过的数据,所以是弱一致性。

对于分布式系统,为了保证高可用性,一般设置 N >= 3。不同的 N,W,R 组合,是在可用性和一致性之间取一个平衡,以适应不同的应用场景。

如果 N=W,R=1,任何一个写节点失效,都会导致写失败,因此可用性会降低,但是由于数据分布的 N 个节点是同步写入的,因此可以保证强一致性。

如果 N=R,W=1,只需要一个节点写入成功即可,写性能和可用性都比较高。但是读取其他节点的进程可能不能获取更新后的数据,因此是弱一致性。这种情况下,如果 W < (N+1) / 2,并且写入的节点不重叠的话,则会存在写冲突。