一、背景
最近在做负责产品的性能测试,在测试过程中,发现某个服务有内存泄漏的情况,随着服务的不断运行,其占有的内存越来越高,没有被释放的迹象。
二、服务器性能监控
一般来说做得好的服务,都会有专门监控服务对运行服务的机器进行实时的监控,也应该有专门的工具对服务进行各种指标的监控,在接着说内存泄漏的之前,先看看如何对服务的各种指标进行手动查看:
1、free -h
可以查看内存整体使用情况:总的内存、使用内存、空闲内存、共享内存、缓冲/缓存、可用内存
2、df -h
查看磁盘占用情况
3、top
top -c可查看服务器整体运行情况,包括启动了多久,登录的用户数,cpu总体负载情况,任务运行情况、cpu使用情况统计、内存占用情况、各个进程的资源占用情况
shift+m 以内存排序 ,shift+p 以cpu占用排序
top -H -p $(pid) 查看该进程占用资源情况
4、htop 一个交互式的进程查看器
5、vmstat
vmstat是Linux中监控内存的常用工具,可对操作系统的虚拟内存、进程、CPU等的整体情况进行监视
常用命令vmstat -a -w -S M 3
三、pprof介绍
golang程序性能分析工具,常用进行cpu分析、内存分析、阻塞分析、互斥锁分析等,针对不同类型的应用,pprof的用法有些许差异。
1、工具性应用
package main
import (
"fmt"
"os"
"runtime/pprof"
)
func main() {
//CPU Profile
f, err := os.Create("./cpuprofile")
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
//Memory Profile
fm, err := os.Create("./memoryprofile")
if err != nil {
fmt.Println(err)
return
}
defer fm.Close()
pprof.WriteHeapProfile(fm)
for i := 0; i < 100; i++ {
fmt.Println("hello world")
}
}
工具性应用引入runtime/pprof包,需要在代码里手动的调用各种收集数据的函数,生成对应的文件
然后使用go tool pprof 文件名 命令可以进入交互式终端,使用命令查看占用情况(先用help命令看下有哪些命令)
输入web可以以可视化的方式查看,也可以用go tool pprof -http localhost:3001文件名 命令可视化地查看(这种方式需要安装graphviz工具包)
其中块比较大、比较红的是需要注意的
2、服务型应用
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"strings"
)
func sayhelloName(w http.ResponseWriter, r *http.Request) {
r.ParseForm() //解析参数,默认是不会解析的
fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息
fmt.Println("path", r.URL.Path)
fmt.Println("scheme", r.URL.Scheme)
fmt.Println(r.Form["url_long"])
for k, v := range r.Form {
fmt.Println("key:", k)
fmt.Println("val:", strings.Join(v, ""))
}
fmt.Fprintf(w, "Hello world!")
}
func main() {
http.HandleFunc("/", sayhelloName) //设置访问的路由
http.ListenAndServe(":9090", nil) //设置监听的端口
}
这种类型的应用引入net/http/pprof就行了,会自动的注册/debug/pprof路由,可在线查看各种指标
在/debug/pprof下有各种子路由,查看不同的指标
四、项目内存泄漏排查
1、pprof数据抓取
在对pprof有了基本的了解之后,对出问题的服务进行了了内存和协程的数据抓取。
用到以下路由:
/debug/pprof/heap?debug=1 内存分配的详情文件
/debug/pprof/goroutine?debug=1 当前goroutine的数目和大致信息
/debug/pprof/goroutine?debug=2 当前goroutine的详细信息
抓取文件进行分析后发现,内存(有正常的获取和释放,整体占用也不大)和协程(数量不大,也没有阻塞的现象)的数据都没有问题,这个过程我进行了很多次,一度有点儿怀疑人生(老板多次表达了关切)
另附一张有协程阻塞问题的图:
2、go新版本内存管理问题
既然pprof查不出问题,那就继续查资料,发现Go1.12~1.15中使用的新的MADV_FREE模式进行内存回收,这个模式会更有效的释放无用的内存,但可能会让RSS增高,但我们生产环境用的go版本是1.17,然而我测试docker里的go是1.15,所以还是将go升级到了1.17进行测试,果不其然,还是未能解决问题。
3、cgo
最后查到如果项目用到cgo,go无法管理c申请的内存,c申请内存用完必须手动释放,并且pprof无法监控到这块儿的内存使用情况,刚好这个服务用到了cgo,又去了解cgo的一些基础用法,排查了相关的代码,最终确实发现一处用完未释放内存的变量,而这个变量会在服务运行的过程中不断地创建,导致占用内存越来越大。发现问题后,就简单了,修改,测试,内存泄漏的现象消失。俗话说,golang10次内存泄漏,8次goroutine泄漏,1次是真正内存泄漏,还有1次是cgo导致的内存泄漏,没想到让我给遇见了。