muh*_*mad 4 linux go memory-profiling pprof heap-profiling
我一直在尝试分析用 cobra 构建的 cli 工具的堆使用情况。该pprof工具显示如下,
Flat Flat% Sum% Cum Cum% Name Inlined?
1.58GB 49.98% 49.98% 1.58GB 49.98% os.ReadFile
1.58GB 49.98% 99.95% 1.58GB 50.02% github.com/bytedance/sonic.(*frozenConfig).Unmarshal
0 0.00% 99.95% 3.16GB 100.00% runtime.main
0 0.00% 99.95% 3.16GB 100.00% main.main
0 0.00% 99.95% 3.16GB 100.00% github.com/spf13/cobra.(*Command).execute
0 0.00% 99.95% 3.16GB 100.00% github.com/spf13/cobra.(*Command).ExecuteC
0 0.00% 99.95% 3.16GB 100.00% github.com/spf13/cobra.(*Command).Execute (inline)
0 0.00% 99.95% 3.16GB 100.00% github.com/mirantis/broker/misc.ParseUcpNodesInspect
0 0.00% 99.95% 3.16GB 100.00% github.com/mirantis/broker/cmd.glob..func3
0 0.00% 99.95% 3.16GB 100.00% github.com/mirantis/broker/cmd.getInfos
0 0.00% 99.95% 3.16GB 100.00% github.com/mirantis/broker/cmd.Execute
0 0.00% 99.95% 1.58GB 50.02% github.com/bytedance/sonic.Unmarshal
Run Code Online (Sandbox Code Playgroud)
但ps最后播种时它几乎消耗了6752.23 Mb(rss)。
另外,我将defer profile.Start(profile.MemProfileHeap).Stop()函数放在最后执行。将探查器放入func main不会显示任何内容。于是我跟踪了一下函数,发现最后一个函数占用了相当大的内存。
我的问题是,如何找到丢失的 ~3GB 内存?
kos*_*tix 15
有多个问题(与您的问题有关):
\nps(top等)显示多个内存读数。唯一感兴趣的通常称为RES或RSS。你不知道那是哪一个。
\n基本上,看通常命名的读物VIRT并不有趣。
正如 Volker 所说,pprof它不测量内存消耗,它测量(在您运行它的模式下)内存分配率\xe2\x80\x94,即“多少”,而不是“频率”。
要理解它的含义,请考虑其pprof工作原理。\n在分析过程中,计时器会滴答作响,每次滴答时,分析器都会对正在运行的程序进行快照,扫描所有活动 Goroutine 的堆栈,并将堆上的活动对象属性化为所包含的变量在这些堆栈的堆栈帧中,每个堆栈帧都属于一个活动函数。
这意味着,如果您的进程将调用os.ReadFile\xe2\x80\x94,根据其约定,它会分配一个足够长的字节片以包含要读取的文件的全部内容,\xe2\x80\x94100 次每次读取 1\xc2\xa0GiB 文件,探查器的计时器将设法精确定位这 100 个调用中的每一个(在采样时可能会错过一些调用),将归因于已分配了 100 个os.ReadFile调用\xc2\xa0GiB。
\n但是,如果您的程序的编写方式不是保存这些调用返回的每个切片,而是对这些切片执行某些操作并在处理后将它们丢弃,则过去调用的切片可能已经被收集当新的分配时,由 GC 分配。
虽然规范没有要求,但 Go\xe2\x80\x94 的两个“标准”当代实现(最初被称为“gc”,大多数人认为是实现)以及 GCC 前端\xe2\x80\x94 具有垃圾收集器,与您自己的流程并行运行;它实际收集进程产生的垃圾的时刻受到一组复杂的启发式控制(如果感兴趣,请从这里开始),这些启发式尝试在花费 CPU 时间用于 GC 和花费 RAM 不执行 GC 之间取得平衡;-),这意味着对于短命进程,GC 甚至可能不会启动一次,这意味着您的进程将结束,所有生成的垃圾仍然浮动,并且当进程结束时,操作系统将以通常的方式回收所有内存。
\n当GC收集垃圾时,释放的内存并不会立即返回给操作系统。相反,涉及两阶段过程:
\n首先,释放的区域返回到内存管理器,这是为正在运行的程序提供支持的 Go rutime 的一部分。\n这是一件明智的事情,因为在典型的程序中,内存变动通常足够高,并且释放的内存可能会快速分配回来再次。
\n其次,对保持足够长空闲时间的内存页进行标记,让操作系统知道它可以将其用于满足自己的需要。
\n基本上这意味着即使 GC 释放了一些内存,您也不会在正在运行的 Go 进程之外看到它,因为这些内存首先会重新调整到进程自己的池中。
\n不同版本的 Go(同样,我的意思是“gc”实现)实现了将释放的页面返回到操作系统的不同策略:首先它们被标记为madvise(2)as MADV_FREE,然后是 as MADV_DONTNEED,然后再次被标记为MADV_FREE。\n如果您碰巧使用以下版本Go 的运行时将释放的内存标记为, RSSMADV_DONTNEED的读取将更加不明智,因为以这种方式标记的内存仍然计入进程的RSS,即使操作系统被暗示它可以在需要时回收该内存。
回顾一下。\n这个主题足够复杂,您似乎太快得出某些结论了;-)
\n更新。\n我决定稍微扩展一下内存管理,因为我觉得你头脑中这些东西的大局中可能缺少某些点点滴滴,正因为如此,你可能会发现对你的问题的评论是没有意义的和不屑一顾。
\n建议不要测量使用 Go和朋友编写的程序的内存消耗的建议的理由植根于这样一个事实,即在为ps当代高级编程语言编写的程序提供支持的运行时环境中实现的内存管理与底层相去甚远。在操作系统内核及其运行的硬件中实现的直接内存管理。top
让我们考虑一下 Linux 有具体的例子。
\n你当然可以直接要求内核为你分配一块内存:mmap(2)是一个执行此操作的系统调用。\n如果你用MAP_PRIVATE(通常也用MAP_ANONYMOUS)调用它,内核将确保你的进程的页表有一个或多个为尽可能多的内存页添加新条目,以包含您所请求的字节数的连续区域,并返回序列中第一页的地址。
\n此时您可能会认为进程的RSS已增长了该字节数,但事实并非如此:内存被“保留”但实际上并未分配;为了真正分配内存页面,进程必须通过读取或写入来“接触”页面\xe2\x80\x94中的任何字节:这将在CPU上生成所谓的“页面错误”,并且内核处理程序将要求硬件实际分配真正的“硬件”内存页。只有在此之后,该页面才会真正计入进程的RSS。
好吧,这很有趣,但你可能会看到一个问题:操作完整的页面不太方便(在不同的系统上它可能有不同的大小;通常它是 4\xc2\xa0KiB x86 血统的系统):当你用高级语言编程时,你不会在这么低的层面上考虑内存;相反,您期望正在运行的程序以某种方式实现您需要的“对象”(我在这里不是指 OOP;只是包含某些语言或用户定义类型的值的内存片段)。\n这些对象可以是任何大小,大多数时候比单个内存页小,并且\xe2\x80\x94更重要的是,\xe2\x80\x94大多数时候你甚至不会考虑这些对象在分配时消耗了多少空间。\即使使用 C 等语言进行编程(目前 C 语言被认为是相当低级的),您通常也习惯于使用malloc(3)标准 C 库提供的系列中的内存管理函数,这些函数允许您分配内存区域任意大小。
解决此类问题的一种方法是在内核可以为您的程序执行的操作之上设置一个更高级别的内存管理器,事实上,每个用高级语言(甚至 C 语言)编写的通用程序而 C++!)则使用一种:对于解释型语言(例如 Perl、Tcl、Python、POSIX shell 等),它由解释器提供;对于字节编译语言(例如 Java),它由执行该代码的进程提供(例如 Java 的 JRE);对于编译为机器(CPU)代码\xe2\x80\x94的语言,例如Go\xe2\x80\x94的“库存”实现,它由包含在生成的可执行映像文件中或链接到当程序被加载到内存中执行时,它是动态的。
\n这样的内存管理器通常非常复杂,因为它们必须处理许多复杂的问题,例如内存碎片,并且它们通常必须尽可能避免与内核对话,因为系统调用很慢。
\n后一个要求自然意味着进程级内存管理器尝试缓存它们曾经从内核获取的内存,并且不愿意将其释放回来。
所有这些意味着,在一个典型的活动Go 程序中,您可能会出现疯狂的内存搅动\xe2\x80\x94 成群的小对象一直被分配和释放,这对“来自”监控的RSS值几乎没有影响进程的“外部”:所有这些扰动都由进程内内存管理器和 \xe2\x80\x94 处理,就像在普通 Go 实现\xe2\x80\x94 的情况下一样,GC 自然与 MM 紧密集成。
\n因此,为了对长期运行的生产级 Go 程序中发生的事情有有用的、可操作的想法,此类程序通常提供一组不断更新的指标(交付、收集和监控它们称为遥测)。对于 Go 程序,负责生成这些指标的程序部分可以定期调用runtime.ReadMemStats或runtime/debug.ReadGCStats直接使用所runtime/metrics提供的内容。在 Zabbix、Graphana 等监控系统中查看此类指标非常有启发性:您可以从字面上看到进程内 MM 可用的空闲内存量在每个 GC 周期后如何增加,而 RSS 保持大致相同。
另请注意,您可能会考虑在此处GODEBUG描述的特殊环境变量中使用各种与 GC 相关的调试设置来运行 Go 程序:基本上,您使为正在运行的程序提供动力的 Go 运行时发出有关 GC 如何工作的详细信息(另请参阅此) 。
希望这会让您好奇并进一步探索这些问题;-)
\n您可能会发现这是关于 Go 运行时\xe2\x80\x94 与内核和硬件相关的内存管理实现的很好的介绍;推荐阅读。
\n| 归档时间: |
|
| 查看次数: |
2195 次 |
| 最近记录: |