Fabric 1.4源码解读 9:从账本角度看Peer

区块链最核心的是可信数据,所有的功能与设计根源都是数据。本次从数据存储的角度,看一看Peer。

账本

区块链的数据存储在账本中,账本包含:

  • 区块存储
    • 区块文件
    • 区块索引数据库
  • 世界状态数据库
  • 历史数据库
  • 私有数据数据库

关于账本以上各数据库的工具,官方文档中对区块存储和世界状态数据库介绍的比较详细了,但我们介绍下它没有提到的。

区块文件和区块索引数据库

区块是保存在文件中的,为了快速查找区块、交易,Fabric建立了索引,指明某通道某区块高度的第x个交易,是存在哪个文件,偏移量是多少。当然,索引还包含了区块高度、区块hash等,方便根据高度、hash查询区块。

上图展示了一个区块文件存储区块的情况,每个区块包含:

  • 区块长度
  • 区块头
  • 每条交易长度、交易数据

每个区块的开始位置、交易的开始位置,在写区块的时候记录下来,然后写到索引数据库(Index DB)。

整个Fabric网络只有1个区块索引数据库,也就是多通道共用一个

历史数据库

用来记录交易中每个状态数据的历史信息,直白点可以理解为链码中某个key的历史数值。它的key实际是{通道id+链码id, key, 区块高度, 交易在区块中的序号}组成的复合key,值为空,并且只包含有效的交易。

有这样一个问题:值为空,到底怎么查询到历史状态呢?

答:通过历史数据库合成复合key,但复合key中没有交易在区块中的序号,创建一个迭代器,迭代器可以获取包含key的复合key,然后从复合key中提取到交易在区块的序号,然后去区块文件中提取交易,再提取到写集的Value,就可以合成某个key的所有历史值。

因此查询历史状态,需要结合历史数据库和区块文件

各数据库实现

区块文件使用文件直接存储区块,没有使用数据库的原因是:区块是一种自然的追加操作,写入后不再修改,即不会覆盖历史区块,使用文件系统直接存储区块,可以达到区块最快落盘的目的,因为向文件写区块是顺序写,而写数据库是随机写,磁盘(包含HD、SSD)的顺序写性能要高于随机写。

世界状态数据库可以使用leveldb或者CouchDB,CouchDB支持富查询功能,当链码数据按JSON建模时,CouchDB可以提供更好的数据查询,更多CouchDB的信息见文档使用 CouchDB

其他数据库都使用leveldb作为底层存储。

提醒:Fabric支持多通道,逻辑上每个通道拥有一个账本。实现上区块文件是按通道名隔离开了,使用leveldb的各数据库,被各通道共用。

从数据看Peer功能

和账本相关的概念还有区块、交易和状态,从账本的角度看,账本向上支撑了2类功能:

  1. 数据同步:广播与同步区块
  2. 交易背书:模拟执行交易

在下图中,数据同步和交易背书分别使用蓝色和橙色的线圈出,底部剩下的2层为账本。

账本

core/ledger实现了Peer的账本功能,包含了账本中的各项数据库,它依赖common/ledger实现区块文件存储,区块文件存储包含3类:

  • File:把区块保存在文件中,生产环境使用,orderer和peer皆可使用
  • Json:把文件保证JSON格式的文件中,使用在非生产环境,仅供orderer使用
  • Ram :把区块保存在内存中,使用在非生产环境,仅供orderer使用

core/ledger中的:

  • PeerLedger接口,代表Peer账本,主要用来向账本写区块和私有数据,查询区块、交易和私有数据
  • Txsimulator接口,代表交易模拟器,用来模拟执行1条交易
  • QueryExecutor接口用来查询最新的数据
  • HistoryQueryExecutor接口用来查询历史状态

同步数据

同步数据有2种方式:

  • Deliver服务,Peer使用事件从Orderer获取区块
  • Peer向其他节点请求获取某个区间的区块

虽然Peer获取区块的方式有2种,但收到区块,处理区块的方式只有1种,所以下面分3小节介绍。

使用Deliver同步区块

Deliver用来以事件的方式获取区块,场景有2点:

  • Peer从Oderer获取区块
  • 客户端/SDK从Peer获取区块

Fabric 1.4源码解读 8:Orderer和Peer的交互中已经介绍了Peer从Orderer获取区块,这里再做一点补充。

Deliver服务是Orderer和Peer都使用的功能,但Orderer并没有core/ledger,所以从设计和实现上,common/deliver是从common/ledger中直接读区块,而不是core/ledger读区块。

Peer请求区块

每个Peer可以通过Gossip得知同通道的、所连接的Peer信息,其中一项就是对方Peer账本的高度。账本高度低的Peer可以向高度高的Peer发送StateRequest,请求获取某个连续区间的区块。

Peer上负责StateRequest的是gossip/state模块,它负责:

  • 创建StateRequest请求
  • 处理StateRequest请求,生成StateRequest响应
  • 处理StateRequest响应

创建请求:假设Peer1比Peer2少50个区块,并且配置了Peer每次最多取10个区块,Peer1会创建5个StateRequest请求,顺序的向Peer2进行请求,Peer1收到前一个请求的响应后,才发出下一个请求。

处理请求:实际是从账本读取所请求区块的过程,这个过程主要是读取区块文件,如果区块涉及私密数据,也涉及读取私密数据库,这部分功能主要由gossip/privdata完成,gossip/state把读到的区块和私密数据生成请求响应。

Peer处理收到的区块

Peer从Orderer和其他Peer哪获取的区块,最终都会进入到gossip/state,区块会被放入到一个区块缓冲区:PayloadsBuffer,默认大小为存储200个区块。

每个通道账本都有一个goroutine,从各自的PayloadsBuffer拿下一个高度的区块,交给gossip/privdata进行区块的验证和写入。

验证区块

这部分功能由core/handler/validation完成。在Fabric 1.4中,StateImpl会调用QueryExecutor查询状态,但实际StateImpl没有被调用。

验证区块主要是并发验证区块中的交易:

  • 验证交易中的字段
  • 验证是否满足背书策略
  • 验证交易是否调用最新版本的链码
  • 验证交易是否重复

交易验证的结果,即交易是否有效,并不会保存在交易中,这样区块中记录所有交易的DataHash就变化了。区块中所有交易的有效性存储在区块的元数据中,区块元数据中有一个有效性数组,依次存放了每个交易的有效性,使用数组的下标,与交易在数组中的顺序,一一对应。

交易验证后,会修改区块的元数据,把无效的交易设置为响应的无效序号。

如果缺失区块的私有数据,gossip/privdata会创建获取私有数据的请求,并获取私有数据,当区块和私有数据都准备齐全后,开始commit区块和私有数据。

区块写入账本

包含2大块:

  • 交易MVCC验证
    • Fabric要求世界状态数据库支持MVCC,即多版本并发控制,以便交易能够并发执行(背书),在真正修改状态的时候,才判断读写的数据是否冲突,冲突的交易会被标记为无效。关于MVCC我们在下文的背书部分再详细介绍。
  • 把区块写入数据库,以及修改各数据库:
    • 把区块写入到区块文件
    • 把区块、交易的索引写入到索引数据库
    • 有效交易的写集更新到世界状态
    • 提交历史数据库
    • 提交私密数据库
写区块完成后

写区块完成后,还需要做一些修剪操作:私密数据是有有效期的,比如存活100个区块时间,假如在1000高度写入了某私有数据,第1100写入账本后,私密数据就要从私密数据库被抹除。

背书

Peer除了记账的另外一个角色就是背书,背书很重要的一个环节就是模拟执行交易。

MVCC

Fabric为了提供更高的系统性能,支持并发的执行交易,交易在执行过程中会读写世界状态数据库,也就存在并发访问数据库的场景,为了安全的访问数据库数据,就需要对数据库的并发进行限制。

Draveness在浅谈数据库并发控制 - 锁和 MVCC种介绍了并发控制3种手段:悲观锁、乐观锁和MVCC。

Fabric选择了MVCC,它要求世界状态数据库支持MVCC,本质上讲任何支持MVCC的数据库,都可以用来实现状态数据库。

MVCC是多版本并发控制的缩写,它是一种思想,而不是一种具体的算法,所以不同的数据库实现的MVCC不同。

在MVCC的数据存储中,数据有版本的概念,写一个数据的值,实际上是创建了一个新的版本来保存数据。

MVCC可以实现并发读写的能力,当读数据时,先确定待读数据的版本,然后从该版本读取数据,写数据时,创建新的版本保存数据。读数据必然是已经存在的版本,而写数据是新的版本,因此读写可以并行。

Fabric对MVCC的使用

背书节点在模拟执行交易的过程中,会生成读写集,读集和写集分别是所有待写key读出来时的版本和待写入的新值

交易并发执行到写入区块的过程中存在2种读写冲突的情况:

  1. 同一个区块中的前后两笔交易,后面的交易读集包含某个key,但key在前面交易的写集:也就说后面交易读的是老版本的数据,是一种脏读的情况
  2. 区块中交易的读集的某个key,某之前区块的交易写集修改:背书跟写区块是并发执行的,背书时产生的写集,直到写区块才会更新到世界状态数据库,这里存在一段时间,即key已经有了新版本的数据,只是还没有提交到数据库。如果这期间有新的交易模拟执行,就会读到老版本数据,也是一种脏读的情况

有效交易的写集会被应用到世界状态数据库,被修改数据都会有一个新的版本,这个版本是逻辑版本,成为Hight,由{区块高度,交易在区块内的顺序}组成。

注:验证函数为 validateTx,读写集冲突错误为 TxValidationCode_MVCC_READ_CONFLICT ,另一个读写冲突错误为 TxValidationCode_PHANTOM_READ_CONFLICT, 因为执行过程中有RangeQuery,查询某个区间的Key,也需要验证这些Key是否冲突,底层本质还是读写集的验证。

总结

本文从账本的视角,介绍了Peer的账本,以及和账本打交道的功能。

真正企业级的区块链、大用户规模的区块链,必然能够支撑大量的并发交易,这对账本以及底层存储,都会提出更高的性能要求、磁盘利用率要求,所以理解和掌握账本和存储机制是非常有必要的。

参考