Golang内存泄漏问题分析

Yishto 2021-08-20 21:46:16
Categories: Tags:

title: Golang内存泄漏问题分析 date: 2019-02-10 13:12:48

tags: golang,内存泄漏,内存碎片

自己基于Golang开发的feedgen RSS采集工具一直有内存泄漏的嫌疑:在N1上运行多天后,会出现内存上涨不释放的问题。

这两天趁有空进行了分析,内存泄漏问题有之前Java-SQLite内存碎片问题分析的经验,虽然是Golang,但万变不离其宗,很快就找到了根因,在此简单记录一下过程。

定位过程

参考文章:https://my.oschina.net/zhangxc73912/blog/1627410 在main函数添加代码:

go func() {
http.ListenAndServe(“localhost:6060”, nil)
}()

goroutine排查

在 http://127.0.0.1:6060/debug/pprof/ 查看goroutine的变化,发现goroutine数量会随着采集任务的开始和完成增大和减小,所以说明goroutine是正常的。

heap泄漏排查

执行命令

go tool pprof -inuse_space http://127.0.0.1:6060/debug/pprof/heap

回显如下:

Entering interactive mode (type “help” for commands, “o” for options)
(pprof) top -20
Showing nodes accounting for 19622.33kB, 100% of 19622.33kB total
flat flat% sum% cum cum%
16027.23kB 81.68% 81.68% 16027.23kB 81.68% main.FilterEmoji
515.19kB 2.63% 84.30% 1027.20kB 5.23% main.dealPagesContent
515.19kB 2.63% 86.93% 515.19kB 2.63% strings.NewReplacer
514.38kB 2.62% 89.55% 514.38kB 2.62% bytes.(*Buffer).String (inline)
514kB 2.62% 92.17% 514kB 2.62% regexp.(*bitState).reset
512.19kB 2.61% 94.78% 512.19kB 2.61% runtime.malg
512.14kB 2.61% 97.39% 512.14kB 2.61% net/http.(*Transport).dialConn

此时OS侧的RES内存占用1GB+,而堆内存只有几十MB,说明GC正常工作,Heap并没有发生泄漏。

访问 http://127.0.0.1:6060/debug/pprof/goroutine?debug=1 在下方查看Heap的占用情况:

runtime.MemStats

Alloc = 143508344

TotalAlloc = 634112594352

Sys = 1246181624

Lookups = 0

Mallocs = 53849547

Frees = 53839465

HeapAlloc = 143508344

HeapSys = 1198358528

HeapIdle = 1049632768

HeapInuse = 148725760

HeapReleased = 0

HeapObjects = 10082

Stack = 1212416 / 1212416

MSpan = 278768 / 3276800

MCache = 3392 / 16384

BuckHashSys = 2308922

GCSys = 40034304

OtherSys = 974270

NextGC = 242189584

LastGC = 1549772896307189100

HeapSys 代表向系统申请了1142MB内存,而HeapInuse 代表此时使用的Heap只有141MB内存。

综合以上,说明并没有显示的内存泄漏,GC正常工作,实际是内存碎片导致tcmalloc将申请的内存全部缓存了,内存碎片是老朋友了,当年定位Java+SQLite内存碎片问题花了太大的功夫,没想到又在Golang上面遇到了。

内存碎片触发点

内存碎片一般是小量内存频繁的申请,malloc会对其进行缓存,而单次小量而次数超大时,就会导致随着运行时间越长,系统内存占用越大的问题,从OS层面看与内存泄漏的现象一致,内存碎片问题对于long live类型服务是致命的。要根治内存碎片,就要查找代码中小量内存频繁申请的位置。

使用命令:

go tool pprof –alloc_space http://127.0.0.1:6060/debug/pprof/heap

回显是自应用启动以来所有的内存分配情况:

Time: Feb 10, 2019 at 1:02pm (CST)
Entering interactive mode (type “help” for commands, “o” for options)
(pprof) top
Showing nodes accounting for 230.58GB, 99.33% of 232.12GB total
Dropped 245 nodes (cum <= 1.16GB)
flat flat% sum% cum cum%
230.48GB 99.29% 99.29% 230.48GB 99.29% main.FilterEmoji
0.10GB 0.042% 99.33% 232.08GB 100% main.genFeed

找到了问题函数FilterEmoji

func FilterEmoji(content string) string {
new_content := “”
for _, value := range content {
_, size := utf8.DecodeRuneInString(string(value))
if size <= 3 {
new_content += string(value)
}
}
return new_content
}

这是一段拷贝自网上的过滤RSS中Emoji的函数,FeedDemon不支持Emoji,因此加入了这个兼容。问题就出在new_content字符串拼接,这会在栈内不断申请小内存,而频次是内存的每个字符就会拼接一次,因此长时间运行后,出现了内存碎片问题。

问题解决

问题出在大量小字符串拼接,那么修改字符串拼接的方式即可,Golang 1.10版本以前可以使用byte.Bufffer,Golang 1.10版本以后,使用strings.Builder即可

func FilterEmoji(content string) string {
var buffer bytes.Buffer
for _, value := range content {
_, size := utf8.DecodeRuneInString(string(value))
if size <= 3 {
buffer.WriteRune(value)
}
}

return buffer.String()

}

修改后,长时间使用FeedGen也只有几十MB的RES内存占用,内存碎片导致的“内存泄漏”问题得到解决。