提案详情
概述
我提议实现一个新的性能剖析类型,该类型允许按帧分解 goroutine 栈空间使用情况。
- 没有新的 API。作为
goroutine
性能剖析的新样本类型添加 - 每个堆栈跟踪的值是叶帧的空间之和
- 通过虚拟
runtime._FreeStack
叶节点表示可用的栈空间 - 总和应该接近或等于
/memory/classes/heap/stacks:bytes
- 这里有一个粗略的原型 CL:https://go-review.googlesource.com/c/go/+/574795(不包括测试,共 200 LoC)
鉴于上述情况,也许这个小而简单,可以跳过正式提案过程。但是,由于设计中包含一些有争议的选择,最好提前进行一些讨论和达成共识。
动机
我提出这个的主要动机来自于调试部署 PGO 到生产环境时出现的显著栈空间增长的情况(#65532),我能够通过在用户空间实现的一个hacky栈空间剖析器来根本原因(repo)。此外,我认为这种性能剖析类型对于其他场景也很有用,例如高 CPU 使用率(morestack
)(#18138)。
实现
我提议通过将每个 goroutine 性能剖析中的堆栈跟踪与其帧大小进行查找来实现此性能剖析类型(❶ 显示单个堆栈跟踪的帧大小)。然后,每个堆栈跟踪被拆分为一个根级别的前缀(从根开始)的堆栈跟踪,这些堆栈跟踪被分配为其叶帧的帧大小作为值(❷)。这将产生一个火焰图,其中每个帧的“自身值”对应于其帧大小,其总值对应于其帧大小加上其子节点的帧大小(❸)。
这些值然后乘以捕获给定堆栈跟踪的 goroutines 数量,得到栈空间使用的总和。
最后但同样重要的是,添加一个 runtime._FreeStack
叶节点以捕获帧之间使用的栈空间与 goroutine 的总栈大小之间的差异。此外,还使用 runtime._FreeStack
在根级别显示当前未使用的 goroutine 栈内存量。这些虚拟帧的灵感来自于产生一个总和为 /memory/classes/heap/stacks:bytes
的性能剖析,并让用户能够推理潜在的 morestack
问题。
原型
我已经在这里上传了一个原型的粗糙 CL:https://go-review.googlesource.com/c/go/+/574795(不包括测试,共 200 LoC)。
使用此原型,我们可以查看具有以下 goroutines 的程序的实际堆栈分析:
- 一个具有一千字节帧的 goroutine
- 一个具有两千字节帧的 goroutine
- 两个具有三千字节帧的 goroutines
代码片段
func launchGoroutinesWithKnownStacks() func() {
c1 := make(chan struct{})
c2 := make(chan struct{})
c3 := make(chan struct{})
c4 := make(chan struct{})
go oneThousand(c1)
go twoThousand(c2)
go threeThousand(c3)
go threeThousand(c4)
<-c1
<-c2
<-c3
<-c4
// hacky way to ensure all goroutines reach the same <-ch statement
// TODO(fg) make caller retry in the rare case this could go wrong
time.Sleep(10 * time.Millisecond)
return func() {
c1 <- struct{}{}
c2 <- struct{}{}
c3 <- struct{}{}
c4 <- struct{}{}
}
}
//go:noinline
func oneThousand(ch chan struct{}) [1000]byte {
var a [1000]byte
ch <- struct{}{}
<-ch
return a
}
//go:noinline
func twoThousand(ch chan struct{}) [2000]byte {
var a [2000]byte
ch <- struct{}{}
<-ch
return a
}
//go:noinline
func threeThousand(ch chan struct{}) [3000]byte {
var a [3000]byte
ch <- struct{}{}
<-ch
return a
}
注意:原型尚未实现提议的根级别的 runtime._FreeStack
帧。
性能
我还没有测量,但我怀疑所有这些都可以在不影响 goroutine 性能剖析开销的情况下完成。
下一步
请告诉我您的想法。cc @prattmic@mknyszek@nsrip-dd@rhysh(这是最近一次运行时诊断同步中讨论过的,参见备注)
7条答案
按热度按时间o3imoua41#
我认为整体的想法,感谢提交提案。
从查看示例图像中,我有一个轻微的“本能React”,那就是
_FreeStack
帧的显示有点奇怪。乍一看,它看起来像是 chanrecv1 路径正在“使用”大量的堆空间,但实际上它与堆使用没有真正的关系,只是恰好是 goroutine 停放的地方,也是帧附加的地方。话虽如此,我立刻就想不出如何更好地显示它。这里的基本问题似乎是通过共同的堆栈帧进行聚合,使事情变得复杂。
我能想到的“修复”方法有:
_FreeStack
。我认为这会使单个 goroutine 更容易理解,但代价是使其更难以看到聚合效应(例如,我有128个重复的工作 goroutines,它们在某个框架中总共使用了大量的堆栈)。_FreeStack
帧,而是在根级别的_FreeStack
帧中分组额外的堆空间。这是数据丢失,尽管对我来说还不是完全清楚这些数据有多有用。我可以想象的一个有用的方面是学习在实际增加堆栈大小之前,你还有相当大的余量可以增加堆栈大小。wswtfjt72#
感谢您的反馈 @prattmic。我也将此添加为明天运行时诊断同步的可能议程项目,因为实时解决这些问题可能更容易。
从查看示例图像中,我有一个轻微的“下意识”React,那就是
_FreeStack
帧的显示有点奇怪。乍一看,它似乎 chanrecv1 路径占用了很多堆栈空间,但实际上它与堆栈使用无关,只是 goroutine 停放的地方,以及帧附加的位置。是的,我同意。这确实令人困惑。
(但我不认为这是关于现实的误导。真正的空闲堆栈内存位于
gopark
下方,gopark 的自大小是正确的。但是的,这里的现实有点不直观,我同意这一点:p)话虽如此,我立刻就想不出如何更好地显示它。这里的基本问题似乎是通过公共堆栈帧进行聚合使事情变得复杂。
我能想到的“修复”方法有:
_FreeStack
。我认为这会使单个 goroutine 更容易理解,但代价是使其更难以看到聚合效果(例如,我有128个重复的工作 goroutines,它们在某个框架中总共占用了大量堆栈)。_FreeStack
框架,而是在根级别_FreeStack
框架中分组额外的堆栈空间。虽然这会丢失一些数据,但对我来说还不是完全清楚这些数据有多有用。我可以想象的一个有用的方面是学习在实际增加堆栈大小时之前,您有相当大的余量可以增加堆栈大小。好主意。那么关于版本1的一个变体,即在 goroutine 的根框架下方添加
_FreeStack
,但没有为每个 goroutine 创建一个根节点。以下是这将看起来像什么的截图:您有什么想法吗?
e3bfsja23#
嗯,我更喜欢这种方式,但顺序对我来说仍然感觉有些倒置(当第一个帧正在使用实际的堆栈空间时,免费堆栈空间是第二个帧,这似乎很奇怪)。但是这给了我另一个想法:
如果我们颠倒“免费堆栈空间”的概念,而是显示总分配的堆栈大小呢?具体来说,每个堆栈在“[总计]”帧结束,其值为总分配的堆栈大小。在火焰图中,你会得到类似这样的东西(原谅ASCII艺术):
左下角的空白空间是程序中的总空闲堆栈空间。
这听起来有点像我上面建议的(2),因为我们看不到每个goroutine类型的空闲空间,但我不确定我们必须失去这些信息。从理论上讲,运行
-focus threeThousand
应该只给我们threeThousand
goroutine部分,我希望它能从threeThousand
goroutines中仅显示一个“[总计]”条目。我不太确定这是否会立即起作用,考虑到我们以略微奇怪的方式爆炸样本。我们可能需要为每个goroutine使用唯一的伪PC(
0x1d0000001
、0x1d0000002
等),即使这样,我也不确定-focus
会表现得如我们所愿。wecizke34#
我认为如果我看到这样的profile,我很快就想回到过去看看goroutine在运行时确定需要增长堆栈时调用的堆栈。但看起来目前的提议是将信息与goroutine的当前堆栈绑定在一起。
我希望一个CPU profile查看
morestack
会给出一些提示,但这似乎类似于追踪运行时内部锁竞争所需的工作:你需要在行动中抓住它,并且你需要对最近的行为是否代表历史行为做出假设。具体来说,考虑一个HTTP服务器,其中:每个请求都处理得很快,有些请求会导致深层次的调用堆栈,所有请求都经常使用空闲的http/1.1 keep-alive连接。听起来目前的提议会在堆栈上显示很多具有
net/http.(*conn).serve
但没有...ServeHTTP
的goroutine,其中一些具有大量的runtime._FreeStack
,但没有明确指向哪个部分的http.Handler
负责增长。l2osamch5#
我不确定这个方法是否能直接生效,考虑到我们正在以一种稍微奇怪的方式爆炸样本。
@prattmic 我喜欢这个想法,但我认为
-focus
不会做我们需要它做的事情。即使是伪造的PC。问题在于节点下方的“空空间”是由仅包含该节点的堆栈跟踪创建的。在上面的示例中,
a
下方的“空空间”是由仅包含a
的堆栈跟踪创建的。因此,根据下面的焦点定义,我认为
-focus b
将匹配堆栈a;b;c
和a;b
,但不匹配a
(这相当于您示例中的total
节点)。查看其他pprof选项,
-show
(仅适用于图形视图?)或-show_from
都不会满足我们的需求。理论上,我们可以实现一个新的-focus_parents
选项来满足我们的需求。但我仍然不确定这是否能与您的伪造PC概念一起工作。因为默认情况下,火焰图将在函数级别进行聚合,所以任何操作都应该考虑单个pc值🤔。无论如何,我还意识到其他几个地方堆栈内存可能会隐藏:
M
上的g0
和gsignal
堆栈将此作为根级别的
runtime._FreeStack
跟踪可能会误导。因此,我们可能至少需要runtime._StackFree
(用于堆栈池)和runtime._StackSystem
分别计算这些堆栈使用区域。当然,我们也可以考虑不处理任何这些内容,只显示当前堆栈上的帧的堆栈内存使用情况。但这将留下一个与
/memory/classes/heap/stacks:bytes
相比非常大的空白区域,除非我们决定将其分成两个或更多指标。然而,这样做的优势是它更接近于heap_inuse
分析,在那里我们也只显示活动内存使用情况而不显示空闲空间(死对象 + 空闲堆 + 未使用的堆)。这对我在 #65532 中的分析已经足够好了。bogh5gae6#
我明白你的观点。我在连续分析中存在隐性偏见,假设用户可以访问许多堆栈分析并能够聚合它们。实际上,这应该允许用户在CPU分析中看到包括
morestack
帧在内的堆栈分析,尽管它们破坏了触发增长的贡献。你觉得这样够好吗?
我也可以理解专门针对
morestack
的分析的价值,但我认为这只是我次要的使用场景。主要的使用场景是分析与 goroutines 数量变化不相符的堆栈内存使用情况的变化。tpxzln5u7#
CPU分析可以像堆内存分析一样显示触发栈增长的原因。但是对于堆分析,
inuse_*
变体可以直接回答哪些是保留的。我想到了三种内存分析:
对于堆内存,我们可以从
alloc_*
视图中获取第一个,从inuse_*
视图中获取第二个。第三个来源是从完整的堆转储/核心文件构建支配树,或者类似的方式。对于栈内存,我们可以直接跳到第三种方式(它不是图,所以计算支配者很容易)。专注于
morestack
的CPU分析将提供第一种视图。但在我看来,第二种方式对于你描述的问题最有帮助,与另一种内存分析(堆分析)的概念更接近。