我们有时会看到空引用异常的“峰值”。我想做的是告诉服务器(通过procdump或一些机制)“每当看到空引用异常以特定频率在给定时间内发生时,就用堆栈跟踪捕获转储”。
换句话说,如果空引用异常发生率很高(比如说,每秒一次),持续一段时间,比如说,10秒,那么我想得到一个转储文件,它有一个“完全捕获”的异常。完全捕获我的意思是有一个完整的堆栈跟踪,它将识别抛出异常的方法,和信息,这将允许我在转储中的汇编代码视图中深入研究有问题的代码行(使用WinDbg或类似工具)。我们的环境是Windows服务器。
这可能吗?如果可能,我该怎么做?还有,有没有方法可以最小化对服务器性能的影响,同时仍然可以获得我想要的堆栈跟踪信息?
我们只有AppInsights来处理这种异常峰值,虽然它确实指出了抛出异常的方法,但它没有给予行号。除非方法很小,以至于可以清楚地看到抛出异常的是哪一行,否则它可能是一个纯粹的猜测游戏,关于抛出的是哪一行,特别是如果方法很大的话。
1条答案
按热度按时间pdtvr36n1#
你已经标记了问题 ProcDump,所以我假设你知道这个工具,它不适合你的目的。虽然有人实现了ProcDump,但似乎其他人(你?)也可以实现它,并添加你想要的特定行为。但这是一个很大的努力。
要明确的是:我不知道有什么工具可以完成你想要的任务。因此,你可能得不到任何答案。但让我解释一下我是如何处理类似情况的。也许这也适合你。
从术语 NullReferenceException 来看,我假设你正在处理一个.NET异常。因此,这个答案将考虑. NET。
我建议的方法是在WinDbg中使用调试。附加调试器总是会对性能产生影响,因为异常的调度方式。恕我直言,ProcDump也有这种性能影响-可能没有WinDbg那么大。
考虑:看起来你有服务器。如果你有很多服务器,他们做负载平衡或其他事情,你可能会设置一个服务器,它接受较少的客户端。在那个服务器上,你可以做调试。这就像A/B测试:一些用户将被调试,而另一些用户则不会。这样,大多数用户将不会注意到性能下降。
程序概述
1.我们提前下载所有符号,以便对符号的任何访问都将是快速的,而不是从互联网下载符号(这是缓慢的)。
1.我们将WinDbg(或cdb)附加到受影响的进程。在此之前,让我们有所有可用的命令。
1.我们为.NET设置了一些东西
1.我们设置了日志记录,因为我们不希望每个异常都有巨大的崩溃转储。获取完整的内存转储对于分析来说是很好的,但是将GB写入磁盘可能需要很长时间。
1.我们设置异常处理来记录每个NullReferenceException的调用堆栈。
1.我们将分隔符输出到日志文件中,以便稍后可以拆分它,并且您可以构建一些关于哪个方法发生NullReferenceException的频率的统计信息。
1.正确分离
测试程序
在生产机器上执行以下步骤之前,编写一个简单的应用程序,它只抛出一个NullReferenceException。使用它来验证过程并熟悉它。
下载所有符号
这将在生产调试之前完成一次。其他一切都将是生产调试的一部分。
1.为了提前下载符号,你需要一个进程的minidump。采用minidump不会对性能有很大的影响。我有listed various options here,但只是采用minidump,而不是完全转储。作为一个GUI工具,我认为Process Explorer是最简单的使用。
1.在生产计算机上的WinDbg中打开崩溃转储。
1.键入
ld *
下载所有符号。或者,您也可以使用download all symbols for the whole system,但我认为这有点大材小用。
加载.NET扩展
对于上面的演示程序,您将没有足够的时间附加调试器。您可以插入控制台读取行,或者在WinDbg中启动可执行文件并使用
sxe ld clr;g
等待SOS命令工作。对于.NET Framework,使用
.loadby sos clr
的load the SOS extension。使用上一步中的minidump尝试此操作。对于.NET Core,运行
dotnet tool install -g dotnet-sos
并使用.load
和SOS.dll
的完整路径。设置日志
.logopen /t /u NullReferences.log
如果WinDbg告诉您没有访问权限,请使用完整路径。
/t
将添加时间戳,/u
写入Unicode。设置异常处理
首先让我们忽略所有异常:
setting all exceptions中详细解释了该命令,但请注意,我们使用
sxi
而不是sxd
。现在我们可以考虑.NET异常。最简单的事情是为所有类型的.NET异常设置异常处理。您可以使用
sxe -c "!pe;!clrstack;g" clr
。这将打印异常(!pe
),打印.NET调用堆栈(!clrstack
)并立即继续(g
)。为什么我们需要
!clrstack
?异常不是伴随着调用堆栈吗?AFAIK并不总是。如果异常被捕获并且调用堆栈从未被编程访问,则异常对象可能没有调用堆栈信息。这就是为什么我显式地使用!clrstack
。也许你可以去掉
!pe
部分,因为NullReferenceException看起来很相似。我怀疑我是否见过一个带有InnerException的异常(这可能有点有趣)。对于特定的.NET异常,我们需要来自SOS扩展的.NET特定命令:
!soe -create System.NullReferenceException 1
。这将使用伪寄存器$t1
作为布尔标志,然后我们可以使用。因此命令是sxe -c "!soe System.NullReferenceException 1; .if (@$t1==1){!pe;!clrstack};g" clr
。获取拆分点
我们通过另一个
.echo XXXXXSPLITXXXXX
和.echo XXXXXSTACKXXXXX
扩展了异常分析命令,以便您稍后能够处理该文件。所以命令是
sxe -c "!soe System.NullReferenceException 1; .if (@$t1==1){.echo XXXXXSPLITXXXXX;!pe;.echo XXXXXSTACKXXXXX;!clrstack -a};g" clr
。正确拆包退出
在退出前要分离的生产系统上,使用
qd
,它基本上是.detach
和q
。在开发过程中进行调试时,您可能习惯于简单地退出,这会终止正在运行的程序。不要这样做!在生产调试中使用
qd
成为习惯。作为一个好习惯,首先关闭日志文件,这样它就肯定被写入了。这使它成为一个
.logclose;qd
。结果
最后,您将拥有一个日志文件,其中包含(示例来自演示应用程序):
要分析它,你应该写一个小程序(也许用Python),在每个异常之后分割文件,并尝试通过调用堆栈对异常进行分组并构建统计数据。
快速获取
理想情况下,您希望将调试所需的所有命令放在一行中,以便附加到进程并完成所有工作的中断尽可能短。
然而,很难将所有内容都放在一行中。这与WinDbg有时会转义特殊字符有关,有时不会。有时甚至与空格有关。有时WinDbg只是有错误。一些issues are discussed here。
您也可以尝试将命令放入脚本文件中,并使用$〈,$〉〈,$$〈,$$〉〈,$$〉a〈命令之一执行该脚本文件。
我将把这项工作留给你,因为写这一切已经花了太多时间。
你当然想在一秒钟内闯入并继续。这会被用户注意到,但它会被视为网络延迟或其他原因。