昨天早上,EOS 1.5.0 release 版本发布了。这次比较大改动点是在多线程签名上面。它将同步区块时的 block 签名验证和 trx 签名验证都使用多线程签名验证,来节省同步所需要的时间, 但是生产区块所需要的成本是不变的,但为什么生产区块成本不变呢。接下来介绍一下具体的改动。
区块多线程签名改动:同步区块时进行多线程签名, replay 过程中依然是单线程签名。因为区块同步时需要回滚 pending block 的 trx 操作, 这块时间刚好可以用来并行处理签名, 但 replay 的时候没有这一步,即使用多线程签名也无法节省时间,反而会让主线程阻塞等待异步结果返回。
trx 多线程签名改动:同步区块以及 replay 过程都会进行多线程签名, 因为有多个 trx 要执行,所以执行 trx 的时间可以供其他 trx 的签名并行进行。 但生产区块的时候无法使用,因为执行 BP 接受到一个 广播的 trx 就立马去执行了,执行完之后才回去接受下一个广播 trx, 所以无法使用多线程签名。
代码解析:
块签名:
因为 replay 不适用多线程签名, 所以 replay 依旧沿用之前的签名代码, 而同步则使用了新的部分。
// producer_plugin.cpp 接受到广播块
void on_incoming_block(const signed_block_ptr& block) {
// ...
// start processing of block
// 调用一个线程去对块进行签名验证
auto bsf = chain.create_block_state_future( block );
// abort the pending block
// 回滚掉 pending block 的执行 trx, 这段时间刚好可以用来并发执行区块签名验证
chain.abort_block();
// ...
}
// controller.cpp
std::future<block_state_ptr> create_block_state_future( const signed_block_ptr& b ) {
//验证区块是否存在。
EOS_ASSERT( b, block_validate_exception, "null block" );
auto id = b->id();
// no reason for a block_state if fork_db already knows about block
auto existing = fork_db.get_block( id );
EOS_ASSERT( !existing, fork_database_exception, "we already know about this block: ${id}", ("id", id) );
auto prev = fork_db.get_block( b->previous );
EOS_ASSERT( prev, unlinkable_block_exception, "unlinkable block ${id}", ("id", id)("previous", b->previous) );
// 进行多线程签名
return async_thread_pool( [b, prev]() {
const bool skip_validate_signee = false;
return std::make_shared<block_state>( *prev, move( b ), skip_validate_signee );
} );
}
void push_block( std::future<block_state_ptr>& block_state_future ) {
controller::block_status s = controller::block_status::complete;
EOS_ASSERT(!pending, block_validate_exception, "it is not valid to push a block when there is a pending block");
auto reset_prod_light_validation = fc::make_scoped_exit([old_value=trusted_producer_light_validation, this]() {
trusted_producer_light_validation = old_value;
});
try {
// 获取验证结果, 当区块验证失败时会抛出异常,中止 push block
block_state_ptr new_header_state = block_state_future.get();
auto& b = new_header_state->block;
emit( self.pre_accepted_block, b );
fork_db.add( new_header_state, false );
if (conf.trusted_producers.count(b->producer)) {
trusted_producer_light_validation = true;
};
emit( self.accepted_block_header, new_header_state );
if ( read_mode != db_read_mode::IRREVERSIBLE ) {
maybe_switch_forks( s );
}
} FC_LOG_AND_RETHROW( )
}
交易签名
从改动得知,apply_block 的时候才会启动交易的多线程验证签名,而 bcast_transaction 则不会,因为并没有多余的动作可以与验证签名并行。
void apply_block( const signed_block_ptr& b, controller::block_status s ) { try {
try {
EOS_ASSERT( b->block_extensions.size() == 0, block_validate_exception, "no supported extensions" );
auto producer_block_id = b->id();
start_block( b->timestamp, b->confirmed, s , producer_block_id);
// 按顺序启动每个 trx 的多线程验证签名,生产对应公钥
std::vector<transaction_metadata_ptr> packed_transactions;
packed_transactions.reserve( b->transactions.size() );
for( const auto& receipt : b->transactions ) {
if( receipt.trx.contains<packed_transaction>()) {
auto& pt = receipt.trx.get<packed_transaction>();
auto mtrx = std::make_shared<transaction_metadata>( pt );
if( !self.skip_auth_check() ) {
std::weak_ptr<transaction_metadata> mtrx_wp = mtrx;
mtrx->signing_keys_future = async_thread_pool( [chain_id = this->chain_id, mtrx_wp]() {
auto mtrx = mtrx_wp.lock();
return mtrx ?
std::make_pair( chain_id, mtrx->trx.get_signature_keys( chain_id ) ) :
std::make_pair( chain_id, decltype( mtrx->trx.get_signature_keys( chain_id ) ){} );
} );
}
packed_transactions.emplace_back( std::move( mtrx ) );
}
}
// 执行 trx
// ...
commit_block(false);
return;
} catch ( const fc::exception& e ) {
edump((e.to_detail_string()));
abort_block();
throw;
}
} FC_CAPTURE_AND_RETHROW() } /// apply_block
// trx 执行时获取签名返回的公钥
const flat_set<public_key_type>& recover_keys( const chain_id_type& chain_id ) {
// Unlikely for more than one chain_id to be used in one nodeos instance
if( !signing_keys || signing_keys->first != chain_id ) {
if( signing_keys_future.valid() ) {
// 获取公钥,如果未签名完则阻塞等待签名完毕
signing_keys = signing_keys_future.get();
if( signing_keys->first == chain_id ) {
return signing_keys->second;
}
}
// 当没开启多线程签名时, 直接验证生成对应公钥
signing_keys = std::make_pair( chain_id, trx.get_signature_keys( chain_id ));
}
return signing_keys->second;
}
总结
从这次的改动可以看出主要优化的地方是节点同步区块的速度, 因为开启了多线程签名,所以在 block 验证以及 apply_block 时节省了一定 CPU 时间, 可供其他地方使用。 例如 EOS 现在是当线程的,所以当你进行 RPC 访问的时候,如果涉及到数据提取,主线程的同步时会暂停的,等待你的操作结束, 这样就会影响节点的同步,所以 get_table_rows API 才会限制 10 ms。 现在同步所需时间减少,降低了节点既要同步数据也要提供 RPC API 的压力。
当大家比较关注的 CPU 使用并没有得到改善, 因为多线程签名无法应该在生产区块上。所以在生产区块时, trx 执行所需要的 CPU 时间并不会减少,也就是 CPU 资源的使用并没有得到改善。