初次尝试翻译,如有不好的地方,望多多包涵。
ForkBase:区块链和可分叉应用的高效存储引擎
摘要:
现有的数据存储系统提供了广泛的功能,以适应各种不同的应用程序。然而,新的应用类别已经出现,例如,区块链和协作分析,其特点是数据版本控制、分叉语义、篡改证据或其任何组合。它们为存储系统提供了新的机会,通过将上述要求嵌入到存储中,从而有效地支持此类应用程序。
在本文中,我们介绍了一个为区块链和可分叉应用而设计的存储引擎ForkBase。通过将核心应用程序属性集成到存储中,ForkBase不仅提供了高性能,而且还减少了开发工作量。存储管理多版本数据,并支持fork语义的两个变体,这两个变体支持不同的fork工作流。由于一个新的索引类支持高效的查询以及跨数据对象、分支和版本的重复内容的有效检测,F orkBase既快速又节省空间。我们使用三个应用程序展示了F orkBase的性能:区块链平台、wiki引擎和协作分析应用程序。我们针对各自最先进的解决方案进行广泛的实验评估。结果表明,ForkBase在显著降低开发工作量的同时取得了优异的性能。
介绍
由于许多存储系统提供了不同的数据模型和操作语义,现在开发新应用程序变得更加容易。在一个极端,键值存储[22,38,8,37]提供了一个简单的数据模型和语义,但具有高度可扩展性。在另一个极端,关系数据库[59]支持更复杂的关系模型和更强的语义,如ACID,这使得它们的可伸缩性较差。介于两者之间的是在数据模型、语义和性能之间进行其他权衡的系统[16,20,15,7]。尽管有这么多选择,我们注意到在现代应用程序的需求和现有存储系统提供的功能之间出现了差距。
许多现代应用程序需要的属性(或功能)与现有存储系统不太适合。首先,区块链系统,如比特币[47]、以太坊[2]和超级账本[5]实现了分布式账本抽象——对某些全球状态所做的全球一致的变更历史。由于区块链系统在不可信的环境中运行,因此它们要求账本具有防篡改性,即状态及其历史记录在未被检测的情况下无法更改。第二,协作应用程序,从Dropbox[26]、GoogleDocs[4]和Github[3]等传统平台到更新和更先进的分析平台(如Datahub[43]),允许许多用户一起处理共享数据。这类应用程序需要显式的数据版本控制来跟踪数据派生历史,并需要fork语义来允许用户处理独立的数据副本。第三,最近支持可用性而非一致性的系统允许对数据进行并发写访问,这导致了隐式分叉,最终必须由上层应用程序解决[22,21]。
如果没有适当的存储支持,应用程序必须在通用存储(如键值存储或文件系统)之上实现上述属性。这种方法的一个问题是额外的开发成本。另一个问题是,结果实现可能无法推广到其他应用程序。但更重要的是,定制实现可能会产生不必要的性能开销。例如,当前的区块链平台(如以太坊、Hyperledger)在键值存储(如LevelDB[6]和RocksDB[9])上构建其防篡改的数据结构。然而,我们观察到,这些即席实现并不总是很好地扩展,并且当前的区块链数据结构没有针对分析查询进行优化。另一个例子是,大型关系数据集上的协作应用程序使用基于文件的版本控制系统,如git。但它们不能扩展到大的数据集,而且只能提供有限的查询功能。
在本文中,我们介绍了一种新颖、高效的存储引擎ForkBase,它满足了现代应用程序对版本控制、分叉和篡改证据1的高要求。构建ForkBase的一个挑战是在维护大量数据版本时保持较小的存储开销。另一个挑战是为许多类别的应用程序提供优雅而灵活的语义。ForkBase以两种新颖的方式克服了这些挑战。首先,它定义了一种新的索引类别,称为结构不变的可重用索引(SIRI),这有助于快速查找和有效识别重复内容。后者有助于大幅降低多版本数据的存储开销。ForkBase实现了一个称为POS-Tree的SIRI实例,它结合了基于内容的切片[45]、Merkle树[44]和b+树[19]的思想。位置树直接提供篡改证据,使叉基成为不可信环境中应用程序的自然选择。其次,ForkBase提供了一种通用的分叉语义,它为应用程序提供了隐式和显式分叉的灵活性。由于位置树使用了写时复制,消除了不必要的副本,因此分叉操作非常有效。
ForkBase公开了简单的API和扩展的键值数据模型。它提供了内置的数据类型,有助于减少开发工作,并在查询效率和存储开销之间实现多重权衡。ForkBase还可以很好地扩展到许多节点,因为它采用了两层分区方案,有助于在节点之间平均分配倾斜的工作负载。
为了展示我们设计的价值,我们在ForkBase上构建了三个有代表性的应用程序,即区块链平台、维基服务和协作分析应用程序。我们观察到,将这些应用程序的主要组件移植到我们的系统上只需要数百行代码。应用程序从引擎提供的特性中受益匪浅,例如,用于协作的分叉语义和用于区块链的篡改证据。此外,由于在存储层捕获了更丰富的语义,因此提供高效的查询处理是可行的。具体而言,ForkBase能够在不扫描整个供应链的情况下快速跟踪区块链的出处,从而为分析做好准备。
概括起来,我们做出以下贡献:
- 我们识别现代应用程序中的公共属性,即版本控制、分叉和篡改证据。我们研究集成了所有这些属性的存储的优势。
- 我们引入了一个新的索引类,称为SIRI,它可以有效地消除跨多版本数据的重复。我们介绍了POS-Tree,这是一个额外提供篡改证据的SIRI实例。我们提出了一个通用的分叉语义,它捕获了许多不同应用程序的工作流。
- 我们实现了有效支持区块链和可分叉应用的ForkBase。ForkBase从POS-Tree中获得效率,从通用叉语义中获得灵活性。凭借优雅的界面和丰富的数据类型,ForkBase为高级系统和应用程序提供了强大的构建模块。
- 我们通过实现三个有代表性的应用程序,即一个区块链平台、一个维基服务和一个协作分析应用程序,展示了ForkBase的可用性。我们通过广泛的实验评估表明,在编码复杂性、存储开销和查询效率方面,ForkBase帮助这些应用程序超越了各自的技术水平。
在下文中,我们首先在第二节讨论相关的背景和动机。我们在第3节介绍了设计,然后分别在第4节和第5节介绍了接口和实现。我们在第6节和第7节中描述了三种应用的建模和评估。在第9节结束之前,我们将在第8节讨论相关工作。
2. 背景和动机
在本节中,我们将讨论支撑许多现代应用程序的几个常见属性。我们通过强调应用程序对这些属性的要求和现有解决方案提供的内容之间的差距,来激励F orkBase的设计。
2.1 多版本数据的重复数据消除
数据版本化是跟踪数据变化的应用程序中的一个重要概念。对数据的每次更新都会创建一个新版本,版本历史可以是线性的,也可以是非线性的(即由分叉和分支组成)。支持线性版本历史的系统包括多版本文件系统[55,60,58]和时态数据库[11,54,61]。具有非线性历史的系统包括用于文件的软件版本控制,如git、svn和mercurial,以及用于关系表的协作数据集管理,如Dbybe[43]和OrFeusDB[31]。区块链也可以被视为版本化系统,其中每个块代表一个版本的全局状态。
支持数据版本化的一个主要挑战是减少存储开销。数据集版本控制系统中最常用的方法是记录级增量编码,例如Decibel 和 OrpheusDB。在这种方法中,新版本只存储从以前版本修改的记录。因此,当连续版本之间的差异很小时,它非常有效,但需要组合多个增量来重建版本的内容。OrpheusDB通过将增量展平为完整的记录引用列表来优化重建。但是,这种方法不适用于大型表。增量编码的问题在于,每当版本被修改时,即使新内容与一些旧版本或不同分支中的版本相同,它也会创建一个新副本。换句话说,增量编码对于非连续版本、分叉分支或数据集无效。例如,在像Datahub [13]这样的多用户应用程序中,跨分支和数据集的重复是常见的。即使在用户上传相同数据作为新数据集的极端情况下,增量编码也没有任何好处。另一个例子是没有显式版本控制的应用程序实现,如WordPress和Wikipedia [63],它们将版本存储为独立的记录。在这些情况下,存储看不到两个版本之间的关系,并且不能利用增量编码来消除重复内容。
另一种检测和删除重复数据的方法是基于块的重复数据消除。与增量编码不同,这种方法适用于独立的对象。它广泛应用于文件系统[52,62],是git的核心特性。在这种方法中,文件被分成称为块的单元,每个单元被赋予唯一的标识符(例如,通过对块内容应用抗冲突散列函数)以检测相同的块。对于很少修改的大型文件,基于区块的重复数据消除在消除重复方面非常有效。当更新导致改变现有组块时,可以使用基于内容的切片[45]来避免昂贵的重新组块,即边界移动问题[29]。
在这项工作中,我们对结构化数据集(如关系表)采用了基于区块的重复数据消除。我们在主索引的数据页级别应用重复数据消除,而不是像增量编码那样对单个记录进行重复数据消除。一个直接的好处是,为了访问一个版本,我们不再需要从记录中重建它,并且可以直接访问索引和数据页,以进行快速查找和扫描。但是,当应用于索引中的数据页时,例如B+树[19],重复数据消除的效果可能较差。主要原因是,一个数据页的内容不仅由存储在索引中的项决定,还由索引的更新历史决定。图1显示了一个例子,其中两个B±树包含相同的项目集,但是具有不同的数据和索引页面。因此,即使索引包含相同的数据项,拥有相同页面的概率仍然很小。此外,它们的结构差异使得比较不同版本的差异和合并操作变得更加复杂。
在ForkBase中,我们通过定义一个新的索引类来解决上述问题,该类称为结构不变的可重用索引(3.1),其属性支持有效的重复数据消除。然后我们设计了一个属于这个索引类的索引结构——位置树(3.2)。这种结构不仅可以检测独立对象之间的重复2,还可以提供高效的操作操作,如查找、更新、比较和合并。
2.2 无处不在的分叉
分叉和分支的概念抓住了版本历史的非线性。它可以在广泛的应用中找到,从git和Datahub等协作应用,到迪纳摩[22]和TARDiS [21]等高度可用的复制系统,再到以太网[2]等区块链系统。这些应用程序中的两个核心操作是分叉和合并。前者创建一个新的数据逻辑副本,称为分支,可以独立操作,这样一个分支上的修改就可以与其他分支隔离开来。后者整合来自不同分支的内容,并解决潜在的冲突。
当前的应用程序有两种不同类型的分叉操作,即按需(或显式)和冲突(或隐式)。按需分叉用于明确需要隔离或私有分支的应用程序。一个例子是软件版本控制系统,例如git,它允许分叉一个分支进行开发,并且只有在经过良好测试之后,才合并对主代码库(主干)的更改。另一个例子是协作分析应用程序,如Datahub,它允许从关系数据集分支来执行数据转换任务,如清理、校正和集成。
冲突时分叉用于在冲突更新时自动创建分支的应用程序。例如,分布式应用程序以一致性换取更好的可用性、延迟、分区容忍度和可伸缩性(或ALPS [42])。特别是,当增量比块小得多时,迪纳摩[22]和费克斯[30]将冲突叉暴露给用户2基于块的重复数据消除不如增量编码有效。以冲突的形式。TARDiS [21]提出了弱一致性应用的分支合并语义。每当发生冲突写入时,就会创建包含整个状态的分支。通过隔离不同分支中的更改,应用程序逻辑得到了极大的简化,特别是因为从关键路径中消除了锁和回滚。在比特币[47]和以太网[2]等加密货币应用中,当多个块同时追加到分类账中时,叉会隐式出现。然后,通过使用最长的链条或更复杂的机制(如GHOST [57])来解决分叉问题。
ForkBase是第一个本地支持按需和冲突分叉语义的存储引擎(3.3)。应用程序决定何时以及如何创建和合并分支,而存储优化与分支相关的操作。通过提供通用的分叉语义,ForkBase有助于简化应用程序逻辑和降低开发成本,同时保持高性能。
2.3 区块链
安全意识强的应用程序要求数据完整性,以防止恶意修改,不仅是外部攻击者,还有恶意内部人员。例子包括外包服务,如存储[34]或文件系统[41],能够检测数据篡改。区块链系统[47,37,2]依靠篡改证据来保证分类账是不可变的。实现篡改证据的一种常见方法是使用加密哈希[36]和Merkle树[44]。事实上,目前的区块链系统在简单的键值存储(如LevelDB [6]或RocksDB [9])的基础上包含了定制的Merkle树型数据结构实现。然而,这种实现并不是通用的,因此很难重用或移植到不同的区块链。
随着区块链系统的发展,对区块链数据进行分析的需求越来越大[56,1,35]。但是,目前的区块链存储引擎并不适合此类任务。更具体地说,区块链数据被序列化并作为不可解释的字节存储在存储器中,因此存储引擎不可能支持高效的分析。因此,要直接在存储上执行区块链分析,唯一的选择是了解数据模型和序列化方案,并通过扫描整个存储来重建数据结构。
ForkBase通过提供多版本、防篡改数据类型和fork语义,促进了区块链系统和应用程序的开发。事实上,ForkBase中的所有数据类型都是防篡改的。ForkBase有助于减少开发工作,因为它的数据类型使得构建复杂的区块链数据模型变得容易,同时抽象出完整性问题(6.1)。更重要的是,随着丰富的结构信息在存储中被捕获,ForkBase使区块链分析就绪。
3. 设计
在这一节中,我们将介绍ForkBase的设计。我们首先定义一个新的索引类SIRI来促进重复数据消除。然后,我们讨论了一个名为位置树的特殊信息检索实例,它还提供了篡改证据。最后,我们描述了通用分叉语义的模型。
3.1 SIRI索引
数据库中现有的主索引侧重于提高读写性能。他们不考虑数据页面共享,这使得页面级重复数据消除无效,如图1所示。我们提出了一种新的索引类,称为结构不变可重用索引,它有助于不同索引实例之间的页面共享。
设 L L L为索引结构。实例 I I I存储一组记录 r e c ( I ) = ( r 1 , r 2 , . . . , r n ) rec(I)= ({r_1,r_2,...,r_n }) rec(I)=(r1,r2,...,rn)。 I I I的内部结构由页面集合(即索引和数据页面)组成, 页 面 ( I ) = ( p 1 , p 2 , . . . , p m ) 页面(I) = ({p_1,p_2,...,p_m}) 页面(I)=(p1,p2,...,pm)。如果两个页面具有相同的内容,则它们是相等的,因此可以共享(即消除重复)。如果它具有以下属性,则称为SIRI的实例:
- 结构不变。对于任何
I
1
,
I
2
I_1,I_2
I1,I2属于
L
L
L:
r e c ( I 1 ) = r e c ( I 2 ) ⇐ ⇒ p a g e ( I 1 ) = p a g e ( I 2 ) rec(I_1) = rec(I_2) ⇐ ⇒ page(I_1) = page(I_2) rec(I1)=rec(I2)⇐⇒page(I1)=page(I2) - 递归相同。对于任何
I
1
,
I
2
I_1,I_2
I1,I2属于
L
L
L,使得
r
e
c
(
I
2
)
=
r
e
c
(
I
1
)
+
r
rec(I2) = rec(I_1) + r
rec(I2)=rec(I1)+r,任意
r
∉
I
1
r \notin I_1
r∈/I1:
∣ p a g e ( I 2 ) − p a g e ( I 1 ) ∣ ≪ ∣ p a g e ( I 1 ) ∩ p a g e ( I 2 ) ∣ |page(I_2) − page(I_1)| \ll |page(I_1) ∩ page(I2)| ∣page(I2)−page(I1)∣≪∣page(I1)∩page(I2)∣ - 普遍可重复使用。对于任何
I
1
I_1
I1属于
L
L
L且page
p
∈
p
a
g
e
(
I
1
)
p\in page(I_1)
p∈page(I1),存在另一个实例
I
2
I_2
I2使得:
( ∣ p a g e ( I 2 ) ∣ > ∣ p a g e ( I 1 ) ∣ ) ∧ ( p ∈ p a g e ( I 2 ) ) (|page(I_2)| > |page(I_1)|) ∧ (p ∈ page(I_2)) (∣page(I2)∣>∣page(I1)∣)∧(p∈page(I2))
第一个属性意味着索引实例的内部结构由记录集唯一确定。通过避免由修改顺序引起的结构变化,两个逻辑上相同的索引实例之间的所有页面可以成对共享。第二个属性意味着索引实例可以用较小的实例递归地表示,而开销很小,而第三个属性确保一个页面可以被许多索引实例重用。通过避免由索引基数引起的结构差异,一个大的索引实例可以重用来自较小实例的页面。因此,具有重叠内容的实例可以共享其大部分子结构。
B+树和许多其他平衡搜索树不具有第一个属性,因为它们的结构依赖于更新序列。类似地,需要定期重建的索引,如哈希表和LSM-树[50],不具有第二个属性。大多数哈希表没有第三个属性。例如,小表中的存储桶页不太可能在大表中重用,因为记录将被放在多个较小的存储桶中。存在满足所有属性的现有索引结构,例如基数树或尝试。然而,它们是不平衡的,因此无法约束运营成本。
3.2面向模式的拆分树
我们提出了一个称为模式导向树的索引实例。除了上面的SIRI属性,它还有三个附加属性:它是一个概率平衡的搜索树;找到差异并合并两个实例是有效的;这是防篡改的。这种结构的灵感来自基于内容的切片[45],类似于B±树和Merkle树的组合[44]。在位置树中,节点(即页面)边界被定义为从包含的条目中检测到的模式,这避免了结构差异。具体来说,为了构建一个节点,我们扫描目标条目,直到出现预定义的模式,然后创建一个新节点来保存扫描的条目。由于叶节点和内部节点的不同特征,我们为它们定义了不同的模式。
3.2.1 树结构
图2展示了位置树的结构。树中的每个节点都存储为一个页面,这是重复数据消除的单元。该节点以检测到的模式终止,除非它是某个级别的最后一个节点。类似于b+树,索引节点为每个子节点包含一个条目。每个条目由一个子节点的标识符和相应的拆分键组成。为了查找特定的键,我们采用了与B+树中相同的策略,即遵循由分割键引导的路径。从子节点的标识符是子节点的加密散列值(例如,从SHA-1散列函数中导出)而不是内存或文件指针的意义上来说,位置树也是一个Merkle树。从节点标识符到存储指针的映射由外部维护。
3.2.2叶节点分割
为了避免叶节点的结构差异,我们定义了类似于文件重复数据消除系统中使用的基于内容的切片[45]的模式。这些模式有助于将节点分割成大小相似的较小节点。给定k字节序列 ( b 1 , . . , b k ) (b_1,..,b_k) (b1,..,bk),设P为以k个字节为输入,返回至少q位的伪随机整数的函数。当且仅当出现以下情况时,才会出现该模式: P ( b 1 . . . b k ) M O D = 0 P(b_1...b_k) MOD = 0 P(b1...bk)MOD=0
换句话说,当函数P为q个最低有效位返回0时,就会出现这种模式。这种模式可以通过滚动散列(例如拉宾-卡普、循环多项式和移动和)来实现,滚动散列支持序列窗口上的连续计算并提供令人满意的随机性。特别地,我们使用循环多项式[18]散列,其形式为:
P
(
b
1
.
.
.
b
k
)
=
s
k
−
1
(
h
(
b
1
)
)
⊕
s
k
−
2
(
h
(
b
2
)
)
⊕
.
.
.
⊕
s
0
(
h
(
b
k
)
)
P(b_1...b_k) = s^{k−1}(h(b1)) ⊕ s^{k−2}(h(b_2)) ⊕ ... ⊕ s^0(h(b_k))
P(b1...bk)=sk−1(h(b1))⊕sk−2(h(b2))⊕...⊕s0(h(bk))
其中⊕是异或运算符,h将一个字节映射为[0,2q]中的一个整数。s是一个函数,它将输入向左移动1位,然后将第q位推回最低位置。对于滑动窗口,可以连续计算该函数:
P
(
b
1
.
.
.
b
k
)
=
s
(
P
(
b
0
.
.
.
b
k
−
1
)
)
⊕
s
k
(
h
(
b
0
)
)
⊕
s
0
(
h
(
b
k
)
)
P(b_1...b_k) = s(P(b_0...b_{k−1})) ⊕ s^k(h(b_0)) ⊕ s^0(h(b_k))
P(b1...bk)=s(P(b0...bk−1))⊕sk(h(b0))⊕s0(h(bk))
每次,我们移除最老的字节并添加最新的字节。
最初,整个数据条目列表被视为一个字节序列,模式检测过程从头开始扫描它。当模式出现时,从最近扫描的字节创建叶节点。如果一个模式出现在一个条目的中间,页面边界被扩展以覆盖整个条目,这样就不会有条目存储在多个页面上。这样,每个叶节点(除了最后一个节点)都以一个模式结束,如图2所示。
3.2.3索引节点拆分
用于分割叶节点的滚动散列具有良好的随机性,这使得该结构与倾斜的应用数据保持平衡。然而,我们观察到它的成本很高:它占构建位置树成本的20%。因此,对于索引节点,我们应用了一个更简单的函数Q,它利用了用作子节点id的加密散列的内在随机性。特别地,对于索引条目的列表,Q检查每个子节点id(即字节序列),直到出现一个模式:
i
d
M
O
D
2
r
=
0
id MOD 2^r= 0
idMOD2r=0
检测到模式时,所有扫描的索引条目都存储在新的索引节点中。
3.2.4 构建和更新
给定上面的节点分裂策略,位置树构造如下。首先,数据记录按关键字排序,并被视为一个序列。接下来,应用模式函数P来创建叶节点和相应索引条目的列表。之后,在每一级索引条目上重复应用函数Q来构造索引节点,直到到达根节点。算法1演示了这种自下而上的构造。预期的节点大小由模式函数中的参数q和r控制。为了确保节点不会变得无限大,强制实施了一个附加约束:节点大小不能大于平均大小的α倍;否则它会强力分裂。分力概率等于 ( 1 / e ) α (1/e)^α (1/e)α,可以很低(例如α = 8时为0.03%)。
为了更新单个条目,位置树首先寻找目标节点,应用更改,最后将其传播到返回根节点的路径中的索引节点。写入时拷贝用于确保旧节点不会被删除。修改后,如果出现新的模式,节点可能会分裂,如果模式被破坏,节点可能会与下一个节点合并。在任何情况下,每个级别最多有两个节点。因为树是概率平衡的,所以更新复杂度是 O ( l o g ( N ) ) O(log(N)) O(log(N))。为了进一步分摊许多更改的成本,支持多次更新,其中索引节点只有在所有更改应用于多个数据节点后才会更新。
位置树支持一种特殊类型的更新,在这种更新中,整个记录集被导出、外部修改,然后重新导入。最终的再进口操作是高效的。具体来说,位置树根据给定的记录重建一个完整的新树,但是该树与旧树共享其大部分节点。由于SIRI属性,重建新树的结果与直接对旧树应用更新的结果相同。
3.2.5差异和合并
位置树支持快速比较操作,这种操作可以识别两个位置树实例之间的差异。因为具有相同内容的两个子树必须具有相同的根id,所以可以通过跟随具有不同id的子树并修剪具有相同id的子树来递归地执行diff操作。因此,diff的复杂度为 O ( D l o g ( N ) ) O(D log(N)) O(Dlog(N)),其中D是不同叶节点的数量,N是数据条目的总数。
位置树支持三向合并,包括一个差异阶段和一个合并阶段。在第一阶段,两个对象A和B与一个公共的基本对象C相区别,这分别产生
Δ
A
\Delta _A
ΔA和
Δ
B
\Delta _B
ΔB。在合并阶段,差异应用于两个对象中的一个,将
Δ
A
\Delta _A
ΔA应用于B或将
Δ
B
\Delta _B
ΔB应用于A。在传统方法中,这两个阶段是按元素执行的。在位置树中,两个阶段都可以在子树级别高效地完成。更具体地说,我们不需要在差异阶段到达叶节点,因为合并阶段可以直接在覆盖差异的最大不相交子树上执行,而不是在单个叶节点上执行,如图3所示。
3.2.6顺序位置树
位置树是为用唯一键索引记录而设计的,因此适用于集合抽象,如映射和集合。它也可以稍加修改以支持序列抽象,如列表和斑点(即字节序列)。我们称这种变异序列为位置树。此变体中的每个索引条目都用一个计数器来替换拆分键,该计数器指示该子树中叶级数据条目的总数。这允许计算位置访问的路径,例如,读取第I个元素。差异操作也不同于原始位置树。找到两个序列之间的差异通常使用编辑距离来计算,例如,Linux中的差异工具[46]。顺序位置树能够在索引条目上递归地执行这个操作,而不是在数据条目的扁平序列上。
3.3通用分叉语义
我们提出了一个通用的分叉语义,它支持按需分叉和冲突分叉。应用程序选择使用哪种语义,而存储侧重于优化与分叉相关的操作。
3.3.1 按需工作
在这种情况下,分支会根据需要明确分叉,以创建独立的可修改数据副本。每个分支都有一个用户定义的标记,因此我们称之为标记分支。分支机构的最新版本称为分支头,这是唯一可修改的状态。例如,在图4(a)中,版本S1从一个现有的分支分叉到一个新的分支。然后,将更新W应用于新分支,创建版本S2,该版本成为新分支的负责人。最重要的操作如下:
- fork——从另一个分支(或版本)创建一个独立的可修改分支,并附加一个标签;
- read——从分支(或版本)返回提交的数据;
- Commit——用新数据更新(或推进)分支;
- diff——查找分支(或版本)之间的差异;
- Merge——合并两个分支及其提交历史;
3.3.2冲突研究
在这种情况下,分支是从并发和冲突的修改中隐式创建的,以避免阻塞任何操作和延迟冲突解决。例如,在图4(b)中,两个冲突的更新 W 1 W_1 W1和 W 2 W_2 W2同时应用于标题版本S1。结果是创建了两个不同的具有头部 S 2 S_2 S2和 S 3 S_3 S3的分支。这样的分支只能通过它们的头部版本来识别,因此我们称它们为未标记的分支。最重要的操作如下:
- Read——选择并阅读基于以下策略之一的版本:
- any-branch:从任意分支头读取;
- exact-version:读取指定版本;
- version-desendant:从给定版本读取任意分支头;
- Commit——根据策略更新(或推进)分支,或者在策略失败时创建新分支:
- exact-version:如果是分支头,则写入给定版本;
- version-descedat:从给定版本写入到一个非冲突的分支头;
- ListBranches——返回所有的分支头;
- Merge——解决冲突并合并分支;
许多可分叉的应用程序可以使用上述操作来实现。例如,多主复制数据服务可以通过提交(版本后代)应用远程更改,指定远程节点提交的最后一个版本。如果更改与本地更改冲突,则会创建一个新的分支。该服务定期使用列表分支和合并来检查和解决未解决的冲突。另一个例子是加密货币,在这种情况下,每当客户端接收到一个块时,它都会调用提交(exactversion),其中以前的版本是从块本身中提取的。使用列表分支可以识别最长的链。最后一个例子是TARDiS [21],接下来我们可以扩展我们的语义来实现复杂的分支策略,并支持广泛的一致性级别。
4.数据模型和应用编程接口
在这一节中,我们介绍了ForkBase的数据模型和支持的操作,展示了它如何集成上述设计。
4.1 FNode
ForkBase采用扩展的键值数据模型:每个对象都由一个键来标识,并且包含一个特定类型的值。一个键可以有多个分支。给定一个关键字,我们不仅可以检索每个分支中的当前值,还可以检索它的历史版本。与其他数据版本化系统类似,ForkBase在一个称为版本派生图的有向无环图(DAG)中组织版本。图中的每个节点都是一个名为FNode的结构,它与一个唯一的标识符uid相关联。FNode之间的链接表示它们的派生关系。FNode的结构如图5所示。上下文字段是为应用程序元数据保留的,例如,提交git消息或区块链工作证明的现时值[28]。
4.2 防篡改版本
每个FNode都与一个代表其版本的uid相关联,该uid可用于检索值。uid根据FNode中存储的内容唯一地标识对象值及其派生历史。当两个节点具有相同的值和派生历史时,它们被认为是等价的,即具有相同的uid。这是由于使用位置树——一种结构不变的Merkle树——来存储值。此外,派生历史本质上是由基字段链接而成的散列链,因此两个相等的FNodes必须具有相同的历史。
基本类型包括简单值——字符串、元组和整数。它们是为快速访问而优化的原子值。这些值没有明确进行重复数据消除,因为共享小数据的好处无法抵消额外的开销。除了基本的获取和设置操作之外,还提供了许多特定于类型的操作,如追加、字符串和元组的插入以及数字类型的加法、乘法。
可分块类型是复杂的数据结构——斑点、列表、映射和集合。每个可分块值都存储为位置树(或序列位置树),并因此进行重复数据消除。可分块类型适用于可能变得相当大并有许多更新的数据。读取一个可分块的值只是返回一个处理程序,而实际的数据页是通过迭代器接口按需逐步获取的。位置树自然支持细粒度的访问方法,如查找、插入、更新和删除。
内置数据类型的丰富集合使得构建高级数据抽象变得容易,例如关系表和区块链(6)。请注意,有些数据类型可能具有相同的逻辑表示,但有不同的性能权衡,例如字符串和斑点,或元组和列表。应用程序可以灵活选择更适合其工作负载的类型。
4.4 APIs
表1列出了ForkBase支持的基本操作。给定一个新的FNode值和它的基uid,就可以创建一个新的FNode(M5)。现有的节点可以使用它们的uid进行检索(M2)。可以使用分支标记来代替uid,在这种情况下,分支头返回给Get (M1),用作Put (M4)的基础版本。当既没有指定分支标记也没有指定uid时,将使用默认分支。
3.3中讨论的分叉相关操作可以映射到这些API。对于FoD操作,一个标记的分支可以从另一个分支(M13)或一个非头版本(M14)分叉。提交和读取分别由(M4)和(M1)支持。可以通过首先找到基本版本(M19),然后执行三向差分(在可分块类型的位置树上)来实现差分操作。标记的分支可以使用(M7)与另一个分支合并,或者使用(M8)与特定版本合并。在任一情况下,只有活动分支的头被更新,使得新的头包含来自两个分支的数据。对于FoC操作,提交和读取分别由(M6)和(M3)支持。所有基于策略的操作实际上都是在基于版本的访问之上实现的(M2,M5),这也允许应用程序实现自己的策略。分支通过(M12)列出,它们可以在单个操作(M9)中合并。F orkBase中的每个键都为两个分叉语义提供了独立的空间,因此一个语义的分支更新不会影响另一个语义的状态。因此,一个关键字可以同时包含已标记和未标记的分支。
总之,可以在两种粒度上操作数据库中的数据:在单个对象或对象分支上。ForkBase公开了结合了对象操作和分支管理的易于使用的接口。图6显示了一个分叉和编辑Blob对象的例子。因为Put用于插入和更新,所以它的值字段可以是一个完整的新值,也可以是经过一系列更新的基对象。我们可以在同一个对象上批量提交多个更新,而ForkBase只保留最终版本。
5. 系统实现
在这一节中,我们将介绍ForkBase的实现细节。该系统既可以作为嵌入式存储运行,也可以作为分布式服务运行。
5.1 架构
图7显示了由四个主要组件组成的ForkBase集群的体系结构:主服务器(master)、调度器(dispatcher)、控制器(servlet)和块存储(chunk storage)。主服务器维护集群运行时信息,而请求调度器接收请求并将请求转发给相应的控制器。每个控制器管理一个不相交的密钥空间子集,由路由策略决定。servlet还包含三个子模块,用于执行请求:访问控制器在执行之前验证请求权限;分支表维护标记和未标记分支的分支头;对象管理器处理对象操作,对主执行逻辑隐藏内部数据表示。块存储保持并提供对数据块的访问。所有块存储实例形成一个大型共享存储池,远程控制器可以访问该存储池。事实上,每个控制器都与一个本地块存储位于同一位置,以实现快速数据访问和持久性。当ForkBase用作嵌入式存储时,例如在区块链节点中,只实例化一个控制器和一个块存储。
5.2 内部数据表示
数据对象以数据块的形式存储。一个基本对象由单个块组成,而一个可分块对象由多个块组成。
Chunk和cid。区块是ForkBase中的基本存储单元,它包含一个字节序列。一个块通过它的cid唯一地被识别,使用加密散列函数,例如SHA-256,从块中的字节序列计算。由于散列函数是抗冲突的,每个块都有一个唯一的cid,即两个具有相同cid的块应该包含相同的字节序列。数据块在数据块存储(5.3)中进行存储和重复数据消除,并可通过其cid进行检索。
FNode和位置树。FNode被序列化并存储为元块。FNode的uid实际上是元块的cid的别名。位置树存储在多个块中,每个节点一个块。具体而言,索引节点存储在索引块中,叶节点存储在blob/list/map/set块中。存储在索引条目中的子节点id是相应块的cid。
数据类型。对于一个基本对象,它的值被嵌入到元块的数据字段中以便快速访问。对于一个可分块的对象,数据字段包含一个cid,表示对应的位置树的根。访问一个大的可分块对象是有效的,因为只有相关的位置树节点是按需获取的,而不是一次获取整个树。默认情况下,所有位置树节点的预期区块大小为4 KB,但也支持特定类型的区块大小。例如,存储大文件内容的blob块可以有较大的大小,而索引块可能需要较小的大小,因为它们只包含微小的索引条目。
5.3 块存储
块存储保存数据块,并支持使用cid检索。它公开了一个键值接口,其中键是提交块的cid。块存储是内容可寻址的:它从块的内容中获得cid。因此,当一个请求包含一个现有的块时,存储将检测到它并立即返回。区块是不可变的这一事实有两种利用方式。首先,块以日志结构的布局被持久化,这为从位置树连续生成的块提供了位置,并使用LRU策略进行缓存。其次,每个块都被复制以获得更好的容错能力,而不会引入一致性问题,因为没有更新。增量编码可以应用于类似的块,以进一步减少空间消耗[63]。我们把这个增强留给以后的工作。
5.4 分支管理
对于每个键,都有一个保存所有分支头的分支表。分支表包括分别用于标记和未标记分支的两种结构。
TB-表。标记的分支保存在一个名为TB-表的映射结构中,其中每个条目由一个标记(即分支名称)和一个头cid组成。放入分支操作(M4)首先更新值(在位置树中),然后创建一个节点,最后用这个新节点在TB表中的cid替换旧的分支头。分支操作(M13)只是创建一个指向引用的节点的新表条目。所以fork操作在F orkBase中是极其轻量级的。标记分支上的并发更新由servlet序列化。为了防止意外覆盖其他人的更改,提供了额外的保护性应用编程接口,以确保只有当前分支头在其最后一次读取后没有前进时,放操作才会成功。
UB-table。未标记的分支保持在一个称为子表的集合结构中,其中每个条目只是一个冲突分支的头cid。卖出策略(M6)和卖出版本(M5)操作会相应地更新UB表。一旦一个新的FNode被创建,它的cid被添加到UB表中,它的基本cid从表中被删除。如果在表中找不到基本cid,这意味着基本版本已经由其他版本派生,因此会出现新的冲突分支。如果新的FNode已经存在于块存储中(来自等价操作),那么UB-table就会忽略它。
冲突解决。在合并(M7-M9)操作中使用三向合并策略。为了合并两个分支头v1和v2,三个版本(v1,v2和LCA(v1,v2))的位置树被馈送到合并功能。如果两个分支都修改了一个键(在映射和集合中)或一个位置(在列表和斑点中),就会发生冲突。如果合并失败,它将返回一个冲突列表,要求解决冲突。这可以在应用层处理,合并后的结果将被发送回存储。此外,简单的冲突可以使用内置的解决函数(如追加、聚合和选择一个)来解决。ForkBase还允许用户挂钩定制的冲突解决函数。
5.5 集群管理
当ForkBase作为分布式服务部署时,它使用基于哈希的两层分区,在集群中的节点之间平均分配工作负载:
- **请求调度器到servlet:**调度器接收的请求被分区,并根据请求关键字的散列发送到相应的servlet。
- **servlet到块存储:**在Servlet中创建的块根据cids进行分区,然后转发到相应的块存储。
然而,由一个servlet生成的所有元块总是存储在其本地块存储中,因为它们不被其他servlet访问。通过将元块保存在本地,可以高效地返回原始对象或跟踪历史版本。此外,servlets缓存频繁访问的远程块,因为它们是不可变的。当读取位置树节点时,请求分派器绕过servlet,将获取块请求直接转发到块存储。