本記事は、本人の許可を得て翻訳する内容です。
翻訳元:EOS LIVE - 发现 EOS 最热门产品及最新优质资讯
昨日の朝(訳者:2018/12/05) EOS 1.5.0
リリースされました。このバージョンの大きい変更点は、マルチスレッドサインのところです。ブロック同期する際の block
サイン検証と trx
サイン検証処理をマルチスレッドにすることで、ブロック同期の時間を短縮しました。ただ、ブロック生成のコストは変わっていません。何故でしょうか。具体的な変更点を紹介します。
マルチスレッドサイン検証の変更
ブロック同期する際、マルチスレッドでサインしますが、replay
の処理ではまだシングルスレッドのままです。それは、ブロック同期する際は pending block
の trx
操作をロールバックする必要があるからです。この時間は、並列でサインを処理することができますが、 replay
の時のこのステップがないので、マルチスレッドでサインしても時間を短縮できませんし、メインスレッドが非同期処理結果を待たないと行けません。
trx
マルチサイン変更
ブロックの同期および replay
処理ともマルチスレッドでサインするようになります。複数の 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
時間は減りません。