当前位置: 首页 > ops >正文

分布式数据库的历史演变与核心原理

导论:什么是分布式数据库?聊聊它的前世今生

基本概念

分布式数据库集中式数据库
由位于不同站点的多个数据文件组成由单个中央数据库文件组成
允许多个用户访问和操作数据多个用户同时访问同一文件时发生瓶颈问题
从离用户最近的位置快速传送文件文件传递给用户可能需要更长时间
如果其中一个站点发生故障,数据可以恢复单站点意味着系统发生故障时停机
来自分散数据库的多个文件必须同步在单一的中央系统中更简单地更新和管理数据

从表中,我们可以总结出分布式数据库的核心——数据分片、数据同步。

1. 数据分片
该特性是分布式数据库的技术创新。它可以突破中心化数据库单机的容量限制,从而将数据分散到多节点,以更灵活、高效的方式来处理数据。

分片方式包括两种。

  • 水平分片:按行进行数据分割,数据被切割为一个个数据组,分散到不同节点上。
  • 垂直分片:按列进行数据切割,一个数据表的模式(Schema)被切割为多个小的模式。

2. 数据同步

它是分布式数据库的底线。由于数据库理论传统上是建立在单机数据库基础上,而引入分布式理论后,一致性原则被打破。因此需要引入数据库同步技术来帮助数据库恢复一致性。

简而言之,就是使分布式数据库用起来像“正常的数据库”。所以数据同步背后的推动力,就是人们对数据“一致性”的追求。这两个概念相辅相成,互相作用。

当然分布式数据库还有其他特点,但把握住以上两点,已经足够我们理解它了。下面我将从这两个特性出发,探求技术史上分布式数据库的发展脉络。我会以互联网、云计算等较新的时间节点来进行断代划分,毕竟我们的核心还是着眼现在、面向未来。

SQLvS NoSQL:一次搞清楚五花八门的“SQL”

区别维度SQL数据库NoSQL数据库
数据模型关系型,有固定行和列的表格文档型、键值对型、JSON文档型、列不固定的列式存储、图类型
发展历史开发于20世纪70年代产生于2000年左右
典型代表Oracle、MySQL、Microsoft SQL Server、PostgreSQL文档型:MongoDB and CouchDB
键值对:Redis and DynamoDB
列式:Cassandra and HBase
图数据库:Neo4j
Schemas数据表格灵活结构
扩展性垂直扩展,需要升级机器硬件配置水平扩展
事务支持ACID一般不支持ACID,Base
数据连接支持不支持
应用访问模式数据中间层 ORM直接映射为应用语言数据结构

数据分片:如何存储超大规模的数据?

数据分片概论

分片是将大数据表分解为较小的表(称为分片)的过程,这些分片分布在多个数据库集群节点上。分片本质上可以被看作传统数据库中的分区表,是一种水平扩展手段。每个分片上包含原有总数据集的一个子集,从而可以将总负载分散在各个分区之上。

数据分片的方式一般有两种。

  1. 水平分片:在不同的数据库节点中存储同一表的不同行。
  2. 垂直分片:在不同的数据库节点中存储表不同的表列。

如下图所示,水平和垂直这两个概念来自原关系型数据库表模式的可视化直观视图。
在这里插入图片描述
分片可以增加数据库集群的总容量并加快处理速度,同时可以使用比垂直扩展更低的成本提供更高的可用性。

分片算法

分片算法一般指代水平分片所需要的算法。经过多年的演化,其已经在大型系统中得到了广泛的实践。

哈希分片

哈希分片,首先需要获取分片键,然后根据特定的哈希算法计算它的哈希值,最后使用哈希值确定数据应被放置在哪个分片中。数据库一般对所有数据使用统一的哈希算法(例如 ketama),以促成哈希函数在服务器之间均匀地分配数据,从而降低了数据不均衡所带来的热点风险。通过这种方法,数据不太可能放在同一分片上,从而使数据被随机分散开。

这种算法非常适合随机读写的场景,能够很好地分散系统负载,但弊端是不利于范围扫描查询操作。
在这里插入图片描述

范围分片

范围分片根据数据值或键空间的范围对数据进行划分,相邻的分片键更有可能落入相同的分片上。每行数据不像哈希分片那样需要进行转换,实际上它们只是简单地被分类到不同的分片上。
在这里插入图片描述
范围分片需要选择合适的分片键,这些分片键需要尽量不包含重复数值,也就是其候选数值尽可能地离散。同时数据不要单调递增或递减,否则,数据不能很好地在集群中离散,从而造成热点。

范围分片非常适合进行范围查找,但是其随机读写性能偏弱。

融合算法

以上介绍的哈希和范围的分片算法并不是非此即彼,二选一的。相反,我们可以灵活地组合它们。

例如,我们可以建立一个多级分片策略,该策略在最上层使用哈希算法,而在每个基于哈希的分片单元中,数据将按顺序存储。

这个算法相对比较简单且灵活,下面我们再说一个地理位置算法。

地理位置算法

该算法一般用于 NewSQL 数据库,提供全球范围内分布数据的能力。

在基于地理位置的分片算法中,数据被映射到特定的分片,而这些分片又被映射到特定区域以及这些区域中的节点。

然后在给定区域内,使用哈希或范围分片对数据进行分片。例如,在美国、中国和日本的 3 个区域中运行的集群可以依靠 User 表的 Country_Code 列,将特定用户(User)所在的数据行映射到符合位置就近规则的区域中。

那么以上就是几种典型的分片算法,下面我们接着讨论如何将分片算法应用到实际的场景中。

手动分片 vs 自动分片

手动分片,顾名思义,就是设置静态规则来将数据根据分片算法分散到数据库节点。这一般是由于用户使用的数据库不支持自动的分片,如 MySQL、Oracle 等。这个问题可以在应用层面上做数据分片来解决,也可以使用简单的数据库中间件或 Proxy 来设置静态的分片规则来解决。

手动分片的缺点是数据分布不均匀。数据分布不均可能导致数据库负载极其不平衡,从而使其中一些节点过载,而另一些节点访问量较少。

因此,最好避免在部分节点上存储过多数据,否则会造成这些节点成为访问热点,进而导致其运行速度降低,甚至使服务器崩溃。此外,当整体数据集过小时,也会导致这个问题,因为集群中只有部分节点才有数据。

这在开发和测试环境中是可以接受的,但在生产环境中是不可以接受的。因为数据分布不均,热点以及将数据存储在太少的分片上,都会导致数据库集群内的节点计算资源耗尽,造成系统不稳定。

但如果精心设计,且数据分布变化不大,采用手动分片也是一个较为简单、维护成本低廉的方案。

而使用自动分片意味着计算节点与分片算法可以相互配合,从而使数据库进行弹性伸缩。

使用基于范围的分片很容易实现自动分片:只需拆分或合并每个分片。

假设现在有一个范围为 [1,100)的分片,我们想要将它分裂为两个范围,先选择 50 作为切分点;然后将该区域分为 [1,50)和 [50,100)之后,将两个区域移动到两台不同的数据库节点中,从而使系统负载达到平衡。

基于范围的分片可能会带来读取和写入热点,我们可以通过拆分和移动分片消除这些热点。

而使用基于哈希的分片的系统实现自动分片代价很高昂。

当前系统有 4 个节点,然后添加一个新的数据库节点。在哈希函数中,“ n”从 4 更改为 5,这会导致较大的系统抖动。尽管你可以使用像 Ketama 这样的一致性哈希算法来尽可能减少系统抖动,但数据迁移与再平衡操作还是必须要有的。

这是因为在应用哈希函数后,数据是随机分布的,并且调整散列算法肯定会更改大多数数据的分布情况。

自动分片是分布式数据库的主流功能,所有主要的分布式数据库,甚至数据库中间件都在尝试自动分片。下面我将结合几个案例来说明。

分片算法案例

数据分片是数据库中间件的核心功能,且该领域开源项目较多。我这里以 Apache ShardingShpere 的分片内容为例,向你介绍分片算法的相关实践案例。

分片键生成

ShardingShpere 首先提供了分布式的主键生成,这是生成分片键的关键。由于分布式数据库内一般由多个数据库节点参与,因此基于数据库实例的主键生成并不适合分布式场景。

常用的算法有 UUID 和 Snowfalke 两种无状态生成算法。

UUID 是最简单的方式,但是生成效率不高,且数据离散度一般。因此目前生产环境中会采用后一种算法。下图就是用该算法生成的分片键的结构。
在这里插入图片描述
其中有效部分有三个。

  1. 时间戳:算法类似 UNIX 时间的表示形式,它是从一个特定时间开始到当前时间点之间的毫秒数,本案例中该算法可以使用近 70 年。
  2. 工作节点 ID:保证每个独立工作的数据库节点不会产生重复的数据。
  3. 访问序列:在同一个进程、同一个毫秒内,保证产生的 ID 不重复。
灵活的分片算法

为了保证分片计算的灵活性,ShardingShpere 提供了标准分片算法和一些工具,帮助用户实现个性化算法。

  1. PreciseSharingAlgorithm配合哈希函数使用,可以实现哈希分片。RangeSharingAlgorithm可以实现范围分片。
  2. 使用ComplexShardingStrategy可以使用多个分片键来实现融合分片算法。
  3. 有的时候,数据表的分片模式不是完全一致。对于一些特别的分片模式,可以使用HintShardingStrategy在运行态指定特殊的路由规则,而不必使用统一的分片配置。
  4. 如果用户希望实现诸如地理位置算法等特殊的分片算法,可以自定义分片策略。使用 inline 表达式或 Java 代码进行编写,前者基于配置不需要编译,适合简单的个性化分片计算;后者可以实现更加复杂的计算,但需要编译打包的过程。

用户通过以上多种分片工具,可以灵活和统一地制定数据库分片策略。

自动分片

ShardingShpere 提供了 Sharding-Scale 来支持数据库节点弹性伸缩,该功能就是其对自动分片的支持。下图是自动分片功能展示图,可以看到经过 Sharding-Scale 的特性伸缩,原有的两个数据库扩充为三个。
在这里插入图片描述
自动分片包含下图所示的四个过程。
在这里插入图片描述
通过该工作量,ShardingShpere可以支持复杂的基于哈希的自动分片。同时我们也应该看到,没有专业的自动化的弹性扩缩容工具,想要实现自动化分片是非常困难的。

以上就是分片算法的实际案例,使用的是经典的水平分片模式。而目前水平和垂直分片有进一步合并的趋势,下面要介绍的 TiDB 正代表着这种融合趋势。

垂直与水平分片融合案例

TiDB 就是一个垂直与水平分片融合的典型案例,同时该方案也是 HATP 融合方案。
其中水平扩展依赖于底层的 TiKV,如下图所示。
在这里插入图片描述
TiKV 使用范围分片的模式,数据被分配到 Region 组里面。一个分组保持三个副本,这保证了高可用性。当 Region 变大后,会被拆分,新分裂的 Region 也会产生多个副本。

TiDB 的水平扩展依赖于 TiFlash,如下图所示。
在这里插入图片描述
可以看到 TiFlash 是 TiKV 的列扩展插件,数据异步从 TiKV 里面复制到 TiFlash,而后进行列转换,其中要使用 MVCC 技术来保证数据的一致性。

上文所述的 Region 会增加一个新的异步副本,而后该副本进行了数据切分,并以列模式组合到 TiFlash 中,从而达到了水平和垂直扩展在同一个数据库的融合。这是两种数据库引擎的融合。

以上的融合为 TiDB 带来的益处主要体现在查询层面,特别对特定列做聚合查询的效率很高。TiDB 可以很智能地切换以上两种分片引擎,从而达到最优的查询效率。

数据复制:如何保证数据在分布式场景下的高可用?

分片技术:主要目的是提高数据容量和性能。

复制的主要目的是在几个不同的数据库节点上保留相同数据的副本,从而提供一种数据冗余。这份冗余的数据可以提高数据查询性能,而更重要的是保证数据库的可用性。

本讲主要介绍两种复制模式:单主复制与多主复制,并通过 MySQL 复制技术的演化来进行相应的展示。

单主复制

单主复制,也称主从复制。写入主节点的数据都需要复制到从节点,即存储数据库副本的节点。当客户要写入数据库时,他们必须将请求发送给主节点,而后主节点将这些数据转换为复制日志或修改数据流发送给其所有从节点。从使用者的角度来看,从节点都是只读的。
在这里插入图片描述
这种模式是最早发展起来的复制模式,不仅被广泛应用在传统数据库中,如 PostgreSQL、MySQL、Oracle、SQL Server;它也被广泛应用在一些分布式数据库中,如 MongoDB、RethinkDB 和 Redis 等。

那么接下来,我们就从复制同步模式、复制延迟、复制与高可用性以及复制方式几个方面来具体说说这个概念。

复制同步模式

复制是一个非常耗费时间而且很难预测完成情况的操作。可以从以下三点来分析。

  1. 同步复制:如果由于从库已崩溃,存在网络故障或其他原因而没有响应,则主库也无法写入该数据。
  2. 半同步复制:其中部分从库进行同步复制,而其他从库进行异步复制。也就是,如果其中一个从库同步确认,主库可以写入该数据。
  3. 异步复制:不管从库的复制情况如何,主库可以写入该数据。而此时,如果主库失效,那么还未同步到从库的数据就会丢失。

可以看到不同的同步模式是在性能和一致性上做平衡,三种模式对应不同场景,并没有好坏差异。用户需要根据自己的业务场景来设置不同的同步模式。

复制延迟

如果我们想提高数据库的查询能力,最简便的方式是向数据库集群内添加足够多的从节点。这些从节点都是只读节点,故查询请求可以很好地在这些节点分散开。

但是如果使用同步复制,每次写入都需要同步所有从节点,会造成一部分从节点已经有数据,但是主节点还没写入数据。而异步复制的问题是从节点的数据可能不是最新的。

以上这些问题被称为“复制延迟”,在一些的材料中,我们会听到诸如“写后读”“读单增”等名词来解决复制延迟。但是这些概念其实是数据一致性模型的范畴。我们知乎会深入介绍它们。

复制与高可用性

高可用(High availablity)指系统无中断地执行其功能的能力。系统中的任何节点都可能由于各种出其不意的故障而造成计划外停机;同时为了要维护系统,我们也需要一些计划内的停机。采用主从模式的数据库,可以防止单一节点挂起导致的可用性降低的问题。

系统可用程度一般使用小数点后面多个 9 的形式,如下表所示。

可用性年故障时间
99.9999%32秒
99.999%5分15秒
99.99%52分34秒
99.9%8小时46分
99%3天15小时36分

一般的生产系统都会至少有两个 9 的保证,追求三个 9。想要做到 4 个 9 是非常最具有挑战的。

在主从模式下,为了支撑高可用,就需要进行故障处理。这里总结了两种可能的故障及其处理方案。

  1. 从节点故障。由于每个节点都复制了从主库那里收到的数据更改日志,因此它知道在发生故障之前已处理的最后一个事务,由此可以凭借此信息从主节点或其他从节点那里恢复自己的数据。
  2. 主节点故障。在这种情况下,需要在从节点中选择一个成为新的主节点,此过程称为故障转移,可以手动或自动触发。其典型过程为:第一步根据超时时间确定主节点离线;第二步选择新的主节点,这里注意新的主节点通常应该与旧的主节点数据最为接近;第三步是重置系统,让它成为新的主节点。
复制方式

为了灵活并高效地复制数据,下面我介绍几种常用的复制方式。

1. 基于语句的复制
主库记录它所执行的每个写请求(一般以 SQL 语句形式保存),每个从库解析并执行该语句,就像从客户端收到该语句一样。但这种复制会有一些潜在问题,如语句使用了获取当前时间的函数,复制后会在不同数据节点上产生不同的值。

另外如自增列、触发器、存储过程和函数都可能在复制后产生意想不到的问题。但可以通过预处理规避这些问题。使用该复制方式的分布式数据库有 VoltDB、Calvin。

2. 日志(WAL)同步
WAL 是一组字节序列,其中包含对数据库的所有写操作。它的内容是一组低级操作,如向磁盘的某个页面的某个数据块写入一段二进制数据,主库通过网络将这样的数据发送给从库。

这种方法避免了上面提到的语句中部分操作复制后产生的一些副作用,但要求主从的数据库引擎完全一致,最好版本也要一致。如果要升级从库版本,那么就需要计划外停机。PostgreSQL 和 Oracle 中使用了此方法。

3. 行复制
它由一系列记录组成,这些记录描述了以行的粒度对数据库表进行的写操作。它与特定存储引擎解耦,并且第三方应用可以很容易解析其数据格式。

4. ETL工具
该功能一般是最灵活的方式。用户可以根据自己的业务来设计复制的范围和机制,同时在复制过程中还可以进行如过滤、转换和压缩等操作。但性能一般较低,故适合处理子数据集的场景。

关于单主复制就介绍到这里,下面我们再来说说多主复制。

多主复制

也称为主主复制。数据库集群内存在多个对等的主节点,它们可以同时接受写入。每个主节点同时充当主节点的从节点。

多主节点的架构模式最早来源于 DistributedSQL 这一类多数据中心,跨地域的分布式数据库。在这样的物理空间相距甚远,有多个数据中心参与的集群中,每个数据中心内都有一个主节点。而在每个数据中心的内部,却是采用常规的单主复制模式。

这么设计该类系统的目的在于以下几点。

  1. 获得更好的写入性能:使数据可以就近写入。
  2. 数据中心级别的高可用:每个数据中心可以独立于其他数据中心继续运行。
  3. 更好的数据访问性能:用户可以访问到距离他最近的数据中心。

但是,此方法的最大缺点是,存在一种可能性,即两个不同的主节点同时修改相同的数据。这其实是非常危险的操作,应尽可能避免。这就需要下一讲要介绍的一致性模型,配合冲突解决机制来规避。

还有一种情况是处理客户端离线操作的一致性问题。为了提高性能,数据库客户端往往会缓存一定的写入操作,而后批量发送给服务端。这种情况非常类似于大家使用协作办公文档工具的场景。在这种情况下,每个客户端都可以被看作是具有主节点属性的本地数据库,并且多个客户端之间存在一种异步的多主节点复制的过程。这就需要数据库可以协调写操作,并处理可能的数据冲突。

典型的多主复制产品有 MySQL 的 Tungsten Replicator、PostgreSQL 的 BDR 和 Oracle 的 GoldenGate。

目前,大部分NewSQL、DistributedSQL的分布式数据库都支持多主复制,但是大部分是用Paxos或Raft等协议来构建复制组,保证写入线性一致或顺序一致性;同时传统数据库如MySQL 的 MGR 方案也是使用类似的方式,可以看到该方案是多主复制的发展方向

历史的发展潮流是从单主复制向多主复制演变的,以上我们抽象地总结了复制的发展模式和需要关注的技术点。下面我将通过 MySQL 高可用技术的发展路径,向你直观地展示数据库复制技术的发展脉络。

MySQL复制技术的发展

MySQL 由于其单机机能的限制,很早就发展了数据复制技术以提高性能。同时依赖该技术,MySQL 可用性也得到了长足的发展。

截止到现在,该技术经历了四代的发展。第一代为传统复制,使用 MHA(Master High Available)架构;第二代是基于 GTID 的复制,即 GTID+Binlog server 的模式;第三代为增强半同步复制,GTID+增强半同步复制;第四代为 MySQL 原生高可用,即 MySQL InnoDB Cluster。

数据库的复制技术需要考虑两大因素:数据一致性RPO和业务连续性RTO。所以,就像前面的内容所强调的,复制与一致性是一对如影随形的概念,本讲内容聚焦于复制,但是会提到关于一致性相关的概念。

MHA复制控制

下面我就从第一代复制技术开始说起。
在这里插入图片描述
MHA作为第一代复制架构,有如下适用场景:

  1. MySQL 的版本≤5.5,这一点说明它很古老;
  2. 只用于异步复制且一主多从环境;
  3. 基于传统复制的高可用。

MHA 尽最大能力做数据补偿,但并不保证一定可以成功;它也尽最大努力在实现 RPO,有 RTO 概念支持。可以看到它只是一个辅助工具,本身的架构与机制对 RPO 和 RTO 没有任何保障。

那么由此可知,它会存在如下几个问题:

  1. 它的 GTID 模型强依赖 binlog server,但是对于 5.7 后的 binlog 却不能识别,同时对并行复制支持不好;
  2. 服务 IP 切换依赖自行编写的脚本,也可以与 DNS 结合,其运维效果取决于运维人员的经验;
  3. 运维上需要做 SSH 信任、切换判断等人工操作,总体上处于“刀耕火种”的状态,自动化程度较低,维护难度高;
  4. 现在项目基本无维护。

从上述问题中可以看到,MHA 作为第一代复制架构,功能相对原始,但已经为复制技术的发展开辟了道路,特别是对 GTID 和 binlog 的应用。但如果不是维护比较古老的 MySQL 集群,目前已经不推荐采用它了。

半同步复制

这是第二代复制技术,它与第一代技术的差别表现在以下几点。

  1. binlog使用半同步,而第一代是异步同步。它保障了数据安全,一般至少要同步两个节点,保证数据的数据一致性RPO。
  2. 同时保留异步复制,保障了复制性能。并通过监控复制的延迟,保证了业务连续性RTO。
  3. 引入配置中心,如 consul。对外提供健康的 MySQL 服务。
  4. 这一代开始需要支持跨 IDC 复制。需要引入监控 Monitor,配合 consul 注册中心。多个 IDC 中 Monitor 组成分布式监控,把健康的 MySQL 注册到 consul 中,同时将从库复制延迟情况也同步到 consul 中。

下图就是带有 consul 注册中心与监控模块的半同步复制架构图。
在这里插入图片描述
第二代复制技术也有自身的一些缺陷。

  1. 存在幻读的情况。当事务同步到从库但没有ACK时,主库发生宕机;此时主库没有该事务,而从库有。
  2. MySQL 5.6 本身半同步 ACK 确认在 dump_thread 中,dump_thread 存在 IO 瓶颈问题。

基于此,第三代复制技术诞生。

增强半同步复制

这一代需要 MySQL 是 5.7 以后的版本。有一些典型的框架来支持该技术,如 MySQL Replication Manager、GitHub-orchestrator 和国内青云开源的 Xenon 等。

这一代复制技术采用的是增强半同步。首先主从的复制都是用独立的线程来运行;其次主库采用 binlog group commit,也就是组提交来提供数据库的写入性能;而从库采用并行复制,它是基于事务的,通过数据参数调整线程数量来提高性能。这样主库可以并行,从库也可以并行。

这一代技术体系强依赖于增强半同步,利用半同步保证 RPO,对于 RTO,则取决于复制延迟。

下面我们用 Xenon 来举例说明,请看下图。
在这里插入图片描述
从图中可以看到。每个节点上都有一个独立的 agent,这些 agent 利用 raft 构建一致性集群,利用 GTID 做索引选举主节点;而后主节点对外提供写服务,从节点提供读服务。

当主节点发生故障后,agent 会通过 ping 发现该故障。由于 GTID 和增强半同步的加持,从节点与主节点数据是一致的,因此很容易将从节点提升为主节点。

第三代技术也有自身的缺点,如增强半同步中存在幽灵事务。这是由于数据写入 binlog 后,主库掉电。由于故障恢复流程需要从 binlog 中恢复,那么这份数据就在主库。但是如果它没有被同步到从库,就会造成从库不能切换为主库,只能去尝试恢复原崩溃的主库。

MySQL组复制

组复制是 MySQL 提供的新一代高可用技术的重要组成。其搭配 MySQL Router 或 Proxy,可以实现原生的高可用。

从这一代开始,MySQL 支持多主复制,同时保留单主复制的功能。其单主高可用的原理与第三代技术类似,这里我们不做过多分析了。

它的多主模式,原理是使用 MySQL Router 作为数据路由层,来控制读写分离。而后组内部使用 Paxos 算法构建一致性写入。

它与第三代复制技术中使用的一致性算法的作用不同。三代中我们只使用该算法来进行选主操作,数据的写入并不包含在其中;而组复制的多主技术需要 Paxos 算法深度参与,并去决定每一次数据的写入,解决写入冲突。

组复制有如下几个优点。

  • 高可用分片:数据库节点动态添加和移除。分片实现写扩展,每个分片是一个复制组。可以结合对于 TiDB 的介绍,原理类似。
  • 自动化故障与容错:如果一个节点无法响应,组内大多数成员认为该节点已不正常,则自动隔离。
  • 方案完整:前面介绍的方案都需要 MySQL 去搭配一系列第三方解决方案;而组复制是原生的完整方案,不需要第三方组件接入。

当然,组复制同样也有一些限制。主要集中在需要使用较新的特性,一些功能在多组复制中不支持,还有运维人员经验缺乏等。

相信随着 MySQL 的发展,将会有越来越多的系统迁移到组复制中,多主模式也会逐步去替代单主模式。

一致性与CAP模型:为什么需要分布式一致性?

一致性是高可用的必备条件

高可用必须要尽可能满足业务连续性和数据一致性这两个指标。

CAP 理论与注意事项

可用性是用于衡量系统能成功处理每个请求并做出响应的能力。
我们希望每个操作都保持一致性。
我们希望在容忍网络的分区的同时实现一致性和可用性。

可用性要求任何无故障的节点都可以提供服务,而一致性要求结果需要线性一致。

CAP 描述了一种组合性选择,也就是要有取舍。从 CAP 理论的定义,我们可以拥有以下几种系统。

CP 系统:一致且容忍分区的系统。更倾向于减少服务时间,而不是将不一致的数据提供出去。一些面向交易场景构建的 NewSQL 数据库倾向于这种策略,如 TiDB、阿里云 PolarDB、AWS Aurora 等。但是它们会生成自己的 A,也就是可用性很高。
AP 系统:可用且具有分区容忍性的系统。它放宽了一致性要求,并允许在请求期间提供可能不一致的值。一般是列式存储,NoSQL 数据库会倾向于 AP,如 Apache Cassandra。但是它们会通过不同级别的一致性模式调整来提供高一致性方案。

CP系统的场景实现思路是需要引入共识算法,需要大多数节点参与进来,才能保证一致性。如果要始终保持一致,那么在网络分区的情况下,部分节点可能不可用。

而 AP 系统只要一个副本就能启动,数据库会始终接受写入和读取服务。它可能最终会丢失数据或产生不一致的结果。这里可以使用客户端模式Session 模型,来提供一致性的解决方案。

使用 CAP 理论时需要注意一些限制条件

其实 CA 类系统是不存在的,这里你需要特别注意。

一致性模型

一致性模型是分布式系统的经典内容,也是入门分布式数据库的重要知识点。但很少有人知道,其实一致性模型来源于单机理论中的共享内存。

从用户的角度看,分布式数据库就像具有共享存储的单机数据库一样,节点间的通信和消息传递被隐藏到了数据库内部,这会使用户产生“分布式数据库是一种共享内存”的错觉。一个支持读取和写入操作的单个存储单元通常称为寄存器,我们可以把代表分布式数据库的共享存储看作是一组这样的寄存器。

每个读写寄存器的操作被抽象为“调用”和“完成”两个动作。如果“调用”发生后,但在“完成”之前该操作崩溃了,我们将操作定义为失败。如果一个操作的调用和完成事件都在另一个操作被调用之前发生,我们说这个操作在另一个操作之前,并且这两个操作是顺序的;否则,我们说它们是并发的。

如下图所示,a)是顺序操作,b)和 c)是并发操作。
在这里插入图片描述
多个读取或写入操作可以同时访问一个寄存器。对寄存器的读写操作不是瞬间完成的,需要一些时间,即调用和完成两个动作之间的时间。由不同进程执行的并发读/写操作不是串行的,根据寄存器在操作重叠时的行为,它们的顺序可能不同,并且可能产生不同的结果。

当我们讨论数据库一致性时,可以从两个维度来区别。

  1. 滞后性。它是数据改变的时刻与其副本接收到数据的时刻。这是上一讲所介绍的复制延迟场景,一般被归类为“客户端一致性”范畴。
  2. 顺序性。讨论的是各种操作在系统所有副本上执行的顺序状态。这是本讲一致性模型所讨论的重点。

下面我按照顺序性的保障由强到弱来介绍一致性模型。

严格一致性

严格的一致性类似于不存在复制过程:任何节点的任何写入都可立即用于所有节点的后续读取。它涉及全局时钟的概念,如果任何节点在时刻 T1 处写入新数据 A,则所有节点在 T2 时刻(T2 满足 T2>T1),都应该读到新写入的 A。

线性一致性

线性一致性是最严格的且可实现的单对象单操作一致性模型。在这种模型下,写入的值在调用和完成之间的某个时间点可以被其他节点读取出来。且所有节点读到数据都是原子的,即不会读到数据转换的过程和中间未完成的状态。

线性一致需要满足的是,新写入的数据一旦被读取出来,那么所有后续的读操作应该能读取到这个数据。也就是说,一旦一个读取操作读到了一个值,那么后续所有读取操作都会读到这个数值或至少是“最近”的一个值。

上面的定义来自早期的论文,我将里面的关键点提炼一下,如下所示。

  1. 需要有全局时钟,来实现所谓的“最近”。因为没有全局一致的时间,两个独立进程没有相同的“最近”概念。
  2. 任何一次读取都能读到这个“最近”的值。

现在有三个节点,其中一个共享变量 x 执行写操作,而第三个节点会读取到如下数值。

  1. 第一个读操作可以返回 1、2 或空(初始值,两个写操作之前的状态),因为两个写操作仍在进行中;第一次读取可以在两次写入之前,第一次写入与第二次写入之间,以及两次写入之后。
  2. 由于第一次写操作已完成,但第二次写操作尚未完成,因此第二次读操作只能返回 1 和 2。
  3. 第三次读只能返回 2,因为第二次写是在第一次写之后进行的。
    在这里插入图片描述
    线性一致性的代价是很高昂的,甚至 CPU 都不会使用线性一致性。有并发编程经验的朋友一定知道 CAS 操作,该操作可以实现操作的线性化,是高性能并发编程的关键,它就是通过编程手段来模拟线性一致。

一个比较常见的误区是,使用一致性算法可以实现线性一致,如 Paxos 和 Raft 等。但实际是不行的,以Raft为例,算法只能保证了复制Log的线性一致,而没有描述Log是如何写入最终的状态机的,这就暗含状态机本身不是线性一致的。

顺序一致性

由于线性一致的代价高昂,因此人们想到,既然全局时钟导致严格一致性很难实现,那么顺序一致性就是放弃了全局时钟的约束,改为分布式逻辑时钟实现。
顺序一致性时指所有的进程以相同的顺序看到所有的修改。读操作未必能及时得到此前其他进程对同一数据的写更新,但是每个进程读到的该数据的不同值的顺序是一致的。

下图展示了 P1、P2 写入两个值后,P3 和 P4 是如何读取的。以真实的时间衡量,1 应该是在 2 之前被写入,但是在顺序一致性下,1 是可以被排在 2 之后的。同时,尽管 P3 已经读取值 1,P4 仍然可以读取 2。但是需要注意的是这两种组合:1->2 和 2 ->1,P3 和 P4 从它们中选择一个,并保持一致。下图正是展示了它们读取顺序的一种可能:2->1。

在这里插入图片描述
我们使用下图来进一步区分线性一致和顺序一致。
在这里插入图片描述
其中,图 a 满足了顺序一致性,但是不满足线性一致性。原因在于,从全局时钟的观点来看,P2 进程对变量 x 的读操作在 P1 进程对变量 x 的写操作之后,然而读出来的却是旧的数据。但是这个图却是满足顺序一致性,因为两个进程 P1 和 P2 的一致性并没有冲突。

图 b 满足线性一致性,因为每个读操作都读到了该变量的最新写的结果,同时两个进程看到的操作顺序与全局时钟的顺序一样。

图 c 不满足顺序一致性,因为从进程 P1 的角度看,它对变量 y 的读操作返回了结果 0。那么就是说,P1 进程的对变量 y 的读操作在 P2 进程对变量 y 的写操作之前,x 变量也如此。因此这个顺序不满足顺序一致性。

在实践中,你就可以使用上文提到的一致性算法来实现顺序一致。这些算法可以保证操作在每个节点都是按照一样的顺序被执行的,所以它们能保证顺序一致。

如 Google Megastore 这类系统都是使用 Paxos 算法实现了顺序一致性。也就是说在 Megastore 内部,如果有一个数据更新,所有节点都会同步更新,且操作在各个节点上执行顺序是一致的。

因果一致性

相比于顺序一致性,因果一致性的要求会低一些:它仅要求有因果关系的操作顺序是一致的,没有因果关系的操作顺序是随机的。

因果相关的要求有如下几点。

  1. 本地顺序:本进程中,事件执行的顺序即为本地因果顺序。
  2. 异地顺序:如果读操作返回的是写操作的值,那么该写操作在顺序上一定在读操作之前。
  3. 闭包传递:和时钟向量里面定义的一样,如果 a->b、b->c,那么肯定也有 a->c。

那么,为什么需要因果关系,以及没有因果关系的写法如何传播?下图中,进程 P1 和 P2 进行的写操作没有因果关系,也就是最终一致性。这些操作的结果可能会在不同时间,以乱序方式传播到读取端。进程 P3 在看到 2 之前将看到值 1,而 P4 将先看到 2,然后看到 1。
在这里插入图片描述
而下图显示进程 P1 和 P2 进行因果相关的写操作并按其逻辑顺序传播到 P3 和 P4。因果写入除了写入数据外,还需要附加一个逻辑时钟,用这个时钟保证两个写入是有因果关系的。这可以防止我们遇到上面那张图所示的情况。你可以在两个图中比较一下 P3 和 P4 的历史记录。
在这里插入图片描述
而实现这个逻辑时钟的一种主要方式就是向量时钟。向量时钟算法利用了向量这种数据结构,将全局各个进程的逻辑时间戳广播给所有进程,每个进程发送事件时都会将当前进程已知的所有进程时间写入到一个向量中,而后进行传播。

因果一致性典型案例就是 COPS 系统,它是基于 causal+一致性模型的 KV 数据库。它定义了 dependencies,操作了实现因果一致性。这对业务实现分布式数据因果关系很有帮助。另外在亚马逊 Dynamo 基于向量时钟,也实现了因果一致性。

事务隔离级别与一致性模型

现在我们谈论了一致性模型,但是它与数据库领域之中的事务有什么区别呢?我先说结论:有关系但又没有关系。

怎么理解呢?我先来论证它们之间的无关性。

ACID 和 CAP 中的“C”是都是一致性,但是它们的内涵完全不同。其中 ADI 都是数据库提供的能力保障,但是 C(一致性)却不是,它是业务层面的一种逻辑约束。

以转账这个最为经典的例子而言,甲有 100 元 RMB,乙有 0 元 RMB,现在甲要转给乙 30 元。那么转账前后,甲有 70,乙有 30,合起来还是 100。显然,这只是业务层规定的逻辑约束而已。

而对于 CAP 这里的 C 上文已经有了明确说明,即线性一致性。它表示副本读取数据的即时性,也就是对“何时”能读到“正确”的数据的保证。越是即时,说明系统整体上读取数据是一致的。

那么它们之间的联系如何呢?其实就是事务的隔离性与一致模型有关联。

如果把上面线性一致的例子看作多个并行事务,你会发现它们是没有隔离性的。因为在开始和完成之间任意一点都会读取到这份数据,原因是一致性模型关心的是单一操作,而事务是由一组操作组成的。

现在我们看另外一个例子,这是展示事务缺乏一致性后所导致的问题。
在这里插入图片描述
其中三个事务满足隔离性。可以看到 T2 读取到了 T1 入的值。但是这个系统缺乏一致性保障,造成 T3 可以读取到早于 T2 读取值之前的值,这就会造成应用的潜在 Bug。

那现在给出结论:事务隔离是描述并行事务之间的行为,而一致性是描述非并行事务之间的行为。其实广义的事务隔离应该是经典隔离理论与一致性模型的一种混合。

比如,我们会在一些文献中看到如“one-copy serializability”“strong snapshot isolation”,等等。前者其实是 serializability 隔离级别加上顺序一致,后者是 snapshot 隔离级别加上线性一致。

所以对分布式数据库来说,原始的隔离级别并没有舍弃,而是引入了一致性模型后,扩宽数据库隔离级别的内涵。

实践:设计一个最简单的分布式数据库

分布式数据库核心总结

分片与同步两种特性是分布式数据库的重要特性。

分片是分布式数据库提高数据容量的关键特性。我们学习了主要的分片算法,包括范围分片与哈希分片;也介绍了一些优化方法;最后用 Apache ShardingShpere 的例子来直观介绍了分片算法的应用,包含了分布式唯一 ID 的生成算法等相关内容。

同步其实是复制+一致性两个概念的综合。

以一致性算法为核心的强一致性复制技术是未来的发展方式。

CAP 理论与一致性模型都是抽象化评估分布式数据库的工具。它们的好处之一就是可以是帮助我们快速评估数据库的一致性,比如一个数据库号称自己是线性一致的 CP 数据库,那么对于其特性,甚至大概的实现方式,我们就会心中有数了;另一个益处就是设计数据库时,你可以根据需要解决的问题,设计数据库一致性方面的特点。
C 指的是一致性模型中最强的线性一致。

最后,作为数据库,一个重要的概念就是事务。它与一致性是什么关系呢?其实事务的 ACID 特性中,AID 是数据库提供的对于 C 的保证。其中 I,即隔离性才是事务的关键特性。而隔离性其实解决的是并行事务的问题,一致性模型研究是单对象、单操作的问题,解决的是非并行的事务之间的问题。故隔离性加上一致性模型才是分布式数据库事务特点的总和。

为什么要自己实现分布式数据库?

分布式数据库,特别是 NoSQL 和 NewSQL 数据库,是目前主要的发展方向。同时,这两种数据库的品种也极为丰富。其中很多都是针对特定场景服务的,比如NoSQL中Elasticsearch针对的是搜索场景,Redis针对缓存场景。而NewSQL 更是百花齐放,如国内的滴滴、字节跳动等企业,都针对自己的业务特点实现了 NewSQL 数据库。更不要说如 BAT、Google 这样的大厂,他们都有自己的 NewSQL 类数据库。

设计分布式数据库案例

那么现在我们来尝试给它设计一个数据库。这里我简化了设计流程,只给出了需求分析与概念设计,目的是展示设计方式,帮助你更好地体会分布式数据库的关键点。

需求分析

我们先来介绍一下 SkyWalking 处理数据的特点。

由于 SkyWalking 的 APM 特性,其对写入有很高的诉求。不管是最早使用的 HBase,还是现在的主力存储 Elasticsearch,都对写入很友好。为了保证数据写入高速且一致,OAP 节点层已经将计算指标进行了分片,也就是同一个指标是在相同的节点计算出来的。另外,该应用还采用了批量写入的模式,即每 10 秒进行一些批量写入。

SkyWalking 在使用场景下可以被看成一个查询少写入多的系统,查询很少发生,可以容忍一定的查询延迟。可用性方面是允许牺牲一定的可用性来换取性能的,比如目前对 Elasticsearch 的副本数量建议为 0,也就是说不进行数据复制。

如果开启复制,一致性方面要求也比较低。因为对于最大的工作负载写入来说,几乎不在写入的时候进行数据查询。但是一些低负载操作需要保证一致性,比如写入监控结果,写入后需要马上能查询出来。

由于查询协议的数据结构是非关系型的,且查询种类不多,故不需要一定支持 SQL 语句。

以上围绕着第一模块的核心内容,分析了 SkyWalking 的数据库应该具备的特点。现在让我们来针对需求分析中提到的要点,来设计针对 SkyWalking 的分布式数据库。

概要设计

首先 OAP 节点实际上已经做过哈希分片,这样我们可以将数据库节点与 OAP 节点组成一对一,甚至多对一(二次哈希)的结构,保障一个指标只写入一个数据库节点,这样就避免了数据迁移的麻烦。甚至我们可以将数据库节点与 OAP 节点部署在一起,从而最大限度降低网络延迟,同时提高资源的利用率。

对于弹性扩缩容,由于 SkyWalking 可以容忍部分数据不可用,可以直接增加分片节点,而无须迁移数据。如果想要保证老数据可以查询,可以将扩容时间点做记录;而后老数据查询老节点,新数据查询新节点。由于 SkyWalking 所有数据都有生命周期,一旦节点上旧的数据被删除,缩容场景下,该节点也可以被安全移除。

虽然 SkyWalking 不强制要求可用性,但一些数据如果一旦遭遇故障,也会给使用者带来不好的体验。特别是对于类似一天内的平均响应时间,一旦某个节点故障,在没有副本的情况下,该指标的数据将会有非常大的偏差。

一旦开启数据复制,应该使用什么一致性呢?这个问题需要区分来看。对于大量写入的指标数据来说,弱一致是满足条件的。因为写入和读取是由不同的端点发起的,且写入可以认为是单对象单操作,故弱一致就满足条件。

但告警场景却不是这样,告警产生后会通知相关人员,他们希望能马上查询到数据。如果采用弱一致,很可能无法查询。这里我们不需要使用特别强的一致性,采用因果一致就可以满足需求。实现方式是,将写入告警产生的数据时间戳页传递给用户。用户查询的时候将时间戳发送给一个数据库节点,如果该节点没有该时间戳的数据,它会尝试请求其他节点去同步。

最后关于查询接口,由于不一定需要 SQL,故我们可以使用简单的 RESTful 风格的 API 去实现查询和写入。但为了写入高效,可以独立设计写入协议,采用高效的二进制长连接的协议风格。

案例总结

以上就是根据第一模块学习的知识并结合 SkyWalking 的需求特点,设计的针对该系统的分布式数据库。设计层面我只强调了关键设计要点,并未进行详细说明。而关于底层的存储引擎,相信你在学习完模块二之后,会有自己的答案。

通过这个案例,我们可以看到设计分布式数据库只要结合分片和同步两个特点,就可以大概勾画出一个分布式数据库的外貌。你可以自己在工作和学习中,尝试设计分布式数据库来解决具有一定共性的数据问题。

http://www.xdnf.cn/news/20248.html

相关文章:

  • SpringBoot配置文件
  • 【CSP-S】数据结构 ST 表详解
  • 植物大战僵尸融合版安装包,下载安装教程
  • PCDN工作原理的详细步骤
  • Netty从0到1系列之EventLoopGroup
  • Kafka面试精讲 Day 10:事务机制与幂等性保证
  • CUDA默认流的同步行为
  • 项目升级--kafka消息队列的应用
  • 状压 dp --- 数据范围小
  • 雪球科技Java开发工程师笔试题
  • happen-before原则
  • WSL Ubuntu Docker 代理自动配置教程
  • LeetCode 139. 单词拆分 - 动态规划解法详解
  • 【软考架构】第二章 计算机系统基础知识:计算机网络
  • 主数据系统是否对于企业是必需的?
  • 最大似然估计:损失函数的底层数学原理
  • 基本数据类型和包装类的区别?
  • 2025年大数据专业人士认证发展路径分析
  • MySQL运维补充
  • 【目录-判断】鸿蒙HarmonyOS开发者基础
  • 敏捷scrum管理实战经验总结
  • 贪心算法应用:化工反应器调度问题详解
  • 【LLIE专题】SIED:看穿0.0001lux的极致黑暗
  • NPU边缘推理识物系统
  • 懒加载的概念
  • 新能源风口正劲,“充电第一股”能链智电为何掉队?
  • 操作系统启动过程详解
  • Coze源码分析-资源库-删除插件-前端源码-核心组件实现
  • 03-生产问题-慢SQL-20250926
  • 机器人控制器开发(导航算法——导航栈关联坐标系)