前言

October 12, 2023 · View on GitHub

English

前言

tvar 是 tRPC-Cpp 框架提供给用户的一个多线程统计类库,辅助用户记录和跟踪程序运行过程中的各种状态。

设计原理

本文分析了业界支持统计类库的三个同类型框架,分别从统计类型多样性,写入效率,查询效率以及实现原理四个方面,对它们进行了对比,

比较维度/框架名称envoybrpcflare
统计类型多样性较多较多较少
写入效率很高
查询效率一般
实现原理global atomicthread local atomicthread local atomic

从上表可知,envoy 的实现原理是多线程操作同一个全局原子变量,并发写入时会存在 cache bouncing,因此在写多读少场景下,性能较差,但是在读多写少场景下,性能较好。

相比之下,brpc 和 flare 的实现原理是各个线程写入自己本地私有的原子变量,从而避免了写入时的 cache bouncing,因此在写多读少场景下,性能更好,但是在读的时候,需要先对各个线程的私有变量做聚合,因此在读多写少场景下,性能会变差。

考虑到tvar是面向写多读少的场景设计的,因此同时借鉴了 brpc 和 flare 的设计方案,通过 thread_local 机制来提高写入性能,并且在统计类型多样性上,使用 brpc 的方案。

统计类型

目前 tvar 一共支持11种统计类型,如下表所示,

类型名称功能
Counter只增不减的计数器
Gauge可增可减的计数器
Maxer取最大值
Miner取最小值
Averager取平均值
IntRecorder取平均值,只支持int类型
Status用户传入一个值用于显示,不同线程修改后立即可见
PassiveStatus用户传入一个函数对象,读取结果的时候调用它拿到返回值
Window获取一段时间内的统计值
PerSecond获取一段时间内每秒的平均统计值
LatencyRecorder获取qps以及分位耗时

使用指南

配置

global:
  tvar:
    window_size: 10            # 窗口大小,单位是秒,用于Window,PerSecond和LatencyRecorder三个统计类型
    save_series: true          # 是否存储历史数据,默认是true
    abort_on_same_path: true   # 如果发现注册的两个tvar变量有相同的曝光路径时,是否直接abort退出进程,默认是true
    latency_p1: 80             # 用户自定义分位值1,只能传1-99以内的整数,对应1%到99%
    latency_p2: 90             # 用户自定义分位值2,只能传1-99以内的整数,对应1%到99%
    latency_p3: 99             # 用户自定义分位值3,只能传1-99以内的整数,对应1%到99%

使用

Counter

#include <cstdint>

// 包含头文件
#include "trpc/tvar/tvar.h"

using trpc::tvar::Counter;

int main() {
  // 这里以局部变量举例,实际开发中,推荐使用全局单例或者其他合适的生命周期

  // 通过曝光构造创建tvar变量,后续可以通过admin接口查询
  Counter<uint64_t> counter("user/my_counter");
  // 用户也可以使用不曝光构造,但是后续不能通过admin接口查询
  // Counter<uint64_t> counter;

  // 增加3
  counter.Add(3);
  // 增加1
  counter.Increment();
  // 用户一般不需要调用GetValue,而是通过admin接口查看值,这里是演示用法
  auto ret = counter.GetValue();
  return 0;
}

Gauge

#include <cstdint>

// 包含头文件
#include "trpc/tvar/tvar.h"

using trpc::tvar::Gauge;

int main() {
  // 这里以局部变量举例,实际开发中,推荐使用全局单例或者其他合适的生命周期

  // 通过曝光构造创建tvar变量,后续可以通过admin接口查询
  Gauge<uint64_t> gauge("user/my_gauge");
  // 用户也可以使用不曝光构造,但是后续不能通过admin接口查询
  // Gauge<uint64_t> gauge;

  // 增加2
  gauge.Add(2);
  // 减少2
  gauge.Subtract(2);
  // 增加1
  gauge.Increment();
  // 减少1
  gauge.Decrement();
  // 用户一般不需要调用GetValue,而是通过admin接口查看值,这里是演示用法
  auto ret = gauge.GetValue();
  return 0;
}

Maxer

#include <cstdint>

// 包含头文件
#include "trpc/tvar/tvar.h"

using trpc::tvar::Maxer;

int main() {
  // 这里以局部变量举例,实际开发中,推荐使用全局单例或者其他合适的生命周期

  // 通过曝光构造创建tvar变量,后续可以通过admin接口查询
  Maxer<uint64_t> maxer("user/my_maxer");
  // 用户也可以使用不曝光构造,但是后续不能通过admin接口查询
  // Maxer<uint64_t> maxer;

  // 更新到3
  maxer.Update(3);
  // 更新到9
  maxer.Update(9);
  // 更新后还是9
  maxer.Update(6);

  // 用户一般不需要调用GetValue,而是通过admin接口查看值,这里是演示用法
  auto ret = maxer.GetValue();
  return 0;
}

Miner

#include <cstdint>

// 包含头文件
#include "trpc/tvar/tvar.h"

using trpc::tvar::Miner;

int main() {
  // 这里以局部变量举例,实际开发中,推荐使用全局单例或者其他合适的生命周期

  // 通过曝光构造创建tvar变量,后续可以通过admin接口查询
  Miner<uint64_t> miner("user/my_miner");
  // 用户也可以使用不曝光构造,但是后续不能通过admin接口查询
  // Miner<uint64_t> miner;

  // 更新到9
  miner.Update(9);
  // 更新到3
  miner.Update(3);
  // 更新后还是3
  miner.Update(6);

  // 用户一般不需要调用GetValue,而是通过admin接口查看值,这里是演示用法
  auto ret = miner.GetValue();
  return 0;
}

Averager

#include <cstdint>

// 包含头文件
#include "trpc/tvar/tvar.h"

using trpc::tvar::Averager;

int main() {
  // 这里以局部变量举例,实际开发中,推荐使用全局单例或者其他合适的生命周期

  // 通过曝光构造创建tvar变量,后续可以通过admin接口查询
  Averager<uint64_t> averager("user/my_averager");
  // 用户也可以使用不曝光构造,但是后续不能通过admin接口查询
  // Averager<uint64_t> averager;

  // 更新后平均值是3
  averager.Update(3);
  // 更新后平均值是5
  averager.Update(7);
  // 更新后平均值是5
  averager.Update(5);

  // 用户一般不需要调用GetValue,而是通过admin接口查看值,这里是演示用法
  auto ret = averager.GetValue();
  return 0;
}

IntRecorder

#include <cstdint>

// 包含头文件
#include "trpc/tvar/tvar.h"

using trpc::tvar::IntRecorder;

int main() {
  // 这里以局部变量举例,实际开发中,推荐使用全局单例或者其他合适的生命周期

  // 通过曝光构造创建tvar变量,后续可以通过admin接口查询
  IntRecorder int_recorder("user/my_int_recorder");
  // 用户也可以使用不曝光构造,但是后续不能通过admin接口查询
  // IntRecorder int_recorder;

  // 更新后平均值是3
  int_recorder.Update(3);
  // 更新后平均值是5
  int_recorder.Update(7);
  // 更新后平均值是5
  int_recorder.Update(5);

  // 用户一般不需要调用GetValue,而是通过admin接口查看值,这里是演示用法
  auto ret = int_recorder.GetValue();
  return 0;
}

Status

// 包含头文件
#include "trpc/tvar/tvar.h"

using trpc::tvar::Status;

int main() {
  // 这里以局部变量举例,实际开发中,推荐使用全局单例或者其他合适的生命周期

  // 通过曝光构造创建tvar变量,后续可以通过admin接口查询
  // 如果用户使用的类型是tvar::Status<std::string>,那么单参构造是不曝光的;如果要曝光需要至少传两个参数,第一个是path,第二个是初始值
  Status<int> status("user/status", 0);
  // 用户也可以使用不曝光构造,但是后续不能通过admin接口查询
  // Status<int> status;

  // 设置值
  status.SetValue(3);

  ret = status.GetValue();
  return 0;
}

PassiveStatus

#include <cstdint>
#include <mutex>

// 包含头文件
#include "trpc/tvar/tvar.h"

using trpc::tvar::PassiveStatus;
using std::mutex;

int g_num{0};
mutex g_mutex;

int main() {
  // 这里以局部变量举例,实际开发中,推荐使用全局单例或者其他合适的生命周期

  // 通过曝光构造创建tvar变量,后续可以通过admin接口查询
  // 用户需要自己保证回调函数的线程安全性
  PassiveStatus<int> passive_status("user/passive_status", []() {
    std::unique_lock<std::mutex> lock(g_mutex);
    return g_num;
  });
  // 用户也可以使用不曝光构造,但是后续不能通过admin接口查询
  // PassiveStatus<int> passive_status([]() {
  //   std::unique_lock<std::mutex> lock(g_mutex);
  //   return g_num;
  // });

  // 用户一般不需要调用GetValue,而是通过admin接口查看值,这里是演示用法
  auto ret = passive_status.GetValue();

  {
    std::unique_lock<std::mutex> lock(g_mutex);
    ++g_num;
  }

  ret = passive_status.GetValue();
  return 0;
}

Window

#include <cstdint>

// 包含头文件
#include "trpc/tvar/tvar.h"

using trpc::tvar::Window;
using trpc::tvar::Gauge;

int main() {
  // 这里以局部变量举例,实际开发中,推荐使用全局单例或者其他合适的生命周期

  // 通过曝光构造创建tvar变量,后续可以通过admin接口查询
  Gauge<int64_t> gauge("user/my_gauge");
  // 用户也可以使用不曝光构造,但是后续不能通过admin接口查询
  // Gauge<int64_t> gauge;

  // 定义Window,曝光路径:/user/window,窗口大小10秒,数据来源:gauge变量
  Window<Gauge<int64_t>> window("user/window", &gauge, 10);

  // 对源变量进行更新操作
  gauge.Increment();
  gauge.Add(2);

  // 用户一般不需要调用GetValue,而是通过admin接口查看值,这里是演示用法
  auto ret = window.GetValue();

  return 0;
}

在使用Window时,有些类型可以支持Window,有些则不支持,具体参考下表,

源变量类型是否支持Window说明
数值类型的Counter支持
数值类型的Gauge支持
数值类型的Maxer支持语义特殊,不建议对Maxer使用Window
数值类型的Miner支持语义特殊,不建议对Miner使用Window
数值类型的Averager支持
IntRecorder支持
Status不支持使用Window没有意义
数值类型的PassiveStatus支持
PerSecond不支持本身已经是窗口语义
LatencyRecorder不支持本身已经是窗口语义

PerSecond

#include <cstdint>

// 包含头文件
#include "trpc/tvar/tvar.h"

using trpc::tvar::PerSecond;
using trpc::tvar::Gauge;

int main() {
  // 这里以局部变量举例,实际开发中,推荐使用全局单例或者其他合适的生命周期

  // 通过曝光构造创建tvar变量,后续可以通过admin接口查询
  Gauge<int64_t> gauge("user/my_gauge");
  // 用户也可以使用不曝光构造,但是后续不能通过admin接口查询
  // Gauge<int64_t> gauge;

  // 定义PerSecond,曝光路径:/user/per_second,窗口大小10秒,数据来源:gauge变量
  PerSecond<Gauge<int64_t>> per_second("user/per_second", &gauge, 10);

  // 对源变量进行更新操作
  gauge.Increment();
  gauge.Add(2);

  // 用户一般不需要调用GetValue,而是通过admin接口查看值,这里是演示用法
  auto ret = per_second.GetValue();

  return 0;
}

在使用PerSecond时,有些类型可以支持PerSecond,有些则不支持,具体参考下表,

源变量类型是否支持PerSecond说明
数值类型的Counter支持
数值类型的Gauge支持
数值类型的Maxer不支持使用PerSecond没有意义
数值类型的Miner不支持使用PerSecond没有意义
数值类型的Averager不支持使用PerSecond没有意义
IntRecorder不支持使用PerSecond没有意义
Status不支持使用PerSecond没有意义
数值类型的PassiveStatus支持
Window不支持本身用于实现PerSecond
LatencyRecorder不支持本身已经是窗口语义

LatencyRecorder

// 包含头文件
#include "trpc/tvar/tvar.h"

using trpc::tvar::LatencyRecorder;

int main() {
  // 这里以局部变量举例,实际开发中,推荐使用全局单例或者其他合适的生命周期

  // 定义LatencyRecorder,曝光路径:/user/latency_recorder,窗口大小10秒
  LatencyRecorder latency_recorder("user/latency_recorder");

  // 输入延迟
  latency_recorder.Update(3);

  // 用户一般不需要调用Count和LatencyPercentile,而是通过admin接口查看值,这里是演示用法
  auto qps = latency_recorder.Count();
  auto p99 = latency_recorder.LatencyPercentile(0.99);
  return 0;
}

LatencyRecorder统计的信息及其含义如下表所示,

字段名称含义
latency获取当前窗口下平均值耗时
max_latency获取当前窗口下面最大耗时
count获取历史输入的总数量,这里的数量是用户通过Update()输入的个数
qps获取当前窗口下的qps
latency_9999当前窗口下的99.99分位耗时;即p9999,只有0.01%的耗时比这个数值高
latency_999当前窗口下的99.9分位耗时;即p999,只有0.1%的耗时比这个数值高
latency_p1默认值80,即p80;用户可以通过配置latency_p1自定义分位
latency_p2默认值90,即p90;用户可以通过配置latency_p2自定义分位
latency_p3默认值99,即p99;用户可以通过配置latency_p3自定义分位

查询

查询当前值

curl http://admin_ip:admin_port/cmds/var/{path}

其中,path是tvar变量的路径,可以查询某个具体的tvar变量,也可以查询某个曝光路径下的所有tvar变量,举例如下,

Gauge<int64_t> gauge("user/b/gauge");
gauge.Add(2);
Counter<uint64_t> count("user/counter");
count.Add(1);

查询某个具体的tvar变量gauge,

curl http://admin_ip:admin_port/cmds/var/user/b/gauge

查询得到的结果如下,

{
    "guage":2
}

查询某个曝光路径下的所有tvar变量,包括gauge变量和count变量,

curl http://admin_ip:admin_port/cmds/var/user

查询得到的结果如下,

{
    "counter":1,
    "b":{
        "guage":2
    }
}

查询历史值

curl http://admin_ip:admin_port/cmds/var/{path}?history=true

在查询当前值的基础上,在末尾添加query string history=true

以上文的gauge变量举例,查询方式如下,

curl http://admin_ip:admin_port/cmds/var/user/b/gauge?history=true

查询得到的结果如下,

{
"gauge":{
        "latest_day":[1,2,3,4],
        "latest_hour":[1,2,3,4],
        "latest_min":[1,2,3,4],
        "latest_sec":[1,2,3,4],
        "now":[1]
    }
}

有些类型支持历史值查询,有些类型不支持,具体如下表,

源变量类型是否支持历史值查询说明
数值类型的Counter支持
数值类型的Gauge支持
数值类型的Maxer不支持Maxer如果不重置的话,没办法采集到1秒内的数据,建议使用Window<数值类型的Maxer>
数值类型的Miner不支持Miner如果不重置的话,没办法采集到1秒内的数据,建议使用Window<数值类型的Miner>
数值类型的Averager不支持没办法采集到1秒内的数据,建议使用Window<数值类型的Averager>
IntRecorder不支持没办法采集到1秒内的数据,建议使用Window<IntRecorder>
数值类型的Status支持
Window<数值类型的Counter>支持
Window<数值类型的Gauge>支持
数值类型的PassiveStatus支持
Window<数值类型的Maxer>支持
Window<数值类型的Miner>支持
Window<IntRecorder>支持
Window<数值类型的PassiveStatus>支持
PerSecond<数值类型的Counter>支持
PerSecond<数值类型的Gauge>支持
PerSecond<PassiveStatus>支持