BackIcon记一次 Go 程序内存泄漏

2023年4月24日

Openai logomark

Hamster1963

问题描述

后端功能为采集家庭网络信息并对外开放接口,每1s或每5s进行采集。

采用docker打包Go编译完成后的二进制文件,通过docker-compose部署在云服务器上。

在运行一段时间后,内存报警,内存占用不断增高,怀疑发生了内存泄漏,且GC在其中无法清理内存。

原功能实现

原功能采用for关键词加time.Sleep进行实现,参考于Python服务器探针代码。

while True:
	CPU = get_cpu()
	Uptime = get_uptime()
	Load_1, Load_5, Load_15 = os.getloadavg()
	MemoryTotal, MemoryUsed, SwapTotal, SwapFree = get_memory()
	HDDTotal, HDDUsed = get_hdd()
  get_router_info()

在Go代码中,为获取一段时间内的消耗流量与消耗流量最多用户,创建了两个变量。

var maxFLow int
var maxFLowUser string

并在后续的计算中给变量赋值

// 计算用户流量变化
			for _, user := range userList.Array() {
				userMap := gconv.Map(user)
				for _, userNow := range userListNow {
					userNowMap := gconv.Map(userNow)
					if userMap["id"] == userNowMap["id"] {
						// 计算用户流量变化
						totalFlow := gconv.Int(userNowMap["down"]) - gconv.Int(userMap["down"])
						if totalFlow > maxFLow {
							maxFLow = totalFlow
							maxFLowUser = gconv.String(userNowMap["remark"])
						}
					}
				}
			}

内存泄露原因

由于采用for关键词,在 Go 中,如果在 for 循环中创建了一个新的变量,但在每个循环迭代中没有将其重新分配,则可能会导致内存泄漏。

例如,以下代码段将创建一个新的字符串,并在每个循环迭代中将其连接到另一个字符串上,但它没有在每个迭代中将新字符串重新分配:

var result string
for i := 0; i < 1000000; i++ {
    newString := "hello"
    result += newString
}

在这种情况下,每次迭代都会创建一个新的字符串,但由于新的字符串没有重新分配,因此在每个迭代后,将有一个新的字符串和一个旧的字符串(在 result 变量中)仍然引用它。这会导致大量的内存泄漏,因为旧的字符串仍然存在于内存中,而无法被垃圾回收器清理。

为了避免内存泄漏,可以在每个迭代中将新字符串重新分配给变量,如下所示:

var result string
for i := 0; i < 1000000; i++ {
    newString := "hello"
    result += newString
    newString = "" // 重新分配
}

通过将新字符串分配给一个新变量,可以确保每次迭代都有一个新的变量来引用新的字符串,从而避免内存泄漏。在重新分配新变量后,旧的字符串将不再被引用,并且可以被垃圾回收器清理。

归根结底,则是在每次循环中创建的变量一直处在引用状态,GC无法正确对其进行垃圾回收,导致程序内存占用一直在不断升高。

解决办法

采用Go语言的思路进行书写,项目基于Goframe框架,重构后采用框架中的gcron(定时任务)进行定期的数据拉取,函数内部定义参数可在函数在执行结束后结束引用关系,避免采用for循环造成内存泄露。

_, err = gcron.AddSingleton(ctx, "*/5 * * * * *", func(ctx context.Context) {
				err = network_utils.NodeUtils.GetNodeInfo()
				if err != nil {
					g.Dump(err)
				}
			}, "获取当前代理节点信息")
			if err != nil {
				panic(err)
			}

关于GC

Go语言的垃圾回收(GC)是在运行时进行的,不是在函数结束后。Go语言的垃圾回收器是一种并发、自适应的垃圾回收器,它会在程序运行时自动检测内存使用情况,并在必要时回收不再使用的内存。

Go语言的垃圾回收器会在程序运行时周期性地扫描堆内存中的对象,标记那些仍然被引用的对象,然后清理那些未被引用的对象。垃圾回收器的执行时间由当前程序的内存使用情况和硬件配置等因素决定,并不一定在函数结束后立即进行。