[C#] .net 内存管理[6]

20 篇文章 0 订阅

Memory Measurements(内存测量)

  几乎在书的开头就有这样一个标题的章节,也许让人感到惊讶。 我们还没有真正谈到 .NET 内存管理,我们已经在研究与之相关的工具了吗? 这是一个深思熟虑的决定。 首先,使用这里描述的工具,我将经常说明后面讨论的具体概念。 其次,尽管我试图使这本书保持平衡,但它具有非常实际的意义。 在讨论各种主题时,我们将触及实际问题和示例。 使用本章中概述的工具,您可以了解如何识别和诊断这些问题。 所以只要我们不只是处理垃圾收集器构建的学术讨论,工具就离不开理论。

  在不知道使用什么工具的情况下,我们非常笨拙。 我们不知道如何检查我们的进程是否有内存问题。 我们不知道如何确保高 CPU 或内存消耗与 .NET 内存管理相关联。 至于工具本身,我们不知道观察到的不良行为的可能原因是什么。 事实是,没有一种单一的、超级通用的瑞士军刀。 有时最好检查一个工具,有时检查另一个工具。 要完全熟悉内存管理主题,最好学习如何使用它们中的每一个。 至少如果我们想成为这个领域的专家。

  此处描述的工具范围非常复杂。 一方面,您可以放置诸如 WinDbg 之类的低级工具。 在它的帮助下,我们可以进行真正深入的分析。 了解应该按正确顺序使用的数十种魔法命令将使我们能够进行大量调查。 在另一端可以放置带有方便用户界面的商业产品。 在这里一切都是愉快和轻松的,所以我们可以很快得到很多答案。 甚至在问之前。 另一方面,这些工具只允许其创建者提供的内容。 定制有时非常有限。 在这两个极端之间,还有许多其他工具始终是多功能性和易用性之间的折衷。 根据我的经验,这些 - 让我们称之为 - 高级商业程序几乎总是足够的。 但是这个“几乎”有很大的不同。 时。 我们遇到了在这些程序提供的分析中无法轻易解决的问题。 换句话说,如果我们认真对待这个话题,迟早你的手会被发动机的油脂弄脏。

  您可能会对此处介绍的静态代码分析工具缺乏强大的代表性感到惊讶。 几乎所有工具都基于运行时分析。 这是因为它并不是真的那么简单。 根据使用特性,代码可以转化为许多行为。 如果执行与其关联的操作,即使是最低效的内存管理代码片段也不会对进程产生不利影响——比如每小时一次。 静态代码分析可以提供帮助,但也可能造成伤害。 它可以不必要地专注于代码的不相关部分。

  性能比功能或代码质量更难,因为我们常常不知道“可以”或“应该”是什么。 有一些工具可以帮助我们显示违反特定阈值的情况。 但即便如此,在不了解主题的情况下,我们不确定这些阈值是否适用于我们的应用程序和我们的特定情况。 这就是为什么虽然这一章非常重要,但如果没有整本书的上下文,它就不会特别实用。

  我们衡量 .NET 程序行为的方式完全不同,具体取决于我们使用的操作系统。 这就是本章分为两部分的原因。 每个专用于两种最流行的解决方案之一 - Windows 和 Linux。 由于在 macOS 上使用 .Net 的普及率非常低,因此本书不介绍该平台的工具。

  重要的是,本章将介绍有哪些不同的工具以及如何使用它们的基础知识。 他们的具体使用和结果解释将在本书后面提供。 我们还没有足够的关于垃圾收集器的知识来开始使用这些工具来解决特定的问题。 将本章视为您可以而且应该使用的工具的综合列表。 我鼓励您在阅读时尝试一下,至少尝试一下。 因此,您将获得大量实用知识并熟悉它们。 它将在下一章中派上用场。 显然,您很有可能知道其中的部分或全部工具。 随意跳过它们的描述,尤其是在显示使用它们的基本步骤的部分。

  另请注意,本章存在一些先有鸡还是先有蛋的问题——如果不使用这里描述的工具,就不可能展示许多 GC 相关主题的实际方面,而这里描述的工具通常需要对这些 GC 有很好的理解—— 相关话题。 为了不让整本书因到处介绍的工具描述而变得混乱,现在介绍了一些基本用法,即使它提到了与 GC 相关的概念。 因此,如果您不理解此处描述的每个细节,请不要害怕。 我希望你在日常工作中使用这些工具时偶尔会回到本章,并从本书中获得充分的理解。

Measure Early(尽早测量)

  当我们询问有关性能优化的专家、框架开发人员,或者只是已经看到很多问题的专业人士时 - 关注性能最重要的是什么? – 他们都以同样的方式回应:及早衡量。 每个人都可能听说过过早优化是万恶之源这句话。 首先,花费数小时或数天的时间来优化代码是没有回报的,这种代码会给我们带来微乎其微的回报,同时又不会影响经济或硬件资源,也不会缩短应用程序的处理时间。 更糟糕的是,它肯定会转化为增加的开发成本。 并且可能不必要地复杂并且因此不可读的代码。 好的规则是相反的——不要过早地关注优化,让我们首先衡量我们是否有任何需要。 由于这是一本关于 .NET 中内存管理的书,它引导我们了解下一个通用规则 - 尽早测量 GC - 我将在本章末尾介绍。

  每次测量都可能带有或大或小的误差。 此外,测量可能会干扰观察到的过程。 我们从物理学中知道这些事实,在过程参数测量的情况下也没有什么不同。 因此,“如何衡量”这个问题的答案可以很简单(如果我们不深入细节)也可以很复杂(如果我们考虑精度)。 不同的工具提供不同的精度,我将稍微讨论一下。 然而,关于测量误差的统计讨论超出了本书的范围。 请注意,一旦我们测量某些东西,总会出现某些不准确的情况。

  尽管如此,正因为它在测量的背景下如此重要,我想在这里强调一些主要概念和误解。 我们将在本章的后面部分以及整本书的其余部分遇到这些问题。 最重要的是,在我们的日常工作中。

Overhead and Invasiveness(开销和侵入性)

  当谈到用于衡量我们的应用程序的不同工具时,记住以下两个最重要的概念始终很重要:

开销——很难找到一种工具,其用于测量应用程序不会使其变慢或以某种方式消耗更多资源。 然后我们谈论这个工具的开销,我们通常用百分比表示。 某些工具可能会在几个百分点的水平上造成几乎不明显的开销。 这意味着,例如,Web 应用程序响应时间将延长几个百分点。 或者这些百分比会降低桌面应用程序中动画的流畅度。 这种低开销的工具甚至可以在生产环境中使用。 另一方面,有一些工具可以通过附加到我们的应用程序来减慢它的数量级。 通常,他们会提供大量详细信息作为回报。 但是,由于它们带来的开销,它们只适用于开发环境或仅适用于单开发人员站。

侵入性——这个概念是相似的,是关于工具对应用程序本身的影响有多大。 使用该工具是否需要再次运行此应用程序? 您是否需要任何额外的权限或安装的扩展? 理想情况下,非侵入式解决方案可以在应用程序运行期间打开和关闭而不对其产生任何影响。 另一方面,完全侵入式的解决方案需要重新编译我们的应用程序并将其重新部署到给定的环境中。

Sampling vs. Tracing(采样与追踪)

  工具活动的另一个方面是它如何收集诊断信息。 主要有两种方法:

跟踪 - 在这种方法中,诊断数据是在特定的、突出显示的事件发生时收集的(因此它的另一个名字——基于事件)。 一个例子可能是在打开或关闭文件时、单击鼠标时或启动垃圾收集过程时保存跟踪数据。 这个解决方案的优势无疑是数据的精确性,因为它们来自事件发生的那一刻,我们可以写下给定类型的所有事件。 但是,如果此类事件非常频繁,这将导致非常大的开销。 因此,这种机制不用于函数的进入或返回这样频繁的低级事件。除非我们能够负担得起非常大的开销,例如,在本地开发站。

采样 - 在这种方法中,我们同意数据精度的损失,我们只不时收集诊断数据(因此它的另一个名称 - 基于时间)。 通过这种方式,我们只尝试对应用程序状态进行采样,而且我们这样做的频率越低,我们从测量中获得的结果就越不准确。 这种方法的一个典型示例是所有处理器上的周期性保存函数调用堆栈,例如,每 1 毫秒一次。 这使您可以统计地找出执行时间最长的函数。 当然,我们当然会不幸地丢失有关始终运行速度快于 1 毫秒的函数的信息。

Call Tree(调用树)

  最常用的应用程序行为可视化之一是构建调用树。 在这样的树中,每个节点代表一个函数。 该节点的子节点表示该函数调用的其他函数。 每个函数还附加了一些度量,很可能是总执行时间。 事实上,通常有一对与每个功能(树的每个元素)相关的指标:

  • exclusive(独占) - 只测量这个特定函数的值。 在执行时间的情况下,这将是仅在该特定函数上花费的时间。
  • inclusive(广泛) - 测量此特定函数的值及其所有后代测量值的总和。 在执行时间的情况下,这将是在这个函数中花费的时间,它调用的所有其他函数,它们调用的所有函数等等,递归地。

  此外,给定度量的百分比通常是根据所检查的整个范围来确定的。 这称为包含 % 和排除 % 测量。 让我们看一下图 3-1 中的示例,它显示了一个假设的探查器的结果。

  我们在这里看到函数 main 花费了程序 100% 的包含时间——即 3 秒。 这是调用所有其他函数的主函数,因此这是预期的行为。 但是只有 22% 的时间花在了 main 函数本身上; 其余的都花在了它调用的其他函数上。 例如,78% 的时间花在了 SomeClass 上。 方法 1 函数。 然后,这个函数有 66.7% 的时间用于调用另一个名为 SomeClass.HelperMethod 的方法。 浏览此调用树,我们将很快找出哪些应用程序组件最慢。

  另请注意,此类树通常会呈现聚合数据。 对于图 3-1 中的示例,它汇总了所有提到的方法调用事件。 所以 main 方法只被调用了一次,而 HelperMethod 被调用了两千次(这解释了为什么它的合计包含时间如此之大)。 因此,对这样一棵树的分析涉及寻找持久的方法或不一定慢但调用多次的方法。

在这里插入图片描述
图 3-1。 显示性能数据的调用树示例

  同样的想法可以用来可视化内存使用情况,其中每个节点代表一种特定类型的对象。 它的子对象是其他类型,该对象包含或引用了该类型的实例。 相信我,在分析应用程序的性能或内存消耗时,您会经常使用这些类型的可视化。

Objects Graphs(对象图)

  在内存上下文中,我们经常使用表示内存中对象之间关系的图,称为对象图或引用图。 在第一章的图 1-12 中可以看到此类图的一个示例,并在图 3-2 中进行了说明。 在我们的示例中,它显示了一组对象,其中一些对象引用另一个对象并且只有一个根。 通常,正常程序大小的此类图可能非常大,因此它们的可视化并不容易; 因此通常我们只分析其中的一部分。 您可以使用它们来显示聚合信息(有多少给定类型的实例引用了其他类型)或有关特定实例的信息(给定对象引用了哪些其他对象实例)。

在这里插入图片描述
图 3-2。 对象图示例。 对象 B 的保留子图已被额外标记。

  对于对象图,在您将有机会使用的不同工具中会出现三个重要概念:

  • 最短根路径 - 为所选对象确定,这是从特定对象到某个根的引用的最短路径。 由于对象图可能很复杂,并且根(甚至多个根)和对象之间可能有多条路径,因此显然也有最短的一条。 对于图 3-2,对象 H 的最短根路径是路径 root-A-H。 还有更长的路径:root-A-C-G-H 和 root-A-B-G-H。 到根的最短路径可能很重要,因为它通常指示对象之间的主要和最强关系,并且很好地指示使对象不可能被视为不可访问(因此可移动)的主要原因是什么。 其他路径通常是作为其他复杂依赖项的副作用而创建的。 但是,有时最短根路径可能会产生误导,因为它是由某些(有时是临时的)辅助引用(如缓存)创建的。 在这种情况下,我们似乎在图 3-2 中处理,其中对象 A 可能为了方便(如缓存)持有对对象 H 的引用,而 H 业务所有者位于对象 B、C 或 G 之间。

  • dependency subgraph(依赖子图) - 为选定的对象确定,这是包含对象本身以及直接或间接引用它的所有对象的子图。 例如,在图3-2中,对象B的依赖子图包含B和对象D、E、F、G和H。

  • retained subgraph(保留子图) - 针对选定对象确定,如果删除给定对象本身,则这是将被删除的子图。 由于依赖关系图可能很复杂,因此删除一个对象并不一定意味着依赖于它的所有对象都被删除。 对它们的引用可能仍由其他对象保留。 图 3-2 中对象 B 的保留子图包含对象 B 和对象 D、E 和 F。

除了这些概念之外,对于如何在工具中指示对象大小也有不同的解释:

  • shallow size (浅层大小) - 对象本身的大小(其所有字段,包括对其他对象的引用的大小)。 这显然很容易计算。
  • 总大小 - 对象的浅层大小和它直接或间接引用的对象的所有浅层大小的总和。 换句话说,它是依赖子图中所有对象的总大小。 这也很容易计算,因为我们只需要找到对象的依赖子图并对所包含对象的所有浅层大小求和。
  • retained size(保留大小) - 保留图中所有对象的总和。 换句话说,保留大小是删除给定对象后可以释放的内存量。 对象图中不同引用共享的对象越多,保留大小就小于总大小。 它是最难计数的,因为它需要对整个对象图进行复杂的分析。

  每当我们使用的工具谈论对象的大小时,值得问问自己考虑的是所提到的“大小”中的哪一个。

Statistics(统计数据)

  每当我们以不同方式汇总一些测量结果时,我们都会或多或少地使用统计工具。 如果我们无意识地这样做,就会有得出错误结论的风险。 例如,最常用的聚合数据方法是计算平均值,这应该给人一种“典型值”的感觉。 但平均值有两个主要缺点:它的结果没有指向任何特定样本(有人看到平均家庭有 2.43 个孩子吗?)。 而且它很容易隐藏数据分布的真实性质(很快就会说明)。 与方差等其他简单度量类似,所谓的安斯科姆四重奏完美地说明了这些问题(参见维基百科的图 3-3)。 有时,非常不同的数据集可能会得出统计上相同的结论。

在这里插入图片描述
图 3-3。 Anscombe 的四重奏 - 具有相同 x 和 y 数据平均值和方差的四个数据集。 资料来源:维基百科

  平均值受欢迎的优点和原因在于它的直观性,并且可以轻松计算而无需存储单个样本 - 对于每个额外的样本,我们都会增加总和,然后将其除以观察到的样本数量。 其他聚合方法要求所有样本保持最新。 这会给该工具带来大量开销。

您还应该使用哪些其他聚合方法? 最常见的包括:

  • 中位数 - 分隔样本的上半部分和下半部分的值。 它可以更好地了解典型值,因为它更能抵抗非常不匹配的样本。 而且,它表示的是真实样本之一,而不是人为计算的样本。
  • 百分位数 - 给定百分比的样本低于该值的值。 例如,第 95 个百分位数是低于该值的 95% 的样本。 这是我们感兴趣的数据的一个很好的指标,而不考虑非常不寻常的测量。 我强烈鼓励您测量您使用的工具的百分位数。 百分位数通常也是业务驱动的。 例如,我们希望确保应用程序 90% 的响应时间不会慢于 1 秒,99% 的响应时间不会慢于 4 秒。 测量响应时间的 90% 和 99% 将使我们能够轻松控制这一点。
  • 直方图 - 样本分布的图形表示。 它显示有多少样本落在特定值范围内。 这是最好的测量方法,因为它向我们展示了整个数据分布。

  所有这些指标如图 3-4 所示,显示了响应时间分布的示例直方图 - 每个响应时间范围内有多少个响应(以毫秒表示)。 从直方图中我们可以清楚地看到,最常见的响应时间在 110 +/- 5 ms 之间,响应时间与该值相差越大,出现的频率就越低。 此外,我们还可以说:

  • 平均响应时间为 104.3 毫秒。
  • 所有响应中有 10% 短于 60 毫秒(第 10 个百分位)。
  • 中位数为 100 毫秒
  • 90% 的响应都短于 150 毫秒(第 90 个百分位数)。
    在这里插入图片描述
    图 3-4。 显示中位数、第 10 个百分位数和第 90 个百分位数值的直方图示例 - 数据的正态分布

  图 3-4 所示的分布与所谓的正态分布非常相似,由于其形状特征,通常也称为钟形曲线。 许多测量都属于这一类,使得百分位数(甚至平均值)的解释非常合理。

  然而,要特别小心所谓的双峰(通常是多峰)数据分布的出现,它产生的平均值甚至中位数和百分位数没有多大意义(见图 3-5) 。 显然,测量了两种类型的响应(事实上,两种不同的正态分布),因此对它们进行任何聚合都是相当误导的。 我们宁愿说有两类响应,中位数约为 40 毫秒和 150 毫秒(并且应该首先调查为什么会出现这种双峰响应时间)。

在这里插入图片描述
图 3-5。 显示中位数、第 10 个百分位数和第 90 个百分位数值的直方图示例 - 数据的双峰分布

  幸运的是,多峰分布可以很容易地在直方图上直观地检测到。 因此,在测量某些东西时提供此类数据(或者至少自动指示已检测到多峰分布)变得至关重要。

  该工具提供的除平均值之外的测量值越多越好。 不幸的是,绝大多数仍然只使用平均值(极少数显示任何直方图)。 得出结论时需要非常小心。 最好尝试使用一种工具,该工具还可以通过百分位数或直方图向我们显示结果的分布。

Latency vs. Throughput(延迟与吞吐量)

  在任何性能分析和优化的背景下,两个标题概念都非常重要。 不幸的是,它们有时也被误解和错误解释。 大多数时候,我们认为一个人来自另一个人,并且他们完全相互依赖。 因此,值得给他们几句话的解释。 让我们从它们的简单定义开始:

  • 延迟 - 执行给定操作所需的时间。 它以某些时间单位来测量——天、小时、毫秒等等。
  • 吞吐量 - 每特定时间内执行的操作数。 它以每个时间单位的操作(或任何单个特定项目)来衡量 - 例如每秒字节数、每毫秒迭代次数或每年书籍数。

  一个称为利特尔定律的简单方程指定了这些指标之间的关系:
occupancy = latency * throughput(占用率=延迟*吞吐量)

  其中占用率是指在延迟指定的时间段内的操作次数。 重要的是,该方程适用于稳定的系统,其中不存在不自然的排队或对负载变化的动态适应(例如,在系统启动或关闭期间)。

  这两个概念在计算机网络环境中最常见,但出于我们的目的,我们将使用更有用的 Web 应用程序环境。 单个用户请求的处理时间决定了延迟。 单位时间内的用户请求数决定了吞吐量。 占用率是指在考虑的时间段内我们系统中的请求数量。

  当然,降低延迟(例如,通过使用更强大的 CPU)使我们在单位时间内处理更多的用户请求,因此也提高了吞吐量。 另一方面,我们可以仅通过增加并行处理的请求数量(例如,通过使用更多 CPU 核心等)来提高吞吐量,而无需改变延迟(见图 3-6)。 一般来说,在计算机科学中,增加吞吐量(通过任何类型的并行化)比减少延迟(通过在更复杂的硬件或算法设计中引入复杂性)更容易。

在这里插入图片描述
图 3-6。 吞吐量与延迟的关系:(a) 在一定的基本延迟下,我们能够每 X 秒处理 5 个请求,(b) 在缩短的延迟下,我们能够每 X 秒处理 7 个请求,© 通过加倍并行化,我们使吞吐量加倍 在不改变延迟的情况下每 X 秒处理 10 个请求

  当然,无限期地增加吞吐量是不可能的。 通常在某个阈值之后,进一步增加吞吐量也会对延迟产生负面影响,因为操作并不完全独立。 影响延迟的额外同步成本可能会吞噬吞吐量增加带来的收益。

  还有一个流行的阿姆达尔定律源自这样一个事实:潜在的延迟加速受到程序的串行(不可能并行化)部分的限制。 因此,举例来说,如果程序的 90% 部分可以并行化,那么仍有 10% 的部分可以正常运行。 因此,这种情况下的最大潜在加速被限制为最多 10 倍。

Memory Dumps, Tracing, Live Debugging(内存转储、跟踪、实时调试)

  为了分析我们的应用程序的状态,我们有几种侵入性不同的标准方法:

  • 监控 - 通常意味着非侵入式应用程序监控及其生成的诊断信息的使用(借助跟踪或采样)。 有时它采取更具侵入性的形式(例如重新启动应用程序),但仍然允许您观察它的运行情况,即使在生产环境中也是如此。
  • core dump(内存转储)——意味着保存进程在给定时刻的内存状态。 大多数时候,整个内存的状态都会保存到一个文件中,然后在另一台机器上通过各种工具进行分析。 这样的内存转储可能会占用几GB,但是使用正确的技能可以提供有关应用程序状态的非常详细的信息。 另一方面,它只是给定时刻的过程快照,如果没有时间变化的背景,有时很难得出具体的结论。 因此,经常执行两个或多个内存转储并相互比较。 进行内存转储的侵入性有所不同。 大多数情况下,它会导致该过程暂时暂停一段时间。 内存转储的一个重要应用是它们在应用程序失败后自动执行,这允许稍后调查其原因(称为事后分析) - 因此我们还可以将故障转储名称视为内存转储的特殊情况。 实际上,故障转储和内存转储的概念在您将遇到的工具中可以互换使用。
  • 实时调试 - 最具侵入性的方法是将调试器连接到进程并逐步分析应用程序。 这是最不常见的方法,因为前两种方法通常就足够了。 实时调试完全停止应用程序,因此只有在需要时才可以在开发环境中进行。 得益于广泛的监控和诊断工具,实时调试在内存管理解决方案中相当罕见。

Windows Environment (Windows环境)

  让我们首先了解 .NET 诞生的本机平台上的工具。 它在这里已经存在了大约 15 年。 Windows 上工具的选择能力和完善程度都非常好。 我们将从学习免费的和内置于系统中的低级工具开始。 我们在它们上投入了最多的时间,因为它们将在本书的后面经常使用。 但为了完整起见,我们将回顾商业项目。

概述

  Windows监控和跟踪基础设施已经相当成熟,包括.NET环境的上下文。 有两个主要可用组件:提供测量时间序列的指标驱动性能计数器和称为 Windows 事件跟踪 (ETW) 的事件驱动机制。 这两个工具几乎涵盖了所有的监控和诊断需求。 还有一个 Windows Management Instrumentation 机制,但它根本没有用于我们的目的(因为正如其名称所示,它更致力于管理和管理)。

  开发 .NET 时,所使用的诊断机制领域的选择是显而易见的。 成熟的 .NET Framework 及其多平台对应的 .NET Core 都支持性能计数器和 ETW 作为诊断平台。 更确切地说:

  • .NET 应用程序 - 可以使用 EventSource 类(来自 System.Diagnostics.Tracing 命名空间)来发出 ETW 事件,或者显然可以使用任何其他库直接登录到文件和许多其他可能的目标。
  • .NET 框架 - 发出性能计数器和 ETW 数据
  • 操作系统 API 和内核 - 还发出性能计数器和 ETW 数据

  现在我们将用大量的文字来讨论这两种机制以及如何在各种工具中使用它们。

VMMap

  这个出色的工具是 Microsoft Sysinternals 工具套件的一部分,允许您从操作系统的角度分析进程内存使用情况。 我们将在后面的章节中使用它来了解 .NET 应用程序如何消耗内存,以及第 2 章中描述的组织(可以为各种目的提交或保留的页面)。

  它是一个独立工具,不需要任何安装,可以从 https://docs.microsoft.com/en-us/sysinternals/downloads/vmmap 站点下载。 解压并运行后,我们选择我们感兴趣的进程,立即查看其内存使用情况分析(见图3-7)。 VMMap 检测 .NET 托管堆使用的页面以及专用于堆栈或加载的二进制文件的页面。

在这里插入图片描述
图 3-7。 简单 .NET 应用程序的 VMMap 视图示例(例如,正确检测到托管堆)

Performance Counters(性能计数器)

  用于监视 Windows 几乎各个方面的最常用工具之一是所谓的性能计数器机制。 这是一种非常轻量级的机制,可以用一句话来描述——进程可以使用它以数字时间序列的形式共享诊断数据。 它的巨大优势在于它是一种完全非侵入性的机制,并且没有明显的开销。 缺点是精度 - 它每秒生成样本,这可能不足以满足特定目的。

  这些数据的发布有许多不同的类别。 得益于此,我们可以获得有关系统的非常全面的了解。 通用性能计数器架构如图 3-8 所示。

在这里插入图片描述
图 3-8。 性能计数器架构

  一般来说,每个进程都可以决定在某个特定的性能计数器下发布数据,并且可以有多个进程执行此操作。 该机制在用户空间而不是内核级别工作。

每个性能计数器都有几个重要的属性:

  • category - 定义计数器所涉及的给定主题的一般范围;
  • name - 唯一标识给定类别中的计数器;
  • instance name - 系统中可能存在同一计数器的多个实例。 到目前为止,最常见的实例代表单独的进程。

  唯一标识性能计数器的组合写为“< Category>(< Instance>)< Name>”。 例如,指示记事本进程 (notepad.exe) CPU 使用情况的计数器将被称为“\Process(notepad.exe)% Processor Time”

  通过这种方式我们可以获得哪些样本数据? 我只提到其中的几个,以显示所提供的丰富信息:

  • CPU 使用率如何在内核和程序之间分布(Processor/% Privileged Time, Processor/% 用户时间);
  • 各个进程消耗 CPU 的程度(Process/% Processor Time);
  • 各个进程消耗内存的程度和方式(进程/工作集、进程/工作集 - 私有);
  • 硬盘驱动器的使用方式(每秒处理/IO 读取字节数、每秒处理/IO 写入字节数、每秒处理/页面错误数); • 磁盘写入/读取是否已排队(物理磁盘/当前磁盘队列长度);
  • .NET 应用程序生成多少个异常? (.NET CLR 异常数/秒抛出的异常数)。

  当然,我们最感兴趣的是 .NET CLR Memory 类别,我们在其中找到以下计数器(拼写和大小写不变):

  • 所有堆中的字节数

  • GC 句柄

  • 第 0 代Collections、# 第 1 代Collections、# 第 2 代Collections

  • 引发的GC

  • 固定对象的数量

  • 正在使用的接收器块数量

  • 提交字节总数,

  • 保留字节总数

  • GC 时间百分比
  • 分配的字节/秒
  • 最终确定幸存者
  • Gen 0 堆大小、Gen 1 堆大小、Gen 2 堆大小、大对象堆大小
  • 第 0 代升级字节/秒,第 1 代升级字节/秒
  • 进程ID
  • 从 Gen 0 开始提升最终确定内存
  • 从 Gen 0 升级的内存、从 Gen 1 升级的内存

注意 这些性能计数器名称(与 .NET CLR 类别中的其他名称一样)被翻译成操作系统的语言,因此在您的计算机或服务器中,您可能会在不同的名称和类别下找到它。 这可能非常烦人,因为在许多翻译中,这些名字听起来有点奇怪。 出于这个原因和许多其他原因,我建议您切换到英语作为默认 Windows 语言。

  如果您对垃圾收集主题至少知之甚少,您可能会猜到上述大多数计数器的含义。 我们将在本书的其余部分中陆续看到它们。 可以说这是一组完整的数据,可以非常深入地了解我们的应用程序的状态,这已经足够了。

 计数器的计算与垃圾收集生命周期同步。 特别是,大多数测量发生在 GC 开始或结束时。 从这个意义上说,性能计数器可以提供非常有价值且准确的信息。 然而,在这种情况下应该提及一些重要的评论:

  • 性能计数器值的读取完全由我们使用的工具对其进行采样的频率来控制。 如果采样频率足够高(例如每秒一次),数据将完全准确。 然而,如果采样很少,结果可能会非常错误且具有误导性。 例如,以一种不幸的方式进行采样,以至于我们总是会遇到完整的垃圾收集(消耗最多资源的垃圾收集),我们将得到关于 GC 花费了多少时间的错误视图。 换句话说,我们在使用性能计数器时要密切关注数据采样的方式。 最好的规则是尽可能频繁地对数据进行采样。
  • 性能计数器数据仅在发生特定事件(主要是提到的GC开始和结束)时更新,然后其值保持不变。 这可能会导致读数误导。 例如,假设在我们的进程中最近发生了完整 GC,在此期间 GC 时间百分比处于 50% 的水平。 从此时起,即使观察到的进程不执行任何工作,计数器 % Time in GC 将指示 50% 的高值。 只要没有发生新的GC,这些值就不会更新。 换句话说,通过观察计数器,我们应该更多地关注变化而不是当前值。 观测值只是最近采样的最后一个值。

  从 .NET 4.0 开始,Microsoft 更喜欢使用 ETW 数据(在下面的子章节中描述)而不是性能计数器。 然而,性能计数器的使用比ETW简单得多,因此该机制很受欢迎。 我们将在第 5 章中详细观察性能计数器和 ETW 测量之间的差异。

  性能计数器提供的数据可能有许多不同的消费者。 许多监控工具都在使用底层性能计数器,因为这是一种非常轻量级、无浪费的获取大量信息的方式。 但最简单且经常使用的工具之一是内置的 Windows 性能监视器。 使用 perfmon.exe 命令或通过在“开始”菜单中搜索来运行它。

  然后选择左侧的性能➤监控工具➤性能监视器项。 在出现的图表中,在上下文菜单中选择“添加计数器…”选项(参见图 3-9)。

在这里插入图片描述
图 3-9。 性能监视器 - 带有“添加计数器”上下文选项的总体视图

  使用该对话框选择感兴趣的类别(在我们的例子中为 .NET CLR 内存)以及特定的计数器和实例(参见图 3-10)。

在这里插入图片描述
图 3-10。 性能监视器 - 添加计数器对话框

  添加计数器后,我们通常需要花一些时间来调整图表以满足我们的需求。 它主要是关于:

  • 每个图表的缩放比例(数据选项卡,比例参数),
  • 采样频率和数量(常规选项卡、采样每个参数和持续时间)
  • 图形垂直比例(图形选项卡、垂直比例最小值和最大值参数)
  • 图表的滚动方式(图表选项卡,滚动样式参数)。

  正确选择上述参数(并可能选择每个数据系列的厚度和颜色),我们可以调整图表以进行短期分析或观察每日趋势。 下面图 3-11 和 3-12 中的示例说明了这一点。

在这里插入图片描述
图 3-11。 性能监视器 - 短期分析(100 秒),GC 生成大小可见

在这里插入图片描述
图 3-12。 性能监视器 - 长期分析(50 分钟),GC 生成大小可见

  性能计数器机制有一个令人讨厌的特征,我们必须学会忍受它。 正如我所提到的,每个以相同名称发布计数器的进程都有一个唯一的实例名称。 它对应于进程的名称。 例如,托管在 IIS 上的 Web 应用程序将具有 .NET CLR Memory(w3wp)# Bytes in all Heaps 计数器(因为应用程序池进程的名称为 w3wp.exe)。 但是,如果服务器上有多个应用程序托管在不同的应用程序池中,那么就会有多个按顺序编号的实例,例如w3wp、w3wp#1、w3wp#2等。如何找出哪个实例对应哪个应用程序池呢? 这里可以帮助我们:.NET CLR 内存/进程 ID 计数器。 多亏了它,我们可以找出每个实例进程的PID是什么。 不过要小心! 烦人的部分从这里开始——进程和性能计数器实例之间的分配可能会随着时间的推移而改变! 例如,如果应用程序池之一停止(由于不活动等原因),其余进程将覆盖其实例分配(请参见表 3-1)。

表 3-1。 应用程序池实例动态重命名的问题
在这里插入图片描述
  这是非常烦人的,特别是如果您想创建一个自动机制来观察特定的应用程序池。 然后,确保应用程序池自动停止之类的事情根本不会发生,这一点很重要。 通过类似的机制,我们还可以处理 IIS 是否启用了通过重叠方式重新启动应用程序池的选项。 然后我们暂时有同一个计数器的两个实例,因此这种不幸的重新分配实例是肯定的。

  由于上述不明显的映射,在手动观察 IIS 托管应用程序的情况下,最常见的场景如下:我们检查我们感兴趣的应用程序池的当前 PID,并查找具有对应的 w3wp 实例 .NET CLR 内存/进程 ID 计数器。 然后我们添加这个特定实例的计数器。

  实际上,这完全取决于您对性能监视器的看法。 还有许多其他程序会消耗性能计数器,但我们就到此为止。 我们将使用性能监视器来说明 Windows 上的垃圾收集操作。

Windows 事件跟踪(ETW)

  在各种可用的诊断工具中,最强大的工具无疑是 Windows 事件跟踪 (ETW) 机制。 不幸的是,它的功能似乎仍然被低估了。 也许这是因为这个机制是多年来逐渐发展起来的,尚未赢得他应有的利益。 它自 Windows 2000 以来就存在,但随着系统的每个新版本提供越来越多的功能。 它在 Windows Vista 和 Windows Server 2003 中得到了广泛的开发。在 Windows 7 中,它引入了存储每个事件的调用堆栈的关键日志记录功能(请参阅 https://msdn.microsoft.com/en-us/library/windows/desktop /dd392330)。

  ETW 机制的强大之处在于以非常低的开销(通常小于百分之几)提供大量信息。 因此,它可以毫无问题地用于生产系统。 它可以在运行我们的应用程序时打开或关闭,而无需重新启动它们。 事实上,许多工具都受益于 ETW。 我们甚至可能不知道有多少。 例如,众所周知的事件日志及其浏览器(eventvwr.exe)和资源监视器(resmon.exe)就是基于此机制构建的。 他们只是将通过 ETW 记录的事件可视化。 不过,为了消除疑虑,上一节中描述的性能计数器机制并不是基于 Windows 的事件跟踪。

  在我们介绍特定工具之前,最好先熟悉一下该解决方案的整体架构。 ETW机制可以区分某些概念,这些知识在使用时非常有用。 这些都是:

  • ETW 事件 - 可以记录在系统中的单个事件。
  • ETW 会话 - 整个机制的核心部分。 从概念上讲,顾名思义,它意味着正在进行的跟踪会话。 从技术上讲,这是系统资源的集合,例如内存缓冲区和用于写入磁盘的线程(见图 3-13)。
  • ETW 提供程序 - 每个可以传递事件的用户或内核模式元素。 有许多内置系统提供程序,按某些类别分组,例如网络提供程序、进程等。这还包括 .NET 运行时和我们的代码(如果我们希望发布自定义 ETW 事件)。 提供商通过全局唯一标识符 (GUID) 进行标识。
  • ETW 控制器 - 负责创建会话并将其连接到选定提供者的进程。
  • ETW 使用者 - 以某种方式使用事件数据、将其存储到所谓的事件跟踪日志 (ETL) 文件或实时呈现的任何工具。

  ETW 会话旨在尽可能降低开销(参见图 3-13)。 从进程的角度来看,这只是一个快速操作,涉及对内核级别维护的队列(内存缓冲区)进行非阻塞写入。 当应用程序继续正常操作时,专用内核线程处理这些队列并将事件写入特定目标 - 通常写入文件或另一个内存缓冲区(以进行实时分析)。

在这里插入图片描述
图 3-13。 Windows 内部事件跟踪

  从概念上讲,同一个提供者可以向多个会话提供信息(见图 3-14)。 相反,一个会话可以从多个提供者接收信息。 ETW 的特色是在提供者层面而不是流程层面进行操作。 为了从一个或多个提供者收集信息,在控制器的帮助下,我们创建一个新会话并将其附加到其中。 自会话启动以来,系统中实现该提供程序的所有进程都会将事件记录到我们的会话中。 所以可以说它是在收集整个机器的事件,而不是某个特定的进程。 我们感兴趣的进程的数据过滤仅在消费者程序的分析级别进行。

在这里插入图片描述
图 3-14。 Windows 事件跟踪 (ETW) 构建块,说明了各种配置的可能性。 请注意,一个进程可能具有多个 ETW 提供者的角色; 因此有些进程会被多次列出

  将事件保存在应用程序进程之外的缓冲区中还有另一个优点——应用程序崩溃不会导致诊断数据丢失。 当然,当记录大量事件时,对磁盘的访问可能会成为瓶颈,并为整个机器带来开销。 然而,只有当我们为会话选择太多密集使用的提供者时,我们才会遇到这种情况。 另一个威胁可能是磁盘空间耗尽,但有一个解决方案。 您可以以循环缓冲区模式将数据写入文件,这样我们就不必担心磁盘溢出。 数据将在固定大小的缓冲区中循环覆盖。 最典型的场景是运行会话,将数据存储在循环缓冲区中并等待特定场景的发生。 然后我们关闭会话并将数据从缓冲区保存到文件中。

  从 Windows 7 开始,可以收集与内核和用户事件相关的堆栈跟踪。 此类特殊事件(与源事件配对)的有效负载是堆栈帧上的十六进制地址,仅在分析阶段之后才对其进行解码。 然而,这适用于本机代码(即,也适用于 CLR 代码),但不适用于 Windows 8 之前的托管代码。在这种情况下,64 位 JIT 生成的动态代码的堆栈跟踪将不会被解码(它将 但是,对于 32 位代码而言)。 此问题在 Windows 8 中已得到修复,其中内核中的 ETW 框架已更改为识别 64 位 JIT 帧并毫无问题地遍历它们。

  例如,内置的 CPU 采样 ETW 事件使我们能够跟踪 CPU 使用率高的问题。 在每个采样事件(每 1 毫秒生成一次)中,都会从所有进程收集所有线程的调用堆栈。 多亏了这一点,从统计上来说,我们可以看到问题的原因——CPU最常停留在哪些功能上。 在操作系统提供商的支持下,您还可以跟踪同步问题(例如死锁)。 例如,它被 Visual Studio 的 Concurrency Visualizer 插件使用。

通过在Windows环境中使用各种诊断工具,我们经常需要访问符号文件(PDB - 程序数据库),这使我们能够从调用堆栈中解码有关方法和函数的信息。 最方便的设置是环境变量 _NT_SYMBOL_PATH,我们在其中指定公共 Microsoft 符号服务器的地址:srvC:\Symbolshttps://msdl.microsoft.com/download/symbols
这将使我们能够获取Windows操作系统的PDB文件和CLR库。 此外,在路径中,我们设置了一个本地文件夹,下载后将在其中缓存文件。

  有一个特殊的 NT 内核记录器会话,只能与内核级提供程序一起使用,而不能与用户模式一起使用。 例如,基本内核组记录进程的开始和结束。 例如,Microsoft-Windows-TCPIP 用户提供程序记录来自 tcpip.sys 内核模式驱动程序的事件。

  大多数情况下,当会话使用用户模式提供程序时,还会启动 NT 内核记录器会话。 它提供有关运行/销毁进程和线程的信息。 然后在分析阶段将结果组合在一起。

  操作系统提供了很多有趣的信息,比如进程和线程管理、网络、I/O操作等。但是最让我们感兴趣的是CLR也是一个ETW提供者,这个机制让我们学到了很多东西 关于我们应用程序上下文中的运行时。

  我们可以使用内置的 logman.exe 实用程序来查找系统中所有与 .NET 相关的提供程序(参见清单 3-1)。

清单 3-1。 使用 logman 实用程序列出所有与 .NET 相关的 ETW 提供程序

> logman query providers | findstr DotNET
Microsoft-Windows-DotNETRuntime
{E13C0D23-CCBC-4E12-931BD9CC2EEE27E4}
Microsoft-Windows-DotNETRuntimeRundown
{A669021C-C450-4609-A035-
5AF59AF4DF18}

  我们还可以使用它来找出在特定流程的上下文中可用的提供程序。 例如,如果我们询问 IIS 上托管的 ASP.NET WebAPI,我们将得到一个如清单 3-2 所示的列表(结果仅显示许多列出的提供程序中的几个)。

清单 3-2。 使用 logman 实用程序列出指定 ASP.NET 进程的所有 ETW 提供程序

> logman query providers -pid 6228

在这里插入图片描述
  如果我们询问在 CoreCLR 上运行的控制台应用程序,那么我们将得到一组略有不同的提供程序(参见清单 3-3)。

清单 3-3。 使用 logman utilit 列出控制台 .NET Core 进程的所有 ETW 提供程序

> logman query providers -pid 8528

在这里插入图片描述
  正如我们所看到的,除了许多不同的提供商之外,我们还找到了那些与 .NET 相关的提供商。 它们对于 WebAPI .NET Framework 和控制台 CoreCLR 应用程序具有相同的 GUID。 您还会注意到,同一提供程序有两个可互换使用的名称:Microsoft-Windows-DotNETRuntime 也称为 .NET 公共语言运行时。

给定提供程序内发出的每个 ETW 事件都有几个重要属性:

  • Id - 事件的唯一标识符,
  • Version - 用于事件版本控制
  • Keyword - 它可用于将事件分配给一个或多个含义(关键字),因为该字段实际上是一个位掩码,
  • Level - 日志记录级别,
  • Opcode - 它表示给定事件中的特定操作(阶段)。 最常用的内置值是开始和结束操作码,
  • Task - 它用于将提供程序内的事件分组为某些功能。

  通过 logman 工具,我们还可以了解特定提供商的详细信息。 对于主要的 .NET ETW 提供者,我们将获得如清单 3-4 所示的信息。

清单 3-4。 获取有关 .NET ETW 提供程序的详细信息

> logman query providers “.NET Common Language Runtime”

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  例如,有关 .NET 提供程序生成的事件列表,您可以使用 https://msdn.microsoft.com/en-us/library/dd264810(v=vs.110) 上的 MSDN 文档。 .aspx。 然而,它并不总是最新的。 因此,最好到达源,即给定提供商的清单文件。 ETW 清单文件定义给定提供程序生成的强类型事件信息。 这使得消费者能够正确解释记录的会话数据。 每个 .NET 运行时环境的清单文件都不同。 所以你可以在不同的位置找到它:

  • In case of CoreCLR under- .\coreclr\src\vm\ClrEtwAll.man;
  • 如果是 .NET Framework 4.0,则位于 c:\Windows\ Microsoft.NET\Framework64\v4.0.30319\CLR-ETW.man 下;
  • 对于 .NET Framework 2.0 及更早版本,它不可用,因为第一个版本不支持 ETW。

  当我们查看此文件时,我们将看到有关 MicrosoftWindows-DotNETRuntime 和 Microsoft-Windows-DotNETRuntimeRundown 提供程序的完整信息。 该文件的片段如清单 3-5 所示。

清单 3-5。 .NET ETW 提供程序的 ETW 清单文件片段

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  正如您所看到的,如果我们想在 .NET 环境中使用 ETW,这真是一座知识宝库。 让我们简单看一下这两个提供商生成的事件。 我们将在本书的后续章节中回顾所有这些事件,以便您对每个事件有充分的了解。 然而,在这里,我们将关注其中最有趣的。 这可以让你看到ETW机制提供的信息有多么丰富。

仅查看生成的事件可能会引发一些有趣的问题。 例如,GCSegmentTypeMap 类型的 ReadOnlyHeapMapMessage 段是什么? 我们将在第五章回答这个问题。

  我们最感兴趣的是 Microsoft-Windows-DotNETRuntime 提供程序,它提供分为 29 个不同任务的事件(如 ETW 命名法中,任务的事件属性对应于其功能类别)。 要了解所提供信息的丰富性,这些包括(括号中显示给定任务的事件数量):AppDomainResourceManagement (5),CLRAuthenticodeVerification CLRILStub (2), CLRLoader (18), CLRMethod (25), CLRPerfTrack (1), CLRRuntimeInformation (1), CLRStack (1), CLRStrongNameVerification (4), Contention (3), Exception (3), ExceptionCatch (2), ExceptionFilter (2), ExceptionFinally (2), GarbageCollection (58), IOThreadCreation (4), IOThreadRetirement (4), Thread (2), ThreadPool (5), ThreadPoolWorkerThread (3) and Type (1).

  正如我们所看到的,数量最多的组是垃圾收集器的任务 - 它包含 58 个不同的事件! 实际上,有 44 个不同的版本,因为有些出现在多个版本中。 我们在那里发现了什么? 非常有趣的东西! 您可以在表 3-2 中找到一些选定的事件及其包含的描述和数据。

表 3-2。 与 GC 相关的 ETW 事件示例

在这里插入图片描述
在这里插入图片描述
  如果我们认为每个事件都有精确的时间戳并且可能包含调用堆栈,那么我们就会看到可以在此基础上创建的强大诊断的愿景。 这就是为什么它被许多不同的工具使用的原因。 其中一些将在以下小节中揭示。

  如果您不理解表 3-2 中给出的 ETW 事件的描述,请不要害怕。 显然,需要一些关于 GC 的知识才能正确理解它们。 我们将在接下来的章节中回顾许多 ETW 事件(包括表 3-2 中的事件)。

  NT Kernel Logger 会话还提供许多有价值的信息,包括以下事件:Windows Kernel\ProcessStart、Windows Kernel\ProcessEnd - 进程启动和结束时、Windows Kernel\ImageLoad - 加载动态库时、Windows Kernel\TcpIpRecv - 当 TCP 时 /IP 数据包正在被接收,Windows Kernel\ ThreadCSwitch - 当线程获得或失去对 CPU 的访问权限时。 显然还有很多其他的,但在这里只列出其中的一小部分是没有任何意义的。 请参阅 MSDN 上的 NT 内核记录器跟踪会话文档以了解更多详细信息。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值