一航jason 2019-06-29
昨天早上,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 资源的使用并没有得到改善。
EOS 开发的小伙伴有技术问题可以进群讨论哟