大家之前使用 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


firesWu
10 声望4 粉丝