EOS 源码解析 什么是 read only 模式

邓铜
• 阅读 2513

  大家之前使用 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 开发者社区微信群哦。
EOS 源码解析 什么是 read only 模式

转载请注明来源:https://eos.live/detail/18718

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表(关于日期时间时分秒显示不出来)
在使用皕杰报表设计器时,数据据里面是日期型,但当你web预览时候,发现有日期时间类型的数据时分秒显示不出来,只有年月日能显示出来,时分秒显示为0:00:00。1.可以使用tochar解决,数据集用selecttochar(flowdate,"yyyyMMddHH:mm:ss")fromtablename2.也可以把数据库日期类型date改成timestamp
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这