漫谈分布式系统(九):初探数据一致性

这是《漫谈分布式系统》系列的第 9 篇,预计会写 30 篇左右。每篇文末有为懒人准备的 TL;DR,还有给勤奋者的关联阅读。扫描文末二维码,关注公众号,听我娓娓道来。也欢迎转发朋友圈分享给更多人。

不得不重视的一致性问题

上一篇,我们终于引出分布式系统的第二个核心问题 — 可用性。也提到了 replication 是高可用的唯一出路

上篇末尾还提到,replication 除了提供高可用外,也可能会带来严重的后果。

  • 比如,在异步复制数据的时候,可能由于网络抖动,导致数据没来得及复制到 slave replica 上。这个时候,如果有请求去读 slave replica,就读不到最新的数据。

  • 又比如,在 multi-master 的场景下,可能两个 master 同时接收到修改同一个数据的请求。这个时候,两个写操作成功返回客户端后,再复制到对方时,就可能由于数据冲突导致更新失败。

类似问题,可以统称为数据一致性问题(consistency problems)。

上一篇我们主要关注了 replication 的两个特性 — 主从和时效性,二者组合起来,可能的数据一致性风险如下:

多主 一致性风险
single master
sync multi-master
async single master
multi-master

很明显,多主和异步,都可能带来数据一致性风险。 (leaderless 无主可以看做全主,有点物极必反的感觉。

  • 异步带来复制延迟( replication lag),导致数据同步不及时。

  • 多主带来并发写,导致数据冲突。

数据一致性一旦得不到保证,在系统内就像精神分裂一样;在系统外,则会带来很多现实问题 。比如:

  1. 用户刚付款买了张演唱会门票,然后刷新页面,由于请求被转发到的数据副本还没来得及更新,发现账号里显示没票。

  2. 用户收到推送,自己的文章有了新的留言,但点进去之后,由于访问到的副本还没更新,发现并没有新留言。

  3. 一个用户提了一个问题,另一个用户做了回答,但对某个数据副本来说,可能回答比问题先复制过来,第三个用户就会看到先有回答,后有问题的奇怪现象。

这些现实问题,使得系统在应用层,变得不可信。很显然,失去信任的代价是非常严重的。

因此,解决一致性问题,也就成了分布式系统的重大课题。

解决办法主要有两类:

  • 预防类, 极力避免一致性问题的产生,提供最强的一致性保证。

  • 先污染后治理类,允许不一致的产生,提供较弱的一致性保证。

从收敛性的角度看,第一类方法强制要求不一致数据的的实时收敛(convergence),而第二类方法,则允许不一致数据先发散(divergence),然后再逐渐收敛。

从消息顺序的角度看,预防类的强一致性,保证了对任意节点而言,不会因为 replication lag 等问题,导致先产生的数据被错误的存在后产生的数据后面。也就是说维持了整个系统对消息的全局线性(Linearizability)。而第二类方法,就属于 non-linerizable。(消息顺序非常重要,后面的文章会专门讲。)

预防类的一致性解法

正所谓防患于未然,从源头避免问题的产生,自然是最理想的目标。

尤其是数据一致性这么严重又不好解决的问题,能避免就应该避免。

所以我们就先来看第一类,预防类的一致性。

单主同步复制

最简单的办法,就是前面提到的 single leader + synchronous replication 的单主同步 模式。

  • single leader 保证了所有数据都只有单一的节点处理,避免了写冲突。

  • synchronous replication 保证了所有副本都更新完数据后,才返回给客户端,避免了单机故障导致的数据丢失。

这样,就达到了我们想要的强一致性(strong consistency),整个分布式系统看起来就像单机系统一样,仿佛没有副本。任何时候从任何地方访问系统,都能得到一致的体验。因此也有人把这种一致性叫做 single-copy consistency。

但深究一下,好像也还是有些 corner case。比如下面这个例子 A:

  1. master 收到客户端请求后,持久化,然后发送给 slave

  1. slave 收到转发的请求后,持久化,然后返回 ACK 给 master

  1. master 收到 slave 的 ACK 后,还没来得及返回给客户端,就挂了

这种情况下,客户端会认为系统没有成功处理请求,而实际上 master 和 slave 都已经持久化了数据, 客户端和服务端的认知就不一致了

再比如例子 B:

  1. 由于网络抖动,master 被误判为掉线后

  2. 系统 failover,slave 成为新的 master

  3. 网络恢复,原来的 master 又恢复正常了

这个时候,就会出现两个 master,即所谓的脑裂(split brain)现象, 连单主这个前提都被破坏了

这种情况下,系统就出乎意料的变成 multi-leader 了。仔细设计过的 multi-leader 系统尚且很难保证强一致性,更不用说这种异常陷入的情况了。

最后看一个例子 C:

  1. 在三副本的情况下,master 向另外两个副本同步数据,比如给一个账户扣款 1 元

  2. 其中一个副本成功了拿到数据并持久化到本地,然后发回 ACK 给 master

  3. 但另一个副本持久化结果后,发回的 ACK 却由于网络抖动丢失了

  4. master 没有收到第二个副本的 ACK,判定失败,于是重新发送

这样, 副本之间的数据就不一致了 ,第一个副本上的账户会扣掉 1 元,而第二个却会扣掉 2 元。

所以,单主同步的方法,也提供不了绝对的强一致性,只是在正常情况下 尽可能保证一致(best-effort guarantee) 而已。

(上面这几个 corner case,也和所谓 exactly once 问题有关,这个系列的后续文章会专门讲,这里不展开了。)

上面提到几个 corner case,比如脑裂的问题,似乎是很特殊的情况,但背后却可能藏着一个非常普遍的事实。

试想下,什么原因会导致节点被 误判 为死掉,从而导致脑裂?

  • 网络抖动

  • GC 导致程序停顿

  • ……

类似这些原因,导致了节点间通信不可达,至少是短期内看起来不可达。

或者用更专业的说法,叫做出现了网络分区(network partition),一个集群被分割成了几个网络不通的区域。

于是引出著名的 CAP 定理

一致性(Consistency)、可用性(Avaliabily)和 分区容忍性(Partition Tolerance)这三者,最多同时满足两个。

C 和 A 我们已经说了很多了,正因为我们想要 A,才引入了副本机制,然后导致了 C 危机。现在又多出个网络分区的可能需要处理。

而 CAP 定理居然告诉我们,不用处理,你处理不了的。

这么绝望吗?凭什么?我不信!

那就来推导看看。

  • 先要 C 和 A,这个时候如果发生网络分区,单主同步复制的方法,是无法成功完成数据复制的,所以拿不到 P。

  • 先要 C 和 P,如果发生网络分区,为了保证数据一致性,只能让其中一个分区正常工作,其他分区必须暂停服务,那这些分区就完全不可用,A 就丢了。

  • 先要 A 和 P,如果发生网络分区,并且每个分区都能正常工作,由于写数据时分区间无法同步数据,而等通信恢复后,又可能出现无法解决的数据冲突,也就是 C 丢了。

这样分析下来,确实三者无法兼顾。

另外,在刚才的推导过程中,每种情况的分析都是以「如果发生网络分区」作为初始条件,也揭示了它的与众不同。

事实上,C-A-P 三者并不是同一个层面的东西,C 和 A 是目标,而 P 呢,虽然 Partition-Tolerance 也是目标,但 partition 却是一个无法回避掉的前置条件。无数的生产事故已经告诉了我们,网络分区随时随地都可能发生。

所以,一个舍弃 P 的系统,是不具备真正意义的高可用性的。

而 CAP 落实到生产级分布式系统设计中,更多是在 P 的前提下,对 C 和 A 做取舍罢了。

TL;DR

我们为了高可用,引入了副本机制,但副本机制的副作用是会带来数据一致性问题。

  • replication lag 可能导致数据同步不及时。

  • 多主并发写可能导致数据冲突。

  • 数据一致性问题会带来应用层面很多现实的问题,使得系统对外变得不可信,因此必须解决。

  • 数据一致性问题的解法可以分为两类:预防类和先污染后治理类。

  • 预防类最基本的方法就是单主同步复制,但其实也只能做到 best effort gurantee,解决不了一些 corner case。

  • 在这些 corner case 背后,其实隐藏着一个更基本的难题,即所谓 CAP 定理。

对于上面的例子 C,我们换个角度理解,数据从 master 复制到多个 slave,可以看作几个往不同节点写数据的独立事件,正是这些事件的 部分 成功,导致了数据的不一致。

如果全部失败,大不了重试好了。但如果部分成功部分失败,重试就有重复的可能。

而避免多个事件部分成功,或者说要保持多个事件的原子性 — 要么都成功,要么都失败,早已有了可靠的方案 — 事务(你看,抓住问题的本质多么重要)。

只不过,我们需要的是 分布式事务(distributed transaction)

下一篇,我们就一起了解下分布式事务。

原创不易

关注/分享/赞赏

给我坚持的动力