文章目录
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
[brpc源码解析(十)—— 核心组件bvar详解(1)简介和整体架构](