Go runtime/pprof 怎样读取 goroutineleak profile
来源:17golang原创
时间:2026-10-24 10:37:56 264浏览 收藏
Go 中读取 goroutineleak profile 的直接方式,是在目标进程内调用 pprof.Lookup("goroutineleak"),检查返回值不为 nil 后,用 WriteTo 写到指定输出。debug=1 适合读文本,debug=0 适合保存二进制 profile 后交给 pprof 工具分析。
我的取舍是先把采集做成一个小函数,不急着为它开放 HTTP 端口。下面围绕 Go 1.27 给出接入配方、文件保存和结果解读边界;代码是原创示例,不是已经运行过的故障复现记录。
Go 官方地址:https://go.dev/
我不会把 goroutine 数量上涨直接叫作泄漏
服务压力上来时,goroutine 数量可能上升,阻塞也可能只是等待中的正常状态。读 goroutineleak 的目的,不是再看一次总数,而是获取运行时能够识别出的泄漏协程栈。普通 goroutine profile 和这个 profile 回答的问题不同。
Go 1.27 发布说明说明,这类检测利用垃圾收集器的可达性信息,识别一大类无法再被唤醒的同步阻塞。但它不是所有业务泄漏的完备判定器。我的习惯会是先看返回了哪些栈,再追溯这些任务由谁创建、原本由谁唤醒,以及取消路径为什么没有结束它们。
最小写法是 Lookup 加 WriteTo
把下面函数放在应用的 leakdiag 包中,让调用方选择输出位置。它只读取当前进程的 profile,不能通过传一个 PID 就读取任意其他进程。
package leakdiag
import (
"fmt"
"io"
"runtime/pprof"
)
// WriteLeak 将当前进程的泄漏 profile 写入调用方提供的 Writer。
func WriteLeak(w io.Writer, debug int) error {
if w == nil {
return fmt.Errorf("输出 Writer 为空")
}
if debug != 0 && debug != 1 {
return fmt.Errorf("仅支持二进制 0 或文本 1")
}
profile := pprof.Lookup("goroutineleak")
if profile == nil {
// 旧版运行时或不提供该 profile 时返回明确错误。
return fmt.Errorf("当前运行时没有 goroutineleak profile")
}
if err := profile.WriteTo(w, debug); err != nil {
return fmt.Errorf("写入 goroutineleak profile: %w", err)
}
return nil
}
我保留了三个错误出口:输出目标不合法、运行时没有该 profile、写入失败。profile 不存在时,不要用 NewProfile("goroutineleak") 自己补一个同名对象;那会把“不支持这个观测能力”伪装成“支持但没有泄漏”。
文本调用可以像下面这样接入已有函数;示例片段依赖上面的 WriteLeak,不是另一个完整 main:
// 以下片段放入现有应用的受控诊断函数,需导入 os。
// 文本模式供人工阅读;stdout 必须是可信的输出位置。
if err := WriteLeak(os.Stdout, 1); err != nil {
return err
}

保存文件时同时处理权限和关闭错误
如果材料要交给另一位同事分析,我会保存为二进制,而不是把整份栈文本粘进公共日志。下面函数与 WriteLeak 放在同一包中,使用独占创建保留旧材料,并限制新文件的访问权限。
package leakdiag
import (
"errors"
"fmt"
"os"
)
// SaveLeakBinary 放在与 WriteLeak 相同的包中;目录由调用方准备并控制。
func SaveLeakBinary(path string) (err error) {
// 独占创建,已有文件时报错,避免覆盖先前诊断材料。
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return fmt.Errorf("创建诊断文件: %w", err)
}
defer func() {
// 保留关闭错误;写失败时文件可能不完整,应由调用方标记。
err = errors.Join(err, f.Close())
}()
return WriteLeak(f, 0)
}
这里用 O_EXCL 拒绝覆盖已有文件,文件模式设为 0600。目录也应由诊断操作方控制,不能把来自不可信请求的路径直接传进去。权限位的具体效果受操作系统影响,文件最终还要遵守所在环境的访问控制。
如果写入或关闭返回错误,应把这份文件标为未完整保存,不要将“文件名已经出现”作为采集成功的依据。函数不会自动删除失败产物,以免抹去已有诊断线索;调用方可以按自己的保留策略处理。示例也没有启动新的 goroutine、CPU 采样或定时任务。
文本与二进制是两种不同用途
| 输出选择 | 适合的后续操作 | 容易误用的地方 |
|---|---|---|
| debug=1 | 人工查看带符号说明的文本 | 不能只改文件扩展名就当成二进制 profile |
| debug=0 | 保存 gzip 压缩的 protocol buffer | 不能当作普通日志直接读取 |
| goroutine profile | 观察当前协程栈整体情况 | 数量多不等于每一个都已泄漏 |
| goroutineleak profile | 检查被运行时识别的泄漏栈 | 空结果不是全局无泄漏证明 |
对于二进制输出,可以在受控分析环境使用下列命令。service 表示与材料匹配的应用可执行文件,goroutineleak.pb.gz 是前面保存的材料。两者都是示例名称,不是本文附带的真实采集产物。
# 用与采集时相匹配的可执行文件辅助符号解析 # 此文件必须来自 WriteTo 的 debug=0 模式,不是文本快照 go tool pprof -top ./service ./goroutineleak.pb.gz
我会把 ProfileFile、MatchingBinary 和 CollectionRecord 三项放在一起:前两项是诊断文件与匹配程序,CollectionRecord 是自定义采集记录,保存构建标识、时间和业务场景。TextReader 表示人工文本阅读,PprofTool 表示二进制分析入口;它们是说明性角色,不是标准库新增的五个类型。

空 profile 不能证明没有泄漏
最容易产生误解的结果,是采集成功但看不到预期栈。我的第一反应不会是“那就一定没有问题”,而是检查版本、采集进程以及检测范围是否符合场景。
官方说明特别指出:同步对象若仍可通过全局变量或可运行协程的局部变量到达,这种基于可达性的检测可能识别不到相关泄漏。某个任务对业务而言已经没有价值,与运行时能否证明其永远无法被唤醒,并不是同一个条件。长期等待、遗漏取消和流量积压也需要结合其他观测继续判断。
版本上,Go 1.27 已将该能力转为正式功能,旧的 goroutineleakprofile GOEXPERIMENT 设置已经删除。不要把旧实验构建命令机械套在新版本上;也不要在 Lookup 返回 nil 时继续调用 WriteTo。上面的错误分支用于把不支持与空结果分开,而不是让程序继续伪装成功。
我会把采集入口留在受控诊断面
已有 net/http/pprof 接入的应用,可以使用 /debug/pprof/goroutineleak 端点。它是同类能力的 HTTP 入口,不是必须新增一个公开服务才能读取 profile。应把诊断路由限制在已授权的管理面,不因为排障而直接暴露到公网。
协程栈和符号信息可能透露内部结构,所以采集文件、日志出口和传输渠道都要按诊断材料管理。我会记录材料属于哪个构建和哪次任务,再把栈中相关阻塞点对应回创建者与退出契约;不会只根据单次文件的有无决定自动重启或删除业务数据。
读取 profile 本身也不会杀掉协程或替它们释放资源。真正的修复通常要回到任务所有权、关闭条件和取消信号。本文只提供读取与解释的配方,不声称检测到具体服务的泄漏,也不承诺空结果代表系统健康。
需要 StartCPUProfile 吗?
不需要。这里读取的是命名 Profile 的快照,与 CPU profile 的启动停止接口不是同一条路径。
可以把 debug=2 当成所有栈输出的通用选项吗?
不要类推。官方为不同预定义 profile 赋予的额外 debug 语义可能不同,本文封装仅接受明确使用的 0 与 1。
采集成功后还要关闭 Profile 吗?
不用。这里需要管理的是调用方打开的文件等输出资源,而不是给预定义 Profile 调用 Remove 或手动销毁。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
264 收藏
-
405 收藏
-
418 收藏
-
412 收藏
-
164 收藏
-
444 收藏
-
264 收藏
-
352 收藏
-
319 收藏
-
326 收藏
-
437 收藏
-
436 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习