6-1 区块链存储技术要素
区块链存储结构概述
区块链账本与区块数据
区块数据主要包括区块头与区块体两个部分。所有区块数据按时间顺序进行连接形成单链式存储结构,该链式结构存储所有区块数据称为区块链账本。
世界状态和状态数据
区块链所有节点从同一个创世状态开始,依次运行达成共识的区块内的交易,驱动各个节点的状态按照相同操作序列(增加,删除,修改)不断变化,实现所有节点在执行完相同编号区块内的交易后,状态完全一致,把最终一致的状态称为世界状态。类以太坊区块链系统采用MPT(Merkle Patricia Trie)状态树存储用户世界状态数据
默克尔世界状态树(MPT)里面记录的各种信息称为状态数据,比如账户余额、智能合约字节码、各个智能合约自定义数据、链配置参数等。每次发布新区块都会新建一棵状态树,用于不同的区块下账户状态数据的存储
世界状态表示的是当前状态,即记录的各状态数据的当前数值。为保证执行交易时能够快速地对世界状态进行更新,区块链世界状态存储设计实现要考虑状态数据的快速查找以及高效更新。同时维护状态数据需要付出不少存储成本,随着链的持续运行,状态数据会持续膨胀,根据不同的场景需要,可对状态数据进行裁剪优化,以支持更海量的状态数据存储
区块链数据不可篡改特性
链式哈希指针
哈希指针组成的链块式结构,后一个区块内始终记录前一区块的哈希值
区块哈希值由前一个交易哈希值、Nonce(随机值)、默克尔树根、时间戳作为输入项通过哈希计算获得。如果前一个区块的任何信息发生变化,就会和当前区块原来的哈希值不一致,不会被下一个区块认可。由此要修改任何一个区块中的数据,都需要重新计算生成它之后的所有区块,节点需要在短时间之内完成大量的计算工作,难度很大且得不偿失。
默克尔树存储
区块体中数据是由一笔一笔的交易组成的,这些交易通过交易默克尔树组织起来。其中任何一笔交易被修改,都会导致交易默克尔树根哈希值变化,进而将导致下一个区块头的变化。同样的区块头会使用默克尔状态树根哈希值去锚定数据,若状态数据被修改,也会导致状态树根哈希值变化。由此下一个区块将对这个修改过的区块不识别,从而保证区块数据信息不可篡改特点
区块链数据多版本特性
默克尔树多状态根和增量修改
区块链系统中全节点并非维护一棵MPT(Merkle Patricia Trie)状态树,而是每次发布新区块都要新建MPT状态树,用于不同的区块下账户状态数据的存储。新建的MPT树,对于没有变动的状态数据会共享上一个区块的状态树信息,只是增量修改有变动的节点信息。
每次发布新区块,MPT树中部分节点状态会改变,但改变并非在原地修改,而是新建一些分支,保留原本状态。当仅有新发生改变的节点才需要修改,其他未修改节点直接指向前一个区块中的对应节点
不同的state root,对应着不同的区块状态数据。从某个区块中取出该区块的state root,查询到MPT的根节点,就能索引到该区块当时所有账户的历史状态数据
默克尔树状态回滚
每个区块都保留状态数据是为了便于当区块链某些分叉需要回滚时可以更好地查看历史状态记录
根据不同区块下的state root,就能查询到当时区块下账户历史信息,实现对历史数据的可追溯。但是多版本状态数据的引入带来了大量hash计算和大量冗余存储,在追求可追溯性时,大大地忽略了性能。根据不同的场景需要,区块链系统可对状态数据进行裁剪优化,提高系统性能。
区块链数据可验证特性
区块链节点类型
区块链按照存储数据的形式可以分为全节点、轻节点、归档节点
全节点
一个参与共识的全节点通常会维护这个区块链的数据,每个区块中的区块头信息、所有的交易、回执信息等。全节点历史数据会定期裁剪,减少存储开销
轻节点
轻节点只存储所有区块头和自己相关的交易细节,而不需要存储全量的交易列表等信息,减少节点存储压力(比如智能手机钱包),且不用参与交易打包共识。轻节点通过Merkle证明和SPV交易验证来判断交易是否在当前的区块链交易列表中
归档节点
归档节点存储保留在全节点中的所有内容,并建立历史状态档案,例如想查询区块4,000,000的账户余额等。归档数据以字节为单位,适合诸如区块浏览器、钱包供应商和链分析之类的服务应用
默克尔证明
默克尔证明指一个轻节点向全节点发起一次证明请求,询问全节点完整的默克尔树中是否存在一个指定的交易;全节点向轻节点返回一个默克尔证明路径,由轻节点进行计算,并最终验证交易存在性
简单支付验证(SPV)机制
简单支付验证(Simplified Payment Verification,SPV)机制是区块链系统提供的一种数据验证能力,能够在付出很小的存储代价和数据同步代价情况下完成对区块链系统存储数据的合法性校验。
区块链存储引擎概述
区块链中常见数据库产品对比
数据库类型 | 优点 | 缺点 | 典型应用产品 |
---|---|---|---|
关系型数据库 | 支持schema和丰富的数据查询功能 | 不支持MPT存储模式 不支持历史版本 不支持并发执行 不支持SPV验证 | 长安链/MySQL |
KV键值数据库 | 拥有高性能的写入和随机读性能 支持内存和持久化的存储 | 不支持SQL语句 不支持索引 | 以太坊/LevelDB |
文档数据库 | 基于文档存储 支持JSON格式在账本上建模数据 支持富查询和索引查询 | 不支持MPT结构 不支持SPV验证 | 超级账本/CouchDB |
区块链合约数据模型
Solidity智能合约数据模型
类以太坊区块链平台使用Solidity作为智能合约语言,允许在支持以太坊虚拟机(EVM)的区块链平台节点上运行,其数据存储结构如下
蚂蚁链基于Schema合约数据模型
6-2 数据库产品在区块链应用
6-2-1 Level DB
概述
LevelDB是能够处理十亿级别规模Key-Value型数据持久性存储的C++程序库。Key和value都是任意的字节数组(bytearrays),并且在存储时,key值根据用户指定的比较器(comparator)函数进行排序
LevelDB特性
- 持久化的KV存储
- key值有序存储
- 数据快照保证读取数据可靠性
- 数据压缩减小存储空间
- 原子批量操作
- 提供的基本操作接口:Put()、Delete()、Get()、Batch()
LevelDB限制
- 非关系型数据库(NoSQL),不支持sql语句,也不支持索引;
- 一次只允许一个进程访问一个特定的数据库;
- 没有内置的C/S架构,开发者使用LevelDB库封装server
LevelDB架构
构成LevelDB静态结构包括六个主要部分:内存中的MemTable和Immutable MemTable以及磁盘上的几种主要文件:Current文件,Manifest文件,log文件以及SSTable文件
- MemTable(wTable)
内存数据结构,具体实现是SkipList。接受用户的读写请求,新的数据修改会首先在这里写入
- Immutable MemTable(rTable)
当MemTable的大小达到设定的阈值时,会变成Immutable MemTable,只接受读操作,不再接受写操作,后续由后台线程Flush到磁盘上
- SST Files磁盘数据存储文件
分为level0到levelN多层,每一层包含多个SST文件,文件内数据有序。level0直接由Immutable MemTable Flush得到,其他每一层的数据由上一层进行compaction得到
- Manifest Files
Manifest文件中记录SST文件在不同level的分布,单个SST文件的最大、最小key,以及其他有效LevelDB需要的元信息
- Current File
由于LevelDB支持snapshot,需要维护多版本,可能同时存在多个Manifest文件,Current记录的是当前的Manifest文件名
- LogFiles(WAL)
用于防止MemTable丢数据的日志文件
LevelDB读写性能概述
由于Log Structure设计,LevelDB的顺序写性能比较高,但是当后台compaction比较高时,会产生write stall,阻塞前台写入;LevelDB的随机读性能不错,但是在一定数据规模后由于层数变多,读性能会急剧下降
写数据
- 先写入MemTable
- MemTable的大小达到设定阈值的时候,转换成Immutable MemTable
- Immutable MemTable由后台线程异步Flush到磁盘上,成为level0上的一个SST文件
- 在某些条件下,会触发后台线程对level0~levelN的文件进行Compaction
读数据
- 读MemTable,如果存在,返回
- 读Immutable MemTable,如果存在,返回
- 按顺序读level0~levelN,如果存在,返回
- 返回不存在
由于以太坊的MPT树是内容寻址结构,以hash值作为levelDB中的键值设计会造成巨大的compaction行为。大量compaction会导致后续的随机读能力下降剧烈,有可能导致在高TPS下产生延缓写(write stall)行为。
LevelDB嵌入式使用
LevelDB是一个程序库,直接内置到使用程序里面,不用单独安装,下图为springboot集成LevelDB流程
LevelDB故障恢复
WAL(Write-Ahead Logging)预写日志系统提供了一种高并发、持久化的日志保存与回放机制。每一个业务数据的写入操作(PUT DELETE)执行前,都会记账在WAL中。如果出现服务器宕机,则可以从WAL中回放执行之前没有完成的操作
在区块链应用
账本数据冷热分层快速写入
LevelDB数据存储到磁盘和内存,如果磁盘底层的冷数据被修改了,它会再次进入内存,一段时间后又会被持久化刷回到磁盘文件的浅层,然后再慢慢往下移动到底层
持久化数据文件SST
LevelDB在磁盘上存储了很多SST文件,sst表示Sorted String Table,文件里所有的key都是有序的。每个文件都会对应一个层级,每个层级都会有多个文件。底层的文件内容来源于上一层,最终它们都会来源于0层文件,而0层的文件又来源于内存里的rtable序列化
以太坊数据存储
以太坊采用LevelDB作为底层数据库,用于存储区块、区块交易、状态树信息
以太坊区块数据写入
WriteBody先对区块体进行RLP编码,然后调用WriteBodyRLP将区块体的RLP编码写到LevelDB数据库中
以太坊状态数据存储
账户状态数据库是一个维护链中所有账户地址和其状态对应关系的数据库,以账户地址为key,以账户状态(包含nonce,余额,storageRoot,codeHash)为value的存储
6-2-2 Couch DB
CouchDB概述
CouchDB是用Erlang开发的面向文档的数据库系统,使用JSON来存储数据,使用JavaScript作为查询语言来转换文档
CouchDB特性
- 面向文档的数据库
基于文档存储,数据之间没有关系范式要求,特别适合存储文档,如CMS、地址本、区块链交易等
- 分布式的数据库
CouchDB把存储系统发布到多台物理节点上,并很好地协调和同步节点之间的数据读写一致性
- 支持REST API
使用JavaScript AJAX来操作CouchDB数据库
- 水平扩展性
CouchDB使用replication支持双向的复制(同步)
- MVCC(Multiversion concurrency control)
CouchDB支持多版本控制,支持多个节点写数据以避免在写入期间锁定数据库字段。系统会检测到多个写操作之间的冲突并以一定的算法法则予以解决
- 离线存储
CouchDB能够同步复制到可能会离线的终端设备,同时当设置再次在线时处理数据同步
CouchDB架构
CouchDB是分布式数据库,把存储系统分布到n台物理的节点上,并且很好地协调和同步节点之间的数据读写一致性
- CouchDB引擎
基于B树结构,数据通过键或键范围访问,这些键或键范围直接映射到底层B树操作。CouchDB引擎是管理存储内部数据、文档和视图的核心组件
- HTTP请求
用于创建索引并从文档中提取数据。它是用JavaScript编写的,允许创建由MapReduce作业组成的临时视图
- 文档
存储大量数据
- 副本数据库
用于将数据复制到本地或远程数据库以及同步设计文档
CouchDB安装和使用
访问CouchDB官方网站下载对应版本安装
CouchDB HTTP API介绍
客户端使用HTTP请求与CouchDB进行通信,从数据库检索数据、将数据以文件的形式存储到数据库中
CouchDB使用cURL创建文档
在CouchDB中创建文档,可以通过cURL实用程序向CouchDB服务器发送HTTP PUT方法请求
CouchDB在Fabric使用
Fabric状态数据库
Fabric中CouchDB是一个可选的节点状态数据库,支持以JSON格式在账本上建模数据并支持富查询。CouchDB支持在链码中部署索引,以便高效查询和对大型数据集的支持
链码中使用CouchDB富查询
Fabric中CouchDB索引使用
CouchDB中的索引用来提升JSON查询的效率
Fabric中CouchDB分页使用
Fabric支持对富查询和范围查询结果分页,CouchDB不支持limit关键字,分页是由Fabric来管理并隐式地按照pageSize的设置进行分页
分页只用于读交易中。对于需要读和写的交易,请使用不带分页的链码查询API
6-2-3 区块链关系型数据库存储
关系型数据库概念
关系型数据库Schema概念
Schema是数据库对象的集合(如表、字段、视图、索引、存储过程、子程序、触发器、数据类型、序列等)
为了区分不同的集合,给不同的集合起不同的名字,默认情况下一个用户对应一个集合。访问一个表时,没有指明该表属于哪一个schema,系统就会自动在表上加上缺省的schema名。在数据库中一个对象完整名称为schema.object
数据库中可以有多个应用的数据表,不同应用的表可以放在不同的schema之中,不同schema之间它们没有直接的关系,不同的schema之间的表可以同名,也可以互相引用(但必须有权限)。在没有操作别的schema的操作权限下,每个用户只能操作它自己的schema下的所有的表。不同的schema下的同名的表,可以存入不同的数据(即schema用户自己的数据)
不同数据库对schema的定义可能不一样,比如:
- Oracle数据库会为每个数据库用户关联一个独立的schema,schema是一组schema objects的集合。其中schema object是逻辑上的数据库存储结构(如表、视图、索引、快照、函数等)
- MySQL中schema等价于数据库,CREATE SCHEMA 是CREATE DATABASE的同义词
关系型数据库中的对象
- 表
在关系型数据库中表是一系列二维数组的集合,用来代表和存储数据对象之间的关系。
表由纵向的列和横向的行组成,例如一个有关作者信息的名为authors的表格中,每个列包含的是所有作者的某个特定类型的信息,比如“姓氏”,而每行则包含了某个特定作者的所有信息:姓、名、住址等
- 行、列
对于特定的数据库表,列的数目一般事先固定,各列之间可以由列名来识别。而行的数目可以随时、动态变化,每行通常都可以根据某个(或某几个)列中的数据来识别
- 索引
是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据
- 主键
数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的键。一个数据表只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)
- 视图
是基于SQL语句的结果集的可视化的表。
视图与数据库表不同,表是一种实体结构(Physical Structure),视图是一种虚拟结构(Virtual Structure),在实体表中的改变都可以立刻反映在视图中
- 存储过程
存储过程(stored procedure)是一组为了完成特定功能的SQL语句集合,经编译后存储在数据库中,用户通过指定存储过程的名称并给出参数来执行
关系型数据库SQL语言分类
关系型数据库SQL语言(Structure Query Language)语言共分为四大类:数据查询语言DQL,数据操纵语言DML,数据定义语言DDL,数据控制语言DCL
数据查询语言DQL
基本结构是由SELECT子句,FROM子句,WHERE子句组成的查询块,只读,不产生数据更改。
SELECT<字段名> FROM <表或视图名> WHERE <查询条件>
数据操纵语言DML
主要有三种形式:
- 插入:INSERT
- 更新:UPDATE
- 删除:DELETE
数据定义语言DDL
用来创建数据库中的各种对象:表、视图、索引、同义词、聚簇(CREATE TABLE/VIEW/INDEX/SYN/CLUSTER)
DDL操作是隐性提交的!不能rollback
数据控制语言DCL
DCL命令用于数据库用户的访问控制和权限管理
- GRANT:GRANT命令给数据库的指定组用户授予表(和其他对象)的某些权限
- DENY/REVOKE:禁止、撤销组/用户的某些权限
例如:Grant SELECT,INSERT,UPDATE,DELETE ON Employee To User1
提交数据有三种类型:显示提交、隐式提交及自动提交
- 显示提交:用COMMIT命令直接完成的提交
- 隐式提交:用SQL命令间接完成的提交。命令包含:ALTER, AUDIT, COMMENT, CONNECT, CREATE, DISCONNECT, DROP, EXIT, GRANT, NOAUDIT, QUIT, REVOKE, RENAME。
- 自动提交:把AUTOCOMMIT设置为ON,则在插入、修改、删除语句执行后,系统将自动进行提交。
区块链关系型数据库特点
区块链关系型数据库存储概述
关系型数据库性能较KV数据库低,导致区块链采用关系型数据库作为状态数据库相对不多
- 关系型数据库特点
- 支持SQL富查询,可以让写业务逻辑的开发比较容易上手,并方便审计管理
- 支持schema表结构,可以方便地查看链上数据的大小、结构等信息,如区块的大小、账户的大小
- 现有业务使用关系型数据库开发,业务迁移上链整合相对容易
- 关系型数据库限制
- 需要提前创建库表和索引,不够灵活
- 区块链需要对智能合约所读写的状态数据做严格的控制和校验,而SQL语句相对区块链来说过于灵活,难以控制
- 不支持MPT存储,无数据追溯、锚定功能
- 不支持历史版本,无法查询历史状态数据
- 不支持并发执行
长安链中的关系型数据库
长安链存储模块运行逻辑
存储模块负责持久化存储链上的区块、交易、状态、历史读写集等账本数据,并对外提供上述数据的查询功能。区块链以区块为单位进行批量的数据提交,一次区块提交会涉及到多项账本数据的提交,比如:交易提交,状态数据修改等,所以存储模块需要维护账本数据的原子性。长安链支持常用的数据库来存储账本数据,如LevelDB、MySQL等数据库,业务可选择其中任意一种数据库来部署区块链
长安链支持MySQL存储引擎,在系统数据如Block DB上支持区块元信息、交易信息的关系型语义,状态数据库支持KV的方式和智能合约编写SQL语句方式读写状态数据(world state)
长安链MySQL账本存储模式
国内长安链支持选用MySQL作为账本存储引擎,节点启动会自动创建数据库,使用chainID作为数据库名,同时也会自动创建相应的表。下图为交易表和世界状态表结构
长安链SQL合约支持
长安链支持在智能合约中直接编写SQL语句,对状态数据库(必须配置为关系型数据库类型,比如MySQL)进行操作(合约创建、调用、查询、升级)
用户合约在创建时,系统会自动创建一个新的数据库给该合约,所以不同的用户合约的状态数据以数据库进行区隔
发送交易为query交易时,只能执行DQL语句,不可执行DML语句,若含有则会报错。
在一个交易中,若执行了多条语句(DQL、DML),其中一条或多条语句(DQL、DML)执行失败、则整个交易将执行失败并会回滚到上一个savePoint
Fisco Bcos中的关系型数据库
适配模型
区块链系统提供灵活的插拔适配机制,支持底层关系型数据库
对于MySQL这种关系型数据库,则直接将存储模块的表结构对应到数据库表结构即可
使用什么存储结构,本质上不会改变区块链具有去中心化、不可篡改、不可逆、匿名等特性,可以根据业务需求选择合适的数据库引擎
Table合约CRUD接口
访问分布式存储(Advanced Mass Database,AMDB)需要使用Table合约CRUD接口,Table合约声明于Table.sol,该接口是数据库合约,可以创建表,并对表进行增删改查操作
TableTest.sol调用了AMDB专用的智能合约Table.sol,实现的是创建用户表t_test,并对t_test表进行增删改查的功能
t_test表结构如下,该表记录某公司员工领用物资和编号
6-3 蚂蚁链分布式存储服务
链原生数据管理概述
蚂蚁链针对区块链存储特有的持久化数据多版本及数据可验证要求,建立了链原生数据管理系统。提供对合约数据友好的操作能力,存储规模可扩展,可伸缩,存储引擎高性能,低成本等特性,形成蚂蚁链原生数据管理特色
合约数据服务
合约数据服务(MyBuffer)通过构建一个统一的数据模型,为智能合约提供了一个面向状态数据的完整数据视图,并且基于这个视图提供开发者所熟悉的数据结构和数据查询语言。利用系统提供的基本数据结构,合约开发者能够灵活自由地组合带有业务语义的复杂数据结构,并且无缝地映射到底层存储。
数据查询语言
利用MyBuffer接口定义语言(Interface Definition Language),合约开发者能够简单高效地对之前定义的数据结构进行访问,迭代和查询。数据服务层隐藏了底层的实现细节,大大降低了开发门槛,使得合约开发者可以把主要精力集中在自己的业务逻辑本身
兼容性
MyBuffer接口定义语言以及底层编解码格式,为合约开发者提供了数据兼容性管理机制,确保不同版本的业务数据结构之间保持兼容
其他特点
- 业务逻辑与数据分离,使得合约模块化以及代码复用成为可能;
- 数据与元数据联合上链,大大拓宽合约数据的使用场景,使得数据可以在智能合约之外,甚至是链下场景流通使用;
- 数据权限控制,赋能数据拥有者对数据读写权限进行设置,确保只有合法的用户能够获取或者修改上链数据
数据服务存储持久化
数据服务平台引入基于Schema的存储系统以比较友好的方式实现数据持久化,并支持复杂的数据结构(如map类型嵌套)
存储服务
蚂蚁链自研了分布式存储服务MyGrid,支持存储计算分离,状态数据灵活分片,集群弹性可伸缩,存储引擎可插拔,元数据高可用等,为区块链平台提供高性能低延迟的数据存储服务
计算和存储分离
蚂蚁链的计算层和存储层可分别部署独立集群。计算集群的服务进程通过存储服务SDK接入存储集群,依托高性能网络通讯框架ERPC进行数据读写访问
存储集群支持弹性扩缩容
存储集群可根据业务需要对集群进行扩容或缩容。比如系统初始化的时候,使用2个物理机器来构建一个存储集群,后来随着业务的发展,数据量越来越大,TPS越来越高,可以选择在存储集群里增加2台物理机器,提供更高的带宽和吞吐。如果过了一段时间,业务量有所下降,对带宽和吞吐要求降低,则可以把集群缩容至2台机器来降低成本
存储引擎特性
存储引擎面临挑战
现有区块链在技术和业务上面临的挑战大体主要有三个方面
- 规模
业务合约处理的账户规模可能到数十亿,账户规模越大Merkle树的层数越多;历史版本规模可能到万亿、百万亿,规模越大DB里的数据越多,level层数越大,性能会随之衰减,无法支撑业务的大规模状态数据场景
- 性能
Merkle树的更新和查询,需要从root到leaf更新/查询最多64(256/4)层节点,KVDB内部又需要多个level间compaction或者查找数据,读写流程冗长、资源消耗巨大、导致读写TPS低,延时高难以控制
- 成本
合约执行对存储介质的读写带宽要求高,数据持续写入,空间成本越来越大
蚂蚁存储引擎特点
蚂蚁链存储团队通过结合业界前沿设计以及区块链数据特点,自研了存储引擎Letus,其主要拥有以下三个巨大优势
- 大规模、高性能
4个共识节点下,20亿账户规模下转账TPS能达到10w,实测为20个worker支持20亿账户规模,最高TPS为12.2w
- 降低区块链节点成本
- 冷热数据分层
- 状态数据剪裁
- 支持数据可验证、快速多版本查询等多种能力
蚂蚁链Letus存储引擎支持任意区块号的WorldState高效查询和可验证能力
蚂蚁存储引擎成本优势
Letus引擎通过多种方式可以极大地降低区块链成本
- 空间放大相对小
Letus在区块数据的空间放大在1倍左右,状态数据在2倍左右,相比于传统MPT+RocksDB动辄几十倍的放大而言有了质的提升
- 冷热数据分层
支持数据的冷热划分,可以将不常用并且时间相对久远的区块保存在低价冷介质中,但是可以支持低频的访问
- 状态数据裁剪
通过剪裁算法后台将不需要的历史版本的状态数据删除,不影响正常出块的运行
蚂蚁存储引擎冷热数据分层能力
针对区块数据,考虑到历史区块的低频访问特性,可实现热点区块数据保存高性能存储介质中,满足业务读取以及节点数据同步的高频需求,而历史区块存在在廉价介质中降低成本,同时提供可靠访问能力。蚂蚁链Letus存储支持冷热分层能力,可以将冷数据从高价介质中迁移到更廉价的存储介质中,有效降低用户的成本。
蚂蚁链存储引擎Letus支持区块数据根据规则自动迁移冷热数据,可降低70%的存储成本,并降低运维成本