AppDomain 卸载卡住的原因

Eug*_*kov 5 .net appdomain azure domainunload azure-web-app-service

我仍在尝试了解持续存在的问题,但它几乎可以概括为无法卸载 AppDomain

它发生在将 ASP.NET WebAPI 部署到 Azure 应用服务期间,我们观察到的情况如下:

  1. 进程 ID 不会更改,新部署托管在同一进程中(AFAIU 它是通过卸载旧 AppDomain 并使用更新的二进制文件启动新 AppDomain 来完成的)
  2. Azure PaaS 诊断在错误部分显示以下内容:

“在 w3wp_12396.dmp 中,应用程序 /LM/W3SVC/1523308129/ROOT 的 HttpRuntime 正处于关闭过程中。”

  1. 分析内存转储,我们看到设置了IsAbortRequested标志的线程,但它们似乎永远不会完成(!threads此处 WinDbg 的输出: https: //pastebin.com/7CXYcffy

  2. 在内存转储中,我们还看到许多具有“ UNLOAD_REQUESTED ”阶段的 AppDomain,它们似乎从未完成卸载(完整输出!DumpDomain位于: https: //pastebin.com/kahZQuWN

域 7:000001c67062c800
低频堆:000001c67062cff8
高频堆:000001c67062d088
存根堆:000001c67062d118
阶段:UNLOAD_REQUESTED
安全描述符:000001c6705c5680
名称:/LM/W3SVC/1523308129/ROOT-6-131687140950004974
  1. 未检测到死锁!dlk(至少通过 WinDbg SOSEX 插件的命令,通常涵盖大多数死锁情况)

  2. 没有代码取消线程中止(没有Thread.ResetAbort()调用)

现在解决问题的唯一方法是终止进程(停止 Azure AppService)。

AppDomain无法卸载的可能原因有哪些?

更新。在线程堆栈中,我们得到一个提示,它可能与我们的自定义 Azure Blob Log4net 附加程序有关,我发现当创建此类附加程序时(每个应用程序一次),它会生成具有以下结构的新线程。

while (true)
{
   try
   {
        Flush(); // pseudocode
        Thread.Sleep(10000);
   }
   catch(Exception)
   {
   }
}
Run Code Online (Sandbox Code Playgroud)

不确定我理解为什么它会导致完全不可停止的线程(因为ThreadAbortException不会被 catch 停止),但看起来像是通过更改while (true)while (!Environment.HasShutdownStarted && !_stopping)解决问题(_stoppingOnClose调用 Appender 时设置,这对 log4net 来说是一种优雅的关闭)...

Eug*_*kov 2

这似乎是一个 JIT bug。是的,JIT 中有错误!我发现那里记录了几乎相同的故事:http://labs.criteo.com/2017/04/ryujit-never-ending-threadabortexception/

\n\n

为了演示该问题,您可以运行以下代码。仅适用于发布模式,仅适用于 x64 平台(并且我的目标是 .NET 4.5.2)。

\n\n

除非您手动重新抛出异常,否则您将观察到记录的异常链。为什么这是 CLR/JIT 中的错误?因为 CLR/JIT 负责ThreadAbortException在设置线程的 AbortRequested 标志时将 throw 注入“安全位置”。

\n\n

引用 Jeffrey Richter 的“CLR via C#”(以下代码违规):

\n\n
\n

即使代码捕获了ThreadAbortException,CLR 也不允许\xe2\x80\x99 吞掉异常。换句话说,在 catch\n 块的末尾,CLR 自动重新抛出ThreadAbortException\n 异常。

\n
\n\n

还有 GitHub 中的错误:https://github.com/dotnet/coreclr/issues/16122

\n\n
static void Main(string[] args)\n{\n    var mutex = new ManualResetEventSlim();\n\n    var t = new Thread(() =>\n    {\n        while (true)\n        {\n            try\n            {\n                if (!mutex.IsSet)\n                {\n                    mutex.Set();\n                }\n\n                // Do some stuff\n\n                Thread.Sleep(100);\n            }\n            catch (Exception ex)\n            {\n                Console.WriteLine("Exception: " + ex.Message);\n\n                // the lines below FIX the issue\n                //if (ex is ThreadAbortException)\n                //    throw;\n            }\n\n            // FIXES the issue as well\n            //Thread.Sleep(0);\n        }\n    });\n\n    t.Start();\n\n    // Wait for the thread to start\n    mutex.Wait();\n\n    t.Abort();\n\n    Console.ReadLine();\n}\n
Run Code Online (Sandbox Code Playgroud)\n