Skip to content

Latest commit

 

History

History
41 lines (23 loc) · 6.53 KB

File metadata and controls

41 lines (23 loc) · 6.53 KB

Bucket Tree

MPT树的设计已经足够优秀,但是在超级账本中组织世界状态的时候没有使用MPT树,而是采用了Bucket Tree。软件工程中没有银弹可以一劳永逸的解决所有问题,在联盟链场景中MPT树就暴露了很多问题。

以太坊面临的挑战是在一个公链场景下,有很多的用户在上面部署使用调用智能合约,但是绝大部分合约在部署以后调用量是非常少的,合约的复杂度也有限。而以太坊为了支持如此多的合约和大量合约的状态采用了MPT树,尽管采用了节点划分,多种编码方式的各种优化手段,但是MPT树仍然在复杂性和性能方面存在一些问题,成为制约区块链平台的性能瓶颈。这个性能瓶颈并不是在以太坊这个体系之中,而是在联盟链技术体系之下。

在联盟链场景之下,会发现节点数量变少了,合约数量变少了,共识机制的不同导致联盟链接收执行交易的数量大幅提升,合约状态的变更也非常频繁。联盟链一般来说被用来支持具体的业务,业务流程相对固定,也就导致了不会有大量的合约部署。大量的状态变更操作集中在少数合约之间,这些合约的“热”度会变的非常高。

MPT树针对这种对少量合约调用频繁的场景非常乏力,当整条链只是为少量的合约服务时,MPT树就会成为性能瓶颈,这个时候超级账本采用了另一种数据结构来解决这个问题。

Bucket Tree拓展了哈希表的概念,引入了哈希桶(bucket)的概念。哈希桶算法是为了解决哈希冲突所提出的。举个例子,有一组序列为[1,2,3,4,5,6,9],使用的哈希函数为f(key) = key mod 5,那么依次得到的哈希值就是[1,2,3,4,0,1,4],显然在key值为1,6的时候得到的哈希值都为1,如下所示:

Key 1 2 3 4 5 6 9
f(Key) 1 2 3 4 0 1 4

这个时候就产生了冲突了,也就是不同的key通过映射后得到了同样的值。而所谓的哈希桶算法其实就是链地址解决冲突的方法,如上面的例子所示,就可以设置桶的个数为5,也就是f(key)集合的个数,而这样的话,哈希值就可以作为桶的索引,将1,2,3,4,5分别通过f(key)得到1,2,3,4,0,则可将这几个key放入桶1,2,3,4,0的首地址所指的内存中,然后处理值为6的key,得到哈希值为1,这个时候需要放入桶1中,但桶1的首地址已经有了元素1,怎么办?那么就可以为每个桶开辟一片内存,内存中存放所有哈希值相同的key,冲突的key之间用单向链表进行存储,这样就解决了哈希冲突,在查找对应key的时候,只需要通过key索引到对应的桶,然后从桶的首地址对应的节点开始查找,就是链表顺序找到,对比key的值,直到找到对应key的信息。结构如下图所示;

在冲突率比较高的时候,桶内的链表就会很长,使得查找效率比较低,而在最坏的情况下,所有的key都对应同一个哈希值,当然这种情况不会出现,这样的哈希函数选取的也没有意义了,假设这种情况出现,那么哈希表就退化成了单链表,其他桶内存浪费,且将查找效率从O(1)直接降到了O(N),所以哈希函数的选择就非常的关键。

为了满足区块头保存少量数据即可对区块内容进行校验的需要,可以将哈希桶和默克尔树结合,将每一个桶当做树叶节点,得到如下图所示的结构:

哈希表由一系列的哈希桶(bucket)组成,每个桶中存储着若干被散列到该桶中的数据项(entry)所以数据项按序排列,每一个哈希桶有一个哈希值来标识整个桶,该哈希值是桶内所有数据通过哈希计算所得。相当于将原来需要全量计算的哈希表拆分成了一个接一个的子哈希表,这样当数据发生变化的时候缩小了需要计算哈希值的数据范围。

除了底层的哈希表外,上层是一系列的默克尔树节点,一个默克尔树节点对应着下层的N个哈希桶或者默克尔树节点,这个N也称作默克尔树的聚合度。

通过这样的设计达到了两个目的,利用默克尔树的特点,使每次树状态改变,重新计算哈希的代价最小;利用哈希表进行底层数据的维护,使得数据项均匀分布;最终我们依然可以利用默克尔树根来快速感知世界状态的变更。

例如上图中,一条新的数据项entry5插入,该数据项被散列到POS为1的桶中。该桶,即从该桶至根节点上所有的节点被标为为脏节点。仅对这些脏节点进行哈希重计算,便可得到一个新的哈希值用来代表新的树状态。

由于bucket tree是一棵固定大小的树(即底层的哈希表容量在树初始化之后,就无法更改了),随着数据量的增大,采用散列函数将所有的数据项进行均匀散列可以避免数据聚集的情况发生。

在Bucket tree有两个重要的可调参数;

  • capacity:表示哈希表的容量,该值越大,整课树所能容纳的数据项个数就越多,在聚合程度不变的前提下,树越高,从叶子节点到根节点的路径越长,哈希计算次数也越多。
  • aggreation:表示一个父节点对应的孩子节点的个数,该值越大,表示树的收敛速度越快,在哈希表容量不变的前提下,树更低,从叶子节点到根节点路径越短,哈希计算次数也越少。但是每个默克尔树节点的size就越大,增加磁盘IO开销。

超级账本在实现Bucket tree的时候也采用了一些优化手段,比如当有比较多数据变更的时候,非常容易散列到不同bucket,这个时候可以并行计算新bucket的哈希值,加速构建默克尔树,如果Bucket tree树比较大,并且用数据库来存储的话,加上读cache可以显著的提升性能。超级账本结合具体的应用场景对数据结构通过改造优化极大的提高了系统的性能,其实很多时候需要把握每个数据结构在区块链系统中解决了哪些,高效的解决这些问题就可以了,并不需要拘泥于采用字典树,MPT树,默克尔树或者是Bucket tree。