Tomato Punk

To see a world in a grain of sand,And a heaven in a wild flower

0%

诊断性能问题的工作流程(0)

原文为Maoni发布在Microfost Blog中的,诊断性能问题的工作流程系列.
目前共更新三章.在本章中,Maoni介绍了一些关于GC设计与工具的使用,如果您已经具备了诊断性能问题的经验,可以直接跳过本章.

原文信息

@Maoni Stephens-Twitter
@Maoni Stephens-Github

authorize

如果这篇文章可以帮到您,那么这将是我最大的荣幸,希望您点进原文,在文章下方留下善意的回复,您的支持将是这些可敬的社区磐石保持创作激情中最大的一部分:)

原文

中文版本将不会以任何形式收费,版权属与原作者


正文

我想描述一下我是怎样诊断内存中的性能问题,更确切的说是在进行此类诊断的各种工作流程中可通用的部分.诊断性能问题没有固定的步骤,可以采用很多方式,我将尝试一下分解成可用于进行各种诊断一些基本的模块.

这一部分是针对于初学者的,所以如果您已经进行了一段时间的内存中性能问题分析,您可以安全的跳过这一章节.

首先,在我们开始讨论实际的诊断部分之前,先让我们了解一些高级知识,帮您指明正确的方向.

GC的一些高级知识

1)时间点 vs 时间段

了解性能问题往往不是点状,这一点非常重要.内存问题通常不会突然出现,可能需要一段时间才能积累到明显的程度.

让我们举一个简单的例子,对于一个非常简单的没有世代的GC,它只会紧凑的进行阻塞式GC.这种情况会一直存在.如果您的GC刚刚结束,堆当然处在最小的点.如果您碰巧在这个时间点进行测量,您会认为“太好了;我的堆很小”.但如果您恰好在进行下一次GC之前测量,则堆可能会大得多,并且您会有一些不同的看法.这只是针对于一个简单的GC.想象一下当您有一个世代GC或并发式GC时会发生些什么.

这就是为什么了解一下GC的历史是及其重要的,看看GC是如何作出决策,以及这些决策是如何导致目前的情况.

遗憾的是,很多内存工具或诊断方法,都没有将其考虑在内,它们进行内存诊断都方式是“让我来告诉您,您碰巧问到的那个时间点,堆上的情况”.这往往无济于事,有时候甚至会完全误导人们.浪费时间去追寻一个不存在的问题或在一个完全错误的问题上取得一些完全错误的进展.

并不是说此类工具一点用处都没有-当问题很简单时,它们可能会有所帮助.如果您有一个非常严重的的内存泄漏,并且已经持续了一段时间,而您使用一个工具来显示当前的堆(采取进程转储或使用sos,或者另外的一些堆转储的工具),可能确实会很明显的显示出泄漏的是什么.

2)世代GC

根据设计,具有世代的GC并不会每次触发时收集整个堆.尝试对年轻代GC频率要比对老年代GC高得多,因为对老年代GC的成本往往要高得多.对于并发式的老年代GC来说,STW的的中断时间不会很长,但仍然会需要花费机器周期来完成GC的工作.

这也使查看堆变得更加复杂,如果您刚完成一个老年代的GC,特别是刚经历过压缩的GC,您的堆显然会比在该GC被触发之前小的多.但如果您查看年轻代的GC,它们可能正在压缩,但是区别在于堆大小可能不会有太大变化,这是设计上所实现的.

3)压缩 vs 回收

回收不应该过多的改变堆的大小.在我们的实现中,我们仍然会放弃堆末尾的空间,所以整体的堆大小可能会变小,但总的来说,您可以认为整体的堆大小并没有发生改变,但是为了容纳年轻代的堆分配(或者用户分配在零代/LOH的情况下),会建立起自由空间(free spaces).

因此,如果您看到对二代的两次gc,一次正在压缩另一次正在回收.那么可以预计,压缩阶段结束后,堆大小会缩小很多.回收阶段的碎片化程度则会很高(在设计中,这是我们设计中的自由列表(free list)).

4)分配 vs 存活

虽然很多内存工具都会报告分配情况,但是GC的成本不仅仅是来自分配.当然分配会触发GC,这无疑是成本.但当GC工作时,成本主要取决于存活.当然,您不能同时处于分配率和存活率都很高的情况,这样只会非常快的用光内存.

5)“主线GC方案 vs 非主线”

如果您的程序仅仅是使用栈并且创建了一些要使用的对象,GC多年来一直在优化.基本上就是“扫描堆栈得到根对象然后在那里处理对象”.这就是许多GC论文都将其视为唯一的方案的主线GC方案.当然作为一个存在了几十年的商业产品,为了适应客户的各种需求,我们还有一些其他的东西,例如GC句柄和终结器.

有一个很重要的事情请您理解,虽然多年来我们对这些进行了优化,但我们都是基于“没有太多的这些东西”基础上进行的假设,这显然不会适用于每个人.所以如果您确实使用了很多这些东西,在诊断内存问题时值得一看.换句话说,如果您没有内存问题,则无需在意;但是如果您有(例如high % time in GC),它们是很好的怀疑对象.

所有的这些信息都表示为ETW事件或在Linux上的等效事件-这就是为什么我们多年来投资于分析跟踪的工具的原因.

开始捕捉跟踪

我通常会从两条跟踪开始.第一次是为了获取准确的GC时间:

1
perfview /GCCollectOnly /nogui collect

完成后,在PerfView的cmd窗口按s停止.

这应该运行足够长的时间,充分捕获GC活动,例如,如果您知道问题何时会发生,则应该涵盖导致问题发生的时间(不仅仅只有发生问题的时间).

如果您知道要运行多长时间,您可以这样做(实际上更常用) -

1
perfview /GCCollectOnly /nogui /MaxCollectSec:1800 collect

将1800(半小时)替换成您需要的秒数

这会收集infomational级别的GC事件和足够的OS事件,以及解码后的进程名称.这个命令非常轻量,所以它可以一直保持运行.

请注意,我给出的所有PerfView命令都有/nogui.PerfView确实有一个用于事件收集的UI,可以让您选择需要捕获的事件.就我而言,我从没使用过它(除了在我刚使用PerfView时使用过几次).一部分原因是因为我更喜欢命令行;另一个(更重要的)原因是命令行具有更多的灵活性,对自动化很友好.

当您搜集到跟踪数据后,您可以使用PerfView打开它并查看GCStats视图,有些人倾向于在完成收集后将其发送给我,但我真的鼓励每个需要定期进行内存诊断的人学习阅读这个视图,它非常有用.尽管跟踪如此轻量,但仍旧给我们提供了大量的信息.并且就算不能让我们找到根本原因,也会指出取得更大进展的方向.我在这篇文章及续篇描述了这些视图,在文章中都有链接.所以我在这里不打算展示更多的图片.您可以自己很轻易的打开这些视图.

通过此视图很容易发现的问题类型的例子 -

  • 非常高的“% Time paused for garbage collection(垃圾收集的中断时间百分比)”.除非您正在做一些基准测试,特别是在测试分配性能(类似非常多GC的基准测试),否则这个不指标不应该高于百分之几.如果您发现该指标已经很高了,那就需要调查了.下面是一些会显著导致这个百分比增加的情况.

  • 个别GC的中断事件特别长.60s的GC很长吗?是的,肯定很长!通常来说不是由于GC的工作导致的.根据我的经验,这往往是由于某些因素干扰了GC线程的工作.

  • 过多主动触发GC(当 主动触发的GC/触发GC的总数量 比例很高时),特别是对第二代主动触发的GC时.

  • 过多的针对二代堆GC - 对二代堆进行GC成本是十分昂贵的,尤其是当您有一个大堆时.即便使用了BGC,大部分工作都是并发完成的.但它仍需要花费机器周期,因此,如果您其他的GC都是针对第二代,这通常是指出了一个问题,一种明显的情况是它们大部分的触发理由都是AllocLarge.同样,在某些情况下这不一定是问题,例如,如果您的堆大部分都是LOH,并且没有在容器中运行,这意味着LOH默认不会进行压缩,在这种情况下,二代堆GC只会回收并快速的退出.

  • 长时间的挂起问题 - 挂起通常应该远远小于1ms,如果需要几毫秒-10s,那就是问题,如果需要花费上百毫秒,那毫无疑问是问题.

  • 过多的固定句柄 - 一般情况下,少量的几个固定句柄是可以的,但如果您在短暂的GC中看到数百个,那就值得关注了;如果您看到几千个,通常,这是告诉您需要进行调查了.

这些只是您第一眼就能看到的东西.如果您要进行更加深入的挖掘,还有很多事情要做.我们下次在讨论.

Welcome to my other publishing channels