2021SC@SDUSC BRPC代码分析(一) —— bvar综述、variable类详解

2021SC@SDUSC


一、bvar的背景知识学习

        bvar是多线程环境下的计数器类库,方便记录和查看用户程序中的各类数值,它利用了thread local存储减少了cache bouncing,相比UbMonitor(百度内的老计数器库)几乎不会给程序增加性能开销,也快于竞争频繁的原子操作。brpc集成了bvar,/vars可查看所有曝光的bvar,/vars/VARNAME可查阅某个bvar,在brpc中的使用方法请查看vars。brpc大量使用了bvar提供统计数值,当你需要在多线程环境中计数并展现时,应该第一时间想到bvar。但bvar不能代替所有的计数器,它的本质是把写时的竞争转移到了读:读得合并所有写过的线程中的数据,而不可避免地变慢了。当你读写都很频繁或得基于最新值做一些逻辑判断时,你不应该用bvar。

        单看上面这一段gitee上关于bvar的介绍。对于我来说是很难理解到底bvar到底在框架中起到什么作用的,所以我又进行了更深入的学习。
        多核编程常用锁避免多个线程在修改同一个数据时产生race condition(竞争条件)。当锁成为性能瓶颈时,我们就想用另一种方式解决这种race condition,而不可避免地接触了原子指令(在操作系统课程中我们学习到了这种概念,即要么全做、要么全不做)。C++11正式引入了原子指令,几个常用的语法指令如下:在这里插入图片描述

        我们可以用这些指令做原子计数,比如多个线程同时累加一个原子变量,以统计这些线程对一些资源的操作次数。但是,这个操作没有想象中的快,可靠性也欠佳。
        于是,我们引出bvar的思路。
        没有任何竞争或只被一个线程访问的原子操作是比较快的,“竞争”指的是多个线程同时访问同一个cacheline。现代CPU为了以低价格获得高性能,大量使用了cache,并把cache分了多级。百度内常见的Intel E5-2620拥有32K的L1 dcache和icache,256K的L2 cache和15M的L3 cache。其中L1和L2 cache为每个核心独有,L3则所有核心共享。一个核心写入自己的L1 cache是极快的,但当另一个核心读或写同一处内存时,它得确认看到其他核心中对应的cacheline。对于软件来说,这个过程是原子的,不能在中间穿插其他代码,只能等待CPU完成一致性同步,这个复杂的硬件算法使得原子操作会变得很慢。访问被多个线程频繁共享的内存往往是比较慢的。
        要提高性能,就要避免让CPU频繁同步cacheline。这不单和原子指令本身的性能有关,还会影响到程序的整体性能。最有效的解决方法:尽量避免共享。
        其中的一个方法就是计数器,如果所有线程都频繁修改一个计数器,性能就会很差,原因同样在于不同的核心在不停地同步同一个cacheline。如果这个计数器只是用作打打日志之类的,那我们完全可以让每个线程修改thread-local变量,在需要时再合并所有线程中的值,性能可能有几十倍的差别。
        计数器的应用便是bvar。当很多线程都在累加一个计数器时,每个线程只累加私有的变量而不参与全局竞争,在读取时累加所有线程的私有变量。虽然读比之前慢多了,但由于这类计数器的读多为低频的记录和展现,慢点无所谓。而写就快多了,极小的开销使得用户可以无顾虑地使用bvar监控系统,这便是设计bvar的目的。可以说这是brpc的一个精华部分,你可以在从头到尾的任一部分使用bvar记录你想要的数据。

二、代码分析

先观察下/src/bvar文件夹中的全部内容
在这里插入图片描述
这基本上就是实现bvar所需的全部内容,其中同时调用了部分/src/butil中的部分实现功能,用到了会再分析。
然后简单介绍以下其中比较重要的部分(仅初步理解,如后续分析有问题会更正):

  • variable:是所有bvar的基类,主要提供全局注册,列举,查询等功能。
  • percentile:计算sampler采样latency的百分位数(百分位数的概念举例说明:第90百分位数表示90%的采样的latency最长在多长时间)
  • agent_group:用来管理统计值数据存储的类,负责TLS(Thread Local Storage,线程局部存储)空间的分配。
  • sampler:采样器,用get_value()方法定期获取数据。
  • combiner:存储,合并,重置agent_group中TLS的数据。
  • reducer:用二元运算符把多个值合并为一个值,运算符需满足结合律,交换律,没有副作用。只有满足这三点,我们才能确保合并的结果不受线程私有数据如何分布的影响。像减法就不满足结合律和交换律,它无法作为此处的运算符。
  • IntRecorder(继承自variable):用于计算平均值。
  • LatencyRecorder:专用于计算latency和qps的计数器。只需填入latency数据,就能获得latency / max_latency / qps / count。
  • status(继承自variable):记录和显示一个值,拥有额外的set_value函数。
  • passive_status:类似于status。按需显示值。在一些场合中,我们无法set_value或不知道以何种频率set_value,更适合的方式也许是当需要显示时才打印。用户传入打印回调函数实现这个目的。
  • Window:获得之前一段时间内的统计值。Window不能独立存在,必须依赖于一个已有的计数器。Window会自动更新,不用给它发送数据,Window的数据来自于每秒一次对原计数器的采样。

接下来进行代码分析,由于本周大部分时间在总体理解各类的功能,所以进行的代码分析篇幅较短。
先分析bvar中最基本的variable类,variable作为基类实现了最基本的一系列功能,结合代码简单介绍。

首先我们关注一个数据结构
在这里插入图片描述
其中的DisplayFilter
在这里插入图片描述
VarMap是内部自定义的类似常用C++ stl库中的map映射,在此我们先简单将它理解为映射去看之后的操作。在此处就建立起<名字,VarEntry(重点是其中的variable类)>的对照关系。
用户以默认参数建立一个bvar时,这个bvar并未注册到任何全局结构中,在这种情况下,bvar纯粹是一个更快的计数器。我们称把一个bvar注册到全局表中的行为为“曝光”,可通过expose函数曝光:
下面是expose函数的实现,为避免重名,bvar的名字应加上前缀,为了方便使用,则提供了expose_as函数,接收一个前缀。
步骤为先对名字做合法性判断,之后将前缀prefix和名字name连接起来,同时给display_filter赋值,插入到上面的VarMap中,同样判断是否已经存在,并且报出错误信息。在这里插入图片描述
在这里插入图片描述
接下来是将expose过的bvar再隐藏起来,等同于在VarMap中将对应名字的bvar删除,就不会在下面的*_exposed函数中被统计。这里的BAIDU_SCOPED_LOCK是一个范围锁,禁止多个进程同时访问临界区,CHECK_EQ函数是check_equation、在这里判断是否删除成功。
在这里插入图片描述
下面的功能是输出对某个bvar的描述,同样是在VarMap中根据名字查找,最后调用该bvar的describe函数输出内容,在reducer等继承variable类的里面都编写了descirbe代码。
可以看到在它和下面的各种*_exposed函数中,都有对display_filter的筛选判断,根据上面的enum,1&2=0、2&3=1、1&3=1,也就是说display_on_all兼容任意模式,文本和网页展示方式互不兼容,这是对展示模式的位运算判断。
在这里插入图片描述
与之结构完全相同的还有类型的describe
在这里插入图片描述
以及bvar中变量值的查询(所有的查询展示操作都大同小异,归结为VarMap的查找,并且注意,这些操作的访问都可能被多个进程同时进行,所以加锁时必要的。)
在这里插入图片描述
将所有expose过的bvar的名称放入vector<string>names中。
如果想输出所有bvar,必须通过names这个列表并在每个名称上调用上述的describe_exposed。这可以防止迭代占用锁的时间过长。中间的++n>=256是因为在多线程访问的环境下,VarMap有可能因为加入了一些元素导致需要resize,而一旦被resize,之前的迭代就不作数了,所以将names清空。相当于在多线程环境中用于将迭代大型映射的关键部分划分为较小的部分去避免在多个过程中不一致地迭代VarMap造成的问题。
在这里插入图片描述

dump(转储)
利用结构体DumpOptions提供多种转储服务。提供了诸如通配符黑白名单、展示方式等选项,不再过多翻译其中注释。
在这里插入图片描述
variable内置的dump_exposed函数,找到符合的variable调用dumper,与直接使用std::ostringstream相比,该程序使用了更多的低级方法,从而避免了创建在内部分配内存的std::string。
在这里插入图片描述
在这里插入图片描述
dumper分别被FileDumper和FileDumperGroup两个类继承,两个类的dump函数基本相同,FileDumperGroup中只是遍历每个FileDumper再调用如下的dump函数。使用到了src/butil/file_util.h中的文件目录操作方法。
在这里插入图片描述


总结

以上就是今天介绍的全部内容,本文简单介绍了bvar的背景知识及作用,部分bvar源码分析,之后的博客会继续进行代码的分析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值