引言
Linux服务器端可以使用profile生成火焰图. profile从运行开始收集数据, 结束运行时将结果输出, 过了这村就没这店.
有没有可能持续使用profile进行采集, 将数据收集起来, 然后查询任意时间段的火焰图?
前言
答案是√, 使用Prometheus存储profile采集的数据, 使用Grafana的火焰图插件(V9.5.2)将结果展示出来. 本文结束, 感谢观看!
要解决的问题
profile的原始数据是栈调用(隐含时间信息), 火焰图是树状结构(丢失时间信息), 如果想要查询任意时间段就要存储原始栈调用数据和其对应的时间.
Grafana提供了火焰图插件, 但是只接受火焰图数据, Prometheus中存储的原始栈数据无法直接使用.
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中调用一个函数倒转切割后的栈,就能实现.