实习时,某版本需要上线前,自测某进程内存突然飙高
首先引入pprof性能侦测工具
具体问题定位
1.goroutine泄露
发现某函数下有chanrecv没有关闭,定位到代码发现有一个管道在连接中断后没有正确关闭。
解决方法:在连接关闭函数中添加管道关闭逻辑
2.内存增高
pprof分析工具中看见的内存大小与top命令中看见的内存大小差距较大。查询资料显示可能存在的情况是:
1.大量内存GC了但是没有返回给OS,可能有内核态操作(比如I/O)消耗了大量内存。
2.pprof采样是以512KB单位大小采样,可能有大量不同的方法占用内存<512KB,碎片内存被使用但是并没有被采集到。
3.golang惰性回收问题,go1.12-1.15版本中采用惰性回收,有内存gc了但是没有返还给OS
排除情况3,golang已经升级到1.18
排除情况2,在top中观察到占用已经有200MB之后还是无法观察到内存占用,不可能有大量不同的方法,并且每个方法占用内存小于512KB
排除情况1,如果有内核态操作出现了I/O,在pprof中也应当由用户态的函数开启系统调用,应该也能够捕获到内核态操作消耗的大量内存。
后来发现top中能发现两个相同名称的进程在运行
使用ps aux |grep "进程名"之后发现,该进程启动了子进程,主进程用于充当守护进程,子进程用于处理连接。直接pprof是配置在子进程代码段中,并没有过高的占用,主进程占用过高。所以重新配置pprof在主进程代码段中。
观察到主进程的堆占用确实持续升高。
cmd:=exec.Command(args)
//定位到代码段,该子进程是通过主进程使用cmd.start()来运行的。
//通过声明一个变量来存储子进程的stderr
var tmp Bytes.Buffer
cmd.Stderr=&tmp
cmd.Start()
cmd.Wait()
问题发现,就是因为子进程的err一直在输出,让tmp变量一直囤积,而子进程是持续运行的,主进程会在wait 处持续等待子进程运行,无法将该变量释放。该变量一直会囤积子进程的stderr输出,导致内存增大。
以前没有发现该问题是因为子进程stderr输出较少,而新版本中加入了端口探活,导致stderr量增多,使问题暴露出来。
解决方法:采用流式传输
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Println("Error creating StdoutPipe:", err)
return
}
err = cmd.Start()
if err != nil {
fmt.Println("Error starting command:", err)
return
}
// 在后台持续读取输出,但不做任何处理
go func() {
buffer := make([]byte, 4096)
for {
_, err := stdout.Read(buffer)
if err != nil {
break
}
}
}()
cmd.Wait()
解决该问题