Versions Compared
Key
- This line was added.
- This line was removed.
- Formatting was changed.
背景介绍
笔者编写的一个监控采集上报程序部署遇到了频繁OOM
的问题,之前一直在赶业务需求,虽然知道这块存在一些优化空间,但是一直没有时间去改进完善。终于在交付给客户后由于其他问题的诱发导致了的问题,之前一直在赶业务需求,虽然知道这块存在一些优化空间,记录了需求单跟进但是一直没有时间去完善。终于在交付给客户后由于其他问题的诱发导致了OOM
即便增加资源也不可解决,因此将此问题提高了优先级来处理。即便增加资源也不可恢复,因此将此问题提高了优先级来处理。
首先介绍下这个监控采集程序,它实现了秒级监控的能力,但基础监控指标需要来源于其他开源组件或者服务,比如:首先介绍下这个监控采集程序,它实现了秒级监控的能力,但基础监控指标复用于其他开源组件或者服务,比如:
- 容器监控数据来源于
kubelet
中内置的cadvisor
数据,通过https
请求kubelet
而来。 kubelet
自身的监控数据同样来源于kubelet
,通过https
请求kubelet
而来。- 集群状态数据来源于独立部署的开源组件
kube-state-metrics
而来,通过http
请求kube-state-metrics
服务地址而来。
这3
项数据均涉及到远程访问,并且这些基础指标拿到后根据不同的业务会有独立的项数据均涉及到HTTP
远程访问,并且这些基础指标拿到后根据不同的业务会有独立的plugin
接口注册,用于实现各自独立的数据面监控指标。由于涉及到数据面监控指标,因此这些基础监控数据非常重要,容不得丢失。
排查优化
开启PProf
首先我们的程序需要开启PProf
,以便拿到程序的分析指标。具体可以参考:PProf服务性能分析
拿到PProf数据
具体细节不赘述,具体的命令如下:具体细节不赘述,具体的命令差不多是这样:
Code Block | ||
---|---|---|
| ||
# 拷贝带有pprof功能的新二进制到指定容器 kubectl cp /root/khaos-metrics-agent khaos/khaos-guardian-sjsm4:/root/khaos-metrics-agent -c app # 进入指定容器执行bash,便于独立采集pprof数据 k exec -n khaos khaos-guardian-sjsm4 -c app bash -it cd ~ && chmod +x khaos-metrics-agent # 运行一个独立的khaos-metrics-agent nohup ./khaos-metrics-agent \ --debug=false \ --address=:13142 \ --nodeIP=10.186.19.165 \ --rootPath=/ \ --agentConfigFilePath=/var/run/khaos-guardian/metricsconfig/config.yaml \ --kubeStateUrl=http://127.0.0.1:13043/metrics \ --enabledPlugins=.+ & # 采集pprof相关信息 curl 127.0.0.1:13045/debug/pprof/heap > pprof.heap curl 127.0.0.1:13045/debug/pprof/goroutine > pprof.goroutine curl 127.0.0.1:13045/debug/pprof/profile > pprof.profile # 将pprof信息从容器中拷贝到客户端,便于进一步分析将pprof信息从容器中拷贝到客户端,便于本地进一步分析 kubectl cp khaos/khaos-guardian-sjsm4:/root/pprof.heap /root/pprof.heap -c app kubectl cp khaos/khaos-guardian-sjsm4:/root/pprof.goroutine /root/pprof.goroutine -c app kubectl cp khaos/khaos-guardian-sjsm4:/root/pprof.profile /root/pprof.profile -c app |
执行PProf分析
sample1
文件:pprof.heap
由于我们需要排查的是内存占用问题,所以主要是分析pprof.heap
这个文件即可,其他两个文件(pprof.profile
用以分析cpu
耗时、pprof.goroutine
用以分析goroutine
占用判断有误goroutine
阻塞)主要用来辅助排查。通过以下命令使用go tool
打开pprof
内存分析:
Code Block | ||
---|---|---|
| ||
$ go tool pprof -http :8080 pprof.heap Serving web UI on http://localhost:8080 |
点开火炬图发现内存占用居然会处在框架的缓存组件方法GetOrSetFuncLock
上,说明程序不断地去调用缓存方法来读取和缓存数据,很有可能就是缓存失效了,或者缓存没有起作用,导致频繁地更新缓存。通过查看程序,发现这里的代码写得垃圾得一批,缓存根本没起作用,缓存键名加了一个时间戳是什么鬼?脑子抽了?脑子抽了?😰
为了方便缓存,这里调整一下Gatherer
的接口实现,增加Name
接口实现,并封装了新的方法来聚合这块缓存逻辑:
Image Added
sample2
文件:pprof.heap
修复该问题后重新执行pprof
分析。这次发现问题出在了io.ReadAll
上。由于业务插件在采集业务程序的指标时是通过HTTP GET
拉取,里面使用了ReadAll
方法来读取业务程序的指标数据,并返回给上层转换为方法来完成读取业务程序的指标数据,并返回给上层转换为Prometheus
数据结构,处理后再push
给远端存储。
涉及到的代码如下。看样子是由于业务实例的指标数据过大(几MB到100涉及到的代码如下。但是由于业务实例的指标数据过大(几MB到100+MB不等),这里完整读取的话会申请一块新的临时内存,造成过大的内存压力。MB不等),这里完整读取的话会申请一块新的临时内存,造成过大的内存压力,所以这里的内存问题比较容易凸显。
Tip |
---|
这种问题其实属于常见问题,对于所有的 |
因此去掉临时内存申请,改为直接HTTP
访问操作都容易出现,大多数程序员的思维逻辑都是直接完整读取后再交给上层处理,但是这样会额外占用一块无意义的临时内存。因此去掉临时内存申请,改为直接res.Body
读取。同时程序中其他流式读取。同时程序中其他HTTP
请求也做相应改进。请求也做类似的改进。
sample3
文件:pprof.heap
修复后继续pprof
分析,发现现在内存占用主要是在执行RemoteWrite
远端写入时的第三方包数据结构转换组件调用上。
这个fmtutil
包是属于prometheus
官方社区的组件包,用于执行指标数据的各种数据结构转换。我们看看这个fmtutil.makeLabels
做了什么事情。
可以看到内存分配主要是这里的labels
数组创建。这里的prompb.Label
数据结构很奇怪,它这里使用了值传参形式,也就是说,指标标签的键值对都会复制一遍到这个labels
中。而我们知道,指标数据容量主要是标签键值对的大小容量占用,那么这里就会不断申请很多内存用于拷贝标签键值对数据。
我们这里来梳理一下业务实例的监控指标的数据结构转换流程:
Code Block | ||
---|---|---|
| ||
Metric Text -> dto.MetricFamily -> prompb.WriteRequest |
其中:
Metric Text
:从业务实例采集到的Prometheus
监控数据,文本内存,从监控数据,文本类型,从Response.Body
中流式读取。dto.MetricFamily
:从Prometheus
监控数据文本转换为的监控数据结构,用于方便采集程序内部处理,如果指标过滤、注入等操作。监控数据文本转换为的监控数据结构,用于方便采集程序内部处理,如指标过滤、注入等操作。prompb.WriteRequest
:当监控数据处理好后,需要通过Prometheus RemoteWrite
协议写入到远端存储,需要进行协议转换为该格式。
那么既然这里有两次协议转换,为什么Metric Text
到dto.MetricFamily
没有出现内存占用问题呢?我们来看看它的数据结构:
可以看到,在转换为dto.MetricFamily
的时候,内存容量占比较大的部分使用的是指针,其实并没有涉及到额外的内存申请。因此对转换prompb.WriteRequest
数据结构的优化可以参考该思路。通过查看Prometheus
的源码,发现其实这里不太好改,因为这是个公开的数据结构,并且引用的地方还蛮多,直接修改会有兼容问题。回顾自身的监控采集程序,已经没有更多的优化空间。而针对社区组件的改动,也许未来再详细考虑吧。
其他一些点
除了程序方面的优化,其实在部署上也有一些改进,具体请参考:
回顾总结
- 开发人员应当对自身产出的物料负责。项目周期通常都非常繁忙(且忙中易出错),不可能单独预留改进优化的时间。在评估每个开发迭代的任务时,应当积极介入工作安排,不要被动接受输入。将遗留的可能风险及时和上级Leader沟通,给Leader更多的输入以便综合权衡评估工作安排来改进。不要等待问题影响业务/客户时再回过头来改进,那样的成本太大了。
- Golang的工具链很丰富,特别是PProf的存在确实帮助开发人员快速排查、优化程序提供了高效的辅助。选择Golang作为主力开发语言是正确的!Golang的工具链很丰富,特别是PProf的存在确实帮助开发人员快速排查、优化程序提供了高效的辅助。
Panel | ||||
---|---|---|---|---|
| ||||
|