步入币圈大门后,除了钱包应用之外,用户最先接触的应该还有区块链浏览器。区块链浏览器不同于电脑和手机上浏览网页用的浏览器软件,而是指一个网站可以查询区块链上的具体信息。比如,给定区块高度,可以查询该高度区块的创建时间,包含了多少交易;给定一个地址,可以查询余额,该地址的所有交易记录等。当前以太坊上的数据量级已达亿级,如何进行数据持久化和查询呢?本文以以太坊为例,对区块链浏览器原理及存储细节进行分析总结。
以太坊上 Native Token 交易,就是 ETH 交易,不过以太坊支持智能合约,开发者和机构可以在以太坊上创建合约并发行自己的通证,通过调用合约实现自己发行通证的转账,查询余额等。不同开发者可以分别为自己的合约编写不同的函数名来实现通证转移,那么问题来了,如果钱包类应用要兼容多家的通证,要分别知道他们是用了什么函数名,这么一来通用性较差。为了解决这个问题,以太坊社区制定了 ERC20 标准,该标准规范了通证合约的接口,比如,大家都要把合约转账的函数名编写为 transfer,如果转账成功要提交 transfer event,这么一来,钱包类应用只实现 ERC20 标准里规定的内容,就可以接入符合 ERC20 协议的通证了。
从 Token 的角度来看,以太坊上有 ETH Token,以及基于合约实现的 ERC20 和其他标准的 Token。交易为 ETH 转账交易,ERC20等 Token 转账交易。从合约的角度来看,交易有创建合约和调用合约两种交易类型,其中 ERC20 转账交易是通过调用合约的转账函数实现的。
如果要解析区块数据,那么问题来了,我们从哪里获取区块数据呢?
常见的方法有三种:
分析完交易类型和数据源之后,我们在这里使用 RPC 解析数据的方式,作为 MVP 实现,我们只解析以下交易类型:
当全节点同步完成后,我们从第一个区块依次向后获取每个区块的信息,以及获取区块中的所有交易,对交易进行解析,解析完成的数据存入数据库中,用作查询使用。
区块链本身是个多叉树结构,之所以称为链是因为大家只认可从根节点到子节点最长的路径,以最长链上的块为主块,在区块链产生区块的过程中,会产生很多叔块,因此在解析的过程中要一直沿着最长链的方向进行解析,那么如何保证最长链呢?
因为这里数据源依赖的是全节点 RPC 接口,全节点本身已经维护了最长链,因此,我们无需复杂的回溯逻辑,只需要对比已经解析高度为 h 的区块和全节点中高度为 h 的区块 hash 是否一致即可,如果一致证明本地解析的链为最长链,具体算法可描述为:
在说数据持久化之前,我们先来看看面对的是什么量级的数据,以及需要对数据的查询分析需求:
常见的持久化数据库选择有以下类型:
接下来来分析一下上述数据库的优劣。
图 1 聚簇索引 B+Tree 示意图
以 MySQL 为代表的关系型数据库,对事务支持最好,其中 innoDB 引擎,采用的是 B+Tree 索引,有效减少了读取磁盘数据时 IO 次数,在数据表建立后,建立主键索引,数据实体存储在该索引的叶子节点上,如图 1 所示,主键查询数据时首先在主键索引树中查询数据所在叶节点,然后访问该位置读取数据;当查询非索引字段时进行全表扫描,当数据量大时,速度非常非常非常慢,当然,可以为查询字段建立索引,当查询多个字段时,可以建立联合索引,联合索引为非聚簇索引,查询到结果后需要回表,回表又需要在聚簇索引中进行一次查询,当数据量较小时,MySQL 的 B+Tree 索引树们看着是矮瘦的样子,查询速度较快;当数据量较大时,这些树全都变成了高胖大汉,查询效率自然降低。在早起的开源浏览器 Bitcoin ABE 中,便是使用的 MySQL,存储在单库单表中,不知道现在还能否使用。
对于关系型数据库的存储结构设计,大致可以使用区块表存储所有区块信息,交易表存储所有交易,以查询某个地址所有交易为例,需要在交易表中查询 from 和 to 为该地址的交易。
针对 MySQL 存储和查询当然也有解决方案,可以水平分库分表,比如区块表,交易表可以按照区块和交易 hash 取模进行分库分表;或者交易表可以按照不同的交易类型进行分库分表;查询时,分别从每个库或者每个表中查询数据,最后进行汇总。这么做说起来容易,可是分库分表对应用不透明,需要应用负责实现这些逻辑,实现起来有工作量。
非关系型数据库读写速度快,对事务支持没有 MySQL 那么优秀,在全节点中,BTC,ETH 等使用 LevelDB,CKB 等使用了 RocksDB。在节点解析中 Bitcore 使用了 MongoDB,BlockBook 使用了 RocksDB 。其中 MongoDB 支持分布式存储,LevelDB 与 RocksDB 则不支持分布式存储。
对于 LevelDB 与 RocksDB 这种 K-V 型数据库而言,存储结构可以使用区块 hash 作为键值存储区头信息以及所有交易 hash;使用交易 hash 作为键值存储具体交易内容,使用地址作为 key 存储该地址相关的所有交易 hash;以查询某个地址所有交易为例,首先根据地址查询所有交易 hash,然后使用每个交易 hash 查询具体交易信息。MongoDB 虽然使用 K-V 格式存储,但是针对索引字段支持额外的 B-Tree 索引。
使用 K-V 进行查询时,查询速度快,不像 MySQL 那样需要从 B+Tree 查询,可以使用 key 直接定位数据位置;但是短板也很明显,查询时需要将该 key 下所有数据加载到内存,如需分页等操作,实质上也是查询出所有数据,在内存中进行分页。当然也有可以想到的解决方案,针对地址 address 存储交易时,由于解析是按照时间顺序解析,存储格式可设定为 key = address + page1 进行存储固定 size 交易 hash,当超过该 size 是启用 key = address + page2 进行存储,这样查询时便可以进行分页查询,应用需要实现这个分片逻辑。
ElasticSearch 不支持事务,使用倒排索引,如图 2 所示,通过字典书管理 Term,每个 Term 下存储包含该 Term 的所有文档的 docId 。MySQL 为了减少磁盘 IO 和 Random Access 使用 B+Tree 组织数据,ElasticSearch 通过 Term Dictionary 分块减少 Random Access。为了防止多条件查询时查询出每个条件对应的 docId,然后利用 Skip List 进行 docId 集合合并,范围型查询使用 BKDTree 进行索引。天生支持横向扩容,数据可分布在从节点分片内,查询时主节点从多个从节点查询然后在内存中合并,这个过程由集群处理,对于应用程序来说是无感知的。而且支持设定 Index Lifecycle Policies,自动拆分 Index。
这么来看,ElasticSearch 在区块链数据存储和查询的场景,整体来看内部支持横向分片,查询性能好,且内置了各种聚合功能,似乎综是个比较好的选择。在 EOS 历史数据解析中,Hyperion-History-API 项目选择了 Elasticsearch 作为存储。
在解析数据的过程中,包含区块数据读取解析和数据持久化写入的过程,整个解析器的瓶颈在于数据读取解析速度和数据写入速度。为了均衡二者,常见的方案是在数据读取解析和数据写入之间搭建缓冲管道,整体思路如下:
如何保证数据的完整性呢?当数据读取和数据写入出错时如何处理?
解析器如何优雅的退出(确保区块数据完整):
因为异常退出,或需要检查数据完整性:
亿级数据如何快速查找缺少的高度和重复的高度?
基于分而治之思想快速完整性检查:假设从高度 h 到 h - N 查询区块数量,如果数量等于 N,认定不缺少或不重复(解析过程中发现出现了 Elasticsearch 提交成功,也为报错,但是没有存储上的区块,未遇到重复区块)。在该假设的基础上,当全部区块数和最高区块数不匹配时,N 取 10万,从最大高度向下检查,当不满足查询的区块数等于 N 时,从 h 到 h - N / 2 和 h - N / 2 + 1 到 h - N进行二分区块数判断,当二分区间为 N / 8 时查询所有数据进行遍历。
全量完整性检查:每次取 1 万高度,进行缺少重复判断。
如何优雅的使用 ElasticSearch 进行数据存储?常见的使用 ElasticSearch 进行数据分析的方式是数据存储到关系型数据库中,然后从关系型数据库中同步到 ES 中。考虑到区块解析无需事务支持,仅包含写入操作(完整性检查和处理分叉时包含删除),可以直接将数据存储到 ES 中,参考 Hyperion-History-API 项目,可以进行以下设计:
数据查询上,可以利用 ES 丰富的查询规则实现查询,以满足功能需求,具体细节不再赘述。
共同步了 967 万区块头信息,6 亿 8 千万交易信息。
为了方便查询和展示,使用 Kibana 进行数据查询和可视化,后续根据需求通过 ES 丰富的查询功能制定相应接口即可。
可以根据高度和 hash 进行查询区块数据和对应的交易数据。区块文档数和高度一致,证明没有缺失区块和重复区块。
可以查询某个地址/合约指定日期内交易等。
我们记录 type 1 为 ETH 交易,type 2 为 ERC20 转账,type 3 为合约创建。五年来 3 种交易类型整体趋势如下如。
图 10 以太坊合约创建趋势
图 11 五年内三种交易类型占比
可见链上 ERC20 交易已经基本与 ETH 交易一样多,下面是两年内三种交易类型占比,ERC20 转账数量已经超过 ETH 转账数量。
图 12 两年内三种交易类型占比
下图为两年内 ERC20 交易前十的 Token,USDT 稳坐第一。
图 13 两年内前十交易量的 Token
我们也可以查询某个地址拥有的所有 ERC20 Token 类型及交易数量,比如 0x6465349f1a53ba0097d9aac3f6ef293bdd10cae1 共拥有 71 种 ERC20 Token,合约地址如下。
图 14 某地址拥有的所有 ERC20 种类
3.12 日晚上发生了暴跌,我们来看下一周内的 ETH 大额转账趋势,3.12 号 19:00 出现了一笔 14 万 7 千的 ETH转账。
图 15 ETH 大额转账趋势
最近一周 USDT 大额额转账较多,其中 13 日交易 hash 为 0xd30eeca47682a2f35119c3a465e998b68cde1e94c317eee5851859cb6a6d1c44 进行了高达 7580 万的 USDT 转账。
基于链上全量交易数据,我们还可以做更多数据统计,篇幅限制,不再演示。
本文以以太坊区块链为例,对如何设计一款区块链浏览器从数据来源,完整性检查,数据持久化等方面进行了分析,最后通过解析全量数据对交易进行了统计分析。对于比特币为代表的 UTXO 类型区块链数据解析,方法一致,存储结构进行相应改变即可。