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_database
和 chain::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_database
和 block_database
的 open() 方法, 到这里我们看到了 chain::database
确实是操控 object_database
和 block_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 出一条一模一样的链.
好了, 本文就到此为止. 数据库篇后面就不写这么多了, 以后的文章会偏重于 graphene/bitshares 的应用程序架构和线程模型, 敬请期待.
Congratulations!,@ciferYou just got my free but small upvote
I invite you to the @eoscafe community [https://discord.gg/wdUnCdE]
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit
楼主,我看代码里面还有一个fork_database,它与database是什么关系
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit
fork_database 我也没有仔细看过, 不好意思..
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit