登录
首页 >  Golang >  Go问答

内存使用差异:pprof vs. ps

来源:stackoverflow

时间:2024-02-07 23:18:23 479浏览 收藏

珍惜时间,勤奋学习!今天给大家带来《内存使用差异:pprof vs. ps》,正文内容主要涉及到等等,如果你正在学习Golang,或者是对Golang有疑问,欢迎大家关注我!后面我会持续更新相关内容的,希望都能帮到正在学习的大家!

问题内容

我一直在尝试分析用 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

但是ps在最后播种它几乎消耗了6752.23 mb(rss)。

此外,我将 defer profile.start(profile.memprofileheap).stop() 放在最后执行的函数处。将探查器放入​​ func main 中不会显示任何内容。于是我跟踪了一下函数,发现最后一个函数占用了相当大的内存。

我的问题是,如何找到丢失的 ~3gb 内存?


正确答案


有多个问题(与您的问题有关):

  1. ps(和 top 等)显示多个内存读数。唯一感兴趣的号码通常称为 RES or RSS。您不知道它是哪一个。
    基本上,查看通常名为 VIRT 的读数并不有趣。

  2. 正如 Volker 所说,pprof 不测量内存消耗,它测量(以您运行它的模式)内存分配率,即“多少”,而不是“频率”。

    要理解它的含义,请考虑 pprof 的工作原理。 在分析过程中,计时器会滴答作响,每次滴答时,分析器都会对正在运行的程序进行快照,扫描所有活动 goroutine 的堆栈,并将堆上的活动对象属性赋予这些堆栈的堆栈帧中包含的变量,以及每个堆栈框架属于活动函数。

    这意味着,如果您的进程将调用 os.ReadFile(根据其约定,它会分配一个足够长的字节片以包含要读取的文件的全部内容),则读取 100 次 1 GiB 文件每次,分析器的计时器将设法精确定位这 100 个调用中的每一个(它可能会在采样时错过一些调用),os.ReadFile 将归因于已分配 100 GiB。
    但是如果您的程序编写的方式不是保存这些调用返回的每个切片,而是对这些切片执行某些操作并在处理后将它们丢弃,则来自过去调用的切片当分配新的时,GC 可能已经收集了。

  3. 虽然 the spec 没有要求,但 Go 的两个“标准”当代实现——最初被称为“gc”的一个,大多数人认为是实现,以及 GCC 前端 -具有与您自己的进程流程并行运行的垃圾收集器;它实际收集进程产生的垃圾的时刻是由一组复杂的启发式控制的(如果有兴趣,请启动 here),这些启发式尝试在 GC 花费 CPU 时间和不执行 GC 花费 RAM 之间取得平衡;-),这意味着对于短命进程,GC 甚至可能不会启动一次,这意味着您的进程将结束,所有生成的垃圾仍然浮动,并且当进程结束时,操作系统将以通常的方式回收所有内存。 p>

  4. 当GC收集垃圾时,释放的内存不会立即返回给操作系统。相反,涉及两阶段过程:

    • 首先,释放的区域返回到内存管理器,这是为正在运行的程序提供支持的 Go rutime 的一部分。 这是一件明智的事情,因为在典型的程序中,内存变动通常足够高,并且释放的内存可能会很快再次分配回来。

    • 其次,保持空闲时间足够长的内存页数为 marked,让操作系统知道它可以使用它来满足自己的需求。

    基本上,这意味着即使 GC 释放了一些内存,您也不会在正在运行的 Go 进程之外看到它,因为这些内存首先会重新调整到进程自己的池中。

  5. 不同版本的 Go(同样,我的意思是“gc”实现)实施了将释放的页面返回到操作系统的不同策略:首先,它们被 madvise(2) 标记为 MADV_FREE,然后标记为 MADV_DONTNEED,然后再次标记为 MADV_FREE 。 如果您碰巧使用运行时将释放的内存标记为 MADV_DONTNEED 的 Go 版本,则读数 RSS 将更不合理,因为以这种方式标记的内存仍然计入进程的“RSS”,即使操作系统被暗示它可以回收该内存当需要时。

回顾一下。 这个主题足够复杂,您似乎太快得出某些结论;-)

更新。 我决定稍微扩展一下内存管理,因为我觉得你头脑中的这些东西的大局中可能缺少某些点滴,正因为如此,你可能会发现对你的问题的评论是毫无意义和轻蔑的.

建议不要使用 pstop 和朋友来测量用 Go 编写的程序的内存消耗,其原因是基于以下事实:在当代高级编程语言编写的 runtime environments 驱动程序中实现的内存管理已经相距甚远。来自操作系统内核及其运行的硬件中实现的直接内存管理。

让我们考虑 Linux 有具体的例子。
您当然可以直接要求内核为您分配内存:mmap(2) 是一个 syscall ,它可以做到这一点。 如果您使用 MAP_PRIVATE (通常也使用 MAP_ANONYMOUS)调用它,内核将确保您的进程的页表具有一个或多个新条目,内存数量为 pages,以包含您请求的字节数的连续区域,并返回序列中第一页的地址。
此时您可能会认为您的进程的 RSS 已增长了该字节数,但事实并非如此:内存被“保留”但实际上并未分配;为了真正分配内存页面,进程必须“接触”页面内的任何字节 - 通过读取或写入它:这将在 CPU 和内核处理程序上生成所谓的“页面错误”会要求硬件实际分配一个真正的“硬件”内存页。只有在此之后,该页面才会真正计入进程'RSS

好吧,这很有趣,但您可能会看到一个问题:操作完整页面不太方便(不同系统上的大小可能不同;通常在 x86 系统上为 4 KiB):当您用高级语言编程,你不会在这么低的层面上考虑内存;相反,您期望正在运行的程序以某种方式实现您需要的“对象”(我在这里并不是指 OOP;只是包含某些语言或用户定义类型的值的内存片段)。 这些对象可以是任何大小,大多数时候比单个内存页小,而且更重要的是,大多数时候您甚至不会考虑这些对象在分配时消耗了多少空间。 即使使用像 C 这样的语言(如今被认为是相当低级的语言)进行编程,您通常也习惯于使用标准 C 库提供的 malloc(3) 系列中的内存管理函数,这些函数允许您分配内存区域任意大小。

解决此类问题的一种方法是在内核可以为您的程序执行的操作上设置一个更高级别的内存管理器,事实上,每个通用程序用高级语言(甚至 C 和 C++!)编写的语言使用的是一种:对于解释语言(例如 Perl、Tcl、Python、POSIX shell 等),它由解释器提供;对于字节编译语言(例如 Java),它由执行该代码的进程提供(例如 Java 的 JRE);对于编译为机器 (CPU) 代码的语言(例如 Go 的“库存”实现),它由包含在生成的可执行映像文件中的“运行时”代码提供,或者在将其加载到程序中时动态链接到程序中。用于执行的内存。
这样的内存管理器通常非常复杂,因为它们必须处理许多复杂的问题,例如内存碎片,并且它们通常必须尽可能避免与内核对话,因为系统调用很慢。
后一个要求自然意味着进程级内存管理器尝试缓存它们曾经从内核获取的内存,并且不愿意将其释放回来。

所有这些意味着,比如说,在一个典型的活跃 Go 程序中,你可能会出现疯狂的内存搅动——成群的小对象一直被分配和释放,这使得对从进程“外部”监控的 RSS 的值几乎没有影响:所有这些扰动都是由进程内内存管理器和(就像普通 Go 实现的情况一样)处理的 GC 自然紧密集成的和MM一起。

因此,为了对长期运行的生产级 Go 程序中发生的情况有有用且可行的想法,此类程序通常提供一组不断更新的指标(交付、收集和监控)它们称为遥测)。对于 Go 程序,负责生成这些指标的程序的一部分可以定期调用 runtime.ReadMemStatsruntime/debug.ReadGCStats,或者直接使用 runtime/metrics 提供的内容。在 Zabbix、Graphana 等监控系统中查看此类指标非常有启发性:您可以从字面上看到进程内 MM 可用的空闲内存量在每个 GC 周期后如何增加,而 RSS 保持大致相同。 p>

另请注意,您可能会考虑在特殊环境变量 GODEBUG 中使用各种与 GC 相关的调试设置来运行 Go 程序,描述为 here:基本上,您使为正在运行的程序提供支持的 Go 运行时发出有关 GC 如何工作的详细信息(另请参阅 this)。

希望这会让您好奇并进一步探索这些问题;-)

您可能会发现 this 很好地介绍了 Go 运行时实现的内存管理(与内核和硬件相关);推荐阅读。

到这里,我们也就讲完了《内存使用差异:pprof vs. ps》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于的知识点!

声明:本文转载于:stackoverflow 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>