NewSQL组件的详细解析
免责声明
该文决是我个人根据NewSQL开发供应商的技术博客、各类论文以及其他新闻网站等内容整理而成。
因此,可能包含由于理解不足而导致的误解或错误的可能性。如果需要更深入的理解,请直接参考所列出的各种参考文献。如果可能,我会纳入并进行修正技术方面的指摘,但无法保证快速的反应。
NewSQL的解释分为两个部分。
建议对于对NewSQL还不了解的人来说,可以先阅读前一篇文章,以便理解本篇文章《关于2020年的NewSQL》的续篇。
以下是全书的目录。
-
- NewSQL是什么?
-
- NewSQL的架构
-
- NewSQL与传统数据库的比较
详细解析NewSQL的组件 (本文)
第一部内容的总结
从第一章到第三章作为前篇,介绍了几个被称为Google Cloud Spanner及其克隆产品的NewSQL。
所有这些数据库都被设计成以“具有强一致性并支持ACID事务的(全球范围的)分布式SQL数据库”为目标,并且利用现有技术来弥补传统关系型数据库和NoSQL数据库所妥协的方面进行开发。
在NewSQL的架构中,介绍了以下3个要素。
-
- SQLクエリエンジン
-
- 分散トランザクションマネージャ
- ストレージエンジン(分散ストレージ)
SQL查询引擎是与PostgreSQL或MySQL兼容的,它是NewSQL和客户端接口的组成部分。分布式事务管理器是NewSQL中管理分布式事务的重要元素,而存储引擎则负责最终数据的持久化和一致性保证。
这三个如何在群集内的节点上进行配置在每个产品上都不同。
4. 对NewSQL组件的详解
现在,我们将逐个讨论NewSQL中实际使用的技术要素,并试图对其进行详细解释。具体而言,我们将解释以下四个组件。

从下往上逐个介绍相互关联的组件。如果您了解其中的部分内容,当然可以跳过继续进展。
4.1 存储引擎
NewSQL是一种分布式SQL数据库,但无论分布程度如何,最终数据都将保存在集群内节点的本地磁盘上。从这个意义上说,NewSQL的存储引擎与之前嵌入于关系数据库中的存储引擎在角色上并没有发生重大变化。
但是,长期以来作为关系型数据库(准确来说是它们的存储引擎)底层数据结构的B树正被后起之秀的LSM树(日志结构合并树)所取代,这在这篇博客中解释了这种情况以及采用RocksDB的项目。
基于这种趋势,B+树(B树的派生版本)被优化用于在读写速度较慢的HDD等媒体上的使用,然而近年来也被指出了许多缺点。
例如,尽管B+树能够实现高吞吐量和低延迟的读取,但写入的开销却很大。这是因为即使只是修改部分记录,也需要覆盖整个固定长度块,从而引发了所谓的写放大问题。
对于想深入了解存储引擎的读者来说,强烈建议阅读2019年出版的《Database Internals》。它详细解释了这个领域的学术讨论。
4.1.1 LSM树的优点和缺点
在一般对比中,B树着重于读取性能,LSM树着重于写入性能。
建议有兴趣的读者一定要详细阅读原文,因为它描述了LSM Tree写入的过程,包括从内存写入到多次合并,最终保存到存储设备中。下图展示了这个过程。

来源:SQLite4的开发故事
【LSM Tree的优点】
正如上图所示,LSM Tree将随机写入转变为对Level 0的顺序写入,以高效地进行写入操作。它的处理方式是追加(版本化的),而非类似B Tree的页面覆写,因此可以抑制写放大现象。
对于累积的数据,可以在后台进行合并和层级化。在此过程中,可以删除不必要的数据,以提高磁盘利用效率。
【LSM Tree的缺点】
在B树的数据结构中,与特定键相关联的值在物理上只存在一个。这是因为在更新值时,同一位置会被覆盖。
在LSM树中,由于追加操作,上图的Level 0和Level 1可能存在相同键但不同版本值的情况。因此,在读取时需要遍历多个级别的SSTable(对应图中的每个树)。
在写入时,如果发生与重要的Level 0的刷新和后台合并冲突的情况,会导致双方性能下降。此外,如果合并操作无法长时间进行,可能会导致需要读取的SSTable数量增加,进一步降低读取性能。
总的来说,LSM树在理想状态下能够快速运行,但与B树相比,它在各个方面都存在不可预测性,并需要实施改进机制来解决这些问题。
4.1.2 RocksDB: 使用 RocksDB 在 NewSQL 中的应用
RockSDB是Facebook开发的LSM Tree实现,作为LevelDB的衍生版本。
Spanner克隆版中的所有实例都使用RockSDB,其中一些是作为基础技术使用的,其特点如下:
【RocksDB的特点】
-
- Key-Value型で永続的なデータストアを組込み可能な形式で提供。
-
- SSDなど低レイテンシなデバイスに読取り/書込みが最適化されている。
- マージ、圧縮、SCANなど分散DBに必要な高度な機能セットを提供。
首先,正如前面提到的那样,RocksDB作为LSM Tree的实现,可以在追加操作中保留多个版本,因此可以从存储引擎层面支持分布式SQL数据库所需的MVCC。
在CockroachDB的博客中,也提到了RocksDB在进一步补充LSM树的同时提供了高级功能,避免了需要自行开发这些功能的原因。
作为一个例子,为了改善LSM树的读性能,RocksDB通过引入布隆过滤器/前缀布隆过滤器的功能来加速读取。详细信息可参考前述的CockroachDB博客和TiKV文档。
此外,NewSQL使用行结构,而RocksDB使用键-值结构。
因此,在行结构中频繁进行的操作也需要在RocksDB中高效处理。CockroachDB和TiKV使用RocksDB的功能来支持范围搜索和范围删除(即DELETE FROM)。
4.1.3 RocksDB的优化:DocDB
与CockroachDB和TiKV不同,YugaByteDB使用了经过增强的独特的DocDB作为存储引擎,它增强了RocksDB。关于在NewSQL应用方面增强的必要性和关键点,可以在YugaByteDB的博客中找到详细的解释。
总结要点如下:
-
- Scanに耐性を持つキャッシュの実装(mid-point insertion strategy)
-
- Bloom Filter及びインデックスのマルチレベルのブロック分割対応
-
- RocksDBのマルチインスタンス対応
- 機能が重複するジャーナリングやMVCCに用いられていたシーケンスの利用回避
在这其中,第三个多实例支持似乎经过了大力改进。正如后文所述,在YugaByteDB中,数据被分割成称为Tablet的单元。考虑到在发生故障时所需的数据可移植性等因素,1个Tablet=1个RocksDB实例被视为理想情况。先前提到的博客中提到了这种结构的优点。
一旦一个节点上运行多个RocksDB实例,可能会导致资源利用率低下。为了优化这个问题,在DocDB中还进行了开发能够在全局节点上使用的缓存等措施。
4.1.4 总结至此
在NewSQL的存储引擎中,使用了LSM Tree实现的RocksDB及其衍生版本。
RocksDB通过提供Bloom Filter等高级功能来提高读取性能,尽管LSM Tree通常在写入量大的工作负载中具有高性能。
此外,为了将数据存储在基于键值(Key-Value)结构的RocksDB中,需要对RDB的行数据类型进行转换。关于如何进行映射并确保集群全局的一致性,各个产品都有自己的创新点和努力,比如像DocDB那样采取了独特的改进方法。
儲存引擎的附加功能
聽到嵌入式儲存引擎,可能很多資料庫工程師會聯想到SQLite。對於這樣的人,我推薦介紹一下使用「go-sqlite3來建立Cloud Spanner模擬器」的投影片。換句話說,這是基於SQLite的Spanner克隆版本,內容非常有趣。
另外,对于希望以短时间理解存储引擎的概要的人来说,我推荐从YugaByteDB的技术博客中阅读这篇文章。
4.2 数据分片
在分布式数据库中,需要考虑将记录分布到多个节点上。通常情况下,以记录为单位进行管理效率较低,因此将数据分散到由固定长度分割的记录集合中,并同时创建副本(关于副本在4.3 Raft章节中会进行解释)。
最初,RDB在处理记录时是按照数据块(Oracle)或页面(PostgreSQL)这样的单位来进行分组的,并且数据管理是由存储引擎负责的任务。
在使用Sharding的分布式数据库中,数据以键值为基础,按照一定的规则在节点之间进行分散存储,与数据块不同。这种单位被称为”Shard”。
在NewSQL中,同样也进行了分片(Sharding)操作,但各个产品在分片的名称、大小和分割方法上有所不同。下表显示了前面介绍的Spanner及其三个克隆品的分片情况。
【表1:每种NewSQL的划分方式】
FAQYugaByteDBTablet制限なしhash/range100GB未満を推奨とのこと
4.2.1 分片的方法
要理解之前的表1,必须理解列举的每个Sharding方法的术语。
在YugaByteDB的博客《我们在构建分布式SQL数据库中分析的四种数据切分策略》中,非常详细地介绍了Sharding的方法。这篇文章中大致分为以下四个分类。
-
- Algorithmic Sharding (例: Memcached/Redis)
-
- Linear Hash Sharding (例: 過去のCassandra)
Consistent Hash Sharding (例: DynamoDB、Cassandra)
Range Sharding (例: Spanner、HBase)
虽然具体细节暂不展开,但第一个算法——分片算法和第二个算法——线性哈希分片,在大规模运营和避免热点等方面存在问题,因此在NewSQL中并未使用。
一致性哈希分片(在这里,与表1中的哈希几乎相同)是通过所谓的哈希函数进行数据分散的方法,其优点是如上述文章中所提到的,可以实现节点之间的均匀分布,从而减少热点,提高可扩展性。另外,对于键值存储中常见的按键查值(get)操作具有较强的性能。然而,缺点是在关系数据库中频繁发生的范围查询(诸如0> key> 100等的范围扫描)效率较低。

而Range Sharding(与表1的range同义)具有与hash相对立的特点。换句话说,它在范围搜索方面具有优势,但是如果键的设计不当,就无法避免Hotspot问题。

(赠品)分割
我們已經在Sharding中解釋了數據分割的方法,但這不僅適用於NewSQL。
例如,Oracle Database的分區功能支持以下分割方法。
-
- レンジ・パーティション (rangeとして説明したもの)
-
- リスト・パーティション (NewSQLでは存在しない)
-
- ハッシュ・パーティション (hashとして説明したもの)
- コンポジットパーティション (上記の組み合わせ)
如果具备这些前提知识,将有助于理解。
4.2.2 分片技术的不同带来的影响
让我们对基本术语进行解释之后,再来仔细考察表1所示的含义。
根据文档的显示,Spanner似乎通过range进行Sharding操作,并且将哈希函数应用于键值,以避免Hotspot等问题,这由应用程序设计者决定。因此,需要注意这些最佳实践,例如介绍了如何设计主键。
此外,在Spanner中还提供了一种机制,即在Shard大小超过上限之外,还会在访问Shard增加时进行分割。详细信息请参考此处等。
CockroachDB和TiKV(TiDB)都像Spanner一样,通过范围分片来进行分片。原因是它们的产品博客中都指出,这是为了支持作为RDB的范围搜索。
YugaByteDB以哈希分片为基础,并且强烈关注避免热点问题。同时,为了增强作为关系型数据库(RDB)的检索性能,在创建表时可以指定基于范围的分片。这也成为类似Spanner的应用程序设计的重要考虑因素。
在分散数据库的扩展性上,每种产品的情况都表明,hash是合理的选择;但作为支持SQL的NewSQL,需要以某种形式支持range,这是必要的。
4.2.3 地理分割
在本文中,我們不打算對地理分散(尤其是涉及全球範圍的情況)進行深入解釋,但通過Sharding進行數據分割也可以實現地理分散。
因此,从地理分区的角度来看,在CockroachDB和YugaByteDB中,介绍了有关在哪个区域部署分片的战略和支持功能,这一点也与Spanner类似。
例如,在CockroachDB的文档中,介绍了“地理分区的拓扑结构”作为基于拓扑结构的分片和副本管理方法。
此外,在YugaByteDB中,介绍了广泛的功能,如支持地理分区等,用于构建具有低延迟的云原生、地理分布式SQL应用程序的9种技术。
另外,在谷歌公司中,对于这种地理分散的应对也并不容易,而且对于Spanner的构建似乎也有一些限制。(指Spanner的多地区构建)
由於地理分散性的影響,即使在後續的Raft協議中,Leader / Follower的位置配置也會受到影響,我們將在後面談到這點。
4.2.3 总结至此
在数据拆分中,有两种方法可用于创建Hotspot。一种是哈希方法,它对写入操作具有强大的支持,但对范围搜索较弱;另一种是范围方法,它容易产生Hotspot,但对范围搜索具有强大的支持。
由于不同的产品在对NewSQL中Hash / Range的支持上可能会有所不同,因此需要理解其特性并在键设计中进行相应的反映。
4.3 木筏
现在我们要开始讲解在NewSQL中如何利用Raft。
在上一篇文章中我们避免了深入讨论,但在分散式SQL数据库中,我们不仅需要将数据按照之前提到的分片单元进行分散,还需要具备Shard副本来避免数据丢失并对查询做出响应,以防节点故障。
我们在NewSQL中使用Raft来创建这个数据副本。
4.3.1 首先,Raft是什么意思?
Raft是一种共识算法,于2014年发表了题为《寻求一种易于理解的共识算法》的论文。(点击这里可阅读论文的中译版本)
大体来说,Raft具有以下两个功能。
-
- リーダー選出
- ログ複製
使用这两个工具,这篇博客非常清楚地解释了能做些什么。因为开头的总结简洁地说明了Raft的功能,所以我们引用它。
可以实现一致性的复制和分布式执行。
即使节点长时间停止或严重延迟,并且节点故障导致了领导者交替,也不存在破坏一致数据的情景。
角色、算法和配置都是简单而容易的。引用:《理解分布式一致性算法Raft》
在NewSQL中,根据键值将记录分割到Shard,并使用Raft的日志复制来创建该Shard的副本。
此外,在选择负责读/写操作的领导者时,也会使用Raft(用于选举领导者的功能)。这也是保证容错性的关键,当领导者发生故障时,会再次进行领导者选举。
4.3.1.1 日志复制
为了解释日志复制,假设有像下图这样的分布式数据库。
-
- 左側の緑の丸はクライアント。
-
- 右側の3つの青い丸が分散データベースの各ノード。
- 太線で囲われている青丸、Node aが現在のLeader。残り2ノードはFollower。

在介绍Raft工作原理的网站上,有一个动画展示实际运行情况。如果一步一步跟踪,你会明白它并不太复杂。
简单概述如下:在上述图示中,客户端希望将值 5 设置到分布式数据库中,并发送该请求。随后,按以下序列进行处理。
1. 当Leader接收到更新内容后,会将其重定向给Follower。
2. 更新内容会被写入Leader的日志中,此时还未被提交。
3. 为了提交日志,需要将日志复制给Follower。
4. Leader会等待Follower写入日志。
5. 在确认超过半数节点已经写入日志之后,Leader会提交更新内容并向客户端响应。
6. Leader会向Follower通知这次日志已经被提交(达成共识)。
这种同步方法被称为状态机复制(State Machine Replication),即使有多个节点,如果以相同的顺序执行相同的命令,就会得到相同的状态。在Raft中,这些命令被称为日志。
再来讲述Raft的优点,无论是对Leader还是Follower,即使遇到某一少数障碍或者发生了网络分割,一旦达成共识的值将不被改变。
在分散数据库中,经常使用的2PC(两阶段提交)可以协调事务的调整。然而,由于协调者的故障以及参与节点的停止和恢复,这种一致性很容易被破坏。有关此方面的详细信息,请参考此资料。
4.3.1.2 选出领导者
在正常情况下,之前的日志复制没有问题,但是必须考虑到故障情况下Leader或Follower发生故障的情况下会怎样。
基本上,其他节点会接管Leader的角色以保持集群的一致性,但是必须确保只有一个Leader存在。Raft算法用于实现领导者选举。
在这个与日志复制相同的网站上也可以看到动画,所以请确认一下那部分。
在这种情况下,假设存在一个由三个节点组成的分布式数据库。除了Leader/Follower之外,还有一种节点状态称为Candidate。

在上图中,有三个Follower存在,并处于等待election timeout(圆圈周围的切口部分)的状态。从这个状态开始,将进行下一个序列的Leader选举。
1. 等待选举超时,并成为候选人。
2. 给自己投票,并向其他节点发送投票请求。
3. 如果其他节点没有问题,它们会投票给候选人。同时,重置选举超时。
4. 如果获得多数票,候选人将成为领导者。
5. 领导者以一定间隔向追随者发送附加记录(也称为心跳)。
6. 追随者对附加记录作出响应,并每次都重置选举超时。
7. 如果在选举超时期间没有收到附加记录,则返回步骤1。
请您也参考一下上述网站中有关网络中断时领导者选举的动画解说。
如前所述,Raft使用领导者选举来选择一个领导者(在集群中只有一个),并通过日志复制使Follower复制数据副本。在发生故障时,会选择一个新的领导者,因此能够承受少于指定数量的节点故障并实现高可用性。
4.3.2 单节点Raft和多节点Raft
在之前的投稿中,我提出了一个问题:“NewSQL是否是多主节点的?”并且介绍了Raft Group的概念。现在我将对此再次进行解释。
首先,就像在Raft的解释中提到的那样,“NewSQL通过将记录按照键值分割为Shard,并使用Raft的日志复制来创建该Shard的副本”。
换句话说,作为分布式SQL数据库集群,它管理着一个或多个(通常是巨大的数量)Shard,并且正如4.3.1.1中所述,这些Shard是通过Raft来复制的。
这个复制的Shard相当于Raft Group。在使用Raft(或Paxos)进行数据复制的数据库中,除了我们这次解释的NewSQL之外,还存在其他的形式。但关键是它们是由单个Raft Group(即所谓的单一Raft)管理,还是由多个Raft Group(即多重Raft)管理,这两种情况的特性差异很大。
这张图比较了以单一Raft为基础的etcd和以多Raft为基础的TiKV。
【关于etcd和TiKV的Raft Group比较】

使用Kubernetes的人都知道,etcd不能存储大量的数据,且其可扩展性有限。其中一个原因是将Raft Group限制在一个组中。
另一方面,分布式SQL数据库重视可扩展性,可以管理多个Raft Group,但这会导致配置的复杂性,带来以下缺点。
-
- 4.3.1.2で触れたようなRaftのハートビート通信量が増大する。
- 1Shardの値はRaftで合意可能だが、複数Shardの整合性を保つには分散トランザクションを要する。
关于前者的通信量增加问题,CockroachDB和TiKV已经进行了一些改进来减少这种情况。具体详情请参考这里和这里的博客。
关于后者的分散交易,将在4.4进行解释。
4.3.3 Raft中的读操作
好了,到目前为止,我们已经通过Raft实现了数据的复制,看到了它作为一个集群具有高可用性。那么,读取数据(即Read)是如何处理的呢?
就像之前所述,在Raft中,基本上是由Leader进行Read和Write处理。那么为什么Leader需要处理Read请求呢?原因是为了遵守”返回已提交的最新数据”的原则(这也涉及到线性可转移性问题,在4.4中会进行解释)。
实际上,当Leader收到Read请求时,在“在读取过程中出现新的Leader并且该Leader已经提交了数据”的情况下,无法返回最新数据。
如何使用Raft进行复制以及在复制过程中如何进行读取,以及存在哪些问题,详细情况请参考YugaByteDB的博客文章。
整理博客中读取的顺序(称为Raft日志读取)如下:
1. 领导者接收来自客户端的读请求。
2. 领导者发送心跳通信,并等待超过半数的响应。
3. 作为领导者,读取本地数据并对请求做出回应。
图中显示了当省略了2号心跳并且Leader对Read查询做出响应时可能发生的问题,这在YugaByteDB的博客中有所描述。

引用:《Raft领导者租约在地理分布式SQL中的低延迟读取》
然而,在等待心跳回应之前读取响应可能会导致延迟(特别是在分布式环境中)。另外,在数据读取方面,并不一定所有情况都需要最新的数据。
由于这些情况,Spanner和其他NewSQL中实现了一些低延迟的读取机制。
4.3.3.1 请阅读租赁协议。 .)
作为低延迟的一种读取方式,存在着Lease Read。这可以简单地解释为“领导者在没有心跳通信的情况下进行读取处理”,但仍然需要进行调整以返回已提交的最新数据。
作为实现低延迟读取的前提,Leader需要借助Leader Lease来进行读取等处理。这个Lease是有一个到期时间的,可以类比于DHCP的租约时间来更容易理解。
在4.3.1.2中描述了领导者选举过程中,“如果获得超过半数的投票,候选人将成为领导者”的规定。然而,如果使用Leader Lease,选举后即使新领导者产生,旧领导者在租约过期后将被降级,此后新领导者在获取Leader Lease之前将无法进行读取等操作。
YugaByteDB的博客详细解释了先前介绍的内容,通过使用Leader Lease,集群会在某些瞬间无法进行读取和写入,如下图所示。

一方面,这意味着在租约到期之前,当前的领导者将被降级,并且新的写操作也不会被执行,这与当前的领导者无需心跳即可读取最新数据的情况是等同的。
在进行网络分区切换时稍作等待,这样可以减少通常读操作的往返时间,并不仅适用于YugaByteDB,TiKV也提到了类似的方法。
4.3.3.2 跟随者阅读
为了解释”Lease Read”和”Follower Read”在NewSQL中的用途,我们可以这样说:尽管”Lease Read”的目标是返回已提交的最新数据,但”Follower Read”允许处理不一定是最新的数据,只要Follower能够处理Read请求。因此,一些NewSQL提供了”Follower Read”功能。
举个例子,下图展示了CockroachDB在多区域环境中的情况。图中显示的是从位于客户端所在区域以外的Leader(使用4.3.3.1解释的Leader Lease来保持)进行读取,而是从位于同一区域的Follower进行读取。

另外,在CockroachDB中,Follower Read是面向企業的功能,目前存在一些限制,例如只能读取48秒以上之前的数据(详细信息请点击这里)。
尽管YugaByteDB也提供Follower Read功能,但与CockroachDB一样,只能保证Timeline Consistency的读取。
另一个Spanner克隆:TiDB/TiKV采用了不同的方法。下图展示了TiKV强一致性的Follower Read序列。

在从节点接收到来自客户端的请求后,进行了一次名为ReadIndex的通信来确认“领导者已经提交到哪个位置?”。通过这个通信,从节点可以确定最新的数据,并将已提交的最新数据返回给客户端。
如果您想详细了解ReadIndex的细节或者与常规Raft日志读取的比较,除了上述博客外,同样可以参考TiKV的这篇或者这篇博客。
为了减少Leader的负载平衡和地理分布的往返时间,实现Follower Read功能来降低一致性要求和通信量。
重新检查4.3.1.1中的日志复制序列,我们可以看出,在Raft中,跟随者(Follower)何时将日志反映并提交是不确定的。换句话说,当我们向跟随者读取数据时,我们无法确定返回的是哪个时间点的数据,这是这一实现的基础。
4.3.4 到这里的总结
Raft具有领导者选举和日志复制功能,并且通过它们在NewSQL中实现了Shard的复制和高可用性保证。
然而,在Raft中,仍然存在一些问题,比如需要通过Leader进行读写,为了解决这些问题,NewSQL中实现了Lease Read和Follower Read等机制。
4.4 分散的交易
在这一阶段我们学习了如何在集群节点中分散分布SQL数据库的数据,并且为了确保高可用性管理了副本。
在这个最终部分,我们试图解释分布式事务,从保持分散和复制数据的一致性和更新的角度来看。
这只是我真实的想法,不是免责声明。坦白说,分布式交易对我来说太复杂了。如果你想了解更多,请阅读《数据导向应用程序设计》(第7章和第9章),这样会更有益。以下的博客也非常值得参考。
-
- kumagiさんのQiita
- 御徒町さんのブログ
为了更好地了解Spanner的克隆NewSQL,我们将阅读各种关于分布式事务的文档。
4.4.1 NewSQL的并发控制
在这里介绍的NewSQL中,它宣称支持”ACID事务”。然而,ACID的概念很广泛,具体可以做什么并不容易理解,因此让我们总结一下各种NewSQL支持的事务隔离级别。
表2: NewSQL支持的事务隔离级别。
Externally consistenttransactionsYugaByteDBSnapshot Isolation
Serializableisolation-levels
根据表中所示,在NewSQL中支持Serializable或Snapshot Isolation。与Oracle和PostgreSQL默认的Read Committed,以及MySQL默认的Repeatable Read相比,NewSQL似乎提供了更严格的隔离级别。
如果毫不畏惧地说,NewSQL旨在通过采用Serializable来在数据库层面上(即无需特别考虑应用程序)避免下述的各种异常情况。
4.4.1.1 快照隔离和串行化
让我们在这里回顾一下Serializable和Snapshot Isolation的概念吧。
在ANSI SQL标准中,规定了以下四个事务隔离级别,从右到左的安全性和一致性逐渐提高,但性能下降。相反地,从左到右则是性能较高但安全性和一致性较低。
脏读 << 读提交 << 可重复读 << 串行化
在上述中,并未包含快照隔离(SI)的概念,这是情理之中的,因为SI是在ANSI SQL之后提出的概念。在1995年发表的《对ANSI SQL隔离级别的批判》一文中,提到了ANSI SQL的隔离级别不足以解决的8种异常情况和6种隔离级别。
【8个异常】
-
- P0: Dirty Write
-
- P1: Dirty Read (ANSI SQLと同等)
-
- P2: Fuzzy Read (ANSI SQLと同等)
-
- P3: Phantom (ANSI SQLと同等)
-
- P4: Lost Update
-
- P4C: Cursor Lost Update
-
- A5A: Read Skew
- A5B: Write Skew
【6个分离层次】
-
- Read Uncommitted
-
- Read Committed
-
- Cursor Stability
-
- Snapshot Isolation
-
- Repeatable Read
- Serializable
在上述论文中提到,Read Committed << Repeatable Read 和 Read Committed << Snapshot Isolation 是成立的,但是无法将 Repeatable Read 和 Snapshot Isolation 进行比较。(尽管 Snapshot Isolation 可容许 Write Skew,但是 Repeatable Read 不容许,而 Repeatable Read 可容许一些幻读,但是 Snapshot Isolation 不容许。)
如果想了解上述内容的详细信息,除了论文之外,我还建议阅读这篇解说博客。
另外,如果想了解每个异常的意义,非常推荐阅读《利用 Cloud Spanner 应对各种异常》这篇文章,非常易懂。
此外,快照隔离是在事务开始时基于数据集(即快照)进行读写操作。因此,需要进行多版本的数据管理,而在NewSQL中,RocksDB(已在4.1存储引擎中解释过)负责担当此任务。
4.4.1.2 可序列化快照隔离
已经解释了Serializable和快照隔离的概念,但是严格的Serializable虽然可以防止异常情况,但会影响同时执行能力(即性能),因此需要更高吞吐量的并发控制方法。
这种方法被称为Serializable Snapshot Isolation,它在NewSQL中被实现为Serializable。例如,CockroachDB的博客中有关于此的说明。
由于Serializable Snapshot Isolation(SSI)在“数据导向应用设计”书的第7章中有详细解释,因此可以引用那里的内容。
* 在日文版本中,Serializable Snapshot Isolation被翻译为直列化可能スナップショット分離。
直列化可能スナップショット分離是一种乐观的并发性控制机制。(中略)…
SSI在快照分离之上添加了检测写入间的直列化冲突的算法,并判断应中断的算法。引用:<数据导向应用设计> ISBN978-4-87311-870-3
可以在书中找到关于冲突检测算法的详细解释,建议您去阅读一下。另外,这个博客上也有关于SSI的解释。
如果要概述SSI的话,可以说它是通过使用分离的快照来乐观地并发执行事务,并在检测到写入冲突时中断事务并重试。此外,SSI的一个重要优点是,尽管写入可能会导致冲突,但使用快照隔离不会阻止读取的并发执行,因此可以期望高吞吐量。
在存在交易竞争(冲突)的环境下,不仅有优点,还存在可能导致性能降低的缺点——频繁重试。这也成为了NewSQL的短处之一。
另外,关于并发控制这方面的内容,我强烈推荐阅读这个幻灯片,它对历史等方面整理得非常好。
4.4.2 具体分散事务的例子
迄今为止,NewSQL所支持的事务隔离级别是Serializable,并且我们已经解释了其性能优越的Serializable Snapshot Isolation实现。现在,我们来详细讲解一些实际的数据更新案例。
正如前面已经多次提到的那样,《NewSQL通过将记录根据键值分片,并使用Raft日志复制来复制这些片段》。
这意味着事务涉及的记录是属于单个分片(即单一Raft)还是分散在多个分片(多重Raft),从而产生了区别。
在YugaByteDB的博客中,将分布式SQL数据库的事务分为以下3类。
-
- Single-Row ACID
-
- Single-Shard ACID
- Distributed ACID
以上的两个情况是针对目标记录恰好位于单一Shard内的情况。这种情况不仅适用于目标记录为一行的情况,也适用于目标记录为多行的情况(尽管在使用哈希进行分片的情况下只是偶然发生)。在这种情况下,不需要进行Shard之间的调整,事务管理非常简单。单一Shard的事务在本幻灯片的后半部分有详细解释。
在第三种分布式ACID中,需要协调多个分片之间的操作,分布式事务管理器应该被调用。

首先,根据上一篇文章介绍的YugaByteDB的架构,每个节点都具有分布式查询执行器层,因此可以在任何节点上担任事务管理器的角色,如上图所示。
分布式事务的处理从事务管理器的选择开始,并按照以下流程处理。
1. 选择事务管理器。
2. 在事务状态表中注册数据。
3. 将临时记录(数据)写入每个平板电脑。
4. 解决冲突。
5. 提交事务并向客户端做出响应。
6. 清理掉步骤3中的临时记录。
这个分散事务是基于Spanner设计的,但Google Cloud提供的TrueTime API和GPS·原子钟功能不包含在其中。
因此,需要通过另一种方式获取调整事务之间所需的时间戳(混合时间戳),并将其与事务ID等一起插入状态表中。这个时间戳也用于解释快照隔离中的多版本并发控制(MVCC)。
暫定紀錄是在3.階段被檢查是否與其他並行交易發生競爭,如果發生競爭,則將重新啟動一個交易。這就是在4.4.1.2中描述的SSI中樂觀並行控制的運作方式。
如果竞合问题解决了,事务将在第4步提交,不再需要的临时记录将在第6步的清理阶段被删除。
在CockroachDB的博客中也有对类似的分散事务序列进行了解释。
4.4.2.1 是否可以避免分散事务?
正如前述所解释的,分布式事务是复杂的。因此,在过去的分片机制中,倾向于避免跨多个分片进行事务调整。
例如,Vitess的Sharding在上一次的介绍中推荐了将事务设计为适应单个Shard的键。
如果发生涉及多个Shard的事务,Vitess会如何处理呢?根据这份文档,提交操作会变得”非常缓慢”。另外,正如4.3.1.1所提到的那样,还会存在二阶段提交在故障情况下破坏一致性的缺点。
在使用Vitess和Azure HyperScale(Citus)等传统的分片数据库中,是否可以避免分布式事务(避免性能下降)呢?
对于这一点,我抱有怀疑态度。从设计时就要进行完美的键设计,并且很难创造出在运营后不发生跨分片事务的情况。
简而言之,使用Vitess等工具有可能导致”在多个单一Shard内进行事务”和”在少数分布式事务之间进行混合”。
4.4.3 需要分散交易中必要的时间戳是什么?
现在,让我们谈谈最后在4.4.2版本中提到的Timestamp。
在我个人看来,理解分布式事务时最具挑战性的地方可能就是这里了。
作为Spanner能在全球范围内实现分布式并保持低延迟的交易的原因,我们之前提到了TrueTime API和原子时钟的组合。
虽然4.4.1的表2没有列出Spanner支持的事务隔离级别,但根据文档的描述,Spanner支持比Serializable更严格的External Consistency。
您可以参考这篇博客以了解Spanner如何使用TrueTime API和GPS·原子时钟来实现外部一致性。此外,这些幻灯片也是一个很好的参考资料。
然而,CockroachDB和YugaByteDB在没有GPS和原子时钟的情况下,通过通常的NTP生成时间戳,并进行分布式事务的调整。对于与Spanner相比,这种方法的精确程度存在许多争论。
在这篇博客中,Calvin论文的作者指出了前一篇“1.3 Spanner的缺点和改进”中介绍的Spanner克隆无法像原版一样确保一致性(在这里是指线性可化性)。
此外,本博客还详细解释了HLC(Hyblid Logical Clock)这一替代TrueTime API的方法,并且对其与传统数据库管理系统中使用的LSN(Log Sequence Number)的差异等进行了说明。
在这里需要注意的一点是,CockroachDB和YugaByteDB作为Spanner的克隆,以及TiDB/TiKV分布式事务管理方式之间的区别。它们使用的时钟解决方案各不相同。
-
- Spanner: TrueTime API
-
- CockroachDB: HLC (YugaByteDBとは少し異なる可能性がある)
-
- YugaByteDB: HLC
- TiKV: TSO (Timestamp Oracle)
换句话说,Spanner、CockroachDB和YugaByteDB使用分布式时钟(尽管与原子时钟和HLC略有差异),来实现Spanner论文中提出的分布式事务管理方法。
然而,TiKV基于Google在2010年发布的论文《Large-scale Incremental Processing Using Distributed Transactions and Notifications》中提出的Percolator方式进行事务管理。Percolator是基于集中式时钟(非分布式)的,并且TiKV将集中式时钟作为统一的时钟服务(TSO)安置在了Placement Driver(PD)中。有关详细信息,请参考相关文件。※关于PD,它也在上一篇关于TiKV架构的说明中出现过。
根据这些交易管理方式的差异,会导致吞吐量和提交延迟的差异,这在我的博客中也有解释。
总结
在本次投稿中,我们对被称为 Spanner 克隆的 NewSQL 进行了解析,解释了它是如何通过使用什么样的技术来实现“具有强一致性、支持 ACID 事务的(全球范围的)分布式 SQL 数据库”的。我们通过四个分类分别进行了详细的解释。
然而,LSM Tree基于的存储引擎、Sharding以及Raft,乃至分布式事务,都不是NewSQL中新发现的技术要素。它们都是经过研究,旨在实现广泛应用于分布式系统的成果。
如果您能够理解Spanner和其他NewSQL等技术的组合方式,那么本文的发布就有了意义。
我希望能够找到对理解NewSQL技术要素有帮助的文章,但实际上它几乎没有提到应该在哪些具体用例中使用。关于这一点,我正在考虑Spanner克隆的概念验证(PoC),并且希望与有兴趣并愿意一起尝试的人联系。
目前还没有具体的用例,但我们也在考虑不同于Spanner的用途。例如,尽管Spanner经常被用于需要全球范围内的分布和重写负载较重的工作负载,但从我们整理的特点中可以得出结论,NewSQL并非仅限于这一用途。
在过去,对于在低可靠性和高延迟的分布式系统上构建可扩展数据库这一要求,传统的单体关系型数据库一直不太适合。然而,现在可能存在一种适用于这种要求的可扩展数据库解决方案。
低信任且高延迟?你不打算在这种情况下运行应用程序吗?
不是这样的。根据我个人的观点,这正是容器和当前的Kubernetes所代表的。
一旦PoC的结果出来,我打算将内容发布在Qiita上或者通过会议等的形式进行演讲。
感谢所有的读者朋友们,在这里已经陪伴了两次长篇投稿。
(参考资料)
以下是上次、本次投稿所基于的一系列推文。
修改历史
- 2020/3/15 冒頭の「NewSQLの解説は二部構成」と「前編のまとめ」を更新し、前回記事へのリンクを追加した。