内存使用差异: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 内存?
正确答案
有多个问题(与您的问题有关):
ps
(和top
等)显示多个内存读数。唯一感兴趣的号码通常称为RES
orRSS
。您不知道它是哪一个。
基本上,查看通常名为VIRT
的读数并不有趣。正如 Volker 所说,
pprof
不测量内存消耗,它测量(以您运行它的模式)内存分配率,即“多少”,而不是“频率”。要理解它的含义,请考虑
pprof
的工作原理。 在分析过程中,计时器会滴答作响,每次滴答时,分析器都会对正在运行的程序进行快照,扫描所有活动 goroutine 的堆栈,并将堆上的活动对象属性赋予这些堆栈的堆栈帧中包含的变量,以及每个堆栈框架属于活动函数。这意味着,如果您的进程将调用
os.ReadFile
(根据其约定,它会分配一个足够长的字节片以包含要读取的文件的全部内容),则读取 100 次 1 GiB 文件每次,分析器的计时器将设法精确定位这 100 个调用中的每一个(它可能会在采样时错过一些调用),os.ReadFile
将归因于已分配 100 GiB。
但是如果您的程序编写的方式不是保存这些调用返回的每个切片,而是对这些切片执行某些操作并在处理后将它们丢弃,则来自过去调用的切片当分配新的时,GC 可能已经收集了。虽然 the spec 没有要求,但 Go 的两个“标准”当代实现——最初被称为“gc”的一个,大多数人认为是实现,以及 GCC 前端 -具有与您自己的进程流程并行运行的垃圾收集器;它实际收集进程产生的垃圾的时刻是由一组复杂的启发式控制的(如果有兴趣,请启动 here),这些启发式尝试在 GC 花费 CPU 时间和不执行 GC 花费 RAM 之间取得平衡;-),这意味着对于短命进程,GC 甚至可能不会启动一次,这意味着您的进程将结束,所有生成的垃圾仍然浮动,并且当进程结束时,操作系统将以通常的方式回收所有内存。 p>
当GC收集垃圾时,释放的内存不会立即返回给操作系统。相反,涉及两阶段过程:
首先,释放的区域返回到内存管理器,这是为正在运行的程序提供支持的 Go rutime 的一部分。 这是一件明智的事情,因为在典型的程序中,内存变动通常足够高,并且释放的内存可能会很快再次分配回来。
其次,保持空闲时间足够长的内存页数为 marked,让操作系统知道它可以使用它来满足自己的需求。
基本上,这意味着即使 GC 释放了一些内存,您也不会在正在运行的 Go 进程之外看到它,因为这些内存首先会重新调整到进程自己的池中。
不同版本的 Go(同样,我的意思是“gc”实现)实施了将释放的页面返回到操作系统的不同策略:首先,它们被
madvise(2)
标记为MADV_FREE
,然后标记为MADV_DONTNEED
,然后再次标记为MADV_FREE
。 如果您碰巧使用运行时将释放的内存标记为MADV_DONTNEED
的 Go 版本,则读数 RSS 将更不合理,因为以这种方式标记的内存仍然计入进程的“RSS”,即使操作系统被暗示它可以回收该内存当需要时。
回顾一下。 这个主题足够复杂,您似乎太快得出某些结论;-)
更新。 我决定稍微扩展一下内存管理,因为我觉得你头脑中的这些东西的大局中可能缺少某些点滴,正因为如此,你可能会发现对你的问题的评论是毫无意义和轻蔑的.
建议不要使用 ps
、top
和朋友来测量用 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.ReadMemStats
和 runtime/debug.ReadGCStats
,或者直接使用 runtime/metrics
提供的内容。在 Zabbix、Graphana 等监控系统中查看此类指标非常有启发性:您可以从字面上看到进程内 MM 可用的空闲内存量在每个 GC 周期后如何增加,而 RSS 保持大致相同。 p>
另请注意,您可能会考虑在特殊环境变量 GODEBUG
中使用各种与 GC 相关的调试设置来运行 Go 程序,描述为 here:基本上,您使为正在运行的程序提供支持的 Go 运行时发出有关 GC 如何工作的详细信息(另请参阅 this)。
希望这会让您好奇并进一步探索这些问题;-)
您可能会发现 this 很好地介绍了 Go 运行时实现的内存管理(与内核和硬件相关);推荐阅读。
到这里,我们也就讲完了《内存使用差异:pprof vs. ps》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于的知识点!
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
139 收藏
-
204 收藏
-
325 收藏
-
477 收藏
-
486 收藏
-
439 收藏
-
357 收藏
-
352 收藏
-
101 收藏
-
440 收藏
-
212 收藏
-
143 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 508次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习