使用性能监控分析Golang应用内存泄露问题

本文介绍如何综合运用性能监控(火焰图等)工具,观测并发现应用程序存在的问题。

模拟Golang应用内存泄露的示例程序

package main

import (
	"github.com/pyroscope-io/client/pyroscope"
	"log"
	"os"
	"runtime"
	"time"
)

type demo struct {
	slice []byte
}

var demos = make([]*demo, 0, 1024)
var demosNormal = make([]*demo, 0, 1024)
var beforeTime time.Time

func main() {
	runtime.SetBlockProfileRate(100)
	runtime.SetMutexProfileFraction(100)
	serverAddress := os.Getenv("PYROSCOPE_SERVER_ADDRESS")
	if serverAddress == "" {
		serverAddress = "http://localhost:4040"
	}
	appName := os.Getenv("PYROSCOPE_APPLICATION_NAME")
	if appName == "" {
		appName = "go-leak-app"
	}

	_, err := pyroscope.Start(pyroscope.Config{
		ApplicationName: appName,
		ServerAddress:   serverAddress,
		AuthToken:       os.Getenv("PYROSCOPE_AUTH_TOKEN"),
		Logger:          pyroscope.StandardLogger,
		Tags:            map[string]string{"hostname": os.Getenv("HOSTNAME"), "environment": "test", "version": "1.0"},
	})
	if err != nil {
		log.Fatalf("error starting pyroscope profiler: %v", err)
	}

	for {
		memNormal()
		memLeak()
		time.Sleep(time.Second)
	}
}

func memNormal() {
	if len(demosNormal) > 600 {
		time.Sleep(time.Nanosecond)
		return
	}
	demosNormal = append(demosNormal, &demo{
		slice: make([]byte, 4000),
	})
}

func memLeak() {
	now := time.Now()
	if now.Sub(beforeTime).Minutes() > 60 {
		demos = make([]*demo, 0, 1024)
		println("clear cache data")
		beforeTime = now
	}
	demos = append(demos, &demo{
		slice: make([]byte, 4000),
	})
}

该段代码包含三部分:

  • main函数:程序的入口,每秒执行一次memNormal函数与memLeak函数。main函数中还包含性能监控接入代码。

  • memNormal函数:模拟正常程序运行。在程序运行的前10分钟内,每秒增加4000字节内存;在后续时间段内,不再额外增加内存。

  • memLeak函数:模拟内存泄露情景。在程序运行的所有时间内,每秒增加4000字节内存。

    模拟程序以1小时为1个周期,周期结束后将释放所有内存。

问题发现与排查过程

本排查过程基于上述示例程序的数据。

通过数据查询进行排查

您可以通过性能监控>数据查询页面,排查问题。更多信息,请参见数据查询

  1. 选择元数据为service:golang_leak_applanguage:gotype:profile_memvalueTypes:inuse_space

    通过上述元数据,您可观测目标App使用的内存情况。

  2. 设置主时间范围为4小时。

  3. 选择environment和version标签。

完成上述设置后,您可以观测迷你图和火焰图,确认问题。

  • 观察到迷你图曲线呈1小时周期状上升,符合模拟程序预期。如果真实场景内存泄露,将持续上涨。image.png

    说明

    如果您的程序较复杂,您可以通过表格检索功能高亮指定方法名,并通过火焰图交互定位。具体操作,请参见火焰图

  • 设置主时间范围为2023-03-07 12:11~2023-03-07 12:21,即一个周期的最开始10分钟,该阶段为正常申请使用内存阶段,可以看到memNormal函数与memLeak函数所占大小几乎一致。image.png

  • 设置主时间范围为2023-03-07 12:51~2023-03-07 13:01,即该周期的末尾阶段,该阶段为memNormal函数使用内存不再变化,但memLeak函数一直持续增加的阶段,可以看到此时memLeak函数的使用内存远超memNormal函数。image.png

通过上述数据,可知此时段memLeak函数出现内存泄露问题。为了有更明显的对比,并调查内存泄露的严重程度,您可以单击快速对比,进入数据对比页面,对比该问题所在时段与过去时段的资源占用差异。

通过数据对比进行排查

您在数据查询页面,单击快速对比后,系统将同步您已完成的配置(元数据筛选条件、标签筛选条件、聚合策略、元数据时间范围、主时间范围)到数据对比页面。更多信息,请参见数据对比

  1. 设置预设同比时间为过去30min

  2. 观察并聚焦memNormal函数与memLeak函数的具体变化。image.pngimage.png

通过上述对比可知memNormal函数过去和现在的内存没有变化,但是占用总内存比例却减少了38%,同时memLeak函数的当前值比过去半小时的值涨了0.43 GB内存,占用总内存比例增加了37%。