本文正在参加「技术专题19期 漫谈数据库技术」活动
那些相似却又有些差异的分布式系统概念
分布式系统的学习过程中有大量的概念,”共识“,”分布式事务“,”一致性“,“反熵”,“主从复制”,“Quorum” 等等...
他们看起来相似却又有细微的区别,初学者一不小心就会迷失其中
比如上面这个五个词看起来都是用来保障数据一致的,区别在哪里?
很多资料和书籍都会对这些概念进行逐一的讲解,但是缺乏系统的比较,没有站在分布式设计的角度进行整体梳理,学习者就会比较晕。
本篇文章会先对这些术语进行逐个辨析,站在分布式系统实现的高度对他们逐一梳理,让读者彻底理清分布式系统概念间的关系。
本文假定读者已经初步接触过这些分布式系统概念,不需要非常熟悉,在每个概念首次出现时,笔者也会稍加解释。
本文同时包含部分个人观点,如有错误,欢迎指正。来源于相关书籍中的观点都会用类似 [参考1] 的标记标识出来,可以去结尾的参考资料处寻找出处。
一致性模型
虽然上文中将 “分布式一致性” 和其他概念并列起来讨论,但是它其实是其他所有概念与手段的目标,统领了其他概念。
良好的一致性能够简化应用层的编程,给数据提供可靠的保障,为了衡量一致性的程度,人们定了一些标准,称为 “一致性模型”。
第一种标准是站在数据库服务端值传播的角度。分布式系统中数据一般存储在多个节点上,当更新 A 写入其中一个节点时,系统必须将更新传播到其他节点(称之为 “值传播”)。其他节点可能此时也同时发生了更新 B,需要进行传播,这两个更新谁先谁后呢?在这样混乱的局面下,能多大程度保证这些事件的顺序,构成了值传播的一致性模型 [参考1]:
- 可线性化:这些发生在分布式系统中的事件,无论局面多么混乱,总是等价于按照某种顺序作用在单个节点上。即所有事件总是有一个全局的所有节点公认的顺序,称为 “全序关系”。比如上面的 A, B 虽然是同时发生的,但是所有节点都认可 A 在 B 之前,那么就是可线性化的。Paxos,Raft 等共识算法实现的就是可线性化
- 顺序一致性:事件只有在同一线程中发生,它们之间才有全局的顺序,否则顺序是不确定的,可以一个节点认为 A 在 B 之前,而另一个节点认为 B 在 A 之前。
- 因果一致性:只有事件之间存在因果关系时,才有顺序。因果一致性已经可以满足大多数应用的需求,并且性能会比可线性化好的多,向量时钟 实现的就是因果一致性[参考2](Dynamo 及其后继者 Cassandra 都是用了这个方式[参考3])。
可线性化和可串行化的区别:单机事务追求 "可串行化" 的隔离级别, 听起来很像, 它们的英文分别是 Linearizability (可线性化) 和 Serializability(可串行化)。但是 可串行化 比 可线性化的要求严格多了,可线性化只要求能排序,不出现分叉即可;可串行化不仅要求有序,还要求将相同事务的操作全部放到一起。总结: 可线性化 << 可串行化[参考4]
刚刚只是站在了服务端数据同步的角度看待问题, 现在我们换位思考一下, 站在客户端角度, 毕竟所有的应用程序都是通过客户端访问的。 如果我们往系统写入一条数据后,立即读取,却读不出来,那么无论它值传播实现得多好,我们依旧会觉得它的一致性很差。这就构成了基于客户端会话的一致性模型[参考1],常见的标准有:
- 读自己写(read-own-write):客户端可以读出自己刚刚写入的数据。听起来应该理所当然,但是在分布式系统中,可能会在一个节点写入后却在另一个节点读取,导致读不到。在读写分离的主从架构中,可能在主节点写入,但是从节点读取,就会导致读取不到,此时强制读取主节点,就能解决问题
- 单调读:假设客户端进行了多次读取,后读取的数据一定会比之前读的数据要更加新。这个模型同样很难在单机场景下理解,在单机上稍后读的数据一定就是更新的。但是在分布式系统中,假设是一个一主两从的架构,两次读取分别读的不同的从节点,第一次读取的从节点数据更新,就是给客户端造成一种 “时光倒流” 的假象。假设有一个横坐标为读取时间,纵坐标为数据时间的坐标轴,那么这个函数一定是 “单调递增” 的,因此称为 “单调读”。
- 读后写(write-follow-read,又叫会话因果关系):客户端当前的写入一定在已观测到的写入之后。
这两个模型除了站的角度不同之外,他们还有读写方面的侧重,可以看出,值传播模型强调写,而会话模型更加强调读。
我们先从写的角度将之前提到的概念排个序。
值传播一致性排序
为了确保节点间数据的一致,常见的手段其实只有三种,分别是 分布式事务,复制和反熵。共识,主从等架构本质上都是一种复制,所以就统一称为复制了。
将它们由强到弱罗列:
分布式事务
顾名思义的强,分布式事务算法,无论是两阶段还是三阶段提交,都要求所有节点完成提交后才能返回。但是也正因为如此,导致它无法进行容错,一个节点故障就会导致整个系统瘫痪,完全无法发挥分布式系统的优势。[参考5]
有人认为分布式事务和复制完全是两码事,在一般的数据库架构中,也确实如此,一般会先用 “事务” 算法将数据提交到一个副本后,再用专门的 “复制” 算法将更新同步到其他节点。
但我认为他们是紧密相关的,要不是分布式事务性能太差,完全可以将它用在复制中,只要向多个节点提交相同的数据即可。可见 “复制” 其实是分布式事务的一种特殊场景,分布式事务支持向不同的节点提交不同的数据,而复制只想要提交相同的数据,针对这个特殊场景可以做很多优化(比如只需要多数确认即可),因而产生了专门的 “复制” 算法。
复制
复制包括最简单的主从复制,以及复杂的共识算法,如 Raft, Paxos 等。
复制算法只需要部分节点确认即可返回,比如 MySQL 异步主从复制,只需要主节点确认即可返回。共识算法只需要多数确认即可返回。
共识算法和主从复制有着诸多相似之处,共识算法在稳定状态下其实就是在做到多数节点的主从复制(MySQL 中称之为半同步复制),只是在出现故障时能够做到自动切换主节点,并且确保主节点中含有最新的数据。
反熵
在分布式系统中,熵表示节点之间的分歧程度[参考6],所以反熵表示修复节点之间的不一致状态。(总觉得计算机科学家们把熵的概念套反了,在热力学中,区域间的区别越大熵越低)。
反熵从定义上看就是一种最终一致性,就是数据已经不一致了,我再来想办法修补。因此它是最弱的。
反熵其实也有三类手段:
- 读修复:读取时对比多个节点,发现不一致则进行修复
- 写修复:Dynamo 使用一种称为提示移交(hinted handoff)的反熵方法,当节点 A 出现网络问题写不进去时,先将数据写入其他机器 B,等到 A 恢复后,再由节点 B 还给节点 A [参考3]
- 后台对比修复:后台定期对比数据进行自我修复。比如通过心跳传递一个哈希值(一般是 Merkle 树的根[参考3]),和其他节点对比数据进行修复。
值传播一致性的局限
上一小节提到,值传播一致性缺少对读的关注。即使实现了很强的值传播一致性,比如可线性化,在客户端看来,可能数据依旧很不一致。
即使强如分布式事务,比如两阶段提交算法,在提交的间隙,客户端进行了两次读取,一次读取了提交完成的节点,一次读取到了未完成提交的节点,对于客户端来说,就仿佛发生了 “时光倒流”(即不满足“单调读”)。
在比如 Raft 共识算法,客户端写入之后立即进行读取(读自己写)。因为 Raft 只复制成功大多数节点后就会立即返回,如果客户端刚好读取的是没复制到的 “少数” 节点,就会出现读不到的情况。因此即使用了具有很强一致性的算法,在用户看来也可能是 “很不一致” 的。这就需要我们在分布式系统的一致性实现中,根据场景充分结合读写算法,才能让用户感受到真正的 “一致”。
会话模型的一致性实现
读写整合
从上文中可以看出,值传播一致性实现得再好,对于用户来说都是不可感知的。用户最终是通过客户端视角(即会话模型),来感知系统一致性的。因此仅仅保证值传播的一致性是不够的,系统要想办法将读写相关的算法整合起来,而将读写整合,目前主要有主从模型和 Quorum 两种方式。
主从模型
以 MySQL 的主从为例,所有的写入都走主节点;而读取,根据一致性的要求,要求高的也走主节点(要求写入的数据立刻就要读取到,比如用户加入购物车的商品要求刷新后立刻能看到),要求低的就读取从节点(比如做数据分析)。
Quorum
根据业务特点,灵活调节写入和读取的节点数。
Quorum 定义了三个数字:
- N:数据的副本节点数
- W:成功写入 W 个节点,才返回
- R:成功读取 R 个节点后,才选择其中最新的数据返回
主从模型通过选择读取的节点来,来满足不同业务场景的一致性需求,而 Quorum 则是通过调节这个三个数字,来满足不同场景需求:
- 强一致性场景:设置
W + R > N
,这样读取和写入的节点总会有交集,就能保证用户读到自己刚刚写入的数据。比如在 Raft 复制到多数节点提交后,客户端也同时读取多数节点中的最新数据,这样就不会产生上文的困惑了。 - 弱一致性场景:设置
W + R < N
,其实就是上文中在 Raft 写入返回后,只读取一个节点的场景,此时W=3,R=1,N=5
,3+1<5
,所以客户端才会读取到旧的数据
这里我们发现,在值传播算法相同的情况,我们针对不同的业务场景,采用不同的读算法,就能得到不一样的一致性,我们也可以像上文中对值传播算法排序一样,对所有的读算法进行一致性排序。
读算法的一致性排序
将读算法的一致性由强到弱罗列如下:
读主
从哪里开始的写入,就从哪里读取。比如在主从模型中直接读主节点,在 Raft 这样的共识算法中直接读取领导者。此时所有客户端读取的数据都是一致的,写入的数据也能够被立即读到。拥有最简单粗暴,也是最强的一致性。
很多对一致性要求很高的系统都是这么做,比如 Google Chubby 为了保障强一致性,就只允许读取领导者节点。但是在发生网络故障时,会导致一段时间的不可用[参考7]。(因为网络故障时,共识算法要重新选主,选主成功后才能进行读取,而选主的过程很耗时)。
读主看起性能很差,因为把读写压力都放在了一个节点上,其实不然。很多分布式数据库,比如 TiDB,会将数据分区(称为 Raft Group),然后每一个分区有不同的主,这样就将读主的压力分散开了[参考8]。
读多个
从多个从节点读取数据,虽然不如直接读主,但是至少有多个节点数据可以比对修正:
- 和 Quorum 结合,在读取节点数加上写入节点数大于总节点数时,就能获得和读主相近的一致性
- 可以在过程中加入反熵机制(一般称为 “读修复”),在获得多个节点中最新数据的同时,将数据同步到其他不一致的节点
粘性会话(Sticky Session)
客户端虽然只从一个节点读取数据,但是只要客户端和系统的会话开启,每次读取的都是一个固定节点,不会这次读的是节点 A,下次读的是节点 B。
对于从节点来说,虽然可能存在较长的同步延迟,但是它的数据至少是会不断更新的。因此客户端稍后的读取一定会比之前的读取更新,能够满足 “单调读” 一致性。
随机读
客户端每次都随便选择一个节点读取,或者根据某种不确定的规则,比如离得最近的,网络最好的,负载最低的等等(“离得最近” 这个属性可能会随着网络故障或者集群调整发生变化,因而也是不确定的)。之前举的例子中的诸多问题都是因为这个产生的,比如写入之后,读到了还没同步到节点;再比如两次读取到了不同的从节点,第二次读取的从节点延迟巨大,读取的数据比第一次还旧,就会让客户端产生 “时光倒流” 的错觉。
随机读在大多数情况下都没什么用,除非能够保证很强的写入一致性,比如使用了分布式事务,或者像 GFS 那样确保所有副本都写入成功后。
分布式算法的设计思想
上文中虽然对于分布式算法做了很多辨析,但是在学习他们的细节时,还是会发现诸多相似之处,这些我觉得都可以归纳为分布式算法的设计思想,我简要总结了两个,分别是两阶段和多数确认。可能不全,欢迎读者补充
两阶段
分布式事务中一个两阶段提交的算法,它先用一个准备阶段确保大家都准备完毕,然后再统一进行提交阶段,它因为无法容错的原因,没能被大规模使用,但是两阶段的思想却渗透进了几乎所有分布式算法:
- 共识算法 Paxos:先通过准备阶段确认集群中没有其他提案,再进行接受阶段让其他节点接受自己的提案
- 共识算法 Raft:在网络稳定时,Raft 表面上只有一个阶段,就是由领导者将日志复制给多数节点,然后提交本地的日志。但是这里从节点只是将操作日志复制了过去,还没有进行提交,需要在下次心跳或者专门的一个提交阶段进行提交,所以本质上还是一个两阶段提交。
- Percolator 事务:利用 Percolator 算法可以在任意的 KV 存储引擎上实现事务。它先通过 预写阶段 将事务相关的数据上锁,然后在通过 提交阶段 提交[参考9]。
- ...
多数确认
这个思想或许来源于人类社会的原始经验,每当人们拿不定注意时,就喜欢用举手表决,少数服从多数。共识算法都要求多数确认,Quorum 中如果要保证强一致性,读和写至少要有一个得到多数确认。大多数分布式数据库都只能容忍小于一半节点的故障。
在区块链的场景,因为可能存在恶意节点,需要使用比共识更强的 “拜占庭容错算法”,此时更多的节点确认,只能容忍小于三分之一的节点故障。
全文脑图
学习分布式系统时,很容易陷入这一堆概念中,抽个时间全部梳理下,就会豁然开朗,学习和实践的效率都会高很多,希望大家看完本文也能有类似的感觉。附上全文脑图:
本文一些术语的困扰
- 节点:很多算法中将冗余的数据称为 “副本”,将值传播称为 “更新到副本”,但是一般情况下,副本都不会位于同一台机器的,因为本文比较了好多算法,不想浪费笔墨解释这些差不多的术语,所以就统一将副本称作 “节点” 了,或者说 “从节点”。
- 从:很多算法中称之为跟随者,本文为了统一比较,经常会称之为 “从”
参考资料
[参考1] 《数据库系统内幕》 11.6 会话模型
[参考2] 《数据密集型应用系统设计》第九章 捕获因果依赖关系
[参考3] 《Dynamo: Amazon's Highly Available Key-value Store》
[参考4] 《数据密集型应用系统设计》第九章 可串行化与可线性化
[参考5] 《数据密集型应用系统设计》第九章 分布式事务的限制
[参考6] 《数据库系统内幕》 12 反熵和传播
[参考7] 《The Chubby lock service for loosely-coupled distributed systems》
[参考8] TiKV Multi-raft
[参考9]《Large-scale Incremental Processing Using Distributed Transactions and Notifications》
End
欢迎大家关注微信公众号「技乐书香」,每周都有原创文章更新