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项数据均涉及到远程访问,并且这些基础指标拿到后根据不同的业务会有独立的plugin接口注册,用于实现各自独立的数据面监控指标。由于涉及到数据面监控指标,因此这些基础监控数据非常重要,容不得丢失。

排查优化

开启PProf

首先我们的程序需要开启PProf,以便拿到程序的分析指标。具体可以参考:PProf服务性能分析

拿到PProf数据

具体细节不赘述,具体的命令如下:

Code Block
languagebash
# 拷贝带有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
languagebash
$ go tool pprof -http :8080 pprof.heap
Serving web UI on http://localhost:8080

点开火炬图发现内存占用居然会处在框架的缓存组件方法GetOrSetFuncLock上,说明程序不断地去调用缓存方法来读取和缓存数据,很有可能就是缓存失效了,或者缓存没有起作用,导致频繁地更新缓存。通过查看程序,发现这里的代码写得垃圾得一批,缓存根本没起作用,缓存键名加了一个时间戳是什么鬼?脑子抽了?

sample2

文件:pprof.heap

修复该问题后重新执行pprof分析。这次发现问题出在了io.ReadAll上。由于业务插件在采集业务程序的指标时是通过HTTP GET拉取,里面使用了ReadAll方法来读取业务程序的指标数据,并返回给上层转换为Prometheus数据结构,处理后再push给远端存储。

涉及到的代码如下。看样子是由于业务实例的指标数据过大(几MB到100+MB不等),这里完整读取的话会申请一块新的临时内存,造成过大的内存压力。

这种问题应该属于常见问题,其实对于所有的这种问题其实属于常见问题,对于所有的HTTP访问都有容易出现,大多数程序员的思维逻辑都是直接完整读取后再交给上层处理,但是这样确实会额外占用一块无意义的临时内存访问操作都容易出现,大多数程序员的思维逻辑都是直接完整读取后再交给上层处理,但是这样会额外占用一块无意义的临时内存。因此去掉临时内存申请,改为直接res.Body读取。读取。同时程序中其他HTTP请求也做相应改进。

sample3

文件:pprof.heap

修复后继续pprof分析,发现现在内存占用主要是在执行RemoteWrite远端写入时的第三方包数据结构转换组件。远端写入时的第三方包数据结构转换组件调用上。

这个fmtutil包是属于prometheus官方社区的组件包,用于执行指标数据的各种数据结构转换。我们看看这个fmtutil.makeLabels做了什么事情。

可以看到内存分配主要是这里的labels数组创建。这里的prompb.Label数据结构很奇怪,它这里使用了值传参形式,也就是说,指标标签的键值对都会复制一遍到这个数据结构很奇怪,它这里使用了值传参形式,也就是说,指标标签的键值对都会复制一遍到这个labels中。而我们知道,指标数据容量主要是标签键值对的大小,那么这里就会不断申请很多内存用于拷贝标签键值对数据。中。而我们知道,指标数据容量主要是标签键值对的大小容量占用,那么这里就会不断申请很多内存用于拷贝标签键值对数据。

我们来梳理一下业务实例的监控指标的数据结构转换流程:我们这里来梳理一下业务实例的监控指标的数据结构转换流程:

Code Block
languagebash
Metric Text -> dto.MetricFamily -> prompb.WriteRequest

其中:

  • Metric Text:从业务实例采集到的Prometheus监控数据,文本内存,从Response.Body中流式读取。
  • dto.MetricFamily:动:从Prometheus监控数据文本转换为的监控数据结构,用于方便采集程序内部处理,如果指标过滤、注入等操作。
  • prompb.WriteRequest:当监控数据处理好后,需要通过Prometheus RemoteWrite协议写入到远端存储,需要进行协议转换为该格式。

那么既然这里有两次协议转换,为什么Metric Textdto.MetricFamily没有出现内存占用问题呢?我们来看看它的数据结构:

可以看到,在转换为dto.MetricFamily的时候,内存容量占比较大的部分使用的是指针,其实并没有涉及到额外的内存申请。因此对转换prompb.WriteRequest数据结构的优化可以参考该思路。通过查看Prometheus的源码,发现其实这里不太好改,因为这是个公开的数据结构,并且引用的地方还蛮多,直接修改会有兼容问题。回顾自身的监控采集程序,已经没有更多的优化空间。而针对社区组件的改动,也许未来再详细考虑吧。

回顾总结

  • 开发人员应当对自身产出的物料负责。项目周期通常都非常繁忙(且忙中易出错),不可能单独预留改进优化的时间。在评估每个开发迭代的任务时,应当积极介入工作安排,不要被动接受输入。将遗留的可能风险及时和上级Leader沟通,给Leader更多的输入以便综合权衡评估工作安排来改进。不要等待问题影响业务/客户时再回过头来改进,那样的成本太大了。
  • Golang的工具链很丰富,特别是PProf的存在确实帮助开发人员快速排查、优化程序提供了高效的辅助。选择Golang作为主力开发语言是正确的!




Panel
titleContent Menu

Table of Contents
maxLevel2