分布式数据库(一)

小注

极客时间 - 分布式数据库 30 讲

什么是分布式数据库

由表及里、由外到内是人们认识事物的普遍规律,所以从两个视角来观察:

外部视角,外部特性:分布式数据库是服务于写多读少、低延时、海量并发 OLTP(联机交易)场景的,具备海量数据存储能力和高可靠性的关系型数据库。

内部视角,内部构成:将技术细节收敛到产品内部,以一个整体面对业务应用。


强一致性

对于分布式系统而言,一致性是在探讨当系统内的一份逻辑数据存在多个物理的数据副本时,对其执行读写操作会产生什么样的结果,这也符合 CAP 理论对一致性的表述。而在数据库领域,『一致性』与事务密切相关,又进一步细化到 ACID 四个方面。其中,I 所代表的隔离性(Isolation),是『一致性』的核心内容,研究的就是如何协调事务之间的冲突。因此,在谈论分布式数据库的一致性时,实质上是在谈论 数据一致性事务一致性 两个方面。

数据一致性

数据一致性有一个前提,就是同时存在读操作和写操作。把两个因素加在一起,就是多副本数据上的一组读写策略,被称为『一致性模型』(Consistency Model)。

观察数据一致性有以下两个视角:

  • 状态一致性:数据所处的客观、实际状态所体现的一致性;
  • 操作一致性:外部用户通过协议约定的操作,能够读取到的数据一致性。

状态视角

从状态的视角来看,任何变更操作后,数据只有两种状态:所有副本一致或者不一致。在某些条件下,不一致的状态是暂时,还会转换到一致的状态;而那些永远不一致的情况几乎不会去讨论,所以习惯上会把不一致称为『弱一致』。相对的,一致就叫做『强一致』。

强一致:在全同步复制模式下,主备库同步 binlog 时,主库只有在收到备库的成功响应后,才能向客户端反馈提交成功。客户端获得响应时,主备库的数据副本已经达成一致,后续操作没有问题,但这种模式的副作用非常大,主要有以下两点:

  1. 性能差:主库必须等到多个备库均返回成功后,才能向用户反馈提交成功。主库的响应时间取决于多个备库中延时最长的那个。
  2. 可用性问题:任何设备都有可能出现故障,尤其是 x86 这样的通用商业设备,故障率会更高。但在全同步复制模式下,集群中的多个节点(假设一主两备)被串联起来,如果单机可用性是 95%,那么集群整体的可用性就是 85.7%(95%*95%*95%=85.7%),跟单机相比反而降低了。

弱一致:NoSQL 产品是应用弱一致性的典型代表,但对弱一致性的接受仍然是有限度的,这就是 BASE 理论中的 E 所代表的最终一致性(Eventually Consistency),弱于最终一致性的产品就几乎没有了。

对于最终一致性,可以这样理解:在主副本执行写操作并反馈成功时,不要求其他副本与主副本保持一致,但在经过一段时间后这些副本最终会追上主副本的进度,重新达到数据状态的一致。

这个『经过一段时间』含义比较模糊,需要从操作视角分析。

操作视角

最终一致性,在语义上包含了很大的不确定性,所以很多时候并不是直接使用,而是加入一些限定条件,也就衍生出了若干种一致性模型。因为它们是在副本不一致的情况下,进行操作层面的封装来对外表现数据的状态,所以都可以纳入操作视角。有以下几个模型代表:

  • 写后读一致性:读取刚写入的数据,数据保存在主副本 1 上,立刻反馈保存成功,其他副本在后台异步更新。由于网络关系每个副本更新速度不同,读取未更新的副本 2,读取不到数据。此处,假定系统可以通过某种策略由写入节点的主副本 1 负责后续的读取操作,这样就实现了写后读一致性。
  • 单调读一致性:写入数据成功,读取数据读取到副本 1 上;片刻后再读取,读取到副本 2 上,数据消失。关于单调读一致性的定义,常见的解释是这样的:一个用户一旦读到某个值,不会读到比这个值更旧的值。实现单调读一致性的方式,可以是将用户与副本建立固定的映射关系,比如使用哈希算法将用户 ID 映射到固定副本上,这样避免了在多个副本中切换,也就不会出现上面的异常了。
  • 前缀一致性:两个数据分别写入节点 N1 和 N2,它们与 N3 同步数据时,由于网络传输问题,N3 节点接收数据的顺序与数据写入的顺序并不一致,保持这种因果关系的一致性,被称为前缀读或前缀一致性(Consistent Prefix)。要实现这种一致性,可以考虑在原有的评论数据上增加一种显式的因果关系,这样系统可以据此控制在其他进程的读取顺序。
  • 线性一致性:对于分布式数据库来说,它无法要求应用系统在每次变更操作时附带声明一下,这次变更是因为读取了哪些数据而导致的。更可靠的方式是将自然语意的因果关系转变为事件发生的先后顺序。线性一致性(Linearizability)就是建立在事件的先后顺序之上的。在线性一致性下,整个系统表现得好像只有一个副本,所有操作被记录在一条时间线上,并且被原子化,这样任意两个事件都可以比较先后顺序。集群中的各个节点不能做到真正的时钟同步,这样节点有各自的时间线,需要一个绝对时间,也就是 全局时钟。对于线性一致性,学术界其实是有争议的。反对者的论据来自爱因斯坦的相对论的一个重要结论,『时间是相对的』。没有绝对时间,也就不存在全序的事件顺序,不同的观察者可能对于哪个事件先发生是无法达成一致的。因此,线性一致性是有局限性的。工程实现上,多数产品采用单点授时(TSO),也就是从一台时间服务器获取时间,同时配有高可靠设计; 而 Spanner 以全球化部署为目标,因为 TSO 有部署范围上的限制,所以 Spanner 的实现方式是通过 GPS 和原子钟实现的全局时钟,也就是 TrueTime,它可以保证在全球范围内任意节点能同时获得的一个绝对时间,误差在 7 毫秒以内。
  • 因果一致性:不依赖绝对时间的方法就是 因果一致性。因果一致性的基础是偏序关系,也就是说,部分事件顺序是可以比较的。至少一个节点内部的事件是可以排序的,依靠节点的本地时钟就行了;节点间如果发生通讯,则参与通讯的两个事件也是可以排序的,接收方的事件一定晚于调用方的事件。多数观点认为,因果一致性弱于线性一致性,但在并发性能上具有优势,也足以处理多数的异常现象,所以因果一致性也在工业界得到了应用。

小结

  • 一致性模型林林总总,数量繁多,但我们总可以从状态和操作这两个视角来观察,进而梳理出其读写操作的不同策略。
  • 从状态视角看,数据一致性只有两种状态,强一致或弱一致,而在实际系统中强一致是非常少见的,最终一致性是弱一致性的特殊形式。
  • 从操作视角看,最终一致性可以被封装成多种一致性模型,甚至是最强的线性一致性。
  • 分布式数据库主要应用了线性一致性或因果一致性。线性一致性必须要有全局时钟,全局时钟可能来自授时服务器或者特殊物理设备(如原子钟),全局时钟的实现方式会影响到集群的部署范围;因果一致性可以通过逻辑时钟实现,不依赖于硬件,不会限制集群的部署范围。

综上所述,提到的一致性模型强度排序如下:

线性一致性 > 顺序一致性 > 因果一致性 > { 写后读一致性,单调一致性,前缀一致性 }

注:CAP 的 C 也是 Consistency,是多副本、单操作的数据一致性;而 ACID 里的 C 是指单副本、多操作的事务一致性。Paxos 这类共识算法,可以看作是复制协议的一种,虽然有时也叫做一致性协议,但这个一致性是指 Consensus。Consensus 是实现数据一致性目标下的具体技术,但并不是唯一的选择。

加餐:Aurora 不是分布式数据库,主要原因就是 Aurora 依然是不支持写入能力的水平扩展。Aurora 是亚马逊推出的云原生数据库,它采用计算与存储分离的思想,计算能力垂直扩展,存储能力水平扩展。究其原因,它的存储系统是直接架设在自家的分布式存储系统(S3)之上的;而计算节点仍然是单节点,所以是垂直扩展。当然 Aurora 也像 MySQL 一样是支持一写多读的,根据亚马逊的官方说明,可以配置 15 个备节点来分流读操作的压力。由于 Aurora 的元数据会缓存在主节点上的,在发生变更时,主备同步数据有一个小的延迟(小于 100 毫秒),这就造成备节点不能承接写入功能,读也不能保证严格的数据一致性。


事务一致性

NoSQL 以 BASE 为理论基础,BASE 其实代表了三个特性:

  • BA(Basically Available):基本可用性,是指某些部分出现故障,系统的其余部分依然可用。
  • S(Soft State):软状态或柔性事务,是指数据处理过程中,存在数据状态暂时不一致的情况,但最终会实现事务的一致性。
  • E(Eventual Consistency):最终一致性,是指单数据项的多副本,经过一段时间,最终达成一致。

BASE 的意义只在于放弃了 ACID 的一些特性,从而更简单地实现了高性能和可用性,达到一个新的平衡。


事务的 ACID 特性

在数据库中,『事务』是由多个操作构成的序列。1970 年詹姆斯 · 格雷(Jim Gray)提出了事务的 ACID 四大特性,将广义上的事务一致性具化到了原子性、一致性、隔离性和持久性这 4 个方面。

虽然 ACID 名义上并列为事务的四大特性,但它们对于数据库的重要程度并不相同。

第一个是一致性,它是其中存在感最低的特性,可以看作是对『事务』整体目标的阐述。它并没有提出任何具体的功能需求,所以在数据库中也很难找到针对性的设计。

第二个是持久性,它不仅是对数据库的基本要求。如果仔细琢磨下持久性的定义,就会发现它的核心思想就是要应对系统故障。可以把系统故障分为两种:

  1. 存储硬件无损、可恢复的故障。这种情况下,主要依托于预写日志(Write Ahead Log, WAL)保证第一时间存储数据。WAL 采用顺序写入的方式,可以保证数据库的低延时响应。WAL 是单体数据库的成熟技术,NoSQL 和分布式数据库都借鉴了过去。
  2. 存储硬件损坏、不可恢复的故障。这种情况下,需要用到日志复制技术,将本地日志及时同步到其他节点。实现方式大体有三种:第一种是单体数据库自带的同步或半同步的方式,其中半同步方式具有一定的容错能力,实践中被更多采用;第二种是将日志存储到共享存储系统上,后者会通过冗余存储保证日志的安全性,亚马逊的 Aurora 采用了这种方式,也被称为 Share Storage;第三种是基于 Paxos / Raft 的共识算法同步日志数据,在分布式数据库中被广泛使用。无论采用哪种方式,目的都是保证在本地节点之外,至少有一份完整的日志可用于数据恢复。

第三个是原子性,是数据库区别于其他存储系统的重要标志。在单体数据库时代,原子性问题已经得到妥善解决,但随着向分布式架构的转型,在引入不可靠的网络因素后,原子性又成为一个新的挑战。

最后一个是隔离性,它是事务中最复杂的特性。隔离性分为多个隔离级别,较低的隔离级别就是在正确性上做妥协,将一些异常现象交给应用系统的开发人员去解决,从而获得更好的性能。

可以说,事务模型的发展过程就是在隔离性和性能之间不断地寻找更优的平衡点。甚至可以说事务的核心就是隔离性。而不同产品在事务一致性上的差别,也完全体现在隔离性的实现等级上,所以必须搞清楚隔离等级具体是指什么。


ANSI SQL-92

最早、最正式的对隔离级别的定义,是 ANSI SQL-92(简称 SQL-92),它定义的隔离级别和异常现象如下所示:

隔离级别

虽然 SQL-92 得到了广泛应用,不少数据库也都遵照这个标准来命名自己的隔离级别,但它对异常现象的分析还是过于简单了。所以在不久之后的 1995 年,Jim Gray 等人发表了论文 "A Critique of ANSI SQL Isolation Levels"(以下简称 Critique),对于事务隔离性进行了更加深入的分析。


Critique

Critique 丰富和细化了 SQL-92 的内容,定义了六种隔离级别和八种异常现象。其中最关注的是快照隔离级别。因为在 SQL-92 中可重复读(Repeatable Read, RR)与可串行化(Serializable)两个隔离级别的主要差别是对幻读(Phantom)的处理。但随着 Critique 的发表,快照隔离被明确提出,这个说法就不适用了,因为快照隔离能解决幻读的问题,但却无法处理写倾斜(Write Skew)问题,也不符合可串行化要求。

因此,今天,使用最广泛的隔离级别有四个,就是已提交读可重复读快照隔离可串行化

Critique 对幻读的描述大致是这样的,事务 T1 使用特定的查询条件获得一个结果集,事务 T2 插入新的数据,并且这些数据符合 T1 刚刚执行的查询条件。T2 提交成功后,T1 再次执行同样的查询,此时得到的结果集会增大。这种异常现象就是幻读。

幻读与不可重复读都是在一个事务内用相同的条件查询两次,但两次的结果不一样。差异在于,对不可重复读来说,第二次的结果集相对第一次,有些记录被修改(Update)或删除(Delete)了;而幻读是第二次结果集里出现了第一次结果集没有的记录 (Insert)。一个更加形象的说法,幻读是在第一次结果集的记录『间隙』中增加了新的记录。所以,MySQL 将防止出现幻读的锁命名为间隙锁(Gap Lock)。

跟幻读相比,写倾斜 要稍微复杂一点,这里用一个黑白球的例子来说明。

首先,箱子里有三个白球和三个黑球,两个事务(T1,T2)并发修改,不知道对方的存在。T1 要让 6 个球都变成白色;T2 则希望 6 个球都变成黑色。

/* T1: */
begin;
update ball set color = white where color = black;
commit;

/* T2: */
begin;
update ball set color = black where color = color;
commit;

并发修改

最终的执行结果是,盒子里仍然有三个黑球和三个白球。如果还没有发现问题,可以看看下面串行执行的效果图,比较一下有什么不同。

串行修改

如果先执行 T1 再执行 T2,6 个球都会变成黑色;调换 T1 与 T2 的顺序,则 6 个球都是白色。

根据可串行化的定义,多事务并行执行所得到的结果,与串行执行(一个接一个)完全相同。比照两张图,很容易发现事务并行执行没有达到串行的同等效果,所以这是一种异常现象。也可以说,写倾斜是一种更不易察觉的更新丢失。


快照隔离 && MVCC

既然『快照隔离』这么重要,为什么会被 SQL-92 漏掉呢?

这是由于 SQL-92 主要考虑了基于锁(Lock-base)的并发控制,而快照隔离的实现基础则是多版本并发控制(MVCC),很可能是由于当时 MVCC 的应用还不普遍。当然,后来,MVCC 成为一项非常重要的技术,一些经典教材会将 MVCC 作为一种独立的选择,与乐观并发控制和悲观并发控制并列。其实,在现代数据库中 MVCC 已经成为一种底层技术,用于更高效地实现乐观或悲观并发控制。有了 MVCC 这个基础,快照隔离就成为一个普遍存在的隔离级别了。


小结

  • 数据一致性关注的是单对象、单操作在多副本上的一致性,事务一致性则是关注多对象、多操作在单副本上的一致性,分布式数据库的一致性是数据一致性与事务一致性的融合。
  • 广义上的事务一致性被细化为 ACID 四个方面,其中原子性的实现依赖于隔离性的并发控制技术和持久性的日志技术。
  • 隔离性是事务的核心。降低隔离级别,其实就是在正确性上做妥协,将一些异常现象交给应用系统的开发人员去解决,从而获得更好的性能。所以,除『可串行化』以外的隔离级别,都有无法处理的异常现象。
  • 研究人员将隔离级别分为六级,你需要重点关注其中四个,分别是已提交读、可重复读、快照隔离、可串行化。前三者是单体数据库或分布式数据库中普遍提供的,可串行化仅在少数产品中提供。

架构风格

总的来说,分布式数据库大多可以分为两种架构风格,一种是 NewSQL,它的代表系统是 Google Spanner;另一种是从单体数据库中间件基础上演进出来的,被称为 Prxoy 风格,没有公认的代表系统。Prxoy 这个名字太笼统,没有反映架构的全貌,要有一个具体的架构模板来指代这种风格,这里选择 PostgreSQL-XC(PGXC)。

数据库的基本架构

要搞清楚分布式数据库的架构风格,就要先了解『数据库』的架构。这里说的数据库仍然默认是关系型数据库。以下是一张数据库的架构图:

数据库架构图

这张图从约瑟夫 · 海勒斯坦 (Joseph M. Hellerstein) 等人的论文 "Architecture of a Database System" 中翻译而来。文中将数据库从逻辑上拆分为 5 个部分:

  • 客户端通讯管理器 (Client Communications Manager):这是应用开发者能够直观感受到的模块,通常使用 JDBC 或者 ODBC 协议访问数据库时,连接的就是这个部分。
  • 进程管理器(Process Manager):连接建好了,数据库会为客户端分配一个进程,客户端后续发送的所有操作都会通过对应的进程来执行。当然,这里的进程只是大致的说法。事实上,Oracle 和 PostgreSQL 是进程的方式,而 MySQL 使用的则是线程。还有,进程与客户也不都是简单的一对一关系。
  • 查询处理器(Relational Query Processor):它包括四个部分,功能上是顺序执行的。首先是解析器,它将接收到的 SQL 解析为内部的语法树。然后是查询重写(Query Rewrite),它也被称为逻辑优化,主要是依据关系代数的等价变换,达到简化和标准化的目的。比如会消除重复条件或去掉一些无意义谓词 ,还有将视图替换为表等操作。再往后就是查询算法优化(Query Optimizer),它也被称为物理优化,主要是根据表连接方式、连接顺序和排序等技术进行优化,我们常说的基于规则优化(RBO)和基于代价优化(CBO)就在这部分。最后就是计划执行器(Plan Executor),最终执行查询计划,访问存储系统。
  • 事务存储管理器(Transactional Storage Manager):它包括四个部分,其中访问方式(Access Methods)是指数据在磁盘的具体存储形式。锁管理(Lock Manager)是指并发控制。日志管理(Log Manager)是确保数据的持久性。缓存管理(Buffer Manager)则是指 I / O 操作相关的缓存控制。
  • 共享组件与工具(Shared Components and Utilities):在整个过程中还会涉及到的一些辅助操作,当然它们对于数据库的运行也是非常重要的。例如编目数据管理器(Catalog Manager)会记录数据库的表、字段、视图等元数据信息,并根据这些信息来操作具体数据内容。复制机制(Replication)也很重要,它是实现系统高可靠性的基础,在单体数据库中,通过主备节点复制的方式来实现数据的复制。

PGXC:单体数据库的自然演进

单体数据库的功能看似已经很完善了,但在面临高并发场景的时候,还是会碰到写入性能不足的问题,很难解决。因此,也就有了向分布式数据库演进的动力。要解决写入性能不足的问题,大家首先想到的,最简单直接的办法就是分库分表。

分库分表方案就是在多个单体数据库之前增加代理节点,本质上是增加了 SQL 路由功能。这样,代理节点首先解析客户端请求,再根据数据的分布情况,将请求转发到对应的单体数据库。

代理节点

代理节点需要实现三个主要功能,它们分别是客户端接入、简单的查询处理器和进程管理中的访问控制。

另外,分库分表方案还有一个重要的功能,那就是分片信息管理,分片信息就是数据分布情况,是区别于编目数据的一种元数据。不过考虑到分片信息也存在多副本的一致性的问题,大多数情况下它会独立出来。

显然,如果把每一次的事务写入都限制在一个单体数据库内,业务场景就会很受局限。因此,跨库事务成为必不可少的功能,但是单体数据库是不感知这个事情的,所以要在代理节点增加分布式事务组件。

同时,简单的分库分表不能满足全局性的查询需求,因为每个数据节点只能看到一部分数据,有些查询运算是无法处理的,比如排序、多表关联等。所以,代理节点要增强查询计算能力,支持跨多个单体数据库的查询。

随着分布式事务和跨节点查询等功能的加入,代理节点已经不再只是简单的路由功能,更多时候会被称为协调节点。

协调节点

协调节点与数据节点,实现了一定程度上的计算与存储分离,这也是所有分布式数据库的一个架构基调。但是,因为 PGXC 的数据节点本身就是完整的单体数据库,所以也具备很强的计算能力。


NewSQL:革命性的新架构

相对于 PGXC,NewSQL 有着完全不同的发展路线。NewSQL 也叫原生分布式数据库,它的每个组件在设计之初都是基于分布式架构的,不像 PGXC 那样带有明显的单体架构痕迹。

NewSQL 的基础是 NoSQL,更具体地说,是类似 BigTable 的分布式键值(K / V)系统。分布式键值系统选择做了一个减法,完全放弃了数据库事务处理能力,然后将重点放在对存储和写入能力的扩展上,这个能力扩展的基础就是分片。引入分片的另一个好处是,系统能够以更小的粒度调度数据,实现各节点上的存储平衡和访问负载平衡。

分布式键值系统由于具备这些鲜明的特点,所以在不少细分场景获得了成功(比如电商网站对于商品信息的存储),但在面对大量的事务处理场景时就无能为力了(比如支付系统)。这种状况直到 Google Spanner 横空出世才被改变,因为 Spanner 基于 BigTable 构建了新的事务能力。

除了上述内容,NewSQL 还有两个重要的革新,分别出现在高可靠机制和存储引擎的设计上。

高可靠机制的变化在于,放弃了粒度更大的主从复制,转而以分片为单位采用 Paxos 或 Raft 等共识算法。这样,NewSQL 就实现了更小粒度的高可靠单元,获得了更高的系统整体可靠性。存储引擎层面,则是使用 LSM-Tree 模型替换 B+ Tree 模型,大幅提升了写入性能。

计算节点

从系统架构上看,NewSQL 的设计思想更加领先,具有里程碑意义,而 PGXC 的架构偏于保守。但 PGXC 的优势则在于稳健,直接采用单机数据库作为数据节点,大幅降低了工程开发的工作量,也减少了引入风险的机会。总的来说,NewSQL 的长处在架构设计,PGXC 的长处则在工程实现。


小结

  • 从架构上,数据库可以被拆分为 5 个部分,分别是客户端通讯管理器、进程管理器、查询处理器、事务存储管理器和共享组件与工具。分布式数据库在此基础上增加四个主要功能,包括分片信息管理、分布式事务管理、跨节点查询和全局时钟。
  • PGXC 架构是从分库分表方案演进而来的。它设置了协调节点,在代理功能的基础上增加了分布式事务管理、跨节点查询功能;原有的单体数据继续作为数据节点;新增了全局时钟和分片信息管理两个功能,这两个功能又有两种实现情况,一是拆分为两个独立角色节点,例如 GoldenDB,二是合并为一个角色节点,例如 TBase。
  • NewSQL 架构是原生分布式数据库,架构中的每个层次的设计都是以分布式为目标。NewSQL 是从分布式键值系统演进而来,主要的工作负载由计算节点和存储节点承担,另外由管理节点承担全局时钟和分片信息管理功能。不过,这三类节点是逻辑功能上划分,在设计实现层面是可分可合的。比如,TiDB 是分为独立节点,CockroachDB 则是对等的 P2P 架构。
  • NewSQL 在架构上更加领先,而 PGXC 最大程度复用了单体数据库的工程实现,更加稳健。

加餐:预写日志(WAL)写成功,但是数据表写失败,要怎么处理?

事实上,对大多数的数据库来说,实时写入数据时,并不是真的将数据写入数据表在磁盘中的对应文件里,因为数据表的组织形式复杂,不像 WAL 那样只是在文件尾部追加,所以 I / O 操作的延迟太长。因此,写入过程往往是这样的:记录 WAL 日志,同时将数据写入内存,两者都成功就返回客户端了。这些内存中的数据,在 Oracle 和 MySQL 中都被称为脏页,达到一定比例时会批量写入磁盘。而 NewSQL 所采用的 LSM-Tree 存储模型也是大致的思路,只不过在磁盘的数据组织上不同。写入内存和 WAL 这两个操作构成了一个事务,必须一起成功或失败。


学习资料

Jim Gray 等:A Critique of ANSI SQL Isolation Levels

Joseph M. Hellerstein et al.:Architecture of a Database System

加西亚 - 莫利纳 等:《数据库系统实现》


更新时间:2022-02-07 13:37:05

本文由 caroly 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载 / 出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://caroly.fun/archives/分布式数据库一
最后更新:2022-02-07 13:37:05

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×