cgo内存泄漏排查

发布于:2024-12-06 ⋅ 阅读:(32) ⋅ 点赞:(0)

示例程序:

package main

/*
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
char* cMalloc() {
	char *mem = (char*)malloc(1024 * 1024 * 16);
	return mem;
}
void cMemset(char* mem) {
	memset(mem, '-', 1024 * 1024 * 16);
}
int arrLen = 1000;
int arrIndex;
char* globalMemAddr[1000];
void printAddr(char* mem) {
	if (arrIndex+1 >= arrLen) {
		arrIndex = 0;
	} else {
		arrIndex++;
	}
	globalMemAddr[arrIndex] = mem;
	printf("index: %d, addr: %p\n", arrIndex, globalMemAddr[arrIndex]);
}
*/
import "C"
import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"os"
	"runtime"
	"sync"
	"time"
	"unsafe"
)

var size int = 1024 * 1024 * 16
var memStat runtime.MemStats

func main() {
	go func() {
		_ = http.ListenAndServe("0.0.0.0:9091", nil)
	}()
	if len(os.Args) > 1 && os.Args[1] == "1" {
		var wg sync.WaitGroup
		for {
			wg.Add(1)
			runtime.ReadMemStats(&memStat)
			fmt.Printf("total memory begin: %v mb\n", memStat.TotalAlloc/1024/1024)
			go doCMalloc(&wg)
			wg.Wait()
			runtime.ReadMemStats(&memStat)
			fmt.Printf("total memory   end: %v mb\n", memStat.TotalAlloc/1024/1024)
			time.Sleep(2000 * time.Millisecond)
		}
	} else {
		var wg sync.WaitGroup
		for {
			wg.Add(1)
			go doGoMalloc(&wg)
			wg.Wait()
			time.Sleep(2000 * time.Millisecond)
		}
	}
}

// 无泄漏
func doCMalloc(wg *sync.WaitGroup) {
	defer wg.Done()
	cptr := C.cMalloc()
	C.cMemset(cptr)
	C.printAddr(cptr)
	bs := C.GoBytes(unsafe.Pointer(cptr), C.int(size))
	fmt.Printf("1: %s .. %s\n", string(bs[0:8]), string(bs[size-8:size]))
	C.free(unsafe.Pointer(cptr))
}

// 无泄漏
func doGoMalloc(wg *sync.WaitGroup) {
	defer wg.Done()
	bs := make([]byte, size, size)
	cptr := (*C.char)(unsafe.Pointer(&bs[0]))
	C.cMemset(cptr)
	C.printAddr(cptr)
	fmt.Printf("2: %s .. %s\n", string(bs[0:8]), string(bs[size-8:size]))
}

运行分支1:

将doCMalloc函数内的C.free注释掉。

go build memleak.go
./memleak 1

查看控制台输出:

查看top输出:

查看pprof输出:

#yum install graphviz

go tool pprof -http=192.168.36.5:9000 http://127.0.0.1:9091/debug/pprof/allocs

常规go工具链无法监测cgo内存:

top显示进程占用1.7g,inuse_space显示占用16mb,runtime.ReadMemStats显示分配过1442mb,alloc_space显示doCMalloc函数分配过944mb。

可以发现:不管是runtime.ReadMemStats还是pprof都不包含cgo内通过c语言分配的内存的占用情况,不能反映真实的进程占用内存情况。

内存泄漏检测之valgrind

centos8安装:

yum install valgrind --nogpgcheck

执行泄漏检测:

将doCMalloc函数内的C.free注释掉。

将代码中的arrLen调小便于出现泄漏,如调成10。

valgrind --tool=memcheck --leak-check=full --error-limit=no --trace-children=yes --show-leak-kinds=all --track-origins=yes --log-file=./vlog.txt ./memleak 1

检测结果:

给出了泄漏位置:at 0x4C38185: malloc (vg_replace_malloc.c:442)

内存泄漏检测之bcc/tools/memleak

centos8(kernel:4.18.0)安装:

(centos7不支持,需要自己升级内核到4.x版本)

bcc/INSTALL.md at master · iovisor/bcc · GitHub

yum install bcc-tools --nogpgcheck

执行泄漏检测:

将doCMalloc函数内的C.free注释掉。

将代码中的arrLen调小便于出现泄漏,如调成10。

/usr/share/bcc/tools/memleak -a -p `pidof memleak`

对于golang来说如果从程序一启动就执行监测,效果不理想,因为top10可能都是golang正常管理的内存,需要等到所有运行需要的golang管理的内存都预热到go的三级cache中,此时golang很少需要向系统申请内存了,再监测cgo的内存泄漏。

如从启动开始监测:

最好在golang稳定运行后再开始监测,如:

清晰的定位到泄漏位置:0x00007faa43885a71      sysmalloc+0x7d1 [libc-2.28.so]

还可以指定监测最小的内存泄漏单位(byte),如我们监测大于等于16MB的泄漏:

/usr/share/bcc/tools/memleak --min-size 16777216 -a -p `pidof memleak`

恢复doCMalloc函数内的C.free,再执行监测:

未发现泄漏。

内存泄漏检测之strace

strace只能通过肉眼观察分配和回收是否配对出现或者由于缓存机制不再频繁申请内存,本身不能给出泄漏报告。

执行泄漏检测:

将doCMalloc函数内的C.free注释掉。

将代码中的arrLen调小便于出现泄漏,如调成10。

strace -e 'trace=brk,mmap,munmap' -f -p `ps aux | grep memleak | grep -v 'grep' | awk '{print $2}'`

泄漏情况观察如图:

未泄漏情况观察:

恢复doCMalloc函数内的C.free。

从启动时开始监测:

strace -e 'trace=brk,mmap,munmap' -f ./memleak 1 &> slog &
tail -f slog

显示16mb+的空间分配或映射只在程序启动不久时出现了,之后再没有出现。

--end--