简介
理解问题本身比知道问题的答案要重要的多。本文中,我们辨析了线性一致性、顺序一致性、最终一致性这些概念,以及他们的关系和区别。由此我们了解到了分布式系统的一些核心问题,但我们并未讨论怎么解决这些问题。比如,采用什么算法才能提供线性一致性;面对最终一致性的系统,应该怎样编程,包括怎样处理边界情况,等等。相对于理解问题本身而言,这些反而都是细节。
分布式系统
先把组成分布式系统的一些关键概念定义清楚:
- 整个系统可以看成由多个进程和一个共享的数据存储组成。对于数据存储的读写操作由进程发起。这里的进程,相当于本文前面提到的系统用户或系统使用者。
- 同一个进程发起的读写操作是先后顺序执行的。注意,这里的「进程」概念跟我们平常编程时用到的进程有所不同,进程里面不再分多个线程了。
- 数据存储可能有多个副本,但我们在讨论一致性模型的时候,把它看成一个整体来看待,不区分读写操作提交到了具体哪个副本上。
- 每个操作的执行,从开始调用到执行结束,都需要花一定的时间。因此,一个进程发起的操作还没有执行完的时候,另一个进程的操作可能就已经开始了。
什么是一致性?
分布式事务的基础理论:在一个分布式计算系统来说,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)不可能同时满足,因此分布式系统一般选择AP或者CP架构,因为在分布式系统中,网络分区是无法避免的,如果无法容忍网络分区,就意味着系统需要拒绝请求进而导致不可用。(PS:CAP只是说哪些路走不通,但不是负责落地的具体协议。)根据CAP理论,要想保证事务的ACID特性,那么无疑需要CP架构,但是可用性又是互联网环境下需要得到保障的。BASE理论则是在AP架构上扩展兼容了C,即不严格要求分布式系统中的节点保持强一致性,只需要保障经过一段时间同步后节点最终达到一致性即可。在分布式系统中,网络通信是不可避免的,这也就意味着网络延迟、丢包、分区等情况也是不可避免的,这些情况都会严重影响分布式事务的执行。分布式系统中的任何节点都可能发生故障(如崩溃、宕机等),这会影响事务的执行和完成。根据CAP定理以及BASE理论,分布式事务往往需要在一致性和可用性之间进行权衡。PS:CAP 一般选CP 或AP,A不能不要,所以一般弱化C,按弱化的不同程度搞了各种xx一致性。
ACID中的一致性,是个很偏应用层的概念。原子性、隔离性和持久性,都是数据库本身所提供的技术特性;而一致性,则是由特定的业务场景规定的。要真正做到ACID中的一致性,它是要依赖数据库的原子性和隔离性的(应对错误和并发)。但是,就算数据库提供了所有你所需要的技术特性,也不一定能保证ACID的一致性。这还取决于你在应用层对于事务本身的实现逻辑是否正确无误。ACID中的一致性,甚至跟分布式都没什么直接关系。它跟分布式的唯一关联在于,在分布式环境下,它所依赖的数据库原子性和隔离性更难实现。
事务本来和分布式没什么直接关系的,就算在一个单节点的数据库上,要实现出事务的ACID特性,也不是那么容易的。ACID中的原子性,要求事务的执行要么全部成功,要么全部失败,而不允许出现“部分成功”的情况。在分布式事务中,这要求参与事务的所有节点,要么全部执行Commit操作,要么全部执行Abort操作。换句话说,参与事务的所有节点,需要在“执行Commit还是Abort”这一点上达成一致(其实就是共识)。这个问题在学术界被称为原子提交问题(Atomic Commitment Problem)
原子提交问题与共识问题的关联性:
- 共识问题,解决的是如何在分布式系统中的多个节点之间就某个提议达成共识。Paxos
- 原子提交问题,解决的是参与分布式事务的所有节点在“执行Commit还是Abort”这一点上达成共识。 2PC/3PC 所以,原子提交问题是共识问题的一个特例。
早期的分布式系统设计者,为了让使用系统的开发者能够以比较简单的方式来使用系统,希望分布式系统能提供单一系统视图 (SSI,single-system image),即系统“表现得就好像只有一个单一的副本”。线性一致性和顺序一致性就是沿着这个思路设计的。满足线性一致性或顺序一致性的系统,对读写操作的排序呈现全局唯一的一种次序。
- 我们将要讨论的一致性模型 (consistency model),主要是与复制有关。
- 让所有副本在任何时刻都保持一致,是不可能的。因为副本之间的数据同步即使速度再快,也是需要时间的。不过幸运的是,我们其实并不关心所有时刻的数据一致性情况。只要系统能够保证,每当我们去「观察」的时候(即读取数据副本的时候),系统表现出来的行为是一致的,就可以了。
- 一个系统在数据一致性上的具体表现如何,取决于系统对关键事件(读写操作)的排序和执行采取什么样的规则和限制。
用户A先在第1个副本上执行x=42,然后用户B再在第2个副本上执行x=43,最后用户C在第3个副本上读取x的值。出现了两种对于读写操作的排序。前一种排序是:
- 用户A执行x=42。
- 用户B执行x=43。
- 用户C读取到x的值是43。 后一种排序是:
- 用户B执行x=43。
- 用户A执行x=42。
- 用户C读取到x的值是42。 虽然这两种排序结果不同,但它们都做到了让系统“表现得像只有一个副本”。它们的不同在于,前一种排序遵循了不同用户的操作的时间先后顺序(线性一致性),而后一种排序没有(顺序一致性)。
可以这么说,一个分布式系统对于读写操作的某种排序和执行规则,就定义了一种一致性模型 (consistency model)。当一个系统选定了某种特定的一致性模型(比如线性一致性或顺序一致性),那么你就只能看到这种一致性模型所允许的那些操作序列。
CAP定理中的C,指的就是线性一致性 (linearizability)。它也经常被称为「强一致性」。根据CAP定理,当存在网络分区的时候,我们必须在可用性 (availability) 和强一致性之间进行取舍。另外,即使在没有网络分区存在的情况下,我们也必须在延迟 (latency) 和强一致性之间进行取舍。这是因为,系统维持强一致性是有成本的。想要维持越强的一致性,就需要在副本节点之间做更多的通信和协调工作,因此会降低操作的总延迟,进而降低整个系统的性能。
从20世纪90年代中期开始,互联网开始蓬勃发展,系统的规模也变得越来越大。人们设计大型分布式系统的指导思想,也逐步开始更倾向于系统的高可用性和高性能。取舍的结果就是,降低系统提供的一致性保障。这其中非常重要的一条思路就是最终一致性。最终一致性的设计思路,不再试图提供单一系统视图 (SSI),即不再试图让系统“表现得像只有一个副本”一样。它允许读到旧版本的数据。
Eventual consistency. This is a specific form of weak consistency; the storage system guarantees that if no new updates are made to the object, eventually all accesses will return the last updated value.最终一致性是弱一致性的一种特殊形式;存储系统保证,如果对象没有新的修改操作,那么所有的访问最终都会返回最新写入的值。这意味着,对于系统使用者来说,你必须针对数据不一致的可能性做好补偿措施 (compensation)。这也是最终一致性系统难用的地方。
当且仅当一个一致性模型所能接受的执行过程,都能被另一个一致性模型所接受时(前者的集合是后者集合的子集),我们就说前者是比后者「更强」(stronger) 的一致性模型。
为了提高系统可用性和系统性能,人们放弃了强一致性,采取了几乎最弱的一类一致性模型(最终一致性),但也同时牺牲了系统的能力或系统使用的便利性。那么,到底有没有必要一定采取这么「弱」的一致性模型呢?有没有可能在最终一致性的基础上增加一点safety属性,提供稍强一点的一致性,但同时也不至于对系统可用性和性能产生明显的损害呢?因果一致性。
因果律是这个世界最基础的规律,物理法则决定了我们总是先看到事物的「因」,后看到事物的「果」。
如何得到争取的执行序列/限制错误的执行序列
public class x{
private int lastIdUserd;
public int getNextId(){
return ++lastIdUserd;
}
}
就生成的字节码而言,对于在getNextId方法中执行的两个线程,有12870种不同的可能执行路径,如果lastIdUsed 的类型从int变为long,则可能路径的数量则增至2704156种。当然,多数路径都得到正确的结果,问题是其中一些不能得到正确结果。
一行代码就像 一句sql 一样。对于数据库,开发人员直接写的是sql,会被解释为执行计划。数据库保证事务(sql序列)的ACID。对于java,开发人员直接写的是代码,会被翻译成字节码。无特殊指令,os不保证指令的原子性,数据的可见性; 所以jvm 定义了java内存模型(happen-before规则)。
分布式问题与相对论
分布式领域最重要的一篇论文,到底讲了什么?一个进程内部的多个事件之间排序,这通常还是比较容易的;在不同进程(位于不同节点上)上的事件进行排序,就比较难了。这样一种全局排序有什么用呢?实际上,这是实现任何分布式系统的一种通用方法。只要我们获得了所有事件的全局排序,那么各种一致性模型对于读写操作所呈现的排序要求,很自然就能得到满足。回想一下我们在之前的文章条分缕析分布式:浅析强弱一致性中的分析,线性一致性和顺序一致性所要求的,正是要把所有读写操作(对应这里的事件)重排成一个全局线性有序的序列。
对于任意两个有偏序(Happened Before)关系的事件(或者说可能在因果性上产生影响的两个事件),我们的物理时钟要保证总是会为后一个事件打上一个更大的时间戳。要实现这个目标,我们面临的障碍主要来源于物理时钟的两种误差:
- 时钟的运行速率跟真实时间的流逝速率可能有差异;
- 任意两个时钟的运行速率有差异,它们的读数会漂移得越来越远。
时钟同步算法 ==> 可以对时间排序 ==> 全局序列 ==> 一致性。
2021 年云原生技术发展现状及未来趋势思考的维度就从“在不可靠软硬件体系上提供可靠服务”进一步拓展为“通过各种隔离手段减小事故的爆炸半径”:当不可避免的故障发生时,尽量把故障损失控制到最小,保障在可接受范围内,保证服务可用。