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类功能:
- 数据同步:广播与同步区块
- 交易背书:模拟执行交易
在下图中,数据同步和交易背书分别使用蓝色和橙色的线圈出,底部剩下的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种读写冲突的情况:
- 同一个区块中的前后两笔交易,后面的交易读集包含某个key,但key在前面交易的写集:也就说后面交易读的是老版本的数据,是一种脏读的情况
- 区块中交易的读集的某个key,某之前区块的交易写集修改:背书跟写区块是并发执行的,背书时产生的写集,直到写区块才会更新到世界状态数据库,这里存在一段时间,即key已经有了新版本的数据,只是还没有提交到数据库。如果这期间有新的交易模拟执行,就会读到老版本数据,也是一种脏读的情况
有效交易的写集会被应用到世界状态数据库,被修改数据都会有一个新的版本,这个版本是逻辑版本,成为Hight,由{区块高度,交易在区块内的顺序}
组成。
注:验证函数为
validateTx
,读写集冲突错误为TxValidationCode_MVCC_READ_CONFLICT
,另一个读写冲突错误为TxValidationCode_PHANTOM_READ_CONFLICT
, 因为执行过程中有RangeQuery,查询某个区间的Key,也需要验证这些Key是否冲突,底层本质还是读写集的验证。
总结
本文从账本的视角,介绍了Peer的账本,以及和账本打交道的功能。
真正企业级的区块链、大用户规模的区块链,必然能够支撑大量的并发交易,这对账本以及底层存储,都会提出更高的性能要求、磁盘利用率要求,所以理解和掌握账本和存储机制是非常有必要的。