如何实现火焰图半自由

引言

Linux服务器端可以使用profile生成火焰图. profile从运行开始收集数据, 结束运行时将结果输出, 过了这村就没这店.

有没有可能持续使用profile进行采集, 将数据收集起来, 然后查询任意时间段的火焰图?

前言

答案是√, 使用Prometheus存储profile采集的数据, 使用Grafana的火焰图插件(V9.5.2)将结果展示出来. 本文结束, 感谢观看!

要解决的问题

  1. profile的原始数据是栈调用(隐含时间信息), 火焰图是树状结构(丢失时间信息), 如果想要查询任意时间段就要存储原始栈调用数据和其对应的时间.

  2. Grafana提供了火焰图插件, 但是只接受火焰图数据, Prometheus中存储的原始栈数据无法直接使用.

  3. Grafana和Prometheus对字符串的处理能力极差

所以需要实现一个Proxy, 接受Grafana的查询请求, 转去请求Prometheus, 将结果进行处理后返回Grafana进行展示.

PrometheusProxy实现

profile输出

threadname;stack0;stack1;stack2 num

threadname;stack1;stack4;stack2 num

线程名, 栈帧, 采集到的数量. 这些数据将会上报到Prometheus中进行存储.

上报的格式需要依照Prometheus要求进行下格式化, 这里就不详细介绍了, 只需要知道Prometheus中存储了这些原始的数据(时间, 栈, 对应采集的次数)即可.

Grafana查询协议

# URL
/api/v1/query_range

# BODY
end=1694848740&query=cpu_func_profile{processname="HelloServer"}&start=1694827140&step=15

start 起始时间
end 结束时间
query 向Prometheus请求的表达式
step 步长

Prometheus协议

// Prometheus请求协议即是上面的Grafana协议

// Prometheus回应协议是如下结构对应的json数据
type PrometheusReportData struct {
    Status string `json:"status"`
    Data   struct {
        ResultType string `json:"resultType"`
        Result     []struct {
            Metric struct {
                Name     string `json:"__name__"`
                Instance string `json:"instance"`
                Job      string `json:"job"`
                Stack    string `json:"stack"`
            } `json:"metric"`
            Values [][]any `json:"values"`
        } `json:"result"`
    } `json:"data"`
}

将完整的Grafana请求转发给Prometheus, Prometheus会进行筛选和合并后返回, 所以无需对结果进行额外处理, 只需要从其中拿到汇总后的栈数据即可.

stack: 这个是我们上报的栈数据stack0;stack1;stack2

Values: 是Value的数组, Value有且仅有两个元素, 第一个元素是数据对应的时间戳, 第二个元素是栈数据的num

所以遍历所有数据统计出一个map<stack, num>, Prometheus的任务就完成了.

Grafana火焰图插件回包json格式

type Metric struct {
    Name     string `json:"__name__"`
    Instance string `json:"instance"`
    Job      string `json:"job"`
    Label    string `json:"label"`
    Level    string `json:"level"`
    Self     string `json:"self"`
    Value    string `json:"value"`
}

type Result struct {
    Metric Metric `json:"metric"`
    Values string `json:"values"`
}

type PrometheusFlameData struct {
    Status string `json:"status"`
    Data   struct {
        ResultType string   `json:"resultType"`
        Result     []Result `json:"result"`
    } `json:"data"`
}

PrometheusFlameData的其他数据从Prometheus的回包中拷贝过来即可, 重要的是PrometheusFlameData.Result

Result.Values: 是Value的数组, Value有且仅有两个元素, 第一个元素是数据对应的时间戳, 第二个元素是栈数据的num, 这个已经不重要了, 火焰图并不需要每个节点的时间, 可以任意填充., 如[[1684829000,"1"]]

Result.Metric: 将栈数据转换成多叉树后, 前序遍历节点后得到的结果.

  • Label: 函数名称(节点名称)
  • level: 层级
  • self: 自己作为栈最后一帧的数据数量
  • value: 自己和子节点所有self的合

PrometheusProxy处理

将Grafana的请求原封不动, 转发给Prometheus进行处理, Prometheus数据返回后Proxy进行处理转换为火焰图插件数据格式, 进而返回给Grafana.

需要注意的点

Prometheus存储数据的原理

从上文Prometheus协议的格式来看, stack部分和values部分是分开的, 如果一个stack在多个时间被采集到, 只会存储新数据的时间和次数到values中.

这里提到的stack实际是一个字段, 此外还有线程名, 进程名等字段. 如果一整条上报数据是一样的, Prometheus只会存储时间和次数部分, 如果整条上报数据有一丝一毫差别 就会导致整条数据被记录一次. 导致空间占用暴增(因为stack很长).

说人话就是不要在字段中增加随机值, 否则会导致上报数据无法复用, 每次上报都要存储完成数据, 导致存储量爆炸.

C++编译选项

profile打印出完成的栈结构 需要在程序编译的时候 指定-fno-omit-frame-pointer

针对指定的URL和内容过滤

Grafana查询的URL是/api/v1/query_range, 对应的标签是cpu_func_profile, 可以只针对这种情况处理.

其他情况使用doProxy直接转发, 否则无法使用自动补全等一些提示

profile采集频率

由于profile需要消耗机器CPU, 可以采用每隔X分钟后运行Y分钟的profile, 将数据格式化后供Prometheus提取.

半自由

火焰图准确与否与采集频率有很大关系, profile持续运行会消耗很多的CPU, 所以目前是采取了运行X分钟停止Y分钟的做法, 如果能持续运行profile就能解决这个问题.

扩展-倒转火焰图

func1#func2#func3
func1#func3
func2#func3
func4#func3

如上四个调用栈, 从火焰图上无法轻易看出来func3占用了过多的CPU, 换一种思路将栈倒过来, 就能发现func3占用了过多CPU, 这一步操作只需要在PrometheusProxy中调用一个函数倒转切割后的栈,就能实现.