brpc bvar 完全指南:多线程计数器库的原理、使用与监控导出
【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C++ Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. "brpc" means "better RPC".项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc
bvar 是 brpc 内置的多线程环境计数器类库,通过 thread local 存储把写竞争转移到读端,让高频写入几乎零开销。本文基于 brpc 官方文档(docs/cn/bvar.md)为核心骨架,结合源码(src/bvar)与配套文档(bvar_c++.md、vars.md)深入讲解其设计原理、新增计数器的方法、内置指标的含义,以及 dump 落盘、监控系统接入与 Prometheus 导出(/brpc_metrics)的完整实战方案,读完即可在 brpc 服务中无顾虑地使用 bvar 监控系统。
bvar 是什么:为多线程计数而生的高性能计数器
bvar 是 brpc(工业级 C++ RPC 框架)自带的计数器类库,支持单维度 bvar和多维度 mbvar,方便记录和查看用户程序中的各类数值。它利用了thread local 存储减少了 cache bouncing(缓存行颠簸),相比百度内部的旧计数器库 UbMonitor 几乎不会给程序增加性能开销,也快于竞争频繁的原子操作。brpc 集成了 bvar,通过/vars可查看所有曝光的 bvar,通过/vars/VARNAME可查阅某个 bvar。
brpc 大量使用了 bvar 提供统计数值,因此当你需要在多线程环境中计数并展现时,应该第一时间想到 bvar。但 bvar 不能代替所有的计数器:它的本质是把写时的竞争转移到了读——读得合并所有写过的线程中的数据,而不可避免地变慢了。当你读写都很频繁,或需要基于最新值做一些逻辑判断时,你不应该用 bvar。
设计原理:把写竞争转移到读端
要理解 bvar 的原理,需要先理解 Cacheline(缓存行)这一节:现代 CPU 为了以低价格获得高性能,大量使用了多级 cache(如常见的 L1/L2 每核心独有、L3 所有核心共享)。当一个核心写自己的 L1 cache 是极快的(约 4 cycles、~2ns),但当多个核心同时访问同一个 cacheline 时,CPU 必须完成复杂的缓存一致性同步,竞争激烈时一次fetch_add可能耗费数百纳秒。
bvar 的核心思路正是文档中提到的计数器例子:当很多线程都在累加一个计数器时,每个线程只累加私有的变量而不参与全局竞争,在读取时累加所有线程的私有变量。这样:
- 写操作只触碰本线程的 thread local 变量,几乎不产生 cacheline 同步,开销极小;
- 读操作需要合并所有线程的私有数据,比之前慢多了,但由于这类计数器的读多为低频的记录和展现(如监控展示),慢点无所谓;
- 极小的写开销使得用户可以无顾虑地使用 bvar 监控系统,这正是设计 bvar 的目的。
在源码中,这一机制由detail::AgentCombiner实现(见 src/bvar/detail/combiner.h 的引用关系,实际入口在 src/bvar/reducer.h),Reducer把各线程 agent 的值按运算符合并,读时遍历所有线程私有数据求和。
性能对比:bvar vs 原子变量 vs UbMonitor
下图是 bvar 和原子变量、静态 UbMonitor、动态 UbMonitor 在被多个线程同时使用时的开销。可以看到bvar 的耗时基本和线程数无关,一直保持在极低的水平(约 20 纳秒);而动态 UbMonitor 在 24 核时每次累加的耗时达 7 微秒,这意味着使用 300 次 bvar 的开销才抵得上使用一次动态 UbMonitor 变量。
bvar 类型总览与快速上手
bvar 分为多个具体的类,常用类型及其语义如下(详细用法见单维度 bvar 文档):
| 类型 | 说明 |
|---|---|
| bvar::Adder<T> | 计数器,默认 0,varname << N相当于varname += N |
| bvar::Maxer<T> | 求最大值,默认std::numeric_limits<T>::min(),varname << N相当于varname = max(varname, N) |
| bvar::Miner<T> | 求最小值,默认std::numeric_limits<T>::max(),varname << N相当于varname = min(varname, N) |
| bvar::IntRecorder | 求自使用以来的平均值。注意这里的定语不是"一段时间内",一般要通过 Window 衍生出时间窗口内的平均值 |
| bvar::Window<VAR> | 获得某个 bvar 在一段时间内的累加值,衍生于已存在的 bvar,会自动更新 |
| bvar::PerSecond<VAR> | 获得某个 bvar 在一段时间内平均每秒的累加值,也是自动更新的衍生变量 |
| bvar::WindowEx<T> | 获得某个 bvar 在一段时间内的累加值,不依赖其他 bvar,需要给它发送数据 |
| bvar::PerSecondEx<T> | 获得某个 bvar 在一段时间内平均每秒的累加值,不依赖其他 bvar,需要给它发送数据 |
| bvar::LatencyRecorder | 专用于记录延时和 qps 的变量。输入延时,平均延时/最大延时/qps/总次数就都有了 |
| bvar::Status<T> | 记录和显示一个值,拥有额外的 set_value 函数 |
| bvar::PassiveStatus | 按需显示值。无法 set_value 或不知以何种频率 set_value 时,传入打印回调函数实现按需打印 |
| bvar::GFlag | 将重要的 gflags 公开为 bvar,以便监控它们 |
一个典型的快速示例:
#include <bvar/bvar.h> namespace foo { namespace bar { // bvar::Adder<T>用于累加,下面定义了一个统计read error总数的Adder。 bvar::Adder<int> g_read_error; // 把bvar::Window套在其他bvar上就可以获得时间窗口内的值。 bvar::Window<bvar::Adder<int> > g_read_error_minute("foo_bar", "read_error", &g_read_error, 60); // ^ ^ ^ // 前缀 监控项名称 60秒,忽略则为10秒 // bvar::LatencyRecorder是一个复合变量,可以统计:总量、qps、平均延时,延时分位值,最大延时。 bvar::LatencyRecorder g_write_latency("foo_bar", "write"); // ^ ^ // 前缀 监控项,别加latency!LatencyRecorder包含多个bvar,它们会加上各自的后缀,比如write_qps, write_latency等等。 // 定义一个统计"已推入task"个数的变量。 bvar::Adder<int> g_task_pushed("foo_bar", "task_pushed"); // 把bvar::PerSecond套在其他bvar上可以获得时间窗口内*平均每秒*的值,这里是每秒内推入task的个数。 bvar::PerSecond<bvar::Adder<int> > g_task_pushed_second("foo_bar", "task_pushed_second", &g_task_pushed); // ^ ^ // 和Window不同,PerSecond会除以时间窗口的大小. 时间窗口是最后一个参数,这里没填,就是默认10秒。 } // bar } // foo在应用的地方:
// 碰到read error foo::bar::g_read_error << 1; // write_latency是23ms foo::bar::g_write_latency << 23; // 推入了1个task foo::bar::g_task_pushed << 1;注意Window<>和PerSecond<>都是衍生变量,会自动更新,你不用给它们推值。bvar 也可以作为成员变量或局部变量使用。
新增 bvar:命名、曝光与线程安全
增加 C++ bvar 的具体方法请看快速介绍。这里补充几个关键实践约束:
- 确认变量名是全局唯一的!否则会曝光失败;如果
-bvar_abort_on_same_name为 true,程序会直接 abort(默认 false,重名时打印 FATAL 日志并返回 -1)。 - 程序中有来自各种模块不同的 bvar,为避免重名,建议如此命名:模块_类名_指标。模块一般是程序名(可加产品线缩写,如 inf_ds、ecom_retrbs);类名一般是类名或函数名(如 storage_manager、file_transfer、rank_stage1);指标一般是 count、qps、latency 这类。
- bvar 会做名字归一化:无论你打入的是
foo::BarNum、foo.bar.num、foo bar num还是foo-bar-num,最后都是foo_bar_num。 - 指标后缀约定:个数以
_count为后缀(如 request_count);每秒个数以_second为后缀(如 request_second、process_inblocks_second),不要写成_count_second或_per_second;每分钟个数以_minute为后缀(如 request_minute)。
如果需要使用定义在另一个文件中的计数器,需要在头文件中声明对应的变量:
namespace foo { namespace bar { // 注意g_read_error_minute和g_task_pushed_second都是衍生的bvar,会自动更新,不要声明。 extern bvar::Adder<int> g_read_error; extern bvar::LatencyRecorder g_write_latency; extern bvar::Adder<int> g_task_pushed; } // bar } // foo不要跨文件定义全局 Window 或 PerSecond——不同编译单元中全局变量的初始化顺序是未定义的,在 foo.cpp 中定义Adder<int> foo_count、在 foo_qps.cpp 中定义PerSecond<Adder<int>> foo_qps(&foo_count)是错误做法。
关于线程安全:bvar 是线程兼容的,可以在不同线程里操作不同的 bvar(比如同时 expose/hide 不同的 bvar,是安全的);但除了读写接口,bvar 的其他函数都是线程不安全的——不能在多个线程中同时 expose 或 hide 同一个 bvar,这很可能导致程序 crash。这些约束同样写在基类 src/bvar/variable.h 的注释中。
曝光(expose)机制:用户以默认参数建立 bvar 时,它并未注册到任何全局结构中,此时 bvar 纯粹是一个更快的计数器。通过expose或expose_as函数把 bvar 注册到全局表中,即可被list_exposed、count_exposed、describe_exposed、find_exposed等查询,并出现在/vars中。
brpc 内置的 bvar
bvar 默认统计了进程、系统的一些变量,以process_、system_等开头,例如:
process_context_switches_involuntary_second : 14 process_context_switches_voluntary_second : 15760 process_cpu_usage : 0.428 process_cpu_usage_system : 0.142 process_cpu_usage_user : 0.286 process_disk_read_bytes_second : 0 process_disk_write_bytes_second : 260902 process_faults_major : 256 process_faults_minor_second : 14 process_memory_resident : 392744960 system_core_count : 12 system_loadavg_15m : 0.040 system_loadavg_1m : 0.000 system_loadavg_5m : 0.020还有 brpc 内部的各类计数器:
bthread_switch_second : 20422 bthread_timer_scheduled_second : 4 bthread_timer_triggered_second : 4 bthread_timer_usage : 2.64987e-05 bthread_worker_count : 13 bthread_worker_usage : 1.33733 bvar_collector_dump_second : 0 bvar_collector_dump_thread_usage : 0.000307385 bvar_collector_grab_second : 0 bvar_collector_grab_thread_usage : 1.9699e-05 bvar_collector_pending_samples : 0 bvar_dump_interval : 10 bvar_revision : "34975" bvar_sampler_collector_usage : 0.00106495 iobuf_block_count : 89 iobuf_block_count_hit_tls_threshold : 0 iobuf_block_memory : 729088 iobuf_newbigview_second : 10可以看到命名规范的实际落地:bthread_switch_second(模块=bthread,指标=每秒切换次数)、iobuf_block_count(模块=iobuf,类名=block,指标=count)等。
监控 bvar:开启 dump 导出到文件
打开 bvar 的 dump 功能可以把所有 bvar 导出到文件,格式为每行一对名字:值。有两种开启方式:
- 用 gflags 解析输入参数,在程序启动时加入
-bvar_dump(在 brpc 中也可通过 /flags 服务在启动后动态修改):
#include <gflags/gflags.h> ... int main(int argc, char* argv[]) { google::ParseCommandLineFlags(&argc, &argv, true/*表示把识别的参数从argc/argv中删除*/); ... }- 不想用 gflags 解析参数、希望直接在程序中默认打开:
#include <gflags/gflags.h> ... int main(int argc, char* argv[]) { if (google::SetCommandLineOption("bvar_dump", "true").empty()) { LOG(FATAL) << "Fail to enable bvar dump"; } ... }dump 功能由如下 gflags 控制:
| 名称 | 默认值 | 作用 |
|---|---|---|
| bvar_dump | false | Create a background thread dumping all bvar periodically, all bvar_dump_* flags are not effective when this flag is off |
| bvar_dump_exclude | "" | Dump bvar excluded from these wildcards(separated by comma), empty means no exclusion |
| bvar_dump_file | monitor/bvar.<app>.data | Dump bvar into this file |
| bvar_dump_include | "" | Dump bvar matching these wildcards(separated by comma), empty means including all |
| bvar_dump_interval | 10 | Seconds between consecutive dump |
| bvar_dump_prefix | <app> | Every dumped name starts with this prefix |
| bvar_dump_tabs | <check the code> | Dump bvar into different tabs according to the filters (separated by semicolon), format: *(tab_name=wildcards) |
当bvar_dump_file不为空时,程序会启动一个后台导出线程,以bvar_dump_interval指定的间隔更新bvar_dump_file,其中包含了被bvar_dump_include匹配且不被bvar_dump_exclude匹配的所有 bvar。下图是 gflags 界面上各 dump 参数的配置示例:
打开 dump 功能后应检查 monitor/ 目录下是否有数据,比如:
$ ls monitor/ bvar.echo_client.data bvar.echo_server.data $ tail -5 monitor/bvar.echo_client.data process_swaps : 0 process_time_real : 2580.157680 process_time_system : 0.380942 process_time_user : 0.741887 process_username : "gejun"例如配置了 include/exclude 过滤后,导出文件形如:
$ cat bvar.echo_server.data rpc_server_8002_builtin_service_count : 20 rpc_server_8002_connection_count : 1 rpc_server_8002_nshead_service_adaptor : brpc::policy::NovaServiceAdaptor rpc_server_8002_service_count : 1 rpc_server_8002_start_time : 2015/07/24-21:08:03 rpc_server_8002_uptime_ms : 14740954像iobuf_block_count : 8被bvar_dump_include过滤了,rpc_server_8002_error : 0则被bvar_dump_exclude排除了。
每次导出都会覆盖之前的文件,这与普通 log 添加在后面是不同的,需要注意采集节奏。如果你的程序没有使用 brpc、仍需要动态修改 gflag,可以调用google::SetCommandLineOption(),但请勿直接设置 FLAGS_bvar_dump_file / FLAGS_bvar_dump_include / FLAGS_bvar_dump_exclude:一方面这些 gflag 类型都是 std::string,直接覆盖是线程不安全的;另一方面不会触发 validator(检查正确性的回调),也就不会启动后台导出线程。
汇聚到监控系统
监控系统应定期搜集每台单机导出的数据,并把它们汇总到一起。以百度内部的 noah 为例,bvar 定义的变量会出现在 noah 的指标项中,用户可勾选并查看历史曲线:
导出到 Prometheus:/brpc_metrics
除 dump 到本地文件外,bvar 还内置了对 Prometheus 的原生支持。将 Prometheus 的抓取 url 地址的路径设置为/brpc_metrics即可,例如 brpc server 跑在本机的 8080 端口,则抓取 url 配置为127.0.0.1:8080/brpc_metrics。
从源码看,该路径由 Prometheus 内置服务实现(见 src/brpc/builtin/prometheus_metrics_service.cpp):PrometheusMetricsService::default_method设置Content-Type: text/plain后调用DumpPrometheusMetricsToIOBuf,其中通过bvar::Variable::dump_exposed(&dumper, nullptr)把全部已曝光 bvar 以 Prometheus 文本格式输出;若bvar_max_dump_multi_dimension_metric_number > 0,还会通过bvar::MVariableBase::dump_exposed一并导出多维 mbvar。延时类指标会输出为 Prometheus summary 格式,包含quantile标签(如 0.5、0.9、0.99、0.999、0.9999、avg)以及_sum、_count系列。
配合 vars 服务,你还可以在浏览器中直接访问/vars查看所有曝光 bvar、用通配符过滤(如/vars/foo*,b$r,用$代替?匹配单个字符,因为?是 URL 保留字符)、点击数值型 bvar 查看历史趋势与分位值曲线,让 bvar 的监控链路从"采集—存储—展示"形成完整闭环。
小结
bvar 是 brpc 为多线程计数场景量身打造的高性能计数器库:它以 thread local 存储把写竞争转移为读时的合并开销,换来近乎恒定的纳秒级写性能;配合_count/_second/_minute命名规范与Adder、Maxer、Window、LatencyRecorder等丰富的计数器原语,可以无顾虑地在业务代码中埋点。通过-bvar_dump落盘、/vars在线查询、/brpc_metrics对接 Prometheus,一套从进程内到监控大盘的指标体系即可快速搭建完成。深入源码可继续阅读 src/bvar/window.h(Window/PerSecond 采样实现)、src/bvar/latency_recorder.h(延时统计组合变量)与多维度 mbvar 文档。
【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C++ Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. "brpc" means "better RPC".项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考