在前面我们一路围绕如何构建故障容忍的分布式系统展开, 分别阐述了分布式系统不可靠的网络以及时钟问题, 同时我们也理解了高可用架构以及复制原理, 其中我们都谈及到一致性的问题. 那么如果这个时候如果让我们来回答什么是一致性? 你会怎么解答这个问题呢? 接下来我聊聊自己对一致性的理解与思考.
什么是一致性模型
相信我们对于一致性中常见的术语, 比如强一致性、线性一致性、顺序以及因果一致性以及最终一致性等这些不同的术语描述, 我想我们都不陌生. 除此之外, 我们有时候还会看到什么读己之所写, 单调读等术语, 不知道你会不会觉得一致性咋这么多名词, 咋理解, 咋区分呢?
说实话, 其实我也挺反感这些术语描述的, 不同的文章都有自己的理解和表述, 那么我是怎么去理解的呢? 一般我陷入这种被各种术语混淆不清的时候, 我自己的做法就是直接不去看这些术语的直接定义, 而是基于第一性原理重新去理解我们的一致性模型.
于是我的第一个问题就是为什么需要去理解一致性模型, 它能够在我们实际工作中发挥什么样的作用呢? 在前面讲述高可用架构设计原理的时候我们做了以下的总结:
这个时候回答为什么要理解一致性模型, 它是帮助我们构建一个高可用架构设计方案过程中解释和分析分布式系统的状态表现的一种手段, 模型就是一项工具, 当我们设计的方案面临业务团队甚至是leader对系统状态表现的一致性问题提出挑战的时候, 这个时候我们就可以借助一致性模型来帮助我们分析和解答可能的结果, 从而来验证我们的系统状态是否满足预期.
了解了根因, 下一个问题才是考虑什么是一致性模型? 这其实和我们对一个词语的分解阐述思路差不多, 一致性模型拆分为一致性和模型两个关键词, 一致性描述的是一个对象的状态, 这个对象可以是节点或者服务, 也可以是数据, 用面向对象的思想来阐述就是一切皆对象; 模型就是建模产物, 即面对我们现实世界中的对象在外界施予的压力下随着时间推移所表现的运行状态变化进行建模, 通过模型来描述我们对象在时间线上运行状态的变化.
那么接下来我会问, 有哪些一致性模型能够描述我们的对象运行状态的变化呢? 取决于我们思考问题的视角, 如果是数据库开发人员, 那么关注的一致性模型是单值操作一致性模型; 如果是应用开发人员, 那么关注的一致性模型是单客户端一致性模型, 也有称之为会话一致性模型. 但不论是哪种视角, 之所以会产生一致性是因为我们引入了多副本冗余机制, 而副本之间的状态需要通过复制的手段完成, 而在分布式系统中复制会由于Replication Lag导致状态一致性问题的产生.
单对象操作一致性模型
不论是单值还是单客户端的一致性模型, 这里我统称为单对象一致性模型.从上述的定义我们可以将一致性模型就是描述从时间线上看单对象的状态变化.
如果是单值操作, 那么我们就是思考谁操作单值, 在计算机系统中, 我们一般是交由线程去帮助我们完成的, 因此这里单值的施予压力的操作者是线程, 从线程角度在时间线上看操作数据值状态的变化. 如果是单客户端的一致性模型, 那么我们的视角就是从用户的角度在时间线上看操作数据值的状态变化.
因此我们一般会将一致性模型拆分为三大类别, 即线性一致性、顺序以及因果一致性、最终一致性. 这里我们将围绕单对象一致性模型分类展开. 操作粒度以线程为准.
线性一致性模型
也许你会有疑惑, 线性一致性和强一致性有啥关系? 一般而言我是建议没必要过分解读, 因为它们其实的差异是存在有些细微区别, 如果硬要区分它和强一致性有啥关系, 那么只能看下面的它们之间的区别, 因为用言语其实也很难表达清楚, 假如现在x 的初始值为0, 这个时候有:
强一致性就是在t1之后的任何时刻都要求能够读取到最新的x = 1的数据, 而线性一致性我们可以看到在t1 - t2期间正在发生write x = 1的写操作, 只能保证t2之后才能读取到x = 1, 在 t2 之前只能读取到初始值x = 0. 由此可见, 强一致性只能是停留在理论上的知识, 几乎是无法实际落地. 因为我们知道发起一个写入操作write x = 1 必然需要经过一定的cpu指令周期的, 因此强一致性的定义这里其实没有什么实际意义, 所以这里我们基本忽略它即可.
那么我们的线性一致性模型背后的思想是什么呢? 就是让我们的系统看起来只有一份Replica.什么意思呢? 假如我现在向系统按时间Timeline的方式分别发起write x = 1 以及write x = 2 的写操作, 那么后续的P3以及P4节点看到的x 必须是先看到 x = 1 然后再看到 x = 2, 即如下是我们的线性一致性模型:
从上述的时间轴可以看出, 在t1 - t2时刻, 如果我们的系统满足线性一致性模型, 那么在这个时刻就不能出现既能读取到x = 0以及 x = 1的两种可能性, 也就是说线性一致性模型需要附加一个约束就是时效性保证, 即一旦写入或读取了一个新值,所有后续的读取操作都会看到所写入的值,直到该值再次被覆盖。
这也意味着在t1 - t2 时刻需要保证我们write x = 1具备原子性. 因而线性一致性也称之为原子一致性模型. 而这个时效性的保证就要求线性一致性模型在以下红色圆心点操作通过黑色连线都是在Timeline上向前移动, 不能向后, 即:
因为我们考虑的是单对象操作, 因此换个角度看, 如果我是操作的客户端且每个客户端都拥有一份自己的数据, 那么线性一致性模型就是读己之所写, 也可以采用单调读的方式保证自己路由到同一个Replica副本; 如果是共享数据, 那么就存在并发读写问题,即多个客户端操作同一份单值的共享数据, 怎么解决呢? 那这个时候就是写入操作需要保证原子性+全局时钟(TrueTime API)同步或者全局逻辑时钟(向量时钟版本)的方式解决冲突问题.
顺序与因果一致性
从上述的线性一致性模型我们还发现一个很有意思的现象, 那就是线性一致性在Timeline的时间序列上是有序的. 怎么理解呢? 在讲述顺序之前, 我们先看一个数学例子, 假如现在我给出任意两个元素 a 以及 b, 如果元素a 以及 元素b 都是自然数, 比如元素 a = 10, 元素 b = 13, 那么我们总是可以得到 b > a 这样的结论, 这种顺序我们称之为全序.
那如果元素a 以及元素b 都是属于一个集合, 即a = {9, 11, 10}, b = {12, 13}, 那么元素a 以及元素b 能够比较吗? 一般地说我们认为元素a 以及 元素b 是不可比较的, 但是如果我们按照元素的个数来定义比较策略, 即按元素的个数进行排序, 那么我们能够得到 a > b 这样的结论, 对于这样的顺序排序策略我们称之为偏序.
在一个具有线性一致性的系统中,操作存在全序关系:如果系统的表现就好像数据只有一份副本,并且每个操作都是原子性的,这意味着对于任意两个操作,我们总是能够说出哪个先发生。
那么顺序一致性模型呢? 其实我觉得也是和强一致性一样, 没必要过度解读, 因为抛开因果关系来谈顺序一致性其实是没多大意义的, 为什么呢? 同样地我举一个例子, 假如我现在向系统按Timeline发起write x = 1 以及 write x = 2 的两个写操作, 由于网络延迟导致P1线程在Timeline时间序列上慢于write x = 2, 那么如果我的数据库是满足顺序一致性的话, 这个时候我们看到的过程如下:
如果把这两个操作换到单CPU上执行, 无法保证线程P1以及线程P2执行的顺序性,因此如果我们单纯谈及顺序一致性我个人是觉得意义不大, 增加了理解问题的负担. 但是如果我们的操作需要保证write x = 2 的前提是必须先保证 x 的输入值是1, 即具备因果一致性, 那么这个时候我们来谈顺序一致性才有意义.
因果一致性相比线性一致性模型放弃了全局时钟的同步, 转而保证前后两个操作的因果关系, 如果从数学的角度来看因果一致性模型, 即如果两个操作是并发的,那么就不具备可比较性, 如果两个操作具备因果关系, 那么这两个操作就具备可比较性, 这意味着因果关系定义的是一种偏序关系,而非全序关系:一些操作之间相互存在顺序关系,但有些操作则是不可比较.
比如我们在一个相同的桌号的点餐系统中, 两个客户端同时发起点餐操作, 每个客户端都只能看到自己添加商品的顺序, 如下:
从上述可以看到在不同的分支, 每个客户端看到自己添加商品之后的商品列表顺序, 也就是在自己的客户端能够保证顺序, 而不同的客户端是无法进行顺序比较的, 因此存在并发操作, 最后当我们提交点餐的时候, 就需要通过设计冲突解决方案来实现.
因此根据这个定义, 在一个具有线性一致性的数据存储中不存在并发操作: 必然存在一条单一的时间轴, 所有操作沿着这条时间轴形成全序关系。可能会有多个请求等待处理,但数据存储确保每个请求在单一的时间点上以原子方式进行处理,针对数据的单一副本,沿着单一的时间轴进行,不存在任何并发情况。
并发意味着时间轴会出现分支,然后又重新合并——在这种情况下,不同分支上的操作是不可比较的。
最终一致性
上述我们聊的线性一致性以及因果一致性模型都是属于强一致性模型, 那么这里的强一致性和最终一致性之间的根本区别是什么呢?
最终一致性模型我们在先前讲述BASE理论提到过, 它允许存在混沌状态, 同样我这里分别按Timeline的序列将 write x = 1 以及 write x = 2 操作应用到存储系统上, 那么如果我的数据库满足最终一致性, 也就是意味着在某一个稳定的Timeline 序列t4, 在t4之后所有的线程看到的 x 值最终都是一样的.
也就是说我们的系统能够容忍这样的故障, 即允许容忍在Timeline上不同的线程看到不同的数据不一致的情况, 但是这种情况是短暂的, 且能够在一个稳定的Timeline 即t4时刻之后看到的数据最终都是一致的, 我们称之为最终一致性, 那么对于线性一致性以及因果一致性是不允许容忍这种情况, 因此我们称之为强一致性模型.
总结
最后我画一张图总结今天的一致性模型阐述, 如下:
也许我们会有一点疑惑, 不是还有什么单调读、单调写什么吗? 其实说白了还是在上述的三个模型之中, 所以我觉得是不需要去关注太多的说法, 重点在于我们理清技术本质问题即可.如果是多值操作呢? 那就是我们在数据库引入事务层面去解决了.后面有机会再聊.