brpc源码分析--bvar组件(五)

brpc源码分析–bvar组件

brpc官方解释:bvar是多线程环境下的计数器类库,方便记录和查看用户程序中的各类数值,它利用了thread local存储减少了cache bouncing。但bvar不能代替所有的计数器,它的本质是把写时的竞争转移到了读:读得合并所有写过的线程中的数据,而不可避免地变慢了。当读写都很频繁或得基于最新值做一些逻辑判断时,不应该使用bvar。本质上是将写的竞争转移到了读,特别是在监控这种场景下,通常读是远远小于写的,因此这种转移的提升效果是非常显著的。

1 统计类

bvar::Adder举例

bvar::Adder<int> sum;
sum << 1 << 2 << 3 << 4;
sum.expose("sum");  // expose the variable globally
CHECK_EQ("10", bvar::Variable::describe_exposed("sum"));
或者
bvar::Adder<int> g_error_count("client_error_count");

这里Adder初始化继承了Reducer->Variable,最终都会显示调用 expose中的expose_impl,初始化VarMapWithLock

数据写入,重载<<运算符

template <typename T, typename Op, typename InvOp>
inline Reducer<T, Op, InvOp>& Reducer<T, Op, InvOp>::operator<<(
    typename butil::add_cr_non_integral<T>::type value) {
    // It's wait-free for most time
    agent_type* agent = _combiner.get_or_create_tls_agent();
    if (__builtin_expect(!agent, 0)) {
        LOG(FATAL) << "Fail to create agent";
        return *this;
    }
    agent->element.modify(_combiner.op(), value);
    return *this;
}

_combiner会在初始化的时候,也初始化好_id,每次都会申请static 变量(_s_agent_kinds++),全局递增。根据_id获取tls的变量agent,并将申请的数据,存到LinkNode中。 为后续读取数据的时候,能遍历所有全局数据。使用过的_id也会存放在deque队列中。

申请获取agent
 inline Agent* get_or_create_tls_agent() {
   // 第一次获取,肯定是null
   Agent* agent = AgentGroup::get_tls_agent(_id);
   if (!agent) {
     // 创建一个agent
     agent = AgentGroup::get_or_create_tls_agent(_id);
     if (NULL == agent) {
       LOG(FATAL) << "Fail to create agent";
       return NULL;
     }
   }
   if (agent->combiner) {
     return agent;
   }
   // 存储combiner和element, 当前_element_identity为int()
   agent->reset(_element_identity, this);
   // TODO: Is uniqueness-checking necessary here?
   {
     butil::AutoLock guard(_lock);
     // 将新申请的agent插入到双向链表中LinkNode
     _agents.Append(agent);
   }
   return agent;
 }

inline static Agent* get_or_create_tls_agent(AgentId id) {
  if (__builtin_expect(id < 0, 0)) {
    CHECK(false) << "Invalid id=" << id;
    return NULL;
  }
  if (_s_tls_blocks == NULL) {
    // 申请内存块
    _s_tls_blocks = new (std::nothrow) std::vector<ThreadBlock *>;
    if (__builtin_expect(_s_tls_blocks == NULL, 0)) {
      LOG(FATAL) << "Fail to create vector, " << berror();
      return NULL;
    }
    // 线程退出,执行回调_destroy_tls_blocks
    butil::thread_atexit(_destroy_tls_blocks);
  }
  // 取整
  const size_t block_id = (size_t)id / ELEMENTS_PER_BLOCK; 
  if (block_id >= _s_tls_blocks->size()) {
    // The 32ul avoid pointless small resizes.
    _s_tls_blocks->resize(std::max(block_id + 1, 32ul));
  }
  ThreadBlock* tb = (*_s_tls_blocks)[block_id];
  if (tb == NULL) {
    ThreadBlock *new_block = new (std::nothrow) ThreadBlock;
    if (__builtin_expect(new_block == NULL, 0)) {
      return NULL;
    }
    tb = new_block;
    (*_s_tls_blocks)[block_id] = new_block;
  }
  return tb->at(id - block_id * ELEMENTS_PER_BLOCK);
}

RAW_BLOCK_SIZE(4096)刚好是一个块大小,也就是一个内存页。这里(RAW_BLOCK_SIZE + sizeof(Agent) - 1) / sizeof(Agent),目的是为了sizeof(Agent)整数倍,向下取整。tb->at一开始以为是每次传入的都是整数0,当数据不能被整除非的时候,是能获取到偏移量offset的。agent->reset将当前值进行初始化

更新具体的_value值
class ElementContainer<...> {
  template <typename Op, typename T1>
  void modify(const Op &op, const T1 &value2) {
    T old_value = _value.load(butil::memory_order_relaxed);
    T new_value = old_value;
    call_op_returning_void(op, new_value, value2);
    // 如果_value不等于old_value,old_value将会被更新为_value的值
    while (!_value.compare_exchange_weak(
      old_value, new_value, butil::memory_order_relaxed)) {
      // 说明value被其他线程更新了。
      new_value = old_value;
      // new_value +=value2,在下一个循环更新_value值
      call_op_returning_void(op, new_value, value2);
    }
  }
} 

比较常规的无锁操作,使用compare_exchange_weak来进行校验更新,这里就会调用op,将值存储到_value

Variable类

Variable是所有bvar的基类,主要提供全局注册,列举,查询等功能。如果用默认参数新建bvar,并不会注册到任何全局结构,此时bvar纯粹是一个更快的计数器。把bvar注册到全局表(varmap)中的行为叫”曝光“(expose),可通过显示调用expose函数或者通过带名字的构造参数实现曝光,曝光后就可以通过ip:port/vars查看,也就是说曝光后的bvar可以通过内置服务直接查看,不需要额外的监控服务。

获取全局数据

std::string Variable::describe_exposed(const std::string& name,
                                       bool quote_string,
                                       DisplayFilter display_filter) {
    std::ostringstream oss;
    if (describe_exposed(name, oss, quote_string, display_filter) == 0) {
        return oss.str();
    }
    return std::string();
}

遍历LinkNode节点

ResultTp combine_agents() const {
  ElementTp tls_value;
  butil::AutoLock guard(_lock);
  ResultTp ret = _global_result;
  for (butil::LinkNode<Agent>* node = _agents.head();
       node != _agents.end(); node = node->next()) {
    node->value()->element.load(&tls_value);
    call_op_returning_void(_op, ret, tls_value);
  }
  return ret;
}

2 复合类型

bvar::LatencyRecorder

// bvar::LatencyRecorder是一个复合变量,可以统计:总量、qps、平均延时,延时分位值,最大延时。
bvar::LatencyRecorder g_write_latency("foo_bar", "write");
//                                      ^          ^
//                                     前缀       监控项,别加latency!LatencyRecorder包含多个bvar,它们会加上各自的后缀,比如write_qps, write_latency等等。
// write_latency是23ms
foo::bar::g_write_latency << 23;

核心继承了LatencyRecorderBase

class LatencyRecorderBase {
public:
    explicit LatencyRecorderBase(time_t window_size);
    time_t window_size() const { return _latency_window.window_size(); }
protected:
    // 平均值
    IntRecorder _latency;
    // 最大值
    Maxer<int64_t> _max_latency;
    // 分位数统计
    Percentile _latency_percentile;

    RecorderWindow _latency_window;
    MaxWindow _max_latency_window;
    PassiveStatus<int64_t> _count;
    PassiveStatus<int64_t> _qps;
    PercentileWindow _latency_percentile_window;
    PassiveStatus<int64_t> _latency_p1;
    PassiveStatus<int64_t> _latency_p2;
    PassiveStatus<int64_t> _latency_p3;
    PassiveStatus<int64_t> _latency_999;  // 99.9%
    PassiveStatus<int64_t> _latency_9999; // 99.99%
    CDF _latency_cdf;
    PassiveStatus<Vector<int64_t, 4> > _latency_percentiles;
};

LatencyRecorderBase::LatencyRecorderBase(time_t window_size)
    : _max_latency(0)
    , _latency_window(&_latency, window_size)
    , _max_latency_window(&_max_latency, window_size)
    , _count(get_recorder_count, &_latency)
    , _qps(get_window_recorder_qps, &_latency_window)
    , _latency_percentile_window(&_latency_percentile, window_size)
    , _latency_p1(get_p1, this)
    , _latency_p2(get_p2, this)
    , _latency_p3(get_p3, this)
    , _latency_999(get_percetile<999, 1000>, this)
    , _latency_9999(get_percetile<9999, 10000>, this)
    , _latency_cdf(&_latency_percentile_window)
    , _latency_percentiles(get_latencies, &_latency_percentile_window)
{}
} 

typedef AgentCombiner <GlobalPercentileSamples,
                           ThreadLocalPercentileSamples,
                           AddPercentileSamples>            combiner_type;
// 分位统计
Percentile::Percentile() : _combiner(NULL), _sampler(NULL) {
    _combiner = new combiner_type;
}

平均值,最大值都是类似统计类的使用,分位统计复杂一些,当数据写入的时候,最终会调用AddLatency进行merge合并,merge会采样保留sample的值。而后续的qps、p80、p99等都是依赖这三个变量的写入,Window会自动更新,不用给它发送数据。

写入数据

LatencyRecorder& LatencyRecorder::operator<<(int64_t latency) {
    _latency << latency;
    _max_latency << latency;
    _latency_percentile << latency;
    return *this;
}

我们看到数据的写入,只是把平均值,最大值,和分位数写入即可。之后就是对Window是如何写入的。我们以_latency_window的写入为例。

// 平均值
IntRecorder _latency; 
typedef Window<IntRecorder, SERIES_IN_SECOND> RecorderWindow;
RecorderWindow _latency_window;
_latency_window(&_latency, window_size);
// 
WindowBase(R* var, time_t window_size)
     : _var(var)
       , _window_size(window_size > 0 ? window_size : FLAGS_bvar_dump_interval) // 统计窗口默认10s
       , _sampler(var->get_sampler())
       , _series_sampler(NULL) {
    CHECK_EQ(0, _sampler->set_window_size(_window_size));
 }
class IntRecorder : public Variable {
public:
 ...
 sampler_type* get_sampler() {
   if (NULL == _sampler) {
     _sampler = new sampler_type(this);
     _sampler->schedule();
   }
   return _sampler;
 }
}

构造函数中,window_size为-1,_sampler会调用IntRecorder->get_sampler()进行创建。之后调用schedule。创建SamplerCollector,并创建线程,调用sampling_thread。 schedule会调用CombineSampler << operator();同样的也是插入到LinkNode中,把s1插到s2之前。

struct CombineSampler {
    void operator()(Sampler* & s1, Sampler* s2) const {
        if (s2 == NULL) {
            return;
        }
        if (s1 == NULL) {
            s1 = s2;
            return;
        }
        s1->InsertBeforeAsList(s2);
    }
};

定时SamplerCollector,不断聚合数据

// IntRecorder Sampler
typedef detail::ReducerSampler<IntRecorder, Stat,
                                   AddStat, MinusStat> sampler_type;
// IntRecorder combiner
typedef detail::AgentCombiner<Stat, uint64_t, AddToStat> combiner_type;
void SamplerCollector::run() {
  butil::LinkNode<Sampler> root;
  int consecutive_nosleep = 0;
  while (!_stop) {
    int64_t abstime = butil::gettimeofday_us();
    // combiner调用获取agent,返回IntRecorder::sampler_type
    Sampler* s = this->reset();
    if (s) {
      // 每次获取全量的数据后, 就插入到root节点前,后续统计
      s->InsertBeforeAsList(&root);
    }
    // 遍历统计,获取数据
    for (butil::LinkNode<Sampler>* p = root.next(); p != &root;) {
      // We may remove p from the list, save next first.
      butil::LinkNode<Sampler>* saved_next = p->next();
      Sampler* s = p->value();
      s->_mutex.lock();
      if (!s->_used) {
        s->_mutex.unlock();
        p->RemoveFromList();
        delete s;
      } else {
        s->take_sample();
        s->_mutex.unlock();
      }
      p = saved_next;
    }
    bool slept = false;
    int64_t now = butil::gettimeofday_us();
    // 内部耗时统计
    _cumulated_time_us += now - abstime;
    abstime += 1000000L;
    while (abstime > now) {
      // 一秒钟执行一次
      ::usleep(abstime - now);
      slept = true;
      now = butil::gettimeofday_us();
    }
    if (slept) {
      // 一秒内执行
      consecutive_nosleep = 0;
    } else {     
      // 一秒内数据还未统计完
      if (++consecutive_nosleep >= WARN_NOSLEEP_THRESHOLD) {
        consecutive_nosleep = 0;
        LOG(WARNING) << "bvar is busy at sampling for "
          << WARN_NOSLEEP_THRESHOLD << " seconds!";
      
    }
  }
}

reset中,调用combiner获取数据,每次采样前会将新增的sampler加到队列里InsertBeforeAsList。后遍历链表进行采样,以函数开头获取的时间为基准时间,纪录本轮耗时,并判断是否超过1s,如果没超过,sleep掉剩余的部分,如果超过了,consecutive_nosleep加1,表明本次出现了延迟,不会sleep而是直接进入了下一轮采集,并且如果达到阈值会打warning日志,没超过则consecutive_nosleep清0。

class IntRecorder {
  ...
   struct AddToStat {
   void operator()(Stat& lhs, uint64_t rhs) const {
       lhs.sum += _extend_sign_bit(_get_sum(rhs));
       lhs.num += _get_num(rhs);
     }
   };
} 

take_sample

class ReducerSampler : public Sampler {
	void take_sample() override {
        // 判断是否超过capacity
        if ((size_t)_window_size + 1 > _q.capacity()) {
            const size_t new_cap =
                std::max(_q.capacity() * 2, (size_t)_window_size + 1);
            const size_t memsize = sizeof(Sample<T>) * new_cap;
            void* mem = malloc(memsize);
            if (NULL == mem) {
                return;
            }
            // 新建一个BoundedQueue将item挪过去然后交换
            butil::BoundedQueue<Sample<T> > new_q(
                mem, memsize, butil::OWNS_STORAGE);
            Sample<T> tmp;
            while (_q.pop(&tmp)) {
                new_q.push(tmp);
            }
            //_q会在析构的时候释放空间
            new_q.swap(_q);
        }

        Sample<T> latest;
        // IntRecorder类型不想等
        if (butil::is_same<InvOp, VoidOp>::value) {
            latest.data = _reducer->reset();
        } else {
          	// 获取数据IntRecorder聚合后的_combiner数据
            latest.data = _reducer->get_value();
        }
        latest.time_us = butil::gettimeofday_us();
    		// 存入队列中
        _q.elim_push(latest);
    }
}

该函数首先会判断队列容量是否小于_window_size,如果是则扩充队列大小,实现上是新建一个BoundedQueue将item挪过去然后交换,老的队列会在析构的时候释放空间。butil::is_same<InvOp, VoidOp>::value如果为true表明没有InvOp,也就是不能逆向,比如maxer miner这种reducer。而有inverse op的,以adder为例,每次记录当前值就行,不需要重置,取值的时候只需首尾要减一下即可。最后通过elim_push将sampler存入队列,elim_push在push前会检测是否full,如果是会pop掉队首。

获取具体latency数据

class LatencyRecorder  {
  ...
  int64_t latency() const
   // sample->get_value()
	{ return _latency_window.get_value().get_average_int(); }
}

typedef detail::ReducerSampler<IntRecorder, Stat,
                                   AddStat, MinusStat> sampler_type;
bool get_value(time_t window_size, Sample<T>* result) {
  if (window_size <= 0) {
    LOG(FATAL) << "Invalid window_size=" << window_size;
    return false;
  }
  BAIDU_SCOPED_LOCK(_mutex);
  if (_q.size() <= 1UL) {
    // We need more samples to get reasonable result.
    return false;
  }
  // 获取10s前的数据
  Sample<T>* oldest = _q.bottom(window_size);
  if (NULL == oldest) {
    oldest = _q.top();
  }
  Sample<T>* latest = _q.bottom();
  DCHECK(latest != oldest);
  if (butil::is_same<InvOp, VoidOp>::value) {
    // No inverse op. Sum up all samples within the window.
    result->data = latest->data;
    for (int i = 1; true; ++i) {
      Sample<T>* e = _q.bottom(i);
      if (e == oldest) {
        break;
      }
      _reducer->op()(result->data, e->data);
    }
  } else {
    // Diff the latest and oldest sample within the window.
    result->data = latest->data;
    // 最近值减去10秒前的值
    _reducer->inv_op()(result->data, oldest->data);
  }
  result->time_us = latest->time_us - oldest->time_us;
  return true;
}

这里_latency_window.get_value()获取总的数据,底部数据-10秒前底部数据。这个时间段内的总的数据,在求平均get_average_int获取到最终latency的数据

其他关于cpu、内存、io统计

struct rusage stat;
int result = getrusage(RUSAGE_SELF, &stat);
// 获取CPU时间
double cpu_time = stat.ru_utime.tv_sec + stat.ru_utime.tv_usec / 100000
  0.0;
// 获取最大内存使用量
long max_memory = stat.ru_maxrss;
// 获取I/O操作次数
long io_operations = stat.ru_inblock + stat.ru_oublock;

总结

bvar中很重要的一个组件,bvar除了是一个性能优秀的计数器,设计的最重要的使用场景就是监控,通过了解bvar::Adder和bvar::LatencyRecorder两个类,基本就能比较好的掌握其设计。从源码也能看出为了追求高性能做了很多优化。 关注我,我是dandyhuang。也可wx收dandyhuang_,有什么问题我们可以一起探讨交流。

reference

bvar官方文档

[brpc源码解析(十)—— 核心组件bvar详解(1)简介和整体架构](

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值