thisisid 2019-06-29
大家之前使用 mongodb_plugin 、mysql_plugin 或其他数据持久化插件的时候,可能会发现 transaction 和 trace 的数据重复duplicate ( 多机环境下)。 在最初的时候只能在持久化的时候做去重处理,但 EOS 之后已经推出了 read only 模式,可以避免数据出现 duplicate 的情况, 但笔者发现很多人不知道有这种模式,也不清楚这种情况的发生由来。接下来我们来分析下为什么会出现这种情况,以及 read only 模式起到了什么作用。
首先 read only 模式不能用于出块节点,所以我们以一个同步节点的立场来讲述。
写一个持久化插件,我们必须要有数据源,也就是这几个信号,我们从这里获取数据,这里使用的是观察者模式,每当信号源有新数据 emit 的时候就会调用我们定义的函数,具体观察者模式的实现在这里就不描述了,参考 mongodb_plugin 代码。
signal<void(const signed_block_ptr&)> pre_accepted_block; signal<void(const block_state_ptr&)> accepted_block_header; signal<void(const block_state_ptr&)> accepted_block; signal<void(const block_state_ptr&)> irreversible_block; signal<void(const transaction_metadata_ptr&)> accepted_transaction; signal<void(const transaction_trace_ptr&)> applied_transaction; signal<void(const header_confirmation&)> accepted_confirmation;
出现重复的会是 accepted_transaction 和 applied_transaction 这个信号源,所以我们重点介绍它。
我们会在 controller.push_transaction 发现这两个函数的触发。
transaction_trace_ptr push_transaction( const transaction_metadata_ptr& trx, fc::time_point deadline, uint32_t billed_cpu_time_us, bool explicit_billed_cpu_time = false ) { // ... // call the accept signal but only once for this transaction if (!trx->accepted) { trx->accepted = true; emit( self.accepted_transaction, trx); } emit(self.applied_transaction, trace); // ... } /// push_transaction
OK, 看到这一步我们就知道 push_transaction 执行了 2 次同样的 trx 才会导致这2个信号 duplicate。
为什么会执行 2 次呢?
trx 是通过什么来广播的呢, 块广播以及交易广播, 那我们从这入手。
交易广播
每个节点会接受全网上的交易,尝试执行, 如果成功,则他继续向其他节点广播这个交易
// net_plugin.cpp void net_plugin_impl::handle_message( connection_ptr c, const packed_transaction &msg) { // ... // read only 模式不接受广播交易 if( cc.get_read_mode() == eosio::db_read_mode::READ_ONLY ) { fc_dlog(logger, "got a txn in read-only mode - dropping"); return; } // ... // 接受交易, 并执行 push transaction spatcher->recv_transaction(c, tid); chain_plug->accept_transaction(msg, [=](const static_variant<fc::exception_ptr, transaction_trace_ptr>& result) { if (result.contains<fc::exception_ptr>()) { peer_dlog(c, "bad packed_transaction : ${m}", ("m",result.get<fc::exception_ptr>()->what())); } else { auto trace = result.get<transaction_trace_ptr>(); if (!trace->except) { fc_dlog(logger, "chain accepted transaction"); dispatcher->bcast_transaction(msg); return; } peer_elog(c, "bad packed_transaction : ${m}", ("m",trace->except->what())); } dispatcher->rejected_transaction(tid); }); } // chain plugin.cpp void chain_plugin::accept_transaction(const chain::packed_transaction& trx, next_function<chain::transaction_trace_ptr> next) { // 相当于往该节点 push transaction my->incoming_transaction_async_method(std::make_shared<packed_transaction>(trx), false, std::forward<decltype(next)>(next)); }
第一次 push_transaction 的执行找到啦。
块广播
接下来看块广播, 在网络上广播的交易,最终是会被出块节点打包( 执行失败的例外),每个节点都要去同步块, 接受一个打包好的区块,执行 apply_block 函数。
void apply_block( const signed_block_ptr& b, controller::block_status s ) { try { try { // ... // 多线程签名 // ... transaction_trace_ptr trace; size_t packed_idx = 0; // 执行块上的交易,更新该节点的状态 for( const auto& receipt : b->transactions ) { auto num_pending_receipts = pending->_pending_block_state->block->transactions.size(); if( receipt.trx.contains<packed_transaction>() ) { trace = push_transaction( packed_transactions.at(packed_idx++), fc::time_point::maximum(), receipt.cpu_usage_us, true ); } else if( receipt.trx.contains<transaction_id_type>() ) { trace = push_scheduled_transaction( receipt.trx.get<transaction_id_type>(), fc::time_point::maximum(), receipt.cpu_usage_us, true ); } else { EOS_ASSERT( false, block_validate_exception, "encountered unexpected receipt type" ); } // ... } //... return; } catch ( const fc::exception& e ) { edump((e.to_detail_string())); abort_block(); throw; } } FC_CAPTURE_AND_RETHROW() } /// apply_block
第二次执行 push transaction 也找到啦。
也就是一个 trx 在传播到该节点的时候会被执行一次,trx 被打包后跟随区块到该节点又会被执行一次, 这就造成 accepted_transaction 和 applied_transaction 这两个信号重复,导致重复数据的产生。
解决问题
问题找到了,接下来解决问题。
出现两次调用 push_transaction 的操作,那么肯定要禁掉其中一个,才会使信号只触发一次,那同步区块的步骤肯定不能禁掉, 块广播和交易广播,我们只能选择禁止交易广播的执行,所以为什么出块节点不能用 read only 模式( ps: 交易广播都被你禁掉了,我还怎么打包区块???黑人问号脸)
交易广播有 2 个途径一个是接受链上的交易传播, 一个是通过 chain_api_plugin 的 push_transaction API 推送,所以禁掉这两个就可以了。没错, read only 模式的作用就是禁止 2 途径。
// net_plugin.cpp void net_plugin_impl::handle_message( connection_ptr c, const packed_transaction &msg) { // ... // read only 模式不接受广播交易 if( cc.get_read_mode() == eosio::db_read_mode::READ_ONLY ) { fc_dlog(logger, "got a txn in read-only mode - dropping"); return; } // ... // 接受交易, 并执行 push transaction spatcher->recv_transaction(c, tid); chain_plug->accept_transaction(msg, [=](const static_variant<fc::exception_ptr, transaction_trace_ptr>& result) { if (result.contains<fc::exception_ptr>()) { peer_dlog(c, "bad packed_transaction : ${m}", ("m",result.get<fc::exception_ptr>()->what())); } else { auto trace = result.get<transaction_trace_ptr>(); if (!trace->except) { fc_dlog(logger, "chain accepted transaction"); dispatcher->bcast_transaction(msg); return; } peer_elog(c, "bad packed_transaction : ${m}", ("m",trace->except->what())); } dispatcher->rejected_transaction(tid); }); } // controller.cpp transaction_trace_ptr controller::push_transaction( const transaction_metadata_ptr& trx, fc::time_point deadline, uint32_t billed_cpu_time_us ) { validate_db_available_size(); // 如果是 read only 模式即中断 EOS_ASSERT( get_read_mode() != chain::db_read_mode::READ_ONLY, transaction_type_exception, "push transaction not allowed in read-only mode" ); EOS_ASSERT( trx && !trx->implicit && !trx->scheduled, transaction_type_exception, "Implicit/Scheduled transaction not allowed" ); return my->push_transaction(trx, deadline, billed_cpu_time_us, billed_cpu_time_us > 0 ); }
嗯,问题来了,如何开启 read only 模式呢。
很简单,在config.ini 加上read-mode = read-only 即可。
总结:
accepted_transaction 和 applied_transaction 信号重复的原因在于 trx 被执行了两次,即块广播与交易广播,所以禁止交易广播即可, 但此时节点只供读取数据,不能写入数据。所以如果节点要来提供 push_transaction 这个 http api 的话不能开启此模式。
trx 通过交易广播在非出块节点执行是为了验证该 trx 是否能合法执行,如果不能,则该节点不会向网络传播该交易
为什么单机模式不会出现信号重复,因为单机节点只有一个,不会出现块传播,只有交易传播。
如果你要写持久化插件,记得开启 read only 模式,或者在持久化的时候去重。
有任何疑问或者想交流的朋友可以加 EOS LIVE 小助手,备注 eos开发者拉您进 EOS LIVE DAPP 开发者社区微信群哦。
转载请注明来源:https://eos.live/detail/18718