Graphene 源码阅读 – 数据库篇 – 区块数据管理

在 bitshares 中除了对象数据需要落盘之外, 网络上接收到的区块数据也是需要落盘的, 区块数据的管理依赖的就是 chain::block_database 模块.

chain::block_database

这个模块听起来好像很复杂 – 毕竟管理着区块数据的落盘与加载, 但实际上非常简单. 它维护了两个文件流: _blocks 存储具体区块数据, _block_num_to_pos 则是索引数据. 当我们知道了区块 id 或者区块号时, 就可以从 _block_num_to_pos 查询出此区块在 _blocks 中的偏移和大小, 进而从 _blocks 中取出整个块数据.

区块数据的内存模型

区块数据在内存中由 chain::signed_block 类型表示, 但是与对象在内存中会挂到红黑树上不同, 区块数据从其他节点收到并 apply 完后就会序列化到磁盘并在内存中释放.

区块数据与区块索引的序列化与反序列化

读写 _blocks 文件流时会涉及到区块数据的序列化与反序列化, 这里依赖的也是 对象序列化 中介绍的 fc::raw::pack/fc::raw::unpack, 不再敖述.

而读写 _block_num_to_pos 流时关于区块索引的序列化反序列化就简单了, 它没有使用 fc::raw::pack/fc::raw::unpack, 而是简单粗暴的强转成字符序列:

// 代码 6.1

 78 void block_database::store( const block_id_type& _id, const signed_block& b )
 79 {
 80    block_id_type id = _id;
 81    if( id == block_id_type() )
 82    {
 83       id = b.id();
 84       elog( "id argument of block_database::store() was not initialized for block ${id}", ("id", id) );
 85    }
 86    _block_num_to_pos.seekp( sizeof( index_entry ) * int64_t(block_header::num_from_id(id)) );
 87    index_entry e;
 88    _blocks.seekp( 0, _blocks.end );
 89    auto vec = fc::raw::pack( b );
 90    e.block_pos  = _blocks.tellp();
 91    e.block_size = vec.size();
 92    e.block_id   = id;
 93    _blocks.write( vec.data(), vec.size() );
 94    _block_num_to_pos.write( (char*)&e, sizeof(e) );
 95 }

反序列过程实际上面说过了, 就是拿区块 id 或者区块号从 _block_num_to_pos 查询出此区块在 _blocks 中的偏移和大小, 然后从 _blocks 中取出整个块数据:

// 代码 6.2

184       index_entry e;
185       int64_t index_pos = sizeof(e) * int64_t(block_num);
186       _block_num_to_pos.seekg( 0, _block_num_to_pos.end );
187       if ( _block_num_to_pos.tellg() <= index_pos )
188          return {};
189
190       _block_num_to_pos.seekg( index_pos, _block_num_to_pos.beg );
191       _block_num_to_pos.read( (char*)&e, sizeof(e) );
192
193       vector<char> data( e.block_size );
194       _blocks.seekg( e.block_pos );
195       _blocks.read( data.data(), e.block_size );
196       auto result = fc::raw::unpack<signed_block>(data);
197       FC_ASSERT( result.id() == e.block_id );
198       return result;

chain::database 初始化

如果说 db::object_database 是管理对象数据, chain::block_database 管理区块数据, 那么 chain::database 就是管理 db::object_databasechain::block_database 了. 从结构上来讲 chain::database 的内容应该单独放在一篇文章里, 但是由于 block_database 模块比较简单, 篇幅太少, 而 chain::database 的内容有比较多, 估计下一篇一篇介绍不完, 所以这里就暂且先把 chain::database 的初始化部分挪到这里来. 也能让我们先对 chain::database 有个大概的认识.

chain::database 的初始化涉及到两个过程, 一个是节点实例被构造时, 同时也会构造 chain::database 实例, 而在 chain::database 构造时就会执行我们前面章节提到过的 initialize_indexes(), add_index<>() 等过程. 对应下面这条链路:

node = new app::application() => new detail::application_impl(this) => _chain_db(std::make_shared<chain::database>()

chain::database() => initialize_indexes()
                 => add_index<>()

其次就是在节点 startup 时, 会调用 chain::database::open() 方法, 这里面包含了 chain::database 初始化阶段的主要工作内容.

首先, 读取 witness_node_data_dir/blockchain/db_version 文件, 比较一下数据库版本和当前运行的版本的程序的数据库版本是否一致, 如果版本不一致或者这个文件不存在, 就会先清空对象库, 然后写入当前的版本号.

// 代码 6.3
143       bool wipe_object_db = false;
144       if( !fc::exists( data_dir / "db_version" ) )
145          wipe_object_db = true;
146       else
147       {
148          std::string version_string;
149          fc::read_file_contents( data_dir / "db_version", version_string );
150          wipe_object_db = ( version_string != db_version );
151       }
152       if( wipe_object_db ) {
153           ilog("Wiping object_database due to missing or wrong version");
154           object_database::wipe( data_dir );
155           std::ofstream version_file( (data_dir / "db_version").generic_string().c_str(),
156                                       std::ios::out | std::ios::binary | std::ios::trunc );
157           version_file.write( db_version.c_str(), db_version.size() );
158           version_file.close();
159       }

然后就是调用 object_databaseblock_database 的 open() 方法, 到这里我们看到了 chain::database 确实是操控 object_databaseblock_database 的 “上游” 模块.

// 代码 6.4

161       object_database::open(data_dir);
162
163       _block_id_to_block.open(data_dir / "database" / "block_num_to_block");

接下来, 如果这是节点的第一次启动 (说明从 object_database 中加载的对象中没有找到 global_property_object) 的话, 就要初始化创世状态, 而创世信息从里来呢? 请参考 Genesis 创世信息生成.

// 代码 6.5

165       if( !find(global_property_id_type()) )
166          init_genesis(genesis_loader());

创世过程的主要工作包括创建一些特殊和初始账户, 以及一些核心资产, 这些信息不用经过广播直接在本地出块, 因为所有其他节点也要执行要全同样的过程.

指的一提的是, 创世信息中的账户我本以为都是链上的公共账户或者委员会特殊账户之类的, 但没想到里面还有 9w 多的正常会员账户, 这些会员账户可谓是 bitshares 共链的 “创世居民”.

后记

区块中保存了整条链的原始记录, 只要区块数据正确完好, 我们就能从这些数据中 replay 出一条一模一样的链.

好了, 本文就到此为止. 感谢阅读.

赞赏

微信赞赏支付宝赞赏

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.