大家之前使用 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。