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



我们有时会看到;"尖峰";的空引用异常。我想做的是告诉服务器(通过procdump或某种机制)";每当在给定的时间量内以特定频率出现空引用异常时,使用堆栈跟踪捕获转储";。

换言之,如果空引用异常以高速率(比如每秒一次)发生,持续10秒,那么我想要得到一个具有这些异常之一的转储文件";完全捕获";。我所说的完全捕获是指完整的堆栈跟踪,它将识别抛出异常的方法,以及允许我在转储中的汇编代码视图中钻取有问题的代码行的信息(使用WinDbg或类似工具)。我们的环境是Windows服务器。

这可能吗?如果可能,我该怎么做?还有,有没有办法最大限度地减少对服务器性能的影响,同时仍然获得我想要的堆栈跟踪信息?

我们只有针对此类异常尖峰的AppInsights,虽然它确实指示了抛出异常的方法,但它没有给出行号。除非该方法非常小,以至于可以清楚地从哪一行抛出异常,否则它可能是一个纯粹的猜测游戏,即抛出哪一行,尤其是在方法巨大的情况下。

您已经标记了问题ProcDump,所以我认为您知道该工具,它不适合您的目的。虽然有人实现了ProcDump,但似乎其他人(你?)也可以实现它,并添加你想要的特定行为。但这需要付出很多努力。

需要明确的是:我不知道有什么工具可以执行你想要的任务。因此,你可能得不到任何答案。但让我解释一下我是如何处理类似案件的。也许这也适合你。

根据术语NullReferenceException,我假设您正在处理一个.NET异常。因此,这个答案将考虑.NET.

我将建议的方法将在WinDbg中使用调试。由于调度异常的方式,附加调试器总是会对性能产生影响。IMHO,ProcDump也有这种性能影响——可能没有WinDbg那么大。

考虑:你似乎有服务器。如果你有很多服务器,它们做负载平衡或其他事情,你可能会设置一个服务器,让它接受更少的客户端。在该服务器上,您可以进行调试。这就像A/B测试:有些用户会被调试,有些则不会。这样,大多数用户就不会注意到性能下降。

程序概述

  1. 我们提前下载所有符号,这样对符号的任何访问都会很快,而不是从互联网下载符号(这很慢)。

  2. 我们将WinDbg(或cdb)附加到受影响的进程。在执行此操作之前,让我们先准备好所有可用的命令。

  3. 我们为.NET 设置了一些东西

  4. 我们设置了日志记录,因为我们不希望每个异常都有巨大的崩溃转储。获取完整的内存转储非常适合分析,但将GB写入磁盘可能需要很长时间。

  5. 我们设置了异常处理来记录每个NullReferenceException的调用堆栈。

  6. 我们将分隔符输出到日志文件中,以便以后可以对其进行拆分,并且您可以构建一些统计信息,说明哪个方法具有NullReferenceException的频率。

  7. 正确分离

测试程序

在生产机器上执行以下步骤之前,请编写一个简单的应用程序,该应用程序除了抛出NullReferenceException之外什么都不做。使用它来验证程序并使自己熟悉它。

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

下载所有符号

这将在生产调试之前完成一次。其他一切都将是生产调试的一部分。

  1. 为了提前下载符号,您需要对流程进行小型转储。进行小型转储不会对性能产生巨大影响。我在这里列出了各种选择,但只是进行小型转储,而不是完全转储。作为一个GUI工具,我认为Process Explorer是最容易使用的
  2. 在生产计算机上的WinDbg中打开崩溃转储
  3. 正确设置符号
  4. 键入ld *下载所有符号

或者,您也可以下载整个系统的所有符号,但我认为这有点过头了。

正在加载.NET扩展

对于上面的演示程序,您将没有足够的时间连接调试器。您可以插入控制台读出线,也可以在WinDbg中启动可执行文件,并使用sxe ld clr;g等待SOS命令工作。

对于.NET Framework,请使用.loadby sos clr加载SOS扩展。请使用上一步中的小型转储进行此操作。

对于.NET Core,请运行dotnet tool install -g dotnet-sos并使用.load以及指向SOS.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}}}

在设置所有异常时详细解释了该命令,但请注意,我们使用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只是有错误。这里讨论了一些问题。

您也可以尝试将命令放入脚本文件中,并使用$<>lt$$<$>lt;,$$>a<命令。

我将把这项工作留给你,因为写这一切已经花了太多时间。

你当然想在一秒钟内突破并继续。这会被用户注意到,但会被视为网络滞后或其他什么。

最新更新