记一次go内存消减的过程

go内存问题说明:

工作中,短暂接手了一个go编程的应用程序,其中有有一个升级组件,在升级过程、比较md5等场景中约占用内存500M。

使用工具gops、pprof

gops: goland Attach to Process 工具。通过gops可以连接到本地的进程中,进行断点调试。

获得途径:通过github 获得gops源码,然后进行编译,编译期间遇到几个问题通过修改源码中的go.mod一一修改,得到gops.exe。放在GoPath下,完成。

pprof:go通过 go tool pprof能够观察到go应用程序的CPU、Memory的情况,可以通过graphviz将 pprof中的得到的信息,形成可视化的图像,方便追踪问题。

初步探索:

问题原因是同事使用升级组件时,内存会飙涨,等到10分钟左右,内存才会降下来。

首先,我们从网上了解到,go是一个拥有gc的语言。同时,随着版本的更迭,go的gc愈加的完善。所以,不要轻易怀疑一种语言,怀疑自己的代码。

go的垃圾回收采用的是 标记-清理(Mark-and-Sweep)算法:先标记出需要回收的内存对象,然后清理掉。
go回收内存慢的原因可能有以下几个:
1.go在没有长时间触发gc的情况下,可能需要有一个时间间隔才会主动触发一次gc;
2.go的gc机制,不是立即将内存返回给系统,而是告诉系统,这些内存没有人使用了,但系统不会立即回收,而是延缓回收,已提供给go需要内存是更快的分配速度。

即,go大体上可以看做内含一个内存池的存在。猜测程序爆发500M的内存是因为峰值内存上去导致的。

初次交手

起初,在浏览go的资料时,关于string和[]byte的转换,是我比较关注的一个点。
go中 string和[]byte的相互转换的时候,需要重新分配一次内存,使用拷贝进行操作。

程序中,关于文件操作的Read、Write方法,使用了OS.WriteString等方法,通过string、[]byte的多次转换来实现功能。

结果:程序内存有下降,不过峰值下降不够明显,且不够稳定。

神来之笔 - pprof

仅仅通过外部的猜测,就想要解决程序的内存问题,显然有些天方夜谭。这个时候需要思考和一定顺手的工具。

pprof在这个时候进入我的视野。

.runtime/pprof:采集程序(非 Server)的运行数据进行分析
.net/http/pprof:采集 HTTP Server 的运行时数据进行分析

.net/http/pprof可以通过程序将数据发送到本地端口,可以利用本地浏览器进行可视化追踪。添加的代码如下:

import (
    _"net/http/pprof"
)

func main () {
    go func() {
        http.ListenAndServe("0.0.0.0:8899", nil)
    }()
    ...
}

随后启动程序,走流程测试。

cmd命令:go tool pprof -alloc_space/-inuse_space http://localhost:8899/debug/pprof/heap

执行结果如下图:


pprof-cmd.jpg

之后可以使用pprof的指令。 这里我使用的是 web,即打开本地浏览器,可以看到具体的内存流向图如下。


流向图.png

可以看出内存的保障和一个函数有关:bytes.makeSlice

罪魁祸首

根据pprof所得的流程图可以得到,在bytes.makeSlice的上一级调用时 ioutil.ReadAll。
找到ioutil.ReadAll的的源码,发现会调用buffer.grow这个底层增长函数。且在调用ioutil.ReadAll函数的地方,[]btyes的大小没有一个初始估计,导致go会通过*2的方式去申请足够大小的内存,读取数据,这一定程度上导致了内存的损耗。

措施:在ioutil.ReadAll函数调用前,先预估文件的大小,然后调用。
结果:内存下降了在170M左右。

结果

170M峰值内存,在目前来看是调节的一个极限,因为程序在运行过程中,会下载约100M的文件。而我观察到的下载方式是通过全部读取到内存中,然后再写入文件中。这个业务方式,直接导致峰值内存一定会在100M以上,而且还有协成下载和其他的一些内存使用。

内存优化,暂时告一段落。

心得

这是我初步接手go的程序,短短2周不到的了解,就可以看到内存上面处理问题的诸多不足。这些有可能是因为前人的逻辑漏洞,也有很多是go本身底层的东西就没有看透。
只想说:
1.语言无对错,错在使用方法上
2.不是说高并发就是好的,真正的高并发也是应该考虑实际应用环境才可以的。
3.遇到事情,不能想什么其他的外路(ps:比如内存高就重启之类的)

以上是此次解决问题的一些想法。问题很简单,重要的是编程思路和不断的学习+测试,记录下来给以后的自己一个警醒。

最后贴一个获得md5值的go代码,大家可以看看他们的内存差别。

  1.  f, err := os.Open(file)
         if err != nil {
            return false
         }
     h := md5.New()
     io.Copy(h,f)
     dst := string(h.Sum(nil)[:16])
    
  2.  data, err := ioutil.ReadFile(file)
     if err != nil {
         return
     }
     value = md5.Sum(data)
     return value, nil

你可能感兴趣的:(记一次go内存消减的过程)