EOS用户资源管理resource_limits深入解析

简介

eos中用户资源分3类,1)ram,2)cpu,3)net,其中ram资源的获取主要通过变种的Bancor算法实现ram的自动定价,去中心化交易,具体的实现可以查看eosio.system合约中的exchange_state.cpp,用户的cpu跟net资源通过抵押eos来获取,抵押eos形成抵押权重相应为cpu_weight,net_weight,抵押的越多可以获取的资源越多,这跟ram不一样,ram可以使用的多少是通过eos购买,而net跟cpu是通过eos抵押。

跟resource_limits相关的四张表

这个4张表为:

void resource_limits_manager::add_indices() {
   _db.add_index<resource_limits_index>();
   _db.add_index<resource_usage_index>();
   _db.add_index<resource_limits_state_index>();
   _db.add_index<resource_limits_config_index>();
}
  • resource_limits_index的表结构定义如下:
   struct resource_limits_object : public chainbase::object<resource_limits_object_type, resource_limits_object> {

      OBJECT_CTOR(resource_limits_object)

      id_type id;
      account_name owner;
      bool pending = false;

      int64_t net_weight = -1;    // 自己抵押的eos+别人给你抵押的eos
      int64_t cpu_weight = -1;    // 自己抵押的eos+别人给你抵押的eos
      int64_t ram_bytes = -1;     // 最大可用的内存

   };
  • resource_usage_index表结构定义如下:
   struct resource_usage_object : public chainbase::object<resource_usage_object_type, resource_usage_object> {
      OBJECT_CTOR(resource_usage_object)

      id_type id;
      account_name owner;

      usage_accumulator        net_usage;        // net的指数移动平均累加器
      usage_accumulator        cpu_usage;       // cpu的指数移动平均累加器

      uint64_t                 ram_usage = 0;         // 已使用的内存
   };
  • resource_limits_state_index的表结构定义为resource_limits_config_object,该表定义了块的cpu跟net的限制参数和用户cpu和net的window_size
  • resource_limits_config_index的表结构定义如下:
class resource_limits_state_object : public chainbase::object<resource_limits_state_object_type, resource_limits_state_object> {
      OBJECT_CTOR(resource_limits_state_object);
      id_type id;

      usage_accumulator average_block_net_usage;     // block的net指数移动平均累加器
      usage_accumulator average_block_cpu_usage;    // block的cpu指数移动平均累加器

      uint64_t pending_net_usage = 0ULL;            // 记录pending block的net总和(包含在该块中的所有trx net使用相加)
      uint64_t pending_cpu_usage = 0ULL;          // 记录pending block的cpu总和(包含在该块中的所有trx cpu使用相加)

      uint64_t total_net_weight = 0ULL;            // 所有用户的net_weight相加和
      uint64_t total_cpu_weight = 0ULL;          // 所有用户的cpu_weight相加和
      uint64_t total_ram_bytes = 0ULL;           // 所有用户的ram_bytes相加和

      uint64_t virtual_net_limit = 0ULL;          // 虚拟net最大可使用量/block,只有当所有用户都最大限度使用他们的net时它才是虚拟的
      uint64_t virtual_cpu_limit = 0ULL;         // 虚拟cpu最大可使用量/block,只有当所有用户都最大限度使用他们的cpu时它才是虚拟的

   };

: 关于上述结构体中定义的指数移动平均累加器usage_accumulator 类型,其实它本质上就是K线图中的均线,你可以把resource_limits_state_object 中的pending_net_usage看成是net的使用在块序列上的均线

用户资源的变化

eos中的net跟cpu资源不像ram,net跟cpu资源不需要购买而是靠用户抵押eos来租用这些资源,并且用户已使用的net跟cpu资源会随着时间推移成指数衰减(因为net_usage,cpu_usage的定义都是usage_accumulator )。例如假设usera目前有10k的net最大可用资源(相对一个窗口而言),并且usera已经使用了9k,那么还剩1k,但是随着时间的推移,你最终会发现usera的已使用资源变成了0k,也就是说用户net跟cpu的资源限制只作用在一段时间内,例如上述usera一段时间内net的最大使用值限制在10k以内,这个时间段称为window_size。

  • 用户资源最大限制的设置
    set_account_limits函数用来对一个用户的ram,net,cpu资源最大使用进行设置,该函数主要是在eosio.system系统合约买卖内存,更改bw之后通过调用set_resource_limits函数被调用
bool resource_limits_manager::set_account_limits( const account_name& account, int64_t ram_bytes, int64_t net_weight, int64_t cpu_weight) 
  • 用户资源的使用
    1)ram资源的使用,任何涉及到使用内存的地方最终都会调用add_pending_ram_usage函数,把内存的使用多少记在某个账号上
void resource_limits_manager::add_pending_ram_usage( const account_name account, int64_t ram_delta ) {
...
   _db.modify( usage, [&]( auto& u ) {
     u.ram_usage += ram_delta;
   });
}

2)cpu和net资源的使用,每一笔trx执行成功之后都会调用add_transaction_usage,目的就是更改相应用户的resource_usage_index表中的net_usage和cpu_usage,如果已经超出用户的最大可以使用的资源值则会抛出异常

void resource_limits_manager::add_transaction_usage(const flat_set<account_name>& accounts, uint64_t cpu_usage, uint64_t net_usage, uint32_t time_slot ) 
  • 用户资源的换算
    上面用户的resource_limits_object 中记录的只是cpu跟net的权重cpu_weight,net_weight ,那么究竟如何把权重换算到cpu跟net的带宽呢? 这里以cpu为例(net同理)
auto max_user_use_in_window = (virtual_cpu_capacity_in_window * cpu_weight) / all_user_weight;

max_user_use_in_window:窗口内用户可以使用的最大cpu时间
可以看到max_user_use_in_window 跟all_user_weight成反比,all_user_weight为所有用户的cpu_weight相加的和,这里virtual_cpu_capacity_in_window表示窗口内所有用户可以使用的cpu时间数,这个值等于

uint128_t window_size = config.account_cpu_usage_average_window
uint128_t virtual_cpu_capacity_in_window = (uint128_t)(elastic ? state.virtual_cpu_limit : config.cpu_limit_parameters.max) * window_size;

elastic是否为弹性,一般都为true,那么可以看成 virtual_cpu_capacity_in_window = state.virtual_cpu_limit * window_size,上述可以看到window_size它等于config.account_cpu_usage_average_window,这个值在resource_limits_private.hpp中定义,它最终等于24 * 60 * 60 * 2,因为出块是每0.5s出一块,所以这个窗口大小其实是正好是一天,也就是说virtual_cpu_capacity_in_window表示一天内所有用户可以使用的虚拟cpu时间数,之所以是虚拟的是因为当所有用户都这么去做时系统根本没有这么多的cpu时间,这就好像银行的挤兑,当大家在同一时间去银行提现时银行根本没有那么多的现金给你。接下来我们讲讲的state.virtual_cpu_limit是怎么来的,virtual_cpu_limit你可以简单的把它看成是当前块可以使用的最大虚拟cpu

  • virtual_cpu_limit
    virtual_cpu_limit的更新在函数resource_limits_manager::process_block_usage(uint32_t block_num)中,该函数会在每产生一个区块后都会调用,具体的更新函数为update_virtual_cpu_limit
static uint64_t update_elastic_limit(uint64_t current_limit, uint64_t average_usage, const elastic_limit_parameters& params) {
   uint64_t result = current_limit;
   // params.target 为20000us 即0.02s
   if (average_usage > params.target ) {
      result = result * params.contract_rate;    // 缩小比例 99/100
   } else {
      result = result * params.expand_rate;    // 扩大比例 1000/999
   }
  // params.max 为200000us 即0.2s
   return std::min(std::max(result, params.max), params.max * params.max_multiplier);
}

void resource_limits_state_object::update_virtual_cpu_limit( const resource_limits_config_object& cfg ) {
   virtual_cpu_limit = update_elastic_limit(virtual_cpu_limit, average_block_cpu_usage.average(), cfg.cpu_limit_parameters);
}

上述cfg.cpu_limit_parameters这个值我先直接给出它为{20000(块的缩放参考cpu时间), 20000(块最大cpu时间0.2s)0, 120(average_block_cpu_usage的窗口大小), 1000, {99, 100}(缩小比例), {1000, 999}(扩大比例)},至于这个值是怎么来的,可以通过./cleos -u http://api.eosnewyork.io get table eosio eosio global命令来获取global参数之后代入得到

average_usage:它等于传入的参数average_block_cpu_usage.average(),它表示块cpu使用均线的当前值

update_elastic_limit函数的大体逻辑,如果average_usage > params.target (0.02s),那么对当前virtual_cpu_limit进行缩小,反之则扩大,通过return std::min(std::max(result, params.max), params.max * params.max_multiplier); 可以发现事实上virtual_cpu_limit 最小值为params.max 即0.2s,然后它会根据average_usage是否大于params.target 进行缩放但最大不会超过params.max * params.max_multiplier 即200s,最终 0.2s <= virtual_cpu_limit <= 200s,然而实际上真正的块时间average_usage是不可能超过params.max的(因为在add_transaction_usage函数中有EOS_ASSERT( state.pending_cpu_usage <= config.cpu_limit_parameters.max, block_resource_exhausted, "Block has insufficient cpu resources" );)
所以说这个是virtual,虚拟的cpu限制,不过我们可以把virtual_cpu_limit 看成一个指标,它是一个对cpu使用程度的指标,这个值越大说明cpu越空闲,所以max_user_use_in_window 即用户看到的你可以使用的最大cpu使用时间,没错这确实是你可以使用的最大cpu时间,你完全可以去这么做去使用掉这些cpu时间完全没有问题,但问题是其他人没有跟你一样也在同一个窗口中去使用这些cpu时间,如果其他人也这么做了,那么对不起根本没有这么多cpu时间,这个很有意思就像银行的挤兑,我们知道系统中块的最大cpu使用时间为0.2s,然后0.2s <= virtual_cpu_limit <= 200s,virtual_cpu_limit等于0.2s的时候说明系统已经100%繁忙,这个情况下每个用户能使用的cpu时间为max_user_use_in_window 并且可以同时在同一个窗口内使用,就好比银行如果它的存款准备金率是100%那么就不怕挤兑,同理virtual_cpu_limit>0.2s 意味着系统对cpu时间进行了增发(对应银行存款准备金率小于100%),这里之所以可以这么做的前提跟其实跟银行一样,人们不会在同一时刻来消费他们的所有cpu时间(在银行中就是挤兑)。

最终 virtual_cpu_limit它是什么,它就是块cpu时间的增发,大家想想如果不增发,那么这0.2s块cpu时间按照cpu_weight/total_cpu_weight 分摊每个用户得到的cpu时间恐怕都不够执行一笔最简单的交易

扫描二维码关注公众号,回复: 4863179 查看本文章


转自:https://www.jianshu.com/p/51d2ba3f677c

猜你喜欢

转载自blog.csdn.net/yhc166188/article/details/84862911