6070bf8563fb74f14b64245557072bbf
漫谈分布式系统(14) -- 一致性问题的根源

数据一致性小结

从系列第 8 篇开始引出高可用性和数据副本,接着深入数据一致性问题,这个章节,已经写了 6 篇了。

这块内容涉及的概念、理论都很多,也是分布式系统面临的核心挑战之一。所以,这里有必要先小结下。

首先,为了达到高可用性,唯一的办法就是数据复制(data replication)。当然,数据副本也能带来性能上的提升等其他好处。

对于数据复制主从的选择,我们介绍了 3 种方法:

  • single leader replication。一个 leader 接受写,多个 follower 作为备份。
  • multi-leader replication。多个 leader 都能接受写。
  • leaderless replication。没有 leader,也相当于个个都是 leader。

对于数据复制的时效性,我们也介绍了 3 种方法:

  • synchronous replication。同步复制,虽然慢,但是稳妥。
  • asynchronous replication。异步复制,很快,但是会有延迟。
  • semi-synchronous replication。半同步复制,相对折中,比较稳妥也比较快。

接着,我们发现,多主并发写可能导致数据冲突,异步复制带来的 replication lag 可能导致访问不到最新的数据。也就是说,这类问题可能导致数据一致性问题

数据一致性问题使得系统变得不可信,会导致很多现实问题,必须要解决。

解决数据一致性的方法可以分为两个大类:

  • 预防类,尽可能防止分歧,提供强一致性,对外看起来像一个 single-copy 的系统,哪怕牺牲可用性和性能(性能也是某种程度的可用性)。
  • 先污染后治理类,允许出现分歧后再收敛,提供弱(最终)一致性,对外就坦然以多节点的分布式系统示众,牺牲一部分一致性,来保证可用性。

预防类的一致性方法,又细分为三类:

  • 单主同步复制。看起来很强的保证,实际上处理不了很多 corner case,只能做到 best-effort guarantee。而这些 corner case 后面隐藏着一个普遍的 CAP 定理。
  • multi-phase commit。数据复制也可以看做事务的一种,可以用分布式事务解决。典型的有 2PC 和 3PC,但是都解决不了网络分区的问题。
  • consensus。为了解决网络分区问题,衍生了以 Paxos 为基础的一类算法,包括 Raft、ZAB 等,这类算法的目标是解决所谓共识问题,而数据一致性可以看做是另一个视角的共识。

先污染后治理类的一致方法,又可以分为两类:

  • 以 Dynamo 为典型的提供 probabilistic guarantees 的最终一致性。对有因果先后顺序的事件,可以通过 vector clocks 等手段提供因果一致性。但其他并发写入的事件,就无法确定顺序了,不过很多时候这个顺序也不重要。
  • 以 CRDT 为典型的提供 strong guarantees 的最终一致性。在一些特殊的数据类型及其提供的操作下,事件写入的顺序并不影响最终结果,比如 set 的 union 操作。这类数据结构就可以放开去并发写,不需要节点间的协商,或者说会自动收敛,以达到最终的一致性。当然,这类数据结构是比较有限的,不能满足所有场景的需求。

分布式事务可以用来解决数据一致性问题,但本身也是相对独立和非常重要的应用。而数据一致性又分强和弱两种,那分布式事务也就可以总结为两种实现方式:

  • ACID on 2PC。以 2PC 为典型的分布式事务,提供标准的 ACID 保证,从 CAP 的角度看,提供强一致性,但可用性(包括性能)不够好(其实某些场景下的一致性也得不到保证)。
  • BASE。BASE(碱) 理论是 ACID(酸)之外的另一种选择,可以基于 Dynamo 或基于 MQ 实现分布式事务,只追求最终一致性,牺牲无时无刻的强一致性,来换取性能和可用性。

到这里,我们就能汇总起来,从理论上归纳出一些一致性模型。这些模型,本质上来说,是分布式系统对外提供的承诺和保证(guarantee),让外部系统可以在这些保证的基础上使用分布式系统,而不会抱着不切实际的期望、完全黑盒地用。

一致性模型的分类和实现非常多,我们无法也没有必要一一列举。这里只总结下我们前面文章涉及过的主要的模型。

strong consistency models

  • linerizable consistency,线性一致性,保证 linerizability,全局有序,是最强的一致性,看起来就像单机系统一样。主要实现方式是以 Paxos 为典型的各种共识算法。
  • sequential consistency,顺序一致性,保证所有节点观察到的顺序一致,但不一定和真实的全局顺序完全一致。sequential consistency 再加上真实且准确的时间属性就等于 linerizable consistency。(注意不要和事务里的 serializability 搞混。)

weak consistency models

  • client-centric consistency models,客户端中心一致性,不强求服务端完整的一致性,只追求客户端各自的一致性。
    • read-after-write consistency,自己写的立马就能读。主要实现方式是把请求发给固定的副本。
    • monotonic reads,不能读到已经读到的数据之前的数据。主要实现方式是客户端缓存。
  • eventual consistency models,最终一致性,不追求无时无刻的一致,但保证在不确定的时间后,总是能达成一致。无所谓实现方式,自然状态就是这样。更需要关注的是运维效率和冲突的解决。
  • causal consistency models,因果一致性,有逻辑上因果先后顺序的事件需要保证顺序,其他情况都不保证。主要实现方式是 vector clocks。

一致性问题的根源

从上面的总结不难看出,一致性问题太重要了,影响太大了,为了解决它,我们也想尽了各种办法。

前面我们也提过,产生一致性问题的原因,是我们想要扩展性下的高可用性。但服务器可能宕机甚至无法恢复,于是只能做多副本,多副本间的数据要同步以保证一样,实现的不好,就会出现不一致的情况。

这么看来,服务器故障,就是一致性问题的根源。真的是这样吗?

部分是,但不完全是。

我们退一步,回到最初的单机系统,看看能不能找到问题的根源。

不确定性

在单机系统下,一个程序,接收到一个特定的输入,只会有两个结果。

  • 返回特定的输出。
  • 遇到故障返回错误。

而对于一系列的输入,单机系统会按输入产生的时间顺序处理,得到和输入顺序一样的输出。

所以,单机系统对于特定输入的反馈是确定的(deterministic)。这个确定性体现在两方面:

  • 输出的内容。当发生故障(failure)比如磁盘损坏时,宁愿 crash,也不会给出内容不一样的输出。
  • 输出的顺序。当发生故障时,宁愿终止执行,等恢复后再继续执行,也不会给出顺序不一样的输出。

这种确定性非常重要,是单机系统提供给外界的强力保证,外部系统可以放心大胆的使用单机系统。

同时,这种确定性是个一对一的关系,这就使得反过来,外界也能够通过得到的返回结果,推断出系统的状态。

但是在分布式系统里,为了保证可用性,会允许在出现局部故障(partial failure)后,整个系统继续运转。但 partial failure 发生的数量、位置、持续时间等都是不确定的,这就会让系统处于不确定(nondeterministic)的状态。

外界同样的输入,不再一定能得到同样的输出;也无法再根据输出结果来判断系统的状态

这个判断没问题,局部故障导致的不确定性,就是数据一致性的根源。但还是有点抽象。往细了挖,到底具体又是什么问题,导致了这种不确定性?

不可靠的网络

看一下上面这张简化的网络拓扑图,抛开角色属性后,分布式系统就是许多节点组成的图(graph)。

这些节点,就像太平洋上的一个个岛一样,孤零零的存在,对外界情况几乎一无所知,只能通过唯一的途径 -- 点对点的网络来探索

通过网络发出一条消息,如果得到回应,对对方就多一分了解;但如果没有得到回应,却连对方不在线这样的否定判断都做不到。

因为实际的拓扑图更像是这样:

各个节点间并不是直连,而是通过交换机等层级复杂的网络设备连接在一起。这些网络设备(甚至还包括网络线路)也会出现故障、堵塞等问题。

所以,对一个发出网络请求的节点来说,其实有对方节点和网络两个变量存在。

类似逻辑与计算,返回结果为 1 能判断二者都正常,但结果为 0 却会有三种情况:

  • 对方节点不正常但网络正常。
  • 对方节点正常但网络不正常。
  • 对方节点和网络都不正常。

这样,有别于单机系统的一对一关系,就出现了一对多的关系。根据返回结果反推系统状态,就不可能了

甚至,节点正常还可以细分为 crash 类的真死和 GC 等导致的假死。要分辨这两种情况,只能用超时(timeout)来探活。但超时时间应该设置成多少呢?不同的系统和环境都会有自己并不能百分百保证的经验值,这就是所谓的 unbounded timeout 问题。

这就是分布式系统面临的一大难题 -- 不可靠的网络(unreliable networks)

不可靠的时钟

而对于一系列消息的顺序问题,不再像单机系统那样有唯一的时间来确定顺序。

通常采用本地时钟(local clock)来保证局部有序,然后全局同步时钟来保证整体有序。但时钟的全局同步是很难做的足够高效的,无论是标准的 NTP,还是自己实现的同步协议。

这也就导致在分布式系统下,很难准确而高效的做到全局有序(total order)。

top Created with Sketch.