windows 对于以给定频率发生的给定异常类型,使用堆栈捕获内存转储

fumotvh3  于 2023-04-13  发布在  Windows
关注(0)|答案(1)|浏览(118)

我们有时会看到空引用异常的“峰值”。我想做的是告诉服务器(通过procdump或一些机制)“每当看到空引用异常以特定频率在给定时间内发生时,就用堆栈跟踪捕获转储”。
换句话说,如果空引用异常发生率很高(比如说,每秒一次),持续一段时间,比如说,10秒,那么我想得到一个转储文件,它有一个“完全捕获”的异常。完全捕获我的意思是有一个完整的堆栈跟踪,它将识别抛出异常的方法,和信息,这将允许我在转储中的汇编代码视图中深入研究有问题的代码行(使用WinDbg或类似工具)。我们的环境是Windows服务器。
这可能吗?如果可能,我该怎么做?还有,有没有方法可以最小化对服务器性能的影响,同时仍然可以获得我想要的堆栈跟踪信息?
我们只有AppInsights来处理这种异常峰值,虽然它确实指出了抛出异常的方法,但它没有给予行号。除非方法很小,以至于可以清楚地看到抛出异常的是哪一行,否则它可能是一个纯粹的猜测游戏,关于抛出的是哪一行,特别是如果方法很大的话。

pdtvr36n

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。使用它来验证过程并熟悉它。

class Program
{
    static void Main()
    {
        for (int i = 0; i < 3; i++)
        {
            try { throw new NullReferenceException(); }
            catch (NullReferenceException) { }
        } 
    }
}

下载所有符号

这将在生产调试之前完成一次。其他一切都将是生产调试的一部分。
1.为了提前下载符号,你需要一个进程的minidump。采用minidump不会对性能有很大的影响。我有listed various options here,但只是采用minidump,而不是完全转储。作为一个GUI工具,我认为Process Explorer是最简单的使用。
1.在生产计算机上的WinDbg中打开崩溃转储。

  1. Set up your symbols correctly
    1.键入ld *下载所有符号。
    或者,您也可以使用download all symbols for the whole system,但我认为这有点大材小用。

加载.NET扩展

对于上面的演示程序,您将没有足够的时间附加调试器。您可以插入控制台读取行,或者在WinDbg中启动可执行文件并使用sxe ld clr;g等待SOS命令工作。
对于.NET Framework,使用.loadby sos clrload the SOS extension。使用上一步中的minidump尝试此操作。
对于.NET Core,运行dotnet tool install -g dotnet-sos并使用.loadSOS.dll的完整路径。

设置日志

.logopen /t /u NullReferences.log
如果WinDbg告诉您没有访问权限,请使用完整路径。
/t将添加时间戳,/u写入Unicode。

设置异常处理

首先让我们忽略所有异常:

.foreach(exc {.echo "ct et cpr epr ld ud ser ibp iml out av asrt aph bpe bpec eh clr clrn cce cc dm dbce gp ii ip dz iov ch hc lsq isc 3c svh sse ssec sbo sov vs vcpp wkd rto rtt wob wos *"}) {.catch{sxi ${exc}}}

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,它基本上是.detachq
在开发过程中进行调试时,您可能习惯于简单地退出,这会终止正在运行的程序。不要这样做!在生产调试中使用qd成为习惯。

作为一个好习惯,首先关闭日志文件,这样它就肯定被写入了。这使它成为一个.logclose;qd

结果

最后,您将拥有一个日志文件,其中包含(示例来自演示应用程序):

(59a0.1d5c): CLR exception - code e0434352 (first chance)
r$t1=0
r$t1=1
XXXXXSPLITXXXXX
Exception object: 02a76ec4
Exception type:   System.NullReferenceException
Message:          Object reference not set to an instance of an object.
InnerException:   <none>
StackTrace (generated):
<none>
StackTraceString: <none>
HResult: 80004003
XXXXXSTACKXXXXX
OS Thread Id: 0x1d5c (0)
Child SP       IP Call Site
008ff160 758fe4f2 [HelperMethodFrame: 008ff160] 
008ff210 00d5089e ConsoleNetFramework.Program.Main() [B:\...\Program.cs @ 12]
    LOCALS:
        0x008ff21c = 0x00000001
        0x008ff218 = 0x00000001

008ff3ac 60aa0556 [GCFrame: 008ff3ac]

要分析它,你应该写一个小程序(也许用Python),在每个异常之后分割文件,并尝试通过调用堆栈对异常进行分组并构建统计数据。

快速获取

理想情况下,您希望将调试所需的所有命令放在一行中,以便附加到进程并完成所有工作的中断尽可能短。
然而,很难将所有内容都放在一行中。这与WinDbg有时会转义特殊字符有关,有时不会。有时甚至与空格有关。有时WinDbg只是有错误。一些issues are discussed here
您也可以尝试将命令放入脚本文件中,并使用$〈,$〉〈,$$〈,$$〉〈,$$〉a〈命令之一执行该脚本文件。
我将把这项工作留给你,因为写这一切已经花了太多时间。
你当然想在一秒钟内闯入并继续。这会被用户注意到,但它会被视为网络延迟或其他原因。

相关问题