Go内存泄漏排查pprof和trace使用

一、Goroutine 泄漏

goroutine 被永久阻塞(如无缓冲 channel 收 / 发无对应操作、锁未释放、无限循环无退出条件),导致 goroutine 无法销毁,持续占用内存。

goroutine 泄漏 - channel 阻塞

Go 复制代码
// goroutine_leak.go
package main

import (
	"log"
	"net/http"
	_ "net/http/pprof" // 导入pprof的http服务,默认监听6060端口
	"time"
)

func leakGoroutine() {
	// 无缓冲channel:goroutine发送数据后无接收方,永久阻塞
	ch := make(chan int)
	// 启动goroutine,但数据无法发送(无接收),goroutine永远无法退出
	go func() {
		ch <- 1 // 阻塞点:无接收方,goroutine挂起
		log.Println("goroutine exit") // 永远执行不到
	}()
}

func main() {
	// 启动pprof http服务(用于采集数据)
	go func() {
		log.Println("pprof server start at :6060")
		log.Fatal(http.ListenAndServe(":6060", nil))
	}()

	// 每隔1秒创建一个泄漏的goroutine
	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()
	for range ticker.C {
		leakGoroutine()
		log.Println("create a leak goroutine")
	}
}

运行程序go run goroutine_leak.go

打开浏览器访问:http://localhost:6060/debug/pprof/

打开http://localhost:6060/debug/pprof/地址,不断刷新之后,会发现goroutine的数量会不断的增长。

用 trace 分析泄漏时序

Go 复制代码
采集10秒的trace数据
curl http://localhost:6060/debug/pprof/trace?seconds=10 > goroutine_leak.trace

分析trace文件
go tool trace goroutine_leak.trace
# 自动打开浏览器,进入trace分析界面,点击「Goroutine Analysis」

查看这一行:main.leakGoroutine.func1 10 44.608µs

Total execution time=44.608µs :这 10 个 goroutine 总执行时间极短,说明 goroutine 刚启动就被阻塞(卡在 ch <- 1),没有真正执行业务逻辑,也无法退出;

(Inactive, no stack trace sampled) 311 0s:系统闲置的 goroutine(如调度器、定时器等),属于 Go 运行时正常现象,无需处理。

点开main.leakGoroutine.func1链接显示如下:

Count: 10:表示创建的goroutine 总数

Execution Time: 0.42%:表示这些 goroutine 实际执行代码的时间占比

以第一个 goroutine(394)为例拆解

列名 数值 含义 泄漏结论
Total 9.926013377s 该 goroutine 从创建到 trace 结束的总时长 goroutine 存活了近 10 秒,未退出
Execution time 2.048µs 实际执行代码的时间 仅执行了「准备发送 channel」的极短逻辑,随即阻塞
Block time (chan send) 9.925963969s 阻塞在 channel 发送的时间 占总时长的 99.999%,核心阻塞原因
Block time (syscall) 0s 系统调用阻塞时间 无系统调用阻塞,排除 IO / 网络问题
Sched wait time 47.36µs 等待调度器调度的时间 可忽略,属于正常调度延迟

二、Channel 泄漏

channel 创建后未关闭,且无 goroutine 持续收发,导致 channel 占用的内存无法释放;或 channel 被存储在全局变量 / 长生命周期对象中,无法被 GC 回收。

Go 复制代码
// channel_leak.go
package main

import (
	"log"
	"net/http"
	_ "net/http/pprof"
	"time"
)

// 全局channel:创建后未关闭,且无收发,持续占用内存
var globalCh = make(chan []byte, 100)

func leakChannel() {
	// 向全局channel写入大量数据,且不消费
	bigData := make([]byte, 1024*1024) // 1MB数据
	for i := 0; i < 100; i++ {
		globalCh <- bigData // 填满channel
	}
	log.Println("write data to global channel")
}

func main() {
	// 启动pprof
	go func() {
		log.Println("pprof server start at :6060")
		log.Fatal(http.ListenAndServe(":6060", nil))
	}()

	// 每隔5秒写入数据,导致channel内存持续增长
	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()
	for range ticker.C {
		leakChannel()
	}
}
Go 复制代码
# runtime.MemStats
# Alloc = 2560056          // 当前堆内存已分配(未释放)
# TotalAlloc = 4078480     // 程序启动后累计分配
# Sys = 13255944           
# Lookups = 0 
# Mallocs = 8321
# Frees = 5188
# HeapAlloc = 2560056      // 堆内存已分配(和Alloc一致,无碎片)
# HeapSys = 7995392
# HeapIdle = 4661248
# HeapInuse = 3334144
# HeapReleased = 4390912
# HeapObjects = 3133
# Stack = 393216 / 393216
# MSpan = 55040 / 65280
# MCache = 9664 / 15704
# BuckHashSys = 1445774
# GCSys = 2470616
# OtherSys = 869962
# NextGC = 4935378         // 下次GC阈值
# LastGC = 1767950159448495000
# NumGC = 4                // GC仅执行4次,但内存未下降

三、堆内存泄漏(常见于缓存 / 切片 / 映射)

  • 全局 map/slice 存储大量数据,未及时清理,导致 GC 无法回收;
  • 引用逃逸:局部变量被逃逸到堆上,且被长生命周期对象引用;
  • 第三方库使用不当:如缓存库未设置过期时间,导致内存持续增长。
Go 复制代码
// memory_leak.go
package main

import (
	"log"
	"net/http"
	_ "net/http/pprof"
	"time"
)

// 全局map:存储大量用户数据,未清理
var userCache = make(map[string][]byte)

func leakMemory(userID string) {
	// 每个用户存储10MB数据,且永不删除
	userData := make([]byte, 10*1024*1024)
	userCache[userID] = userData
	log.Printf("add user %s to cache, current cache size: %d\n", userID, len(userCache))
}

func main() {
	// 启动pprof
	go func() {
		log.Println("pprof server start at :6060")
		log.Fatal(http.ListenAndServe(":6060", nil))
	}()

	// 每隔1秒添加一个用户到缓存,内存持续增长
	userID := 0
	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()
	for range ticker.C {
		userID++
		leakMemory(fmt.Sprintf("user_%d", userID))
	}
}

四、文件句柄泄漏

打开文件 / 网络连接后未关闭(defer close() 缺失),导致系统文件句柄耗尽,程序无法创建新连接 / 文件。

Go 复制代码
// fd_leak.go
package main

import (
	"log"
	"net/http"
	_ "net/http/pprof"
	"os"
	"time"
)

func leakFileDescriptor() {
	// 打开文件但未关闭,每次调用都会泄漏一个文件句柄
	f, err := os.Open("test.txt")
	if err != nil {
		log.Println("open file error:", err)
		return
	}
	// 缺失 defer f.Close()
	log.Println("open file success, fd:", f.Fd())
}

func main() {
	// 启动pprof
	go func() {
		log.Println("pprof server start at :6060")
		log.Fatal(http.ListenAndServe(":6060", nil))
	}()

	// 每隔1秒打开一个文件,泄漏句柄
	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()
	for range ticker.C {
		leakFileDescriptor()
	}
}

验证文件句柄泄漏

Go 复制代码
# 查看程序的文件句柄
pid=$(ps -ef | grep fd_leak | grep -v grep | awk '{print $2}')
lsof -p $pid | wc -l
# 输出数值持续增长,说明句柄泄漏

# 查看具体泄漏的文件:
lsof -p $pid | grep test.txt
相关推荐
沐知全栈开发9 小时前
PHP MySQL 插入数据
开发语言
WongLeer9 小时前
Go + GORM 多级分类实现方案对比:内存建树、循环查询与 Preload
开发语言·后端·mysql·golang·gorm
Victor35610 小时前
Hibernate(34)Hibernate的别名(Alias)是什么?
后端
小罗和阿泽10 小时前
Java项目 简易图书管理系统
java·开发语言
superman超哥10 小时前
Rust HashMap的哈希算法与冲突解决:高性能关联容器的内部机制
开发语言·后端·rust·哈希算法·编程语言·冲突解决·rust hashmap
刘一说10 小时前
腾讯位置服务JavaScript API GL与JavaScript API (V2)全面对比总结
开发语言·javascript·信息可视化·webgis
Victor35610 小时前
Hibernate(33) Hibernate的投影(Projections)是什么?
后端
a程序小傲10 小时前
【Node】单线程的Node.js为什么可以实现多线程?
java·数据库·后端·面试·node.js
Aotman_11 小时前
JS 按照数组顺序对对象进行排序
开发语言·前端·javascript·vue.js·ui·ecmascript